diff --git a/client/src/components/Tours/bike/bikePaint.ts b/client/src/components/Tours/bike/bikePaint.ts index 71c4ee692..d017a3666 100644 --- a/client/src/components/Tours/bike/bikePaint.ts +++ b/client/src/components/Tours/bike/bikePaint.ts @@ -1,5 +1,5 @@ import { useMapPaint } from '../../Map/mapPaint'; -import type { SacClass, SurfaceClass, TrailClass } from './routeWays'; +import type { SurfaceClass, TrailClass } from './routeWays'; /** * The colours of a bike tour's or a run's surfaces and grades (styles/bike.css), @@ -30,25 +30,6 @@ export function trailPaintKey(key: TrailClass): BikePaintKey { return key === 'none' || key === 'unasked' ? 'unknown' : key; } -/** - * RUN-02: the paint of a hiking grade, on the same scale from easy to hard as - * the trail grades (T1 as S0, T5 and up as S4), so a map never shows two - * colour scales for one idea; a way without a grade in the grey of the unknown. - */ -const SAC_PAINT: Record = { - t1: 's0', - t2: 's1', - t3: 's2', - t4: 's3', - t5: 's4', - t6: 's4', - none: 'unknown', -}; - -export function sacPaintKey(key: SacClass): BikePaintKey { - return SAC_PAINT[key]; -} - /** The paint of a surface class. */ export function surfacePaintKey(key: SurfaceClass): BikePaintKey { return key; diff --git a/client/src/components/Tours/bike/routeWays.test.ts b/client/src/components/Tours/bike/routeWays.test.ts index e33df96d0..9b3436ff9 100644 --- a/client/src/components/Tours/bike/routeWays.test.ts +++ b/client/src/components/Tours/bike/routeWays.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { encodePolyline } from '../../../../tests/helpers/polyline'; -import { bikePaintCss, sacPaintKey, surfacePaintKey, trailPaintKey } from './bikePaint'; +import { bikePaintCss, surfacePaintKey, trailPaintKey } from './bikePaint'; import { withRouteHighlight } from './routeHighlight'; import { routeWaysFrom, @@ -154,12 +154,6 @@ describe('the paints and the highlight (BIK-01 to BIK-05)', () => { expect(bikePaintCss('s1')).toBe('var(--bike-trail-s1)'); }); - it('FE-TOUR-WAYS-012: a hiking grade takes the paint of the trail grade it matches, T5 and up the hardest (RUN-02)', () => { - const grades = ['t1', 't2', 't3', 't4', 't5', 't6'] as const; - expect(grades.map(sacPaintKey)).toEqual(['s0', 's1', 's2', 's3', 's4', 's4']); - expect(sacPaintKey('none')).toBe('unknown'); - }); - const paint = { gravel: '#a0522d', way: '#111', casing: '#fff' } as Parameters[4]; it('FE-TOUR-WAYS-009: lays the highlighted stretches over the route in their paint, solid', () => { diff --git a/client/src/components/Tours/hike/HikeWaysCard.test.tsx b/client/src/components/Tours/hike/HikeWaysCard.test.tsx index c1351ec7d..fc178d4a9 100644 --- a/client/src/components/Tours/hike/HikeWaysCard.test.tsx +++ b/client/src/components/Tours/hike/HikeWaysCard.test.tsx @@ -12,7 +12,7 @@ vi.mock('./useHikeWays', () => ({ }, })); -// FE-HIKE-WAYSCARD-001 to FE-HIKE-WAYSCARD-012 +// FE-HIKE-WAYSCARD-001 to FE-HIKE-WAYSCARD-013 const LINE: [number, number][] = [ [47.0, 11.0], @@ -52,6 +52,7 @@ function ways(overrides: Partial = {}): HikeWays { focus: null, focusOn: vi.fn(), retry: vi.fn(), + routeWays: null, ...overrides, }; } @@ -73,7 +74,7 @@ describe('HikeWaysCard (MAP-03, MAP-04)', () => { it('FE-HIKE-WAYSCARD-001: without a line to describe there is no card', () => { show(ways({ available: false, shown: false, status: 'off', rows: [] })); expect(screen.queryByTestId('hike-ways-card')).toBeNull(); - expect(hook.input).toEqual({ tripId: 4, route: LINE, current: true }); + expect(hook.input).toEqual({ tripId: 4, route: LINE, current: true, matching: 'foot' }); }); it('FE-HIKE-WAYSCARD-002: switched off it only offers the switch, and the switch turns it on', () => { @@ -202,4 +203,20 @@ describe('HikeWaysCard (MAP-03, MAP-04)', () => { expect(screen.getByTestId('sac-row-unmatched')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /try again/i })).toBeNull(); }); + + it('FE-HIKE-WAYSCARD-013: a run asks for its ways matched as a run and puts its own badges in the head (RUN-02)', () => { + hook.ways = ways(); + render( + {state.facet}} + /> + ); + expect(hook.input).toMatchObject({ tripId: 4, route: LINE, matching: 'run' }); + expect(screen.getByTestId('run-aside')).toHaveTextContent('sac'); + expect(screen.queryByTestId('sac-routing-limit')).toBeNull(); + }); }); diff --git a/client/src/components/Tours/hike/HikeWaysCard.tsx b/client/src/components/Tours/hike/HikeWaysCard.tsx index 5d557d577..8d6a7ef6d 100644 --- a/client/src/components/Tours/hike/HikeWaysCard.tsx +++ b/client/src/components/Tours/hike/HikeWaysCard.tsx @@ -1,4 +1,5 @@ import { Footprints, Layers, MountainSnow, Signpost } from 'lucide-react'; +import type { ReactNode } from 'react'; import { useTranslation } from '../../../i18n'; import ToggleSwitch from '../../Settings/ToggleSwitch'; import { Segmented, type SegmentedOption } from '../../shared/dialogParts'; @@ -7,7 +8,7 @@ import { ReadStatusNotice } from '../ReadStatusNotice'; import { RailCard } from '../tourParts'; import type { HikeFacet } from './hikeFacetSpecs'; import { HikeWaysList } from './HikeWaysList'; -import { useHikeWays } from './useHikeWays'; +import { useHikeWays, type HikeWays } from './useHikeWays'; const ICON = { size: 13, strokeWidth: 2 } as const; @@ -18,13 +19,16 @@ const ICON = { size: 13, strokeWidth: 2 } as const; * surface or the kind of way, three things kept apart, each class with its * share; the unknown stays unknown and says why. The routing limit, where the * hike was planned with one, sits apart as the setting it is. Nothing here - * changes the tour. + * changes the tour. A run (RUN-02) shows its ways in this same card, matched + * as a run, with what a runner reads first in the card's head. */ export function HikeWaysCard({ tripId, route, routeCurrent = true, routingLimit, + matching = 'foot', + aside, }: { tripId: number | string; /** The tour's own line, or null while there is none. */ @@ -33,9 +37,13 @@ export function HikeWaysCard({ routeCurrent?: boolean; /** The routing limit the hike is planned with, or null where it was not routed (an import). */ routingLimit: number | null; + /** How the line is matched to the ways: on foot for a hike, as a run for a run. */ + matching?: 'foot' | 'run'; + /** What the card's head shows beside its title, from what has been read. */ + aside?: (ways: HikeWays) => ReactNode; }) { const { t } = useTranslation(); - const ways = useHikeWays({ tripId, route, current: routeCurrent }); + const ways = useHikeWays({ tripId, route, current: routeCurrent, matching }); if (!ways.available) return null; const facets: SegmentedOption[] = [ { value: 'sac', label: t('tours.hike.ways.facet.sac'), icon: }, @@ -43,7 +51,12 @@ export function HikeWaysCard({ { value: 'way', label: t('tours.hike.ways.facet.way'), icon: }, ]; return ( - } title={t('tours.hike.ways.title')} foldId="hike-ways"> + } + title={t('tours.hike.ways.title')} + foldId={matching === 'run' ? 'run-ways' : 'hike-ways'} + aside={aside?.(ways)} + >