mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
chore(ci): give the server test typecheck room, format new client files
The server's src and tests together are past 570,000 lines and the test typecheck needs about 4.3 GB, over Node's default heap; it now gets 8 GB like the test jobs. Forty new client files are formatted, and the react-hooks/refs baseline drops by the one warning a split hook removed.
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { summarizeTourActivities, type TourActivityStatsEntry, type TourActivityStatsResponse } from '@trek/shared';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '../../../../tests/helpers/render';
|
||||
import { buildSettings } from '../../../../tests/helpers/factories';
|
||||
import { render, screen } from '../../../../tests/helpers/render';
|
||||
import { resetAllStores, seedStore } from '../../../../tests/helpers/store';
|
||||
import { useSettingsStore } from '../../../store/settingsStore';
|
||||
import { AtlasTourActivityPane } from './AtlasTourActivityPane';
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { summarizeTourActivities, type TourActivityStatsEntry, type TourActivityStatsResponse } from '@trek/shared';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { buildSettings } from '../../../../tests/helpers/factories';
|
||||
import { render, screen, within } from '../../../../tests/helpers/render';
|
||||
import { resetAllStores, seedStore } from '../../../../tests/helpers/store';
|
||||
import { buildSettings } from '../../../../tests/helpers/factories';
|
||||
import { useSettingsStore } from '../../../store/settingsStore';
|
||||
import { TourActivityStatsDialog } from './TourActivityStatsDialog';
|
||||
import type { TourActivityStats } from './useTourActivityStats';
|
||||
|
||||
@@ -25,7 +25,8 @@ export function ReadStatusNotice({
|
||||
{messages.loading}
|
||||
</span>
|
||||
);
|
||||
if (status === 'offline') return <TourNotice icon={<WifiOff size={13} strokeWidth={2} />}>{messages.offline}</TourNotice>;
|
||||
if (status === 'offline')
|
||||
return <TourNotice icon={<WifiOff size={13} strokeWidth={2} />}>{messages.offline}</TourNotice>;
|
||||
if (status !== 'failed') return null;
|
||||
return (
|
||||
<TourNotice tone="warning" icon={<AlertTriangle size={13} strokeWidth={2} />}>
|
||||
|
||||
@@ -52,8 +52,8 @@ import { TourGpxExportButton } from './TourGpxExportButton';
|
||||
import TourActivitySection from './activity/TourActivitySection';
|
||||
import type { PlanValues } from './activity/planComparison';
|
||||
import { TourSaveToCollectionButton } from './collection/TourSaveToCollectionButton';
|
||||
import { PaddleCheckSection } from './paddle/PaddleCheckSection';
|
||||
import { TourPackingSection } from './packing/TourPackingSection';
|
||||
import { PaddleCheckSection } from './paddle/PaddleCheckSection';
|
||||
import { PaddleWindSection } from './paddle/PaddleWindCard';
|
||||
import { TourProvenanceBlock } from './provenance/TourProvenanceSection';
|
||||
import { RunHeadPills } from './run/RunHeadPills';
|
||||
|
||||
@@ -236,9 +236,7 @@ export default function TourActivityDialog({
|
||||
variant="primary"
|
||||
data-testid="tour-activity-confirm"
|
||||
disabled={!assignment.ready || state.busy || importing}
|
||||
icon={
|
||||
state.busy ? <Loader2 size={14} className="animate-spin" /> : <Link2 size={14} strokeWidth={2} />
|
||||
}
|
||||
icon={state.busy ? <Loader2 size={14} className="animate-spin" /> : <Link2 size={14} strokeWidth={2} />}
|
||||
onClick={confirm}
|
||||
>
|
||||
{t('tours.activity.confirm')}
|
||||
|
||||
@@ -120,7 +120,17 @@ describe('TourActivitySection (ACT-01)', () => {
|
||||
expect(current.askRemove).toHaveBeenCalledWith(current.activities?.[0]);
|
||||
|
||||
hook.state = state({ assigning: {}, removing: activity() });
|
||||
rerender(<TourActivitySection tripId={1} tour={tour} days={days} assignments={assignments} canEdit plan={plan} planColor="plan-paint" />);
|
||||
rerender(
|
||||
<TourActivitySection
|
||||
tripId={1}
|
||||
tour={tour}
|
||||
days={days}
|
||||
assignments={assignments}
|
||||
canEdit
|
||||
plan={plan}
|
||||
planColor="plan-paint"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('tour-activity-dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('tours.activity.removeTitle')).toBeInTheDocument();
|
||||
});
|
||||
@@ -193,6 +203,9 @@ describe('TourActivitySection (ACT-01)', () => {
|
||||
expect(screen.queryByRole('button', { name: /tours.activity.compare.(show|hide)/ })).toBeNull();
|
||||
|
||||
act(() => useTourActivityCompareStore.getState().setMapAvailable(true));
|
||||
expect(screen.getByRole('button', { name: /tours.activity.compare.show/ })).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(screen.getByRole('button', { name: /tours.activity.compare.show/ })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'false'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,7 +117,9 @@ describe('useActivityCompareMap (ACT-02)', () => {
|
||||
});
|
||||
|
||||
it('FE-ACT-COMPARE-MAP-006: tells the store whether it can compare, and ends a comparison where it no longer can or is gone', () => {
|
||||
const { rerender, unmount } = renderHook((props: typeof input) => useActivityCompareMap(props), { initialProps: input });
|
||||
const { rerender, unmount } = renderHook((props: typeof input) => useActivityCompareMap(props), {
|
||||
initialProps: input,
|
||||
});
|
||||
expect(useTourActivityCompareStore.getState().mapAvailable).toBe(true);
|
||||
act(() => useTourActivityCompareStore.getState().show({ tourPlaceId: 7, trackPlaceId: 20 }));
|
||||
|
||||
|
||||
@@ -183,7 +183,12 @@ describe('the paints and the highlight (BIK-01 to BIK-05)', () => {
|
||||
|
||||
it('FE-TOUR-WAYS-014: lines of different paints each take their own, an unknown key the accent (MAP-03)', () => {
|
||||
const drawn = withRouteHighlight(
|
||||
{ id: 'sac', lines: [LINE.slice(0, 2), LINE.slice(1, 3), LINE.slice(2)], paint: 'gravel', paints: ['way', 'nope'] },
|
||||
{
|
||||
id: 'sac',
|
||||
lines: [LINE.slice(0, 2), LINE.slice(1, 3), LINE.slice(2)],
|
||||
paint: 'gravel',
|
||||
paints: ['way', 'nope'],
|
||||
},
|
||||
[LINE],
|
||||
null,
|
||||
null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TOUR_TRAIL_GRADE_MAX_WAYS } from '@trek/shared';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { TOUR_TRAIL_GRADE_MAX_WAYS } from '@trek/shared';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { encodePolyline } from '../../../../tests/helpers/polyline';
|
||||
import { OnlineOnlyError } from '../../../repo/onlineOnly';
|
||||
|
||||
@@ -34,9 +34,11 @@ describe('GPX import choices', () => {
|
||||
tour_type: 'ski',
|
||||
ski_discipline: 'touring',
|
||||
});
|
||||
expect(
|
||||
importFields({ ...base, type: 'ski', ski: { discipline: 'cross_country', technique: 'unknown' } })
|
||||
).toEqual({ tour_type: 'ski', ski_discipline: 'cross_country', ski_technique: 'unknown' });
|
||||
expect(importFields({ ...base, type: 'ski', ski: { discipline: 'cross_country', technique: 'unknown' } })).toEqual({
|
||||
tour_type: 'ski',
|
||||
ski_discipline: 'cross_country',
|
||||
ski_technique: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-TOUR-GPX-CHOICES-002: holds the import back while a paddle tour or a ski track lacks what it must name', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TourTrailGrade } from '@trek/shared';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { TourTrailGrade } from '@trek/shared';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useTourRouteHighlightStore } from '../../../store/tourRouteHighlightStore';
|
||||
import type { RouteWayEdge, RouteWays } from '../bike/routeWays';
|
||||
|
||||
@@ -13,16 +13,7 @@ import type { HikePaintKey } from './hikePaint';
|
||||
*/
|
||||
|
||||
type HikeSurfaceClass =
|
||||
| 'paved'
|
||||
| 'sett'
|
||||
| 'compacted'
|
||||
| 'gravel'
|
||||
| 'ground'
|
||||
| 'grass'
|
||||
| 'rock'
|
||||
| 'wood'
|
||||
| 'unpaved'
|
||||
| 'other';
|
||||
'paved' | 'sett' | 'compacted' | 'gravel' | 'ground' | 'grass' | 'rock' | 'wood' | 'unpaved' | 'other';
|
||||
|
||||
type HikeWayClass = 'path' | 'footway' | 'steps' | 'track' | 'cycleway' | 'quietRoad' | 'road' | 'viaFerrata' | 'other';
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, within } from '../../../../tests/helpers/render';
|
||||
import { TourDayRowIcon } from '../TourDayRowFacts';
|
||||
import { tourDayRowIcon } from '../tourDayRowIcon';
|
||||
import { movingTimeLabel } from '../tourFacts';
|
||||
import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
|
||||
import { useShapingClick } from '../planner/useShapingClick';
|
||||
import type { TourPlannerController } from '../planner/useTourPlanner';
|
||||
import { WaypointLegRows } from '../planner/WaypointLegRows';
|
||||
import { TourDayRowIcon } from '../TourDayRowFacts';
|
||||
import { tourDayRowIcon } from '../tourDayRowIcon';
|
||||
import { movingTimeLabel } from '../tourFacts';
|
||||
import { HorseLegRow } from './HorseLegRow';
|
||||
import { Horseshoe } from './HorseshoeIcon';
|
||||
import type { HorseSectionLeg } from './horseSection';
|
||||
import { Horseshoe } from './HorseshoeIcon';
|
||||
|
||||
// FE-HORSE-LEG-001 to FE-HORSE-LEG-005
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { fs } from '../../shared/DialogShell';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { lineLengthM } from '../planner/tourFixedLegs';
|
||||
import { ACCESS_TONE_DOT, accessText, accessToneOf } from './horseAccess';
|
||||
import { Horseshoe } from './HorseshoeIcon';
|
||||
import type { HorseSectionLeg } from './horseSection';
|
||||
import { Horseshoe } from './HorseshoeIcon';
|
||||
|
||||
/**
|
||||
* HOR-01: a section of a horse route the ride follows, as a badge on its leg
|
||||
|
||||
@@ -132,9 +132,24 @@ describe('horse route sections on a ride (HOR-01)', () => {
|
||||
expect(accessToneOf(undefined)).toBeNull();
|
||||
expect(accessToneOf([])).toBeNull();
|
||||
expect(accessToneOf([{ value: 'yes', length_m: 10 }])).toBe('open');
|
||||
expect(accessToneOf([{ value: 'yes', length_m: 10 }, { value: 'unknown', length_m: 1 }])).toBe('unknown');
|
||||
expect(accessToneOf([{ value: 'unknown', length_m: 10 }, { value: 'destination', length_m: 1 }])).toBe('limited');
|
||||
expect(accessToneOf([{ value: 'designated', length_m: 10 }, { value: 'private', length_m: 1 }])).toBe('closed');
|
||||
expect(
|
||||
accessToneOf([
|
||||
{ value: 'yes', length_m: 10 },
|
||||
{ value: 'unknown', length_m: 1 },
|
||||
])
|
||||
).toBe('unknown');
|
||||
expect(
|
||||
accessToneOf([
|
||||
{ value: 'unknown', length_m: 10 },
|
||||
{ value: 'destination', length_m: 1 },
|
||||
])
|
||||
).toBe('limited');
|
||||
expect(
|
||||
accessToneOf([
|
||||
{ value: 'designated', length_m: 10 },
|
||||
{ value: 'private', length_m: 1 },
|
||||
])
|
||||
).toBe('closed');
|
||||
});
|
||||
|
||||
it('FE-HORSE-MODEL-009: spells the tags out with the metres each covers', () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { TourFixedLeg } from '@trek/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { tourMapLayersFor } from '../tourMapLayers';
|
||||
import { normalizePlannerWaypoints, tourWaypointKey, type TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
|
||||
import { toggleFreeLeg } from '../planner/tourRouteActions';
|
||||
import { rideTour, routeWalkingTour } from '../planner/tourRouting';
|
||||
import { tourMapLayersFor } from '../tourMapLayers';
|
||||
|
||||
// FE-HORSE-ROUTE-001 to FE-HORSE-ROUTE-006
|
||||
|
||||
|
||||
@@ -22,7 +22,16 @@ const GEAR: TourPackingLink = { category: 'Hiking gear', linked_at: '2026-10-20
|
||||
|
||||
let nextId = 1;
|
||||
function item(name: string, category: string | null, over: Partial<PackingItem> = {}): PackingItem {
|
||||
return { id: nextId++, trip_id: 5, name, category, checked: 0, sort_order: nextId, quantity: 1, ...over } as PackingItem;
|
||||
return {
|
||||
id: nextId++,
|
||||
trip_id: 5,
|
||||
name,
|
||||
category,
|
||||
checked: 0,
|
||||
sort_order: nextId,
|
||||
quantity: 1,
|
||||
...over,
|
||||
} as PackingItem;
|
||||
}
|
||||
|
||||
const ITEMS = [
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { DistanceUnit } from '../../../types';
|
||||
import { formatDistance, getDistanceUnitLabel } from '../../../utils/units';
|
||||
import { SoftPill } from '../../Planner/planParts';
|
||||
import ConfirmDialog from '../../shared/ConfirmDialog';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { INPUT, LABEL, PillSelect } from '../../shared/dialogParts';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { TourNotice } from '../tourParts';
|
||||
import { MapCard, MapCardActions, MapCornerOverlay, StopDisc, VariantFigures, WayChoice } from './mapCardParts';
|
||||
|
||||
@@ -7,9 +7,9 @@ import { INPUT, LABEL } from '../../shared/dialogParts';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { FoldButton, InfoDot, TourNotice, TourSection } from '../tourParts';
|
||||
import { HikingPreferencesField } from './HikingPreferencesField';
|
||||
import { WinterActivityField } from './WinterActivityField';
|
||||
import { TOUR_DURATION_INPUT_MAX_LENGTH, formatTourDuration, parseTourDuration } from './tourDurationInput';
|
||||
import type { TourPlannerController } from './useTourPlanner';
|
||||
import { WinterActivityField } from './WinterActivityField';
|
||||
|
||||
type Difficulty = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useSettingsStore } from '../../../store/settingsStore';
|
||||
import type { TourFerrataCrossing } from '../../../store/tourFerrataCrossingStore';
|
||||
import { formatDistance } from '../../../utils/units';
|
||||
import { FerrataGlyph } from '../../Ferratas/FerrataGlyph';
|
||||
import { HorseLegRow } from '../horse/HorseLegRow';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { HorseLegRow } from '../horse/HorseLegRow';
|
||||
import { PaddleLegFailureRow, PaddleLegNotes } from '../paddle/PaddleLegParts';
|
||||
import { isRegionLoadPending, regionLoadFailure } from '../paddle/paddleRegionLoad';
|
||||
import { PaddleRegionLoadRow } from '../paddle/PaddleRegionLoadRow';
|
||||
|
||||
@@ -102,9 +102,9 @@ function hasNeighbour(grid: Map<string, Sample[]>, sample: Sample, counts: (othe
|
||||
const cy = cellOf(sample.y);
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
const near = grid.get(`${cx + dx}:${cy + dy}`)?.some(
|
||||
(other) => Math.hypot(other.x - sample.x, other.y - sample.y) <= SAME_GROUND_M && counts(other)
|
||||
);
|
||||
const near = grid
|
||||
.get(`${cx + dx}:${cy + dy}`)
|
||||
?.some((other) => Math.hypot(other.x - sample.x, other.y - sample.y) <= SAME_GROUND_M && counts(other));
|
||||
if (near) return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ const { valhallaRun, enrichTourElevations } = vi.hoisted(() => ({
|
||||
vi.mock('../../Map/valhallaRoute', () => ({ valhallaRun }));
|
||||
vi.mock('./tourRouting', () => ({ enrichTourElevations }));
|
||||
|
||||
import { fitsWish, type RoundRequest } from './roundHikes';
|
||||
import { spotToward } from './roundHikeGeometry';
|
||||
import { findRoundHikes, MAX_ROUND_ASKS, type RoundSearchReport } from './roundHikeRouting';
|
||||
import { fitsWish, type RoundRequest } from './roundHikes';
|
||||
|
||||
type Spot = { lat: number; lng: number };
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { valhallaRun } from '../../Map/valhallaRoute';
|
||||
import { figuresOf } from './legVariants';
|
||||
import { loopSpots, retracedShare, sharedShare, STRAIGHT_PER_RADIUS_M } from './roundHikeGeometry';
|
||||
import { fitsWish, ROUND_KEYS, type RoundCandidate, type RoundRequest } from './roundHikes';
|
||||
import { enrichTourElevations } from './tourRouting';
|
||||
import { MAX_TOUR_SNAP_M } from './tourRouteOutcome';
|
||||
import { enrichTourElevations } from './tourRouting';
|
||||
|
||||
type Spot = { lat: number; lng: number };
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { convertDistance } from '../../../utils/units';
|
||||
import type { CostingOptions } from '../../Map/valhallaRoute';
|
||||
import { OTHER_WAY_KEYS, type LegVariant } from './legVariants';
|
||||
import { spotToward } from './roundHikeGeometry';
|
||||
import { closeLoop } from './tourRouteActions';
|
||||
import type { TourPlannerWaypoint } from './tourPlannerWaypoints';
|
||||
import { closeLoop } from './tourRouteActions';
|
||||
|
||||
/**
|
||||
* RTE-03: round hikes of about a wished length out of a start point, as a
|
||||
@@ -139,9 +139,13 @@ export function roundPlanOf(
|
||||
newId: () => string
|
||||
): TourPlannerWaypoint[] {
|
||||
const { fixedLeg: _leg, lock: _lock, carryNext: _carry, auto: _auto, shaping: _shaping, ...own } = start;
|
||||
const steering = via.map(
|
||||
({ lat, lng }): TourPlannerWaypoint => ({ id: newId(), lat, lng, role: 'via', shaping: true })
|
||||
);
|
||||
const steering = via.map(({ lat, lng }): TourPlannerWaypoint => ({
|
||||
id: newId(),
|
||||
lat,
|
||||
lng,
|
||||
role: 'via',
|
||||
shaping: true,
|
||||
}));
|
||||
return closeLoop([{ ...own, role: 'start' }, ...steering], newId());
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ function alongLine(
|
||||
const t =
|
||||
length === 0 ? 0 : Math.max(0, Math.min(1, ((lng - aLng) * cos * dx + (lat - aLat) * dy) / (length * length)));
|
||||
const distance = Math.hypot(aLng * cos + dx * t - lng * cos, aLat + dy * t - lat);
|
||||
if (distance < best.distance) best = { at: travelled + length * t, index: i, vertex: t < 0.5 ? i : i + 1, distance };
|
||||
if (distance < best.distance)
|
||||
best = { at: travelled + length * t, index: i, vertex: t < 0.5 ? i : i + 1, distance };
|
||||
}
|
||||
travelled += length;
|
||||
}
|
||||
|
||||
@@ -87,10 +87,7 @@ describe('useTourPlanner, the winter label of a hike (HIK-01)', () => {
|
||||
await act(async () => {
|
||||
await result.current.save();
|
||||
});
|
||||
expect(createTour).toHaveBeenCalledWith(
|
||||
61,
|
||||
expect.objectContaining({ tour_type: 'hike', winter_activity: null })
|
||||
);
|
||||
expect(createTour).toHaveBeenCalledWith(61, expect.objectContaining({ tour_type: 'hike', winter_activity: null }));
|
||||
});
|
||||
|
||||
it('FE-TOUR-WINTER-002: the label plans nothing again, is no step of the history and is kept in the draft', async () => {
|
||||
@@ -179,10 +176,7 @@ describe('useTourPlanner, the winter label of a hike (HIK-01)', () => {
|
||||
await act(async () => {
|
||||
await result.current.save();
|
||||
});
|
||||
expect(createTour).toHaveBeenCalledWith(
|
||||
65,
|
||||
expect.objectContaining({ tour_type: 'walk', winter_activity: null })
|
||||
);
|
||||
expect(createTour).toHaveBeenCalledWith(65, expect.objectContaining({ tour_type: 'walk', winter_activity: null }));
|
||||
});
|
||||
|
||||
it('FE-TOUR-WINTER-006: keeps the label the switch brings back with the tour, winter hiking for the next one', async () => {
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
MAX_RUN_PACE_SECONDS_PER_KM,
|
||||
MIN_RUN_PACE_SECONDS_PER_KM,
|
||||
RUN_PROFILES,
|
||||
type RunProfile,
|
||||
} from '@trek/shared';
|
||||
import { MAX_RUN_PACE_SECONDS_PER_KM, MIN_RUN_PACE_SECONDS_PER_KM, RUN_PROFILES, type RunProfile } from '@trek/shared';
|
||||
import { Clock, Gauge, Info, Ruler } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
|
||||
@@ -20,7 +20,7 @@ export function skiChoiceMissing(choice: SkiChoice): 'discipline' | 'technique'
|
||||
|
||||
/** The fields a complete choice is sent as; null while something is missing. */
|
||||
export function skiFieldsOf(
|
||||
choice: SkiChoice,
|
||||
choice: SkiChoice
|
||||
): Pick<TourSkiClassificationRequest, 'ski_discipline' | 'ski_technique'> | null {
|
||||
if (choice.discipline === null || skiChoiceMissing(choice)) return null;
|
||||
return choice.discipline === 'cross_country'
|
||||
|
||||
@@ -29,8 +29,7 @@ export function useSkiClassification({ tripId, planner, tours, canEdit, onSaved
|
||||
const [draft, setDraft] = useState<SkiChoice | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const viewed = planner.mode.type === 'view-gpx' ? planner.mode.tour : null;
|
||||
const tour =
|
||||
viewed?.tour_type === 'ski' ? (tours.find((item) => item.place_id === viewed.place_id) ?? viewed) : null;
|
||||
const tour = viewed?.tour_type === 'ski' ? (tours.find((item) => item.place_id === viewed.place_id) ?? viewed) : null;
|
||||
const saved = useMemo(() => (tour ? skiChoiceOf(tour) : NO_SKI_CHOICE), [tour]);
|
||||
|
||||
const open = useCallback(() => {
|
||||
|
||||
@@ -18,7 +18,11 @@ interface StageDayPickerProps {
|
||||
}
|
||||
|
||||
/** The days a stage can go on, the one it is on checked, and a way back to none. */
|
||||
function dayItems({ stageKey, days, stageDays }: StageDayPickerProps, label: (day: Day, index: number) => string, noDay: string) {
|
||||
function dayItems(
|
||||
{ stageKey, days, stageDays }: StageDayPickerProps,
|
||||
label: (day: Day, index: number) => string,
|
||||
noDay: string
|
||||
) {
|
||||
const current = stageDays.dayOf(stageKey);
|
||||
const items: MenuItems = days.map((day, index) => ({
|
||||
label: label(day, index),
|
||||
@@ -27,7 +31,11 @@ function dayItems({ stageKey, days, stageDays }: StageDayPickerProps, label: (da
|
||||
onClick: () => void stageDays.setDay(stageKey, day.id),
|
||||
}));
|
||||
if (current === null) return items;
|
||||
return [...items, { divider: true }, { label: noDay, icon: CalendarX2, onClick: () => void stageDays.setDay(stageKey, null) }];
|
||||
return [
|
||||
...items,
|
||||
{ divider: true },
|
||||
{ label: noDay, icon: CalendarX2, onClick: () => void stageDays.setDay(stageKey, null) },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +63,11 @@ export function StageDayPicker(props: StageDayPickerProps) {
|
||||
menu.close();
|
||||
return;
|
||||
}
|
||||
menu.open(event, dayItems(props, (option, at) => dayMenuLabel(option, at, t, locale), t('tours.stages.noDay')), true);
|
||||
menu.open(
|
||||
event,
|
||||
dayItems(props, (option, at) => dayMenuLabel(option, at, t, locale), t('tours.stages.noDay')),
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
const look = day
|
||||
@@ -86,7 +98,9 @@ export function StageDayPicker(props: StageDayPickerProps) {
|
||||
<span className={day ? 'font-geist tabular-nums' : undefined}>
|
||||
{day ? t('dayplan.dayN', { n: index + 1 }) : t('tours.stages.noDay')}
|
||||
</span>
|
||||
{interactive && <ChevronDown size={11} strokeWidth={2.2} aria-hidden="true" className="flex-none text-content-faint" />}
|
||||
{interactive && (
|
||||
<ChevronDown size={11} strokeWidth={2.2} aria-hidden="true" className="flex-none text-content-faint" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ContextMenu menu={interactive ? menu.menu : null} onClose={menu.close} />
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { paddleSpeedKmhSchema, recommendedPaddleSpeedKmh, type TourListItem, type TourWaypoint } from '@trek/shared';
|
||||
import { parseRenderableRouteGeometry } from '../../../utils/routeGeometry';
|
||||
import { openedTourFields } from '../planner/tourPlannerFacts';
|
||||
import { normalizePlannerWaypoints, plannerWaypointsOf, type TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
|
||||
import {
|
||||
normalizePlannerWaypoints,
|
||||
plannerWaypointsOf,
|
||||
type TourPlannerWaypoint,
|
||||
} from '../planner/tourPlannerWaypoints';
|
||||
import { legLengthsAlong, tourStagesOf, type PaddleStageInputs, type TourStage } from './tourStages';
|
||||
|
||||
/** MDT-02: a stage of a saved tour, with the key of the stop it starts at, by which a day holds it. */
|
||||
@@ -21,7 +25,11 @@ export function savedStagePoints(tour: TourListItem, saved: readonly TourWaypoin
|
||||
* the paddler chose, else the boat's suggestion, and each leg as long as the
|
||||
* saved line runs it. Null for any other type.
|
||||
*/
|
||||
function paddleInputsOf(tour: TourListItem, points: readonly TourPlannerWaypoint[], line: ReadonlyArray<readonly number[]>): PaddleStageInputs | null {
|
||||
function paddleInputsOf(
|
||||
tour: TourListItem,
|
||||
points: readonly TourPlannerWaypoint[],
|
||||
line: ReadonlyArray<readonly number[]>
|
||||
): PaddleStageInputs | null {
|
||||
if (tour.tour_type !== 'kayak') return null;
|
||||
const chosen = tour.paddle_speed_kmh ?? null;
|
||||
return {
|
||||
|
||||
@@ -132,9 +132,7 @@ describe('tourStages', () => {
|
||||
|
||||
it('FE-TOUR-STAGE-MODEL-013: a run stage is its distance at the pace, a ride on horseback has no time (RUN-01, HOR-01)', () => {
|
||||
const run = tourStagesOf(input(withEnds('b'), { tourType: 'run', runPaceSecondsPerKm: 360 }));
|
||||
expect(run.map((stage) => stage.minutes)).toEqual(
|
||||
run.map((stage) => runMinutesAtPace(stage.distanceKm ?? 0, 360))
|
||||
);
|
||||
expect(run.map((stage) => stage.minutes)).toEqual(run.map((stage) => runMinutesAtPace(stage.distanceKm ?? 0, 360)));
|
||||
// The router's walking time never stands in, neither without a pace nor with one out of bounds.
|
||||
const noPace = tourStagesOf(input(withEnds('b'), { tourType: 'run' }));
|
||||
expect(noPace.map((stage) => stage.minutes)).toEqual([null, null]);
|
||||
|
||||
@@ -81,7 +81,12 @@ function line(color: string, coordinates: [number, number][]): TrackData['featur
|
||||
|
||||
/** A line of the planner, [lat, lng] each, as the terrain's [lng, lat]; null for one too short to draw. */
|
||||
function plannerLine(color: string, route: [number, number][]): TrackData['features'][number] | null {
|
||||
return route.length > 1 ? line(color, route.map(([lat, lng]) => [lng, lat] as [number, number])) : null;
|
||||
return route.length > 1
|
||||
? line(
|
||||
color,
|
||||
route.map(([lat, lng]) => [lng, lat] as [number, number])
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
/** A line laid over the track in its own colour, [lat, lng] each. */
|
||||
|
||||
@@ -2,8 +2,8 @@ import { CableCar, ExternalLink, Info, ZoomIn } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { SoftPill } from '../../Planner/planParts';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { LABEL, PopoverListPanel } from '../../shared/dialogParts';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { usePopoverList } from '../../shared/usePopoverList';
|
||||
import { TourNotice } from '../tourParts';
|
||||
@@ -63,7 +63,8 @@ function RunBand({ colour }: { colour: string }) {
|
||||
|
||||
/** A trail as a line in its colour, or as a row of dots where the map dots it. */
|
||||
function WayLine({ colour, dotted = false }: { colour: string; dotted?: boolean }) {
|
||||
if (!dotted) return <span aria-hidden className="h-[3px] w-6 flex-none rounded-full" style={{ background: colour }} />;
|
||||
if (!dotted)
|
||||
return <span aria-hidden className="h-[3px] w-6 flex-none rounded-full" style={{ background: colour }} />;
|
||||
return (
|
||||
<span aria-hidden className="flex w-6 flex-none items-center justify-between px-px">
|
||||
{[0, 1, 2].map((dot) => (
|
||||
@@ -92,7 +93,7 @@ function PisteKeyBody({ belowMinZoom }: { belowMinZoom: boolean }) {
|
||||
</div>
|
||||
{belowMinZoom && (
|
||||
<p
|
||||
className="flex items-center gap-1.5 rounded-lg bg-accent-soft px-2 py-1.5 font-medium text-accent"
|
||||
className="bg-accent-soft flex items-center gap-1.5 rounded-lg px-2 py-1.5 font-medium text-accent"
|
||||
style={fs(11)}
|
||||
data-testid="piste-key-zoom-in"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user