feat(tours): mark the places along the tour that are a stop of it already

This commit is contained in:
Maurice
2026-10-21 20:33:12 +02:00
parent 1797e1c70d
commit 464d4e006c
3 changed files with 50 additions and 11 deletions
@@ -1,6 +1,7 @@
import type { TourPoi, WalkTheme } from '@trek/shared';
import { TOUR_POI_CORRIDORS_M } from '@trek/shared';
import {
Check,
ChevronDown,
Coffee,
Landmark,
@@ -22,13 +23,14 @@ import { useTourPoiStore } from '../../../store/tourPoiStore';
import { formatDistance } from '../../../utils/units';
import { TOUR_POI_LOOKS } from '../../Map/poiCategories';
import { PopoverItem } from '../../Packing/PackingPopover';
import { WhiteBadge } from '../../Planner/planParts';
import { SoftPill, WhiteBadge } from '../../Planner/planParts';
import { PopoverListPanel, Segmented } from '../../shared/dialogParts';
import { fs } from '../../shared/DialogShell';
import { usePopoverList } from '../../shared/usePopoverList';
import type { TourPlannerController } from '../planner/useTourPlanner';
import { RailCard, TourNotice } from '../tourParts';
import { formatPlannedTourDuration } from '../tourPresentation';
import { placesInTour } from './poiInsertion';
import { SearchNotices } from './TourPoiSearchNotices';
import { usePoiInsertion, type InsertionOutcome } from './usePoiInsertion';
import { useSelectedPoi, useTourPoiSearch, type TourPoiSearch } from './useTourPoiSearch';
@@ -155,7 +157,7 @@ function ThemeGrid({
* and how far it lies from the route. A click opens its card over the map,
* where the rest is said and the place is taken in as a stop.
*/
function PoiRow({ poi, selected, unit }: { poi: TourPoi; selected: boolean; unit: Unit }) {
function PoiRow({ poi, selected, inTour, unit }: { poi: TourPoi; selected: boolean; inTour: boolean; unit: Unit }) {
const { t } = useTranslation();
const select = useTourPoiStore((state) => state.select);
const look = TOUR_POI_LOOKS[poi.category];
@@ -191,16 +193,25 @@ function PoiRow({ poi, selected, unit }: { poi: TourPoi; selected: boolean; unit
<Star size={12} strokeWidth={2.2} fill="currentColor" aria-hidden="true" />
</span>
)}
<span className="min-w-0 flex-1 truncate font-medium text-content" style={fs(12, 'body')}>
<span
className={`min-w-0 flex-1 truncate font-medium ${inTour ? 'text-content-muted' : 'text-content'}`}
style={fs(12, 'body')}
>
{poi.name || t(look.labelKey)}
</span>
<span
className="flex-none tabular-nums text-content-faint"
style={fs(11, 'body')}
title={t('tours.poi.fromRoute', { distance })}
>
{distance}
</span>
{inTour ? (
<SoftPill icon={<Check size={10} strokeWidth={2.4} className="flex-none" />}>
{t('tours.placePicker.inTour')}
</SoftPill>
) : (
<span
className="flex-none tabular-nums text-content-faint"
style={fs(11, 'body')}
title={t('tours.poi.fromRoute', { distance })}
>
{distance}
</span>
)}
</button>
</li>
);
@@ -288,6 +299,7 @@ export function TourPoiSearchCard({ planner }: { planner: TourPlannerController
const insertion = usePoiInsertion(planner);
const selected = useSelectedPoi();
const pois = search.answer?.pois ?? [];
const inTour = placesInTour(pois, planner.waypoints);
if (!planner.route || planner.route.length < 2) return null;
return (
<RailCard
@@ -360,7 +372,13 @@ export function TourPoiSearchCard({ planner }: { planner: TourPlannerController
data-testid="tour-poi-list"
>
{pois.map((poi) => (
<PoiRow key={poi.id} poi={poi} selected={selected?.id === poi.id} unit={unit} />
<PoiRow
key={poi.id}
poi={poi}
selected={selected?.id === poi.id}
inTour={inTour.has(poi.id)}
unit={unit}
/>
))}
</ul>
)}
@@ -6,6 +6,7 @@ import {
insertionBlock,
insertionPreview,
insertionSlots,
placesInTour,
straightDetourM,
withPointAfter,
} from './poiInsertion';
@@ -156,4 +157,13 @@ describe('the slots a found place can go into (PLN-04)', () => {
expect(chosenAfterId([A, B, C], null, place, { poiId: 'node:1', afterId: 'gone' })).toBe('b');
expect(chosenAfterId([A, B, C], null, place, null)).toBe('b');
});
it('FE-TOUR-POI-INSERT-010: tells the places that are a point of the tour already by their spot', () => {
const places = [
{ id: 'node:1', lat: B.lat, lng: B.lng },
{ id: 'node:2', lat: B.lat + 0.001, lng: B.lng },
];
expect([...placesInTour(places, [A, B, C])]).toEqual(['node:1']);
expect(placesInTour(places, [A, C]).size).toBe(0);
});
});
@@ -125,3 +125,14 @@ export function straightDetourM(line: ReadonlyArray<[number, number]>): number {
const replaced = line.length > 2 && first && last ? legs([first, last]) : 0;
return Math.max(0, legs(line) - replaced);
}
/** The places of a search that are a point of the tour already, told by where they lie: a taken place keeps its spot. */
export function placesInTour(
places: ReadonlyArray<{ id: string; lat: number; lng: number }>,
points: readonly RoutePoint[]
): Set<string> {
const spots = new Set(points.map((point) => `${point.lat.toFixed(6)},${point.lng.toFixed(6)}`));
return new Set(
places.filter((place) => spots.has(`${place.lat.toFixed(6)},${place.lng.toFixed(6)}`)).map((place) => place.id)
);
}