From 5d265c98de7f8daa0862a19a1a33ec260df3fb42 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 20 Oct 2026 09:47:24 +0200 Subject: [PATCH] feat(tours): places along the tour picked on the map, stops that keep their kind, and calmer station rows Along the tour (PLN-02, PLN-03): the list shows one line per place (its icon, name and distance from the route) and scrolls past ten. A place picked in the list or on the map opens its card over the map, where the other cards of the planner sit, with what the source says and the way to take it in as a stop. What taking it in did is one line in the list. The intro, the theme line and the out-of-date note are gone: the themes say what they look for on hover, and the search button says when to search again. A stop taken in from there keeps the kind of place it was found as (migration 045400, poi_category, read back only where the contract knows the kind) and its number carries that kind's icon at the corner, as a via ferrata's end does. City walks (CW-02, CW-03, CW-10): a stay nobody entered says nothing. While no stay is entered at all the walk is planned as its walking time, with no "unknown" and no "at least". A must-see rings its number, an optional stop has a dashed ring, a protected one a shield at the corner; only the stay stays under the name, below the coordinates. --- .../src/components/Tours/TourDayRowFacts.tsx | 3 +- .../Tours/planner/StationDialog.test.tsx | 64 +++-- .../Tours/planner/TourWaypointList.test.tsx | 22 +- .../Tours/planner/TourWaypointList.tsx | 70 ++++- .../components/Tours/planner/stationMarks.ts | 27 ++ .../components/Tours/planner/stationModel.ts | 18 +- .../components/Tours/planner/stationParts.tsx | 42 ++- .../Tours/planner/tourPlannerWaypoints.ts | 11 +- .../Tours/planner/tourRouteActions.ts | 5 +- .../src/components/Tours/poi/TourPoiCard.tsx | 237 +++++++++++++++++ .../Tours/poi/TourPoiSearchCard.tsx | 242 +++++------------- .../Tours/poi/tourPoiSearch.test.tsx | 75 +++++- .../components/Tours/poi/usePoiInsertion.ts | 23 +- .../components/Tours/poi/useTourPoiSearch.ts | 4 +- .../components/Tours/walk/WalkStations.tsx | 8 +- .../Tours/walk/walkTimeFields.test.ts | 14 +- .../components/Tours/walk/walkTimeFields.ts | 19 +- .../pages/tripPlanner/TourPlannerMapStage.tsx | 8 + client/src/store/tourPoiStore.ts | 30 ++- .../src/db/entities/TourWaypoints.entity.ts | 2 + server/src/db/kysely/TourWaypoints.table.ts | 1 + ...mbers_the_kind_of_place_it_was_found_as.ts | 16 ++ .../repositories/TourWaypoints.repository.ts | 14 +- server/tests/unit/db/legacy-baseline.test.ts | 1 + .../TourWaypoints.repository.test.ts | 24 ++ shared/src/i18n/ar/tours.ts | 4 - shared/src/i18n/az/tours.ts | 4 - shared/src/i18n/br/tours.ts | 4 - shared/src/i18n/ca/tours.ts | 4 - shared/src/i18n/cs/tours.ts | 4 - shared/src/i18n/de/tours.ts | 4 - shared/src/i18n/en/tours.ts | 4 - shared/src/i18n/es/tours.ts | 4 - shared/src/i18n/et/tours.ts | 4 - shared/src/i18n/fr/tours.ts | 4 - shared/src/i18n/gr/tours.ts | 4 - shared/src/i18n/hu/tours.ts | 4 - shared/src/i18n/id/tours.ts | 4 - shared/src/i18n/it/tours.ts | 4 - shared/src/i18n/ja/tours.ts | 4 - shared/src/i18n/ko/tours.ts | 4 - shared/src/i18n/nl/tours.ts | 4 - shared/src/i18n/pl/tours.ts | 4 - shared/src/i18n/ru/tours.ts | 4 - shared/src/i18n/sk/tours.ts | 4 - shared/src/i18n/sv/tours.ts | 4 - shared/src/i18n/th/tours.ts | 4 - shared/src/i18n/tr/tours.ts | 4 - shared/src/i18n/uk/tours.ts | 4 - shared/src/i18n/vi/tours.ts | 4 - shared/src/i18n/zh-TW/tours.ts | 4 - shared/src/i18n/zh/tours.ts | 4 - shared/src/tours/tour-poi-category.ts | 24 ++ shared/src/tours/tour-poi-search.schema.ts | 26 +- shared/src/tours/tours.schema.ts | 7 + 55 files changed, 715 insertions(+), 430 deletions(-) create mode 100644 client/src/components/Tours/planner/stationMarks.ts create mode 100644 client/src/components/Tours/poi/TourPoiCard.tsx create mode 100644 server/src/db/migrations/Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as.ts create mode 100644 shared/src/tours/tour-poi-category.ts diff --git a/client/src/components/Tours/TourDayRowFacts.tsx b/client/src/components/Tours/TourDayRowFacts.tsx index c765b9db3..d19983658 100644 --- a/client/src/components/Tours/TourDayRowFacts.tsx +++ b/client/src/components/Tours/TourDayRowFacts.tsx @@ -167,13 +167,14 @@ export function TourDayRowPills({ tour, compact = false }: { tour: TourListItem; const times = tourPlannedTimes(tour); const walking = times.walkingMinutes == null ? null : formatPlannedTourDuration(times.walkingMinutes); const total = plannedTotalText(times, t); + const staysText = times.walk ? walkStaysText(times.walk, t) : null; const timeParts = [ walking ? `${movingTimeLabel(tour, t('tours.planner.inspector.duration'), t)}: ${walking}` : null, times.breakMinutes ? `${t('tours.planner.breaksAdditional')}: ${formatPlannedTourDuration(times.breakMinutes)}` : null, // CW-02: a city walk's stays, which its total adds to the walking time. - times.walk ? `${t('tours.walk.stays')}: ${walkStaysText(times.walk, t)}` : null, + staysText ? `${t('tours.walk.stays')}: ${staysText}` : null, total ? `${t('tours.planner.plannedTotalDuration')}: ${total}${times.manuallyOverridden ? ` (${t('tours.planner.plannedTotalManual')})` : ''}` : null, diff --git a/client/src/components/Tours/planner/StationDialog.test.tsx b/client/src/components/Tours/planner/StationDialog.test.tsx index a51b617b1..24bde2979 100644 --- a/client/src/components/Tours/planner/StationDialog.test.tsx +++ b/client/src/components/Tours/planner/StationDialog.test.tsx @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; import { fireEvent, render, screen } from '../../../../tests/helpers/render'; import { StationDialog } from './StationDialog'; -import { StationPills } from './stationParts'; +import { stationMarkNote, stationRingClass } from './stationMarks'; +import { StationPills, StationShield } from './stationParts'; import type { TourPlannerWaypoint } from './tourPlannerWaypoints'; const cathedral: TourPlannerWaypoint = { @@ -126,32 +127,53 @@ describe('StationDialog (CW-02, CW-03, CW-10)', () => { }); }); -describe('StationPills (CW-02, CW-03, CW-10)', () => { - const pills = (point: Partial) => { +describe('StationPills (CW-02)', () => { + const pills = (point: Partial) => render(); - return screen.getByTestId('station-pills'); - }; - it('says a stay nobody entered is unknown, and nothing of a normal, unprotected station', () => { - const row = pills({}); - expect(row).toHaveTextContent('Stay unknown'); - expect(row).not.toHaveTextContent('Must see'); - expect(row).not.toHaveTextContent('Optional'); - expect(screen.queryByLabelText('Protected')).not.toBeInTheDocument(); + it('says nothing of a stay nobody entered', () => { + pills({ importance: 'must', protected: true }); + expect(screen.queryByTestId('station-pills')).not.toBeInTheDocument(); }); - it('names a point passed through', () => { - expect(pills({ stayMinutes: 0 })).toHaveTextContent('Passing through'); + it('names a point passed through, and a stay entered as its time alone', () => { + pills({ stayMinutes: 0 }); + expect(screen.getByTestId('station-pills')).toHaveTextContent('Passing through'); }); - it('shows the stay, a must-see and a protection', () => { - const row = pills({ stayMinutes: 45, importance: 'must', protected: true }); + it('shows the stay and nothing of the importance or the protection, which are on the number', () => { + pills({ stayMinutes: 45, importance: 'must', protected: true }); + const row = screen.getByTestId('station-pills'); expect(row).toHaveTextContent('45'); - expect(row).toHaveTextContent('Must see'); - expect(screen.getByLabelText('Protected')).toBeInTheDocument(); - }); - - it('shows an optional station as optional', () => { - expect(pills({ stayMinutes: 90, importance: 'optional' })).toHaveTextContent('Optional'); + expect(row).not.toHaveTextContent('Must see'); + expect(row).not.toHaveTextContent('Protected'); + }); +}); + +describe("a station's marks on its number (CW-03, CW-10)", () => { + const t = (key: string) => + ({ + 'tours.walk.importance.must': 'Must see', + 'tours.walk.importance.optional': 'Optional', + 'tours.walk.station.protected': 'Protected', + })[key] ?? key; + + it('rings a must-see in full, an optional stop dashed, and a normal one not at all', () => { + expect(stationRingClass('must')).toContain('outline-warning'); + expect(stationRingClass('optional')).toContain('outline-dashed'); + expect(stationRingClass(null)).toBe(''); + expect(stationRingClass(undefined)).toBe(''); + }); + + it('names the importance and the protection for the label, and nothing for a plain station', () => { + expect(stationMarkNote({ importance: 'must', protected: true }, t)).toBe('Must see, Protected'); + expect(stationMarkNote({ importance: 'optional' }, t)).toBe('Optional'); + expect(stationMarkNote({ protected: true }, t)).toBe('Protected'); + expect(stationMarkNote({}, t)).toBeNull(); + }); + + it('draws a protection as a shield at the corner of the number', () => { + const { container } = render(); + expect(container.querySelector('[data-station-protected="true"]')).toBeInTheDocument(); }); }); diff --git a/client/src/components/Tours/planner/TourWaypointList.test.tsx b/client/src/components/Tours/planner/TourWaypointList.test.tsx index 5d4c7bc85..a7b279266 100644 --- a/client/src/components/Tours/planner/TourWaypointList.test.tsx +++ b/client/src/components/Tours/planner/TourWaypointList.test.tsx @@ -192,8 +192,13 @@ describe('TourWaypointList', () => { render(); const rows = screen.getAllByTestId('tour-planner-waypoint-row'); expect(within(at(rows, 2)).getByTestId('tour-planner-waypoint-name')).toHaveTextContent('Cathedral'); - expect(within(at(rows, 2)).getByTestId('station-pills')).toHaveTextContent('Must see'); - expect(within(at(rows, 0)).getByTestId('station-pills')).toHaveTextContent('Stay unknown'); + // The stay goes under the name; a must-see rings the number instead of a pill beside it. + expect(within(at(rows, 2)).getByTestId('station-pills')).toHaveTextContent('30 min'); + expect(within(at(rows, 2)).getByTestId('station-pills')).not.toHaveTextContent('Must see'); + const mustSee = within(at(rows, 2)).getByLabelText('Waypoint 2, Must see'); + expect(mustSee.className).toContain('outline-warning'); + // A stop without a stay entered says nothing of it, and a shaping point is no stop. + expect(within(at(rows, 0)).queryByTestId('station-pills')).not.toBeInTheDocument(); expect(within(at(rows, 1)).queryByTestId('station-pills')).not.toBeInTheDocument(); await userEvent.click(within(at(rows, 2)).getByRole('button', { name: 'More actions' })); @@ -220,12 +225,23 @@ describe('TourWaypointList', () => { expect(screen.queryByRole('button', { name: 'Edit stop' })).not.toBeInTheDocument(); }); + it('FE-TOUR-WAYPOINT-LIST-009: a stop taken in along the tour carries its kind of place at the corner of its number (PLN-03)', () => { + const controller = planner({ waypoints: [START, { ...STOP, poiCategory: 'restaurant' as const }, END] }); + render(); + const rows = screen.getAllByTestId('tour-planner-waypoint-row'); + const number = within(at(rows, 1)).getByLabelText('Waypoint 2, Places to eat'); + expect(number.querySelector('[data-waypoint-poi="restaurant"]')).toBeInTheDocument(); + // A point set by hand has no kind to show. + expect(within(at(rows, 0)).queryByLabelText(/Places to eat/)).not.toBeInTheDocument(); + expect(document.querySelectorAll('[data-waypoint-poi]')).toHaveLength(1); + }); + it('FE-TOUR-WAYPOINT-LIST-008: the way back of a loop walk is no stop: no pills and no stop to edit', async () => { const back = { ...START, id: 'back', role: 'end' as const }; const controller = planner({ tourType: 'walk', isLoop: true, - waypoints: [START, STOP, back], + waypoints: [{ ...START, stayMinutes: 10 }, STOP, back], selectedWaypointId: 'back', }); render(); diff --git a/client/src/components/Tours/planner/TourWaypointList.tsx b/client/src/components/Tours/planner/TourWaypointList.tsx index da3e18aa0..8fbce7de3 100644 --- a/client/src/components/Tours/planner/TourWaypointList.tsx +++ b/client/src/components/Tours/planner/TourWaypointList.tsx @@ -1,4 +1,4 @@ -import { paddleLayoutOfPoints, type PaddleRole } from '@trek/shared'; +import { paddleLayoutOfPoints, type PaddleRole, type TourPoiCategory } from '@trek/shared'; import { ArrowDown, ArrowDownUp, @@ -26,6 +26,7 @@ import { } from '../../../store/tourFerrataCrossingStore'; import { toggleTourPlacing, useTourPlaceToolStore } from '../../../store/tourPlaceToolStore'; import { FerrataGlyph } from '../../Ferratas/FerrataGlyph'; +import { TOUR_POI_LOOKS } from '../../Map/poiCategories'; import { SoftPill } from '../../Planner/planParts'; import { ContextMenu, useContextMenu } from '../../shared/ContextMenu'; import { fs } from '../../shared/DialogShell'; @@ -42,7 +43,8 @@ import { usePlannerWalkSchedule } from '../walk/useWalkSchedule'; import { WalkDayLine } from '../walk/WalkDayLine'; import { paddlePlanPoints, paddleRolesById } from './paddlePlanOf'; import { StationDialog } from './StationDialog'; -import { StationPills } from './stationParts'; +import { stationMarkNote, stationRingClass } from './stationMarks'; +import { StationPills, StationShield } from './stationParts'; import { TripPlacePicker } from './TripPlacePicker'; import type { TourPlannerController } from './useTourPlanner'; import { useTripPlaceWaypoints } from './useTripPlaceWaypoints'; @@ -147,15 +149,57 @@ function WaypointEmptyState() { ); } +/** PLN-03: the kind of place a point was found as, its icon at the number's corner in its colour on the map. */ +function PoiCornerIcon({ category }: { category: TourPoiCategory }) { + const look = TOUR_POI_LOOKS[category]; + return ( + + ); +} + +/** A city walk station's own marks on its number (CW-03, CW-10); none on any other point. */ +type StationMarks = Pick | null; + +/** + * What a waypoint's number says besides the number: the via ferrata it stands + * at an end of, or else the kind of place it was found as along the tour (a + * ferrata's end says more about the point, so it wins), and a station's + * importance and protection. + */ +function numberNote( + ferrata: TourWaypointFerrata | null, + poiCategory: TourPoiCategory | null, + station: StationMarks, + t: Translate +): { note: string | null; poi: TourPoiCategory | null } { + const marks = station ? stationMarkNote(station, t) : null; + const join = (first: string | null) => [first, marks].filter(Boolean).join(', ') || null; + if (ferrata) return { note: join(t('tours.ferrata.waypointAtEnd', { name: ferrata.name })), poi: null }; + if (poiCategory) return { note: join(t(TOUR_POI_LOOKS[poiCategory].labelKey)), poi: poiCategory }; + return { note: join(null), poi: null }; +} + /** * A waypoint's number, as the map pins show it. At the foot or the top of a via * ferrata it takes a violet ring and the ferrata's ladder at its corner, named - * by the tooltip. + * by the tooltip. A point taken in from the places along the tour (PLN-03) + * carries that kind's icon at the same corner, in its colour on the map. A + * city walk's station shows how much it matters as a ring around the number + * and its protection as a shield at the top corner. */ function WaypointNumber({ stop, selected, ferrata, + poiCategory = null, + station = null, loopEnd = false, shaping = false, }: { @@ -163,6 +207,10 @@ function WaypointNumber({ stop: number; selected: boolean; ferrata: TourWaypointFerrata | null; + /** PLN-03: the kind of place the point was found as along the tour. */ + poiCategory?: TourPoiCategory | null; + /** CW-03, CW-10: a station's importance and protection, drawn on the number; only a city walk's points carry them. */ + station?: StationMarks; /** The way back to the start of a loop: shown as the return, not as one more stop. */ loopEnd?: boolean; /** P2A-03: a shaping point, a small dot as on the map. */ @@ -194,19 +242,22 @@ function WaypointNumber({ ); } const label = t('tours.planner.waypointLabel', { n: stop }); + const { note, poi } = numberNote(ferrata, poiCategory, station, t); const number = ( {stop} {ferrata && } + {poi && } + {station?.protected && } ); - if (!ferrata) return number; + if (!ferrata) return note ? {number} : number; const grade = ferrata.grade ? ` (${ferrata.grade})` : ''; return {number}; } @@ -231,7 +282,7 @@ function WaypointRowLabel({ paddleRole?: PaddleRole | null; /** PR1-10: the point stands for this place of the trip, named after it. */ linked?: boolean; - /** CW-01: a city walk's station, which shows its stay, importance and protection. */ + /** CW-01: a city walk's station, which shows its stay (its importance and protection are on its number). */ station?: boolean; }) { const { t } = useTranslation(); @@ -260,7 +311,6 @@ function WaypointRowLabel({ )} {point.auto && } - {station && !loopEnd && } {linked && !loopEnd ? ( // A stop standing for a place of the trip says so where the coordinates would be (PR1-10). @@ -285,6 +335,8 @@ function WaypointRowLabel({ ) )} + {/* Under the coordinates: a station's stay, the one mark of it that is not on its number. */} + {station && !loopEnd && } ); } @@ -727,6 +779,8 @@ export default function TourWaypointList({ stop={stop} selected={selected} ferrata={ferrataEnds[point.id] ?? null} + poiCategory={point.poiCategory ?? null} + station={point} loopEnd={loopEnd} shaping={!!point.shaping} /> diff --git a/client/src/components/Tours/planner/stationMarks.ts b/client/src/components/Tours/planner/stationMarks.ts new file mode 100644 index 000000000..e156cb12c --- /dev/null +++ b/client/src/components/Tours/planner/stationMarks.ts @@ -0,0 +1,27 @@ +import type { useTranslation } from '../../../i18n'; +import type { TourPlannerWaypoint } from './tourPlannerWaypoints'; + +type Translate = ReturnType['t']; + +/** + * CW-03: how much a station matters, as a ring around its number: a full ring + * in the warning colour for a must-see, a dashed muted one for an optional + * stop, none for a normal one. + */ +export function stationRingClass(importance: TourPlannerWaypoint['importance']): string { + if (importance === 'must') return 'outline outline-2 outline-offset-1 outline-warning'; + if (importance === 'optional') return 'outline outline-2 outline-offset-1 outline-dashed outline-content-faint'; + return ''; +} + +/** CW-03, CW-10: what the ring and the shield on a station's number say, for its label and tooltip. */ +export function stationMarkNote( + point: Pick, + t: Translate +): string | null { + const parts = [ + point.importance ? t(`tours.walk.importance.${point.importance}`) : null, + point.protected ? t('tours.walk.station.protected') : null, + ].filter((part): part is string => part !== null); + return parts.length ? parts.join(', ') : null; +} diff --git a/client/src/components/Tours/planner/stationModel.ts b/client/src/components/Tours/planner/stationModel.ts index 81f07c78b..cbc6c6322 100644 --- a/client/src/components/Tours/planner/stationModel.ts +++ b/client/src/components/Tours/planner/stationModel.ts @@ -2,10 +2,11 @@ import { walkStationsOf } from '@trek/shared'; import type { TourPlannerWaypoint } from './tourPlannerWaypoints'; /** - * CW-02: a city walk's time, walking and staying kept apart. A stay nobody - * entered is unknown, never nothing: the total then counts only what is - * known and says it is at least that much. A point passed through is a stay - * of 0, entered on purpose. + * CW-02: a city walk's time, walking and staying kept apart. While no stay is + * entered at all, the walk is planned as its walking time alone and nothing + * calls the stays unknown. Once some are entered, one left empty is unknown, + * never nothing: the total then counts only what is known and says it is at + * least that much. A point passed through is a stay of 0, entered on purpose. */ export interface WalkTimes { walkingMinutes: number | null; @@ -23,16 +24,21 @@ export interface WalkTimes { /** The times of a walk from its parts: the walking time, the stays entered, how many are unknown and of how many. */ export function walkTimesOf(parts: Omit): WalkTimes { - const { walkingMinutes, stayMinutes, unknownStays } = parts; + const { walkingMinutes, stayMinutes, unknownStays, stations } = parts; // Without a walking time the stays entered are still part of the whole: at least that much, never nothing. const totalMinutes = walkingMinutes !== null ? walkingMinutes + stayMinutes : stayMinutes > 0 ? stayMinutes : null; return { ...parts, totalMinutes, - partial: totalMinutes !== null && (unknownStays > 0 || walkingMinutes === null), + partial: totalMinutes !== null && (someStaysUnknown({ unknownStays, stations }) || walkingMinutes === null), }; } +/** Some stays entered and some not: none entered at all means none planned, not unknown. */ +export function someStaysUnknown({ unknownStays, stations }: Pick): boolean { + return unknownStays > 0 && unknownStays < stations; +} + export function walkTimes(points: readonly TourPlannerWaypoint[], walkingMinutes: number | null): WalkTimes { // A shaping point is no station, and the way back of a loop is its start again: its stay counts once. const stations = walkStationsOf(points); diff --git a/client/src/components/Tours/planner/stationParts.tsx b/client/src/components/Tours/planner/stationParts.tsx index 2812435e7..def7be9a2 100644 --- a/client/src/components/Tours/planner/stationParts.tsx +++ b/client/src/components/Tours/planner/stationParts.tsx @@ -1,38 +1,36 @@ import { Clock, ShieldCheck } from 'lucide-react'; import { useTranslation } from '../../../i18n'; import { SoftPill } from '../../Planner/planParts'; -import { Tooltip } from '../../shared/Tooltip'; import { formatPlannedTourDuration } from '../tourPresentation'; import type { TourPlannerWaypoint } from './tourPlannerWaypoints'; /** - * CW-02, CW-03, CW-10: what a city walk's station row says besides its name: - * the stay (or that it is not known), an importance other than normal, and a - * protection, each a quiet pill. + * CW-02: a city walk station's stay, where one is entered, as a quiet pill + * under its name. A stay nobody entered says nothing. How much the station + * matters and whether it is protected are drawn on its number instead + * (`stationRingClass`, `StationShield`). */ -export function StationPills({ point }: { point: TourPlannerWaypoint }) { +export function StationPills({ point }: { point: Pick }) { const { t } = useTranslation(); + if (point.stayMinutes == null) return null; return ( - {point.importance && ( - - {t(`tours.walk.importance.${point.importance}`)} - - )} - {point.protected && ( - - - - - )} + + ); +} + +/** CW-10: a protected station's shield, at the top corner of its number. */ +export function StationShield() { + return ( + ); } diff --git a/client/src/components/Tours/planner/tourPlannerWaypoints.ts b/client/src/components/Tours/planner/tourPlannerWaypoints.ts index 08a6e9be8..8fa9e0387 100644 --- a/client/src/components/Tours/planner/tourPlannerWaypoints.ts +++ b/client/src/components/Tours/planner/tourPlannerWaypoints.ts @@ -3,6 +3,7 @@ import type { PlannableTourType, StationImportance, TourFixedLeg, + TourPoiCategory, TourWaypoint, TourWaypointRole, } from '@trek/shared'; @@ -40,6 +41,8 @@ export interface TourPlannerWaypoint { protected?: boolean; /** CW-04: the station's opening hours as OpenStreetMap writes them. */ openingHours?: string | null; + /** PLN-03: the kind of place the point was taken in from along the tour, whose icon its row carries. */ + poiCategory?: TourPoiCategory | null; } /** A saved tour's waypoints as the editor holds them, each under a fresh id. */ @@ -59,6 +62,7 @@ export function plannerWaypointsOf(saved: readonly TourWaypoint[], newId: () => ...(point.importance ? { importance: point.importance } : {}), ...(point.protected ? { protected: true } : {}), ...(point.opening_hours ? { openingHours: point.opening_hours } : {}), + ...(point.poi_category ? { poiCategory: point.poi_category } : {}), })); } @@ -85,6 +89,7 @@ export function savedWaypointsOf(points: readonly TourPlannerWaypoint[], tourTyp ...(walk && point.importance ? { importance: point.importance } : {}), ...(walk && point.protected ? { protected: true } : {}), ...(point.openingHours ? { opening_hours: point.openingHours } : {}), + ...(point.poiCategory ? { poi_category: point.poiCategory } : {}), })); } @@ -144,8 +149,8 @@ export function routeExtrasKey(points: TourPlannerWaypoint[]): string { /** * CW-02: everything a route is planned from: every point with all it holds but - * a station's own fields (its name, stay, importance, protection and opening - * hours) and the key a save gave it. A change to those alone leaves the route + * a station's own fields (its name, stay, importance, protection, opening + * hours and the kind of place it was found as) and the key a save gave it. A change to those alone leaves the route * as it is; any other change plans it again. */ export function routeInputsKey(points: readonly TourPlannerWaypoint[]): string { @@ -157,6 +162,7 @@ export function routeInputsKey(points: readonly TourPlannerWaypoint[]): string { importance: _importance, protected: _protected, openingHours: _hours, + poiCategory: _kind, key: _key, ...routed }) => routed @@ -173,6 +179,7 @@ function stationFieldsKey(points: TourPlannerWaypoint[]): string { point.importance ?? null, !!point.protected, point.openingHours ?? null, + point.poiCategory ?? null, ]) ); } diff --git a/client/src/components/Tours/planner/tourRouteActions.ts b/client/src/components/Tours/planner/tourRouteActions.ts index c573bd5aa..f99ed54d6 100644 --- a/client/src/components/Tours/planner/tourRouteActions.ts +++ b/client/src/components/Tours/planner/tourRouteActions.ts @@ -30,12 +30,13 @@ interface StationParts { importance?: string | null; protected?: boolean; openingHours?: string | null; + poiCategory?: string | null; } /** A point split into what makes it a station and the rest of it; a part it does not have is left out. */ function stationSplit(point: T & StationParts): { station: StationParts; rest: T } { - const { key, name, stayMinutes, importance, protected: kept, openingHours, ...rest } = point; - const parts: StationParts = { key, name, stayMinutes, importance, protected: kept, openingHours }; + const { key, name, stayMinutes, importance, protected: kept, openingHours, poiCategory, ...rest } = point; + const parts: StationParts = { key, name, stayMinutes, importance, protected: kept, openingHours, poiCategory }; const station = Object.fromEntries(Object.entries(parts).filter(([, value]) => value !== undefined)); return { station, rest: rest as T }; } diff --git a/client/src/components/Tours/poi/TourPoiCard.tsx b/client/src/components/Tours/poi/TourPoiCard.tsx new file mode 100644 index 000000000..ecee31630 --- /dev/null +++ b/client/src/components/Tours/poi/TourPoiCard.tsx @@ -0,0 +1,237 @@ +import type { TourPoi, WalkTheme } from '@trek/shared'; +import { WALK_THEMES } from '@trek/shared'; +import { Clock, ExternalLink, MoveHorizontal, Plus, Route, X } from 'lucide-react'; +import { useCallback, useRef } from 'react'; +import { useTranslation } from '../../../i18n'; +import { useSettingsStore } from '../../../store/settingsStore'; +import { useTourPoiStore } from '../../../store/tourPoiStore'; +import { formatDistance } from '../../../utils/units'; +import { MAP_CARD_GLASS } from '../../Map/mapControlShadow'; +import { TOUR_POI_LOOKS } from '../../Map/poiCategories'; +import { useMapCardDismiss } from '../../Map/useMapCardDismiss'; +import { WhiteBadge } from '../../Planner/planParts'; +import { PillSelect } from '../../shared/dialogParts'; +import { fs } from '../../shared/DialogShell'; +import type { TourPlannerController } from '../planner/useTourPlanner'; +import { useWaypointPlaceNames } from '../planner/useWaypointPlaceNames'; +import { usePoiInsertion } from './usePoiInsertion'; +import { useSelectedPoi } from './useTourPoiSearch'; + +type Unit = Parameters[1]; + +const km = (metres: number) => metres / 1000; + +/** The point a place would follow, picked from the tour's points, with why one cannot take it. */ +function AfterPicker({ + planner, + poi, + insertion, +}: { + planner: TourPlannerController; + poi: TourPoi; + insertion: ReturnType; +}) { + const { t } = useTranslation(); + const names = useWaypointPlaceNames(planner.waypoints); + const afterId = insertion.afterIdFor(poi); + const options = planner.waypoints.map((point, index) => ({ + value: point.id, + label: `${index + 1}. ${point.shaping ? t('tours.planner.shapingPoint') : (names[point.id] ?? `${point.lat.toFixed(4)}, ${point.lng.toFixed(4)}`)}`, + })); + const block = afterId ? insertion.blockOf(afterId) : 'none'; + const locked = !planner.canEdit || planner.isSaving; + return ( +
+
+ + {t('tours.poi.addAfter')} + +
+ insertion.chooseAfter(poi, value)} + /> +
+
+ {block && ( +

+ {t(`tours.poi.blocked.${block}`)} +

+ )} + +
+ ); +} + +/** What the source says about the place: its address, its opening hours as written, and its website. */ +function PoiFacts({ poi }: { poi: TourPoi }) { + const { t } = useTranslation(); + if (!poi.address && !poi.opening_hours && !poi.website) return null; + return ( +
+ {poi.address && ( +

+ {poi.address} +

+ )} + {poi.opening_hours && ( +

+ + + {poi.opening_hours} + +

+ )} + {poi.website && ( + + + {t('tours.poi.website')} + + )} +
+ ); +} + +/** + * PLN-02/PLN-03: a place found along the tour, picked on the map or in the + * list, in the look of the other cards over the planner map: what it is, how + * far from the route and how far along it, why a city walk's theme suggested + * it, where the data comes from, and the way to take it in as a stop. + */ +function TourPoiCard({ + poi, + theme, + planner, + insertion, + unit, + onClose, + width, +}: { + poi: TourPoi; + theme: WalkTheme | null; + planner: TourPlannerController; + insertion: ReturnType; + unit: Unit; + onClose: () => void; + width?: number; +}) { + const { t } = useTranslation(); + const look = TOUR_POI_LOOKS[poi.category]; + const kind = t(look.labelKey); + const suggested = theme && (WALK_THEMES[theme] as readonly string[]).includes(poi.category) ? theme : null; + return ( +
+
+ {/* The category's own map colour, as its pin on the map has it: a light wash, the icon in full. */} + + + +
+

+ {poi.name || kind} +

+ {poi.name &&

{kind}

} +
+ +
+
+ }> + {t('tours.poi.fromRoute', { distance: formatDistance(km(poi.distance_m), unit) })} + + }> + {t('tours.poi.along', { distance: formatDistance(km(poi.along_m), unit) })} + +
+ + +

+ {suggested && {t('tours.walk.themeReason', { theme: t(`tours.walk.theme.${suggested}`) })}} + {t(poi.source === 'trek-places' ? 'tours.poi.source.trek' : 'tours.poi.source.osm')} +

+
+ ); +} + +/** + * The chosen place's card over the planner map, where a via ferrata's or a + * paddle object's card sits and closing as those do: on a press outside it + * and on Escape, the press handed on so the map leaves its click alone. The + * choice lives in the places store, so a pin on the map and a row in the list + * open the same card. + */ +export default function TourPoiOverlay({ + planner, + onOutsidePress, + insetEnd, + bottom, + width, +}: { + planner: TourPlannerController; + onOutsidePress?: () => void; + insetEnd: number; + bottom: number; + width?: number; +}) { + const ref = useRef(null); + const unit = useSettingsStore((state) => state.settings.distance_unit) ?? 'metric'; + const poi = useSelectedPoi(); + const theme = useTourPoiStore((state) => state.theme); + const select = useTourPoiStore((state) => state.select); + const insertion = usePoiInsertion(planner); + const close = useCallback(() => select(null), [select]); + useMapCardDismiss(ref, !!poi, close, onOutsidePress); + + if (!poi) return null; + return ( +
+ +
+ ); +} diff --git a/client/src/components/Tours/poi/TourPoiSearchCard.tsx b/client/src/components/Tours/poi/TourPoiSearchCard.tsx index 41f18884c..1eeb72e02 100644 --- a/client/src/components/Tours/poi/TourPoiSearchCard.tsx +++ b/client/src/components/Tours/poi/TourPoiSearchCard.tsx @@ -1,21 +1,18 @@ import type { TourPoi, WalkTheme } from '@trek/shared'; -import { TOUR_POI_CORRIDORS_M, WALK_THEMES } from '@trek/shared'; +import { TOUR_POI_CORRIDORS_M } from '@trek/shared'; import { AlertTriangle, ChevronDown, - Clock, Coffee, - ExternalLink, Landmark, Loader2, - MoveHorizontal, - Plus, RefreshCw, Route, Search, Shuffle, Trees, WifiOff, + X, type LucideIcon, } from 'lucide-react'; import { useEffect, useRef } from 'react'; @@ -26,11 +23,10 @@ import { formatDistance } from '../../../utils/units'; import { TOUR_POI_LOOKS } from '../../Map/poiCategories'; import { PopoverItem } from '../../Packing/PackingPopover'; import { WhiteBadge } from '../../Planner/planParts'; -import { PillSelect, PopoverListPanel, Segmented } from '../../shared/dialogParts'; +import { PopoverListPanel, Segmented } from '../../shared/dialogParts'; import { fs } from '../../shared/DialogShell'; import { usePopoverList } from '../../shared/usePopoverList'; import type { TourPlannerController } from '../planner/useTourPlanner'; -import { useWaypointPlaceNames } from '../planner/useWaypointPlaceNames'; import { RailCard, TourNotice } from '../tourParts'; import { formatPlannedTourDuration } from '../tourPresentation'; import { usePoiInsertion, type InsertionOutcome } from './usePoiInsertion'; @@ -139,6 +135,7 @@ function ThemeGrid({ type="button" onClick={() => onChoose(theme)} aria-pressed={on} + title={t(`tours.walk.themeHint.${theme}`)} data-testid={`tour-poi-theme-${theme}`} className={`flex min-w-0 items-center gap-2 rounded-[10px] px-2.5 py-2 text-start font-medium leading-tight transition-colors ${on ? 'bg-surface-card text-content shadow-sm ring-1 ring-inset ring-edge-faint' : 'bg-surface-tertiary text-content-muted hover:text-content'}`} style={fs(11.5, 'body')} @@ -153,13 +150,14 @@ function ThemeGrid({ } /** - * What the search answered beyond its places: out of date, offline, failed, - * kinds that could not be searched, partly searched, cut, or empty. Nothing + * What the search answered beyond its places: offline, failed, kinds that + * could not be searched, partly searched, cut, or empty. That the answer is + * out of date the search button says, as it is what to press then. Nothing * found is said only when every kind was searched. */ function SearchNotices({ search, unit }: { search: TourPoiSearch; unit: Unit }) { const { t } = useTranslation(); - const { status, answer, outdated } = search; + const { status, answer } = search; if (status === 'offline') return }>{t('tours.poi.offline')}; if (status === 'error') @@ -169,7 +167,6 @@ function SearchNotices({ search, unit }: { search: TourPoiSearch; unit: Unit }) ); if (!answer) return null; - // Out of date is no fault, the route was just changed (often by adding a place from here): said plainly. const unavailable = answer.unavailable.map((category) => t(TOUR_POI_LOOKS[category].labelKey)).join(', '); const notes = [ unavailable && t('tours.poi.unavailable', { kinds: unavailable }), @@ -179,7 +176,6 @@ function SearchNotices({ search, unit }: { search: TourPoiSearch; unit: Unit }) ].filter((note): note is string => !!note); return ( <> - {outdated && }>{t('tours.poi.outdated')}} {notes.map((note) => ( }> {note} @@ -194,79 +190,12 @@ function SearchNotices({ search, unit }: { search: TourPoiSearch; unit: Unit }) ); } -/** The point a place would follow, picked from the tour's points, with why one cannot take it. */ -function AfterPicker({ - planner, - poi, - insertion, -}: { - planner: TourPlannerController; - poi: TourPoi; - insertion: ReturnType; -}) { - const { t } = useTranslation(); - const names = useWaypointPlaceNames(planner.waypoints); - const afterId = insertion.afterIdFor(poi); - const options = planner.waypoints.map((point, index) => ({ - value: point.id, - label: `${index + 1}. ${point.shaping ? t('tours.planner.shapingPoint') : (names[point.id] ?? `${point.lat.toFixed(4)}, ${point.lng.toFixed(4)}`)}`, - })); - const block = afterId ? insertion.blockOf(afterId) : 'none'; - const locked = !planner.canEdit || planner.isSaving; - return ( -
-
- - {t('tours.poi.addAfter')} - - insertion.chooseAfter(poi, value)} - /> -
- {block && ( -

- {t(`tours.poi.blocked.${block}`)} -

- )} - -

- {t('tours.poi.notAPlace')} -

-
- ); -} - -/** One place found: its kind, name and where it lies; opened, what the source says and the way to take it in. */ -function PoiRow({ - poi, - selected, - planner, - insertion, - unit, - theme = null, -}: { - poi: TourPoi; - selected: boolean; - planner: TourPlannerController; - insertion: ReturnType; - unit: Unit; - /** CW-05: the theme the place was suggested for, said as its reason. */ - theme?: WalkTheme | null; -}) { +/** + * One place found, in one line: its kind's icon in its map colour, its name, + * 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 }) { const { t } = useTranslation(); const select = useTourPoiStore((state) => state.select); const look = TOUR_POI_LOOKS[poi.category]; @@ -274,77 +203,33 @@ function PoiRow({ useEffect(() => { if (selected) rowRef.current?.scrollIntoView?.({ block: 'nearest' }); }, [selected]); + const distance = formatDistance(km(poi.distance_m), unit); return ( -
  • +
  • - {selected && ( -
    -

    - {t(look.labelKey)} - {theme && {t('tours.walk.themeReason', { theme: t(`tours.walk.theme.${theme}`) })}} - {t(poi.source === 'trek-places' ? 'tours.poi.source.trek' : 'tours.poi.source.osm')} -

    - {poi.address && ( -

    - {poi.address} -

    - )} - {poi.opening_hours && ( -

    - - - {poi.opening_hours} - - {t('tours.poi.hoursHint')} - - -

    - )} - {poi.website && ( - - - {t('tours.poi.website')} - - )} - -
    - )}
  • ); } @@ -364,24 +249,30 @@ function OutcomeNotice({ outcome, onDismiss, unit }: { outcome: InsertionOutcome ) } > - - {t(`tours.poi.outcome.${outcome.kind}`, { name: outcome.name })} - {outcome.kind === 'measured' && ( - - {signed(formatDistance(km(Math.abs(outcome.extraM)), unit), outcome.extraM < 0)} - {outcome.extraMinutes != null && ( + + + {t(`tours.poi.outcome.${outcome.kind}`, { name: outcome.name })} + {outcome.kind === 'measured' && ( + <> - {signed(formatPlannedTourDuration(Math.abs(outcome.extraMinutes)), outcome.extraMinutes < 0)} + {signed(formatDistance(km(Math.abs(outcome.extraM)), unit), outcome.extraM < 0)} - )} - - )} - - {t('tours.poi.undoHint')} - + {outcome.extraMinutes != null && ( + + {signed(formatPlannedTourDuration(Math.abs(outcome.extraMinutes)), outcome.extraMinutes < 0)} + + )} + + )} +
    @@ -415,15 +306,9 @@ export function TourPoiSearchCard({ planner }: { planner: TourPlannerController } >
    -

    - {t('tours.poi.intro')} -

    {search.themes.length > 0 && ( -
    +
    - - {search.theme ? t(`tours.walk.themeHint.${search.theme}`) : t('tours.walk.themeHint.own')} -
    )} @@ -446,8 +331,10 @@ export function TourPoiSearchCard({ planner }: { planner: TourPlannerController type="button" onClick={() => void search.search()} disabled={!search.canSearch} + title={search.outdated ? t('tours.poi.outdated') : undefined} data-testid="tour-poi-search" - className="inline-flex h-9 items-center justify-center gap-1.5 rounded-[10px] bg-surface-tertiary font-semibold text-content transition-colors hover:bg-surface-hover disabled:cursor-default disabled:opacity-50" + data-outdated={search.outdated || undefined} + className={`inline-flex h-9 items-center justify-center gap-1.5 rounded-[10px] font-semibold transition-colors disabled:cursor-default disabled:opacity-50 ${search.outdated ? 'bg-accent text-accent-text hover:opacity-90' : 'bg-surface-tertiary text-content hover:bg-surface-hover'}`} style={fs(12.5, 'body')} > {search.status === 'loading' ? ( @@ -464,21 +351,12 @@ export function TourPoiSearchCard({ planner }: { planner: TourPlannerController {insertion.outcome && } {pois.length > 0 && ( -
      +
        {pois.map((poi) => ( - + ))}
      )} diff --git a/client/src/components/Tours/poi/tourPoiSearch.test.tsx b/client/src/components/Tours/poi/tourPoiSearch.test.tsx index 91398df8b..4e3b03918 100644 --- a/client/src/components/Tours/poi/tourPoiSearch.test.tsx +++ b/client/src/components/Tours/poi/tourPoiSearch.test.tsx @@ -8,12 +8,13 @@ import { tourPoiSearchRepo } from '../../../repo/tourPoiSearchRepo'; import { useTourPoiStore } from '../../../store/tourPoiStore'; import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints'; import type { TourPlannerController } from '../planner/useTourPlanner'; +import TourPoiOverlay from './TourPoiCard'; import { TourPoiSearchCard } from './TourPoiSearchCard'; import { usePoiInsertion, type PoiInsertionPlanner } from './usePoiInsertion'; import { useTourPoiMarkers } from './useTourPoiMarkers'; import { useTourPoiSearch } from './useTourPoiSearch'; -// FE-TOUR-POI-001 to FE-TOUR-POI-015 +// FE-TOUR-POI-001 to FE-TOUR-POI-016 vi.mock('../planner/useWaypointPlaceNames', () => ({ useWaypointPlaceNames: () => ({}) })); @@ -194,22 +195,63 @@ describe('usePoiInsertion (PLN-03)', () => { }); }); +/** The list in the side panel and the chosen place's card over the map, as the planner shows them. */ +function renderAlong(planner: TourPlannerController) { + return render( + <> + + + + ); +} + describe('TourPoiSearchCard', () => { - it('FE-TOUR-POI-008: lists what was found with where it lies; opening a place offers to add it after a point', async () => { + it('FE-TOUR-POI-008: lists what was found in one line each; a place opens its card over the map, which adds it after a point', async () => { const planner = { ...searchPlanner(), ...insertionPlanner(), } as unknown as TourPlannerController; - render(); + renderAlong(planner); fireEvent.click(screen.getByTestId('tour-poi-search')); const row = await screen.findByTestId('tour-poi-node:1'); expect(row).toHaveTextContent('Spring'); - expect(row).toHaveTextContent('80 m from the route'); + expect(row).toHaveTextContent('80 m'); + expect(within(row).queryByTestId('tour-poi-add')).not.toBeInTheDocument(); + expect(screen.queryByTestId('tour-poi-overlay')).not.toBeInTheDocument(); + fireEvent.click(within(row).getByRole('button', { name: /Spring/ })); - expect(within(row).getByText('Mo-Su 08:00-18:00')).toBeInTheDocument(); - expect(within(row).getByText('As the source has them; check before you go.')).toBeInTheDocument(); - fireEvent.click(within(row).getByTestId('tour-poi-add')); + const card = screen.getByTestId('tour-poi-overlay'); + expect(within(row).getByRole('button', { name: /Spring/ })).toHaveAttribute('aria-pressed', 'true'); + expect(card).toHaveTextContent('80 m from the route'); + expect(within(card).getByText('Mo-Su 08:00-18:00')).toBeInTheDocument(); + expect(within(card).getByText('Mo-Su 08:00-18:00')).toHaveAttribute( + 'title', + 'As the source has them; check before you go.' + ); + fireEvent.click(within(card).getByTestId('tour-poi-add')); await waitFor(() => expect(planner.editWaypoints).toHaveBeenCalledTimes(1)); + // The card closes once the place is in, and the list says what it did. + expect(screen.queryByTestId('tour-poi-overlay')).not.toBeInTheDocument(); + expect(screen.getByTestId('tour-poi-outcome')).toHaveTextContent('Spring'); + }); + + it('FE-TOUR-POI-016: the card over the map closes on its button and on Escape, and a pin on the map opens it too', async () => { + const planner = { ...searchPlanner(), ...insertionPlanner() } as unknown as TourPlannerController; + renderAlong(planner); + fireEvent.click(screen.getByTestId('tour-poi-search')); + await screen.findByTestId('tour-poi-node:1'); + + const markers = renderHook(() => useTourPoiMarkers(), { wrapper: TranslationProvider }); + // The one place found, picked as a click on its pin does. + for (const poi of markers.result.current.pois) act(() => markers.result.current.onPoiClick(poi)); + expect(screen.getByTestId('tour-poi-overlay')).toHaveTextContent('Spring'); + fireEvent.click(within(screen.getByTestId('tour-poi-overlay')).getByRole('button', { name: 'Close' })); + expect(screen.queryByTestId('tour-poi-overlay')).not.toBeInTheDocument(); + + fireEvent.click(within(screen.getByTestId('tour-poi-node:1')).getByRole('button', { name: /Spring/ })); + expect(screen.getByTestId('tour-poi-overlay')).toBeInTheDocument(); + fireEvent.keyDown(document, { key: 'Escape' }); + expect(screen.queryByTestId('tour-poi-overlay')).not.toBeInTheDocument(); }); it('FE-TOUR-POI-009: names the kinds that could not be searched, and does not claim nothing was found', async () => { @@ -272,11 +314,11 @@ describe('a city walk along its places (CW-05)', () => { // The walk starts with its first three categories, which no theme is. for (const theme of ['culture', 'parks', 'food', 'mixed']) expect(themeButton(theme)).toHaveAttribute('aria-pressed', 'false'); - expect(themes).toHaveTextContent('Your own pick of categories below; a theme sets them for you.'); + // What a theme looks for is said on the button, not in a line of its own. + expect(themeButton('food')).toHaveAttribute('title', 'Coffee shops, places to eat and markets along the route.'); fireEvent.click(themeButton('food')); expect(themeButton('food')).toHaveAttribute('aria-pressed', 'true'); - expect(themes).toHaveTextContent('Coffee shops, places to eat and markets along the route.'); expect(screen.getByTestId('tour-poi-categories')).toHaveTextContent('Coffee shops, Places to eat +1'); fireEvent.click(screen.getByTestId('tour-poi-search')); await waitFor(() => @@ -290,7 +332,6 @@ describe('a city walk along its places (CW-05)', () => { fireEvent.click(screen.getByTestId('tour-poi-categories')); fireEvent.click(screen.getByRole('button', { name: 'Markets' })); expect(themeButton('food')).toHaveAttribute('aria-pressed', 'false'); - expect(themes).toHaveTextContent('Your own pick of categories below; a theme sets them for you.'); }); it('FE-TOUR-POI-012: the category field names two categories, counts the rest, and says when none is chosen', () => { @@ -312,12 +353,14 @@ describe('a city walk along its places (CW-05)', () => { it('FE-TOUR-POI-013: a place found for a theme says which theme suggested it', async () => { search.mockResolvedValue({ ...ANSWER, pois: [CAFE, SPRING] }); - render(); + renderAlong(walkPlanner()); fireEvent.click(themeButton('food')); fireEvent.click(screen.getByTestId('tour-poi-search')); const cafe = await screen.findByTestId('tour-poi-node:3'); fireEvent.click(within(cafe).getByRole('button', { name: /Cafe Luitpold/ })); - expect(within(cafe).getByText('Suggested for Food and local life')).toBeInTheDocument(); + expect( + within(screen.getByTestId('tour-poi-overlay')).getByText('Suggested for Food and local life') + ).toBeInTheDocument(); fireEvent.click(within(screen.getByTestId('tour-poi-node:1')).getByRole('button', { name: /Spring/ })); expect(screen.queryByText('Suggested for Food and local life')).not.toBeInTheDocument(); }); @@ -345,7 +388,13 @@ describe('a city walk along its places (CW-05)', () => { (point) => !POINTS.includes(point) ) as TourPlannerWaypoint ); - expect(added[0]).toMatchObject({ name: 'Spring', openingHours: 'Mo-Su 08:00-18:00' }); + expect(added[0]).toMatchObject({ + name: 'Spring', + openingHours: 'Mo-Su 08:00-18:00', + poiCategory: 'drinking_water', + }); + // PLN-03: the kind the place was found as goes with it, so its row carries the kind's icon. + expect(added[1]).toMatchObject({ poiCategory: 'cafe' }); expect(added[1]).not.toHaveProperty('name'); expect(added[1]).not.toHaveProperty('openingHours'); }); diff --git a/client/src/components/Tours/poi/usePoiInsertion.ts b/client/src/components/Tours/poi/usePoiInsertion.ts index d987f10cb..09f6bb8a0 100644 --- a/client/src/components/Tours/poi/usePoiInsertion.ts +++ b/client/src/components/Tours/poi/usePoiInsertion.ts @@ -1,7 +1,7 @@ import type { TourPoi } from '@trek/shared'; import { useCallback, useState } from 'react'; import { useTranslation } from '../../../i18n'; -import { useTourPoiStore } from '../../../store/tourPoiStore'; +import { useTourPoiStore, type InsertedTourPoi } from '../../../store/tourPoiStore'; import { randomId } from '../../../utils/randomId'; import { TOUR_POI_LOOKS } from '../../Map/poiCategories'; import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints'; @@ -16,15 +16,6 @@ interface Measures { minutes: number | null; } -/** A place taken in: which point it became, and the route's measures just before. */ -interface Inserted { - pointId: string; - name: string; - before: Measures | null; - /** How many points the tour had once the place was in; another edit after it ends the comparison. */ - pointCount: number; -} - export type InsertionOutcome = | { kind: 'measuring'; name: string } | { kind: 'measured'; name: string; extraM: number; extraMinutes: number | null } @@ -62,7 +53,9 @@ export function usePoiInsertion(planner: PoiInsertionPlanner) { const { t } = useTranslation(); const select = useTourPoiStore((state) => state.select); const [afterChoice, setAfterChoice] = useState<{ poiId: string; afterId: string } | null>(null); - const [inserted, setInserted] = useState(null); + // Kept in the store: the place is taken in from its card over the map, and the list says what it did. + const inserted = useTourPoiStore((state) => state.inserted); + const setInserted = useTourPoiStore((state) => state.setInserted); const afterIdFor = useCallback( (poi: TourPoi) => @@ -85,6 +78,8 @@ export function usePoiInsertion(planner: PoiInsertionPlanner) { role: 'via', ...(poi.name ? { name: poi.name } : {}), ...(poi.opening_hours ? { openingHours: poi.opening_hours } : {}), + // PLN-03: its row carries the kind's icon, as the map drew the place. + poiCategory: poi.category, }; const next = withPointAfter(planner.waypoints, afterId, point); if (!next) return false; @@ -97,10 +92,10 @@ export function usePoiInsertion(planner: PoiInsertionPlanner) { select(null); return true; }, - [afterIdFor, planner, select, t] + [afterIdFor, planner, select, setInserted, t] ); - const dismiss = useCallback(() => setInserted(null), []); + const dismiss = useCallback(() => setInserted(null), [setInserted]); return { afterIdFor, @@ -117,7 +112,7 @@ export function usePoiInsertion(planner: PoiInsertionPlanner) { * now: nothing once the point is gone again (undone or removed) or the tour * was edited further, so the figure never describes another route. */ -function outcomeOf(inserted: Inserted | null, planner: PoiInsertionPlanner): InsertionOutcome | null { +function outcomeOf(inserted: InsertedTourPoi | null, planner: PoiInsertionPlanner): InsertionOutcome | null { if (!inserted) return null; const stillThere = planner.waypoints.some((point) => point.id === inserted.pointId); if (!stillThere || planner.waypoints.length !== inserted.pointCount) return null; diff --git a/client/src/components/Tours/poi/useTourPoiSearch.ts b/client/src/components/Tours/poi/useTourPoiSearch.ts index 44db95ef9..022fd0cee 100644 --- a/client/src/components/Tours/poi/useTourPoiSearch.ts +++ b/client/src/components/Tours/poi/useTourPoiSearch.ts @@ -112,7 +112,7 @@ export function useTourPoiSearch(planner: TourPoiPlanner) { controller.signal ); setAnswer({ key, status: 'ready', data }); - publish(data.pois); + publish(data.pois, theme); } catch (err: unknown) { if (controller.signal.aborted) return; setAnswer({ key, status: err instanceof OnlineOnlyError ? 'offline' : 'error' }); @@ -123,7 +123,7 @@ export function useTourPoiSearch(planner: TourPoiPlanner) { setLoadingKey(null); } } - }, [categories, clear, corridorM, currentKey, locale, planner.tripId, publish, route, type]); + }, [categories, clear, corridorM, currentKey, locale, planner.tripId, publish, route, theme, type]); // Leaving the planner, or a tour closed, takes the places off the map. useEffect( diff --git a/client/src/components/Tours/walk/WalkStations.tsx b/client/src/components/Tours/walk/WalkStations.tsx index 2d216babd..bc21be464 100644 --- a/client/src/components/Tours/walk/WalkStations.tsx +++ b/client/src/components/Tours/walk/WalkStations.tsx @@ -6,7 +6,8 @@ import type { Assignment, AssignmentsMap, Day } from '../../../types'; import { SoftPill } from '../../Planner/planParts'; import { DialogSection, fs } from '../../shared/DialogShell'; import { useToast } from '../../shared/Toast'; -import { StationPills } from '../planner/stationParts'; +import { stationMarkNote, stationRingClass } from '../planner/stationMarks'; +import { StationPills, StationShield } from '../planner/stationParts'; import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints'; import { TourDayRowPill } from '../TourDayRowFacts'; import { CardAction } from '../tourParts'; @@ -102,9 +103,11 @@ function StationRow({ data-testid="walk-station" className={`flex items-start gap-2.5 rounded-[10px] px-1.5 py-1.5 ${status ? 'opacity-70' : ''}`} > + {/* How much the station matters rings its number; a protection is a shield at its corner (CW-03, CW-10). */} {status === 'visited' ? ( @@ -113,6 +116,7 @@ function StationRow({ ) : ( index + 1 )} + {station.protected && } { ['Stays', 'at least 1 h 30 min'], ['Planned', 'at least 2 h 15 min'], ]); - // No stay entered at all is not known, never nothing. + // No stay entered at all plans the walking time alone, with no stays field to call them unknown. expect(fieldsOf(45, { stayMinutes: 0, unknownStays: 3, stations: 3 })).toEqual([ ['Walking', '45 min'], - ['Stays', 'Not known yet'], - ['Planned', 'at least 45 min'], + ['Planned', '45 min'], ]); }); @@ -59,7 +58,6 @@ describe('walkTimeFields', () => { ]); expect(fieldsOf(null, { stayMinutes: 0, unknownStays: 2, stations: 2 })).toEqual([ ['Walking', 'Not calculated'], - ['Stays', 'Not known yet'], ['Planned', 'Not calculated'], ]); }); @@ -71,7 +69,7 @@ describe('walkTimeFields', () => { ['Planned (Manual override)', '4 h'], ['Computed', 'at least 2 h 15 min'], ]); - expect(fieldsOf(null, { stayMinutes: 0, unknownStays: 1, stations: 1 }, 60)[3]).toEqual([ + expect(fieldsOf(null, { stayMinutes: 0, unknownStays: 1, stations: 1 }, 60)[2]).toEqual([ 'Computed', 'Not calculated', ]); @@ -104,7 +102,9 @@ describe('walkTimeFields', () => { }); describe('walkStaysText', () => { - it('says a walk without stations has no stays to wait for', () => { - expect(walkStaysText({ stayMinutes: 0, unknownStays: 0, stations: 0 }, t)).toBe('0 min'); + it('says nothing of the stays of a walk without stations, or with none entered', () => { + expect(walkStaysText({ stayMinutes: 0, unknownStays: 0, stations: 0 }, t)).toBeNull(); + expect(walkStaysText({ stayMinutes: 0, unknownStays: 2, stations: 2 }, t)).toBeNull(); + expect(walkStaysText({ stayMinutes: 30, unknownStays: 1, stations: 2 }, t)).toBe('at least 30 min'); }); }); diff --git a/client/src/components/Tours/walk/walkTimeFields.ts b/client/src/components/Tours/walk/walkTimeFields.ts index 3919b859e..118572838 100644 --- a/client/src/components/Tours/walk/walkTimeFields.ts +++ b/client/src/components/Tours/walk/walkTimeFields.ts @@ -1,4 +1,4 @@ -import { walkTimesOf, type WalkTimes } from '../planner/stationModel'; +import { someStaysUnknown, walkTimesOf, type WalkTimes } from '../planner/stationModel'; import type { TourMetricField } from '../tourParts'; import { formatPlannedTourDuration, plannedTotalText } from '../tourPresentation'; @@ -8,14 +8,16 @@ type Translate = (key: string, params?: Record) => string; type WalkStays = Pick; /** - * A city walk's stays as said: their sum, at least the sum while a stay is not - * entered yet, and not known while none is. A stop passed through counts as - * the 0 entered for it, so stays all entered as 0 read "0 min". + * A city walk's stays as said: their sum, and at least the sum while some + * stay is not entered yet. Nothing while no stay is entered at all: the walk + * is then planned as its walking time, and saying "unknown" at every turn + * would only be noise. A stop passed through counts as the 0 entered for it, + * so stays all entered as 0 read "0 min". */ -export function walkStaysText(stays: WalkStays, t: Translate): string { - if (stays.stations > 0 && stays.unknownStays === stays.stations) return t('tours.walk.station.stayUnknown'); +export function walkStaysText(stays: WalkStays, t: Translate): string | null { + if (stays.unknownStays >= stays.stations) return null; const time = formatPlannedTourDuration(stays.stayMinutes); - return stays.unknownStays > 0 ? t('tours.walk.totalAtLeast', { time }) : time; + return someStaysUnknown(stays) ? t('tours.walk.totalAtLeast', { time }) : time; } export interface WalkTimeFieldsInput { @@ -47,9 +49,10 @@ export function walkTimeFields({ const times = walkTimesOf({ walkingMinutes, stayMinutes, unknownStays, stations }); const computed = plannedTotalText({ plannedTotalMinutes: times.totalMinutes, atLeast: times.partial }, t) ?? notCalculated; + const staysText = walkStaysText(stays, t); const parts: TourMetricField[] = [ { ...walking, value: walkingMinutes == null ? notCalculated : formatPlannedTourDuration(walkingMinutes) }, - { label: t('tours.walk.stays'), value: walkStaysText(stays, t) }, + ...(staysText ? [{ label: t('tours.walk.stays'), value: staysText }] : []), ]; const planned = t('tours.planner.plannedShort'); if (manualMinutes == null) return [...parts, { label: planned, value: computed }]; diff --git a/client/src/pages/tripPlanner/TourPlannerMapStage.tsx b/client/src/pages/tripPlanner/TourPlannerMapStage.tsx index 18359efbf..5ee811732 100644 --- a/client/src/pages/tripPlanner/TourPlannerMapStage.tsx +++ b/client/src/pages/tripPlanner/TourPlannerMapStage.tsx @@ -13,6 +13,7 @@ import { ChecklistObstaclePanel } from '../../components/Tours/paddle/ObstacleDe import PaddleObstacleOverlay from '../../components/Tours/paddle/PaddleObstacleOverlay'; import { PaddleObstaclePlaceButton } from '../../components/Tours/paddle/PaddleObstaclePlaceButton'; import { useShapingClick } from '../../components/Tours/planner/useShapingClick'; +import TourPoiOverlay from '../../components/Tours/poi/TourPoiCard'; import { useTourPoiMarkers } from '../../components/Tours/poi/useTourPoiMarkers'; import type { TerrainLegendLayout } from '../../components/Tours/terrain/TourTerrainMap'; import { isRtlLanguage, useTranslation } from '../../i18n'; @@ -261,6 +262,13 @@ export function TourPlannerMapStage({ )} /> + {/* PLN-02: the place picked on the map or in the list along the tour, taken in as a stop from here. */} + {/* "Point set", with the way back, above the bottom row and between the panels. */} {placedHint.shown && (
      void; + inserted: InsertedTourPoi | null; + publish: (pois: readonly TourPoi[], theme?: WalkTheme | null) => void; select: (id: string | null) => void; + setInserted: (inserted: InsertedTourPoi | null) => void; clear: () => void; }>((set) => ({ pois: [], + theme: null, selectedId: null, - publish: (pois) => set({ pois, selectedId: null }), + inserted: null, + publish: (pois, theme = null) => set({ pois, theme, selectedId: null }), select: (selectedId) => set({ selectedId }), - clear: () => set({ pois: [], selectedId: null }), + setInserted: (inserted) => set({ inserted }), + clear: () => set({ pois: [], theme: null, selectedId: null, inserted: null }), })); diff --git a/server/src/db/entities/TourWaypoints.entity.ts b/server/src/db/entities/TourWaypoints.entity.ts index 69f783e25..2456dbc50 100644 --- a/server/src/db/entities/TourWaypoints.entity.ts +++ b/server/src/db/entities/TourWaypoints.entity.ts @@ -23,6 +23,7 @@ export class TourWaypoints { importance?: TTourWaypointsImportance | null; protected: number & Opt = 0; opening_hours?: string | null; + poi_category?: string | null; } export const TourWaypointsImportance = { @@ -70,5 +71,6 @@ export const TourWaypointsSchema = defineEntity({ importance: p.enum(() => TourWaypointsImportance).nullable(), protected: p.integer().default(0), opening_hours: p.text().nullable(), + poi_category: p.text().nullable(), }, }); diff --git a/server/src/db/kysely/TourWaypoints.table.ts b/server/src/db/kysely/TourWaypoints.table.ts index 1b7412777..c258b5713 100644 --- a/server/src/db/kysely/TourWaypoints.table.ts +++ b/server/src/db/kysely/TourWaypoints.table.ts @@ -19,4 +19,5 @@ export interface TourWaypointsTable { importance: InsertOptional; protected: InsertOptional; opening_hours: InsertOptional; + poi_category: InsertOptional; } diff --git a/server/src/db/migrations/Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as.ts b/server/src/db/migrations/Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as.ts new file mode 100644 index 000000000..2063134e4 --- /dev/null +++ b/server/src/db/migrations/Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +/** + * PLN-03: a point taken in from the places found along a tour keeps the kind + * of place it was found as (a restaurant, a museum), so the planner can mark + * its row with that kind's icon as the map drew the place. Plain text, read + * back only where the contract knows the kind, so a kind added later needs no + * migration. NULL on every point saved before and on a point set by hand. + */ +export class Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as extends Migration { + override name = 'Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as'; + + override up(): void { + this.addSql('ALTER TABLE tour_waypoints ADD COLUMN poi_category TEXT'); + } +} diff --git a/server/src/db/repositories/TourWaypoints.repository.ts b/server/src/db/repositories/TourWaypoints.repository.ts index 08fcede11..fc8859b6a 100644 --- a/server/src/db/repositories/TourWaypoints.repository.ts +++ b/server/src/db/repositories/TourWaypoints.repository.ts @@ -3,7 +3,7 @@ import type { TourWaypoints, TTourWaypointsImportance } from '../entities/TourWa import { TOUR_WAYPOINT_FIXED_LEG, TOUR_WAYPOINT_LOCK } from '../json-columns'; import type { DB } from '../kysely/db'; import { TrekRepository } from './_shared/trek-repository'; -import type { PaddleLock, TourFixedLeg } from '@trek/shared'; +import { TOUR_POI_CATEGORIES, type PaddleLock, type TourFixedLeg, type TourPoiCategory } from '@trek/shared'; /** One saved routing control point of a tour, in route order. */ export interface TourWaypointRow { @@ -31,6 +31,8 @@ export interface TourWaypointRow { protected?: boolean; /** CW-04: the station's opening hours as OpenStreetMap writes them; absent where there are none. */ opening_hours?: string | null; + /** PLN-03: the kind of place the point was taken in from along the tour; absent on a point set by hand. */ + poi_category?: TourPoiCategory | null; } /** The row as stored, the fixed leg still JSON text. */ @@ -49,6 +51,7 @@ interface StoredWaypoint { importance: TTourWaypointsImportance | null; protected: number; opening_hours: string | null; + poi_category: string | null; } /** The columns a waypoint is read with, as the stored row names them. */ @@ -67,13 +70,20 @@ const WAYPOINT_COLUMNS = [ 'importance', 'protected', 'opening_hours', + 'poi_category', ] as const; +/** A stored kind the contract knows; one it does not (dropped since) reads as none. */ +function poiCategoryOf(stored: string | null): TourPoiCategory | null { + return TOUR_POI_CATEGORIES.find((category) => category === stored) ?? null; +} + /** A stored row as callers read it: every optional part left out entirely where there is none. */ function readWaypoint(row: StoredWaypoint): TourWaypointRow { const { fixed_leg, lock, shaping, linked_place_id, station_key, name, stay_minutes, importance, opening_hours } = row; const leg = decodeJson(TOUR_WAYPOINT_FIXED_LEG, fixed_leg, 'tour waypoint'); const stop = decodeJson(TOUR_WAYPOINT_LOCK, lock, 'tour waypoint'); + const poi_category = poiCategoryOf(row.poi_category); return { lat: row.lat, lng: row.lng, @@ -89,6 +99,7 @@ function readWaypoint(row: StoredWaypoint): TourWaypointRow { ...(importance ? { importance } : {}), ...(row.protected ? { protected: true } : {}), ...(opening_hours ? { opening_hours } : {}), + ...(poi_category ? { poi_category } : {}), }; } @@ -184,6 +195,7 @@ export class TourWaypointsRepository extends TrekRepository { importance: w.importance ?? null, protected: w.protected ? 1 : 0, opening_hours: w.opening_hours ?? null, + poi_category: w.poi_category ?? null, })), ); } diff --git a/server/tests/unit/db/legacy-baseline.test.ts b/server/tests/unit/db/legacy-baseline.test.ts index 4b1e64285..c074c8bf7 100644 --- a/server/tests/unit/db/legacy-baseline.test.ts +++ b/server/tests/unit/db/legacy-baseline.test.ts @@ -65,6 +65,7 @@ const UNNUMBERED = [ 'Migration20200101045100_a_tour_remembers_the_trip_it_was_taken_from', 'Migration20200101045200_a_tour_can_be_ridden', 'Migration20200101045300_a_tour_can_be_a_city_walk', + 'Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as', ]; // The last step the positional runner ever had; planLegacyBaseline refuses a // schema_version past it. diff --git a/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts b/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts index f2d19206a..373719211 100644 --- a/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts +++ b/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts @@ -196,6 +196,30 @@ describe('TourWaypointsRepository', () => { expect(points[1]).not.toHaveProperty('name'); expect(await repo.listForTrip(trip.id)).toEqual(points.map((point) => ({ place_id: place.id, ...point }))); }); + it('TWREPO-011: a point taken in along the tour keeps the kind of place it was found as; a kind no longer known reads as none (PLN-03)', async () => { + const { place } = tourInTrip(); + const restaurant: TourWaypointRow = { + lat: 48.1, + lng: 11.5, + role: 'via', + sequence: 1, + name: 'Café Luitpold', + poi_category: 'restaurant', + }; + await repo.insertForPlace(place.id, [ + { lat: 48, lng: 11.4, role: 'start', sequence: 0 }, + restaurant, + { lat: 48.2, lng: 11.6, role: 'end', sequence: 2 }, + ]); + const points = await repo.listForPlace(place.id); + expect(points[1]).toEqual(restaurant); + expect(points[0]).not.toHaveProperty('poi_category'); + + await t.em.nativeUpdate(TourWaypoints, { place: place.id, sequence: 1 }, { poi_category: 'spaceport' }); + t.clear(); + expect((await repo.listForPlace(place.id))[1]).not.toHaveProperty('poi_category'); + }); + it('TWREPO-010: listForPlaces reads the waypoints of the tours asked for, by place and sequence, and none for none (CW-02)', async () => { const { trip, place } = tourInTrip(); const second = createPlace(testDb, trip.id); diff --git a/shared/src/i18n/ar/tours.ts b/shared/src/i18n/ar/tours.ts index d076c58fa..fc5ba1c06 100644 --- a/shared/src/i18n/ar/tours.ts +++ b/shared/src/i18n/ar/tours.ts @@ -363,7 +363,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'في هذا المسار محطات أكثر مما تتسع له جولة المشي.', 'tours.provenance.takenOverFrom': 'منقولة من', 'tours.poi.title': 'على طول الجولة', - 'tours.poi.intro': 'أماكن للتوقف قريبة من المسار. لا يتغير شيء حتى تضيف واحدًا.', 'tours.poi.categories': 'ما الذي تبحث عنه', 'tours.poi.within': 'ضمن هذه المسافة من المسار', 'tours.poi.search': 'ابحث على طول الجولة', @@ -392,7 +391,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': 'أصبح «{name}» توقفًا الآن؛ يُعاد تخطيط المسار.', 'tours.poi.outcome.measured': 'أصبح «{name}» توقفًا الآن. تتغير الجولة بمقدار:', 'tours.poi.outcome.unmeasured': 'أصبح «{name}» توقفًا الآن. تعذّر حساب مقدار ما يضيفه.', - 'tours.poi.undoHint': 'التراجع في المحرر يلغيه.', 'tours.poi.cat.viewpoint': 'نقاط مشاهدة', 'tours.poi.cat.peak': 'قمم', 'tours.poi.cat.shelter': 'أكواخ وملاجئ', @@ -526,7 +524,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'الفارغ يعني غير معروف بعد؛ 0 لنقطة تمر بها فقط.', 'tours.walk.station.stayInvalid': 'أدخل دقائق كاملة، حتى يوم واحد.', 'tours.walk.station.stayUnknown': 'غير معروف بعد', - 'tours.walk.station.stayUnknownShort': 'مدة غير معروفة', 'tours.walk.station.passThrough': 'مرور فقط', 'tours.walk.station.importance': 'الأهمية', 'tours.walk.station.protected': 'محمية', @@ -568,7 +565,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'حدائق ومتنزهات ونقاط مشاهدة على طول المسار.', 'tours.walk.themeHint.food': 'مقاهٍ ومطاعم وأسواق على طول المسار.', 'tours.walk.themeHint.mixed': 'قليل من كل شيء: معالم ومتاحف وحدائق وإطلالات ومقاهٍ وأسواق.', - 'tours.walk.themeHint.own': 'اختيارك الخاص من الفئات أدناه؛ يضبطها الموضوع لك.', 'tours.walk.themeReason': 'مقترح لموضوع {theme}', 'tours.walk.arrival': 'الوصول نحو {time}', 'tours.walk.arrivalAtLeast': 'الوصول في {time} على أقرب تقدير', diff --git a/shared/src/i18n/az/tours.ts b/shared/src/i18n/az/tours.ts index 9815c730e..6762224dd 100644 --- a/shared/src/i18n/az/tours.ts +++ b/shared/src/i18n/az/tours.ts @@ -350,7 +350,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Bu marşrutda bir gəzintinin götürə biləcəyindən çox dayanacaq var.', 'tours.provenance.takenOverFrom': 'Götürüldüyü yer', 'tours.poi.title': 'Tur boyunca', - 'tours.poi.intro': 'Marşruta yaxın dayanacaq yerləri. Birini əlavə edənə qədər heç nə dəyişmir.', 'tours.poi.categories': 'Nə axtarılsın', 'tours.poi.within': 'Marşrutdan bu məsafədə', 'tours.poi.search': 'Tur boyunca axtar', @@ -381,7 +380,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” artıq dayanacaqdır; marşrut yenidən planlaşdırılır.', 'tours.poi.outcome.measured': '“{name}” artıq dayanacaqdır. Tur bu qədər dəyişir:', 'tours.poi.outcome.unmeasured': '“{name}” artıq dayanacaqdır. Nə qədər əlavə etdiyi hesablana bilmədi.', - 'tours.poi.undoHint': 'Redaktordakı geri al bunu ləğv edir.', 'tours.poi.cat.viewpoint': 'Mənzərə nöqtələri', 'tours.poi.cat.peak': 'Zirvələr', 'tours.poi.cat.shelter': 'Daxmalar və sığınacaqlar', @@ -518,7 +516,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Boş hələ məlum deyil deməkdir; yalnız keçdiyin nöqtə üçün 0.', 'tours.walk.station.stayInvalid': 'Tam dəqiqə daxil et, ən çox bir gün.', 'tours.walk.station.stayUnknown': 'Hələ məlum deyil', - 'tours.walk.station.stayUnknownShort': 'Müddət bilinmir', 'tours.walk.station.passThrough': 'Keçid', 'tours.walk.station.importance': 'Əhəmiyyət', 'tours.walk.station.protected': 'Qorunur', @@ -566,7 +563,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.food': 'Marşrut boyunca kafelər, yemək yerləri və bazarlar.', 'tours.walk.themeHint.mixed': 'Hər şeydən bir az: görməli yerlər, muzeylər, parklar, mənzərələr, kafelər və bazarlar.', - 'tours.walk.themeHint.own': 'Aşağıda öz kateqoriya seçimin; mövzu onları sənin üçün qurur.', 'tours.walk.themeReason': '{theme} üçün təklif olunur', 'tours.walk.arrival': 'Təxmini çatma {time}', 'tours.walk.arrivalAtLeast': 'Ən tez {time} çatma', diff --git a/shared/src/i18n/br/tours.ts b/shared/src/i18n/br/tours.ts index 9d2c9c42d..65cc0a690 100644 --- a/shared/src/i18n/br/tours.ts +++ b/shared/src/i18n/br/tours.ts @@ -356,7 +356,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Essa rota tem mais paradas do que um passeio comporta.', 'tours.provenance.takenOverFrom': 'Trazido de', 'tours.poi.title': 'Ao longo do passeio', - 'tours.poi.intro': 'Lugares para uma parada, perto da rota. Nada muda até você adicionar um.', 'tours.poi.categories': 'O que procurar', 'tours.poi.within': 'Até esta distância da rota', 'tours.poi.search': 'Buscar ao longo do passeio', @@ -387,7 +386,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” agora é uma parada; a rota está sendo planejada de novo.', 'tours.poi.outcome.measured': '“{name}” agora é uma parada. O passeio muda em:', 'tours.poi.outcome.unmeasured': '“{name}” agora é uma parada. Não foi possível calcular quanto ela acrescenta.', - 'tours.poi.undoHint': 'Desfazer no editor reverte isso.', 'tours.poi.cat.viewpoint': 'Mirantes', 'tours.poi.cat.peak': 'Cumes', 'tours.poi.cat.shelter': 'Cabanas e abrigos', @@ -526,7 +524,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Vazio significa ainda desconhecido; 0 para um ponto pelo qual você só passa.', 'tours.walk.station.stayInvalid': 'Informe minutos inteiros, até um dia.', 'tours.walk.station.stayUnknown': 'Ainda desconhecido', - 'tours.walk.station.stayUnknownShort': 'Permanência em aberto', 'tours.walk.station.passThrough': 'Só de passagem', 'tours.walk.station.importance': 'Importância', 'tours.walk.station.protected': 'Protegida', @@ -572,7 +569,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parques, jardins e mirantes ao longo da rota.', 'tours.walk.themeHint.food': 'Cafeterias, lugares para comer e feiras ao longo da rota.', 'tours.walk.themeHint.mixed': 'Um pouco de cada: pontos turísticos, museus, parques, vistas, cafeterias e feiras.', - 'tours.walk.themeHint.own': 'Sua própria seleção de categorias abaixo; um tema as define para você.', 'tours.walk.themeReason': 'Sugerido para {theme}', 'tours.walk.arrival': 'Chegada por volta de {time}', 'tours.walk.arrivalAtLeast': 'Chegada a partir de {time}', diff --git a/shared/src/i18n/ca/tours.ts b/shared/src/i18n/ca/tours.ts index 9ca4e5ca0..390c9eba9 100644 --- a/shared/src/i18n/ca/tours.ts +++ b/shared/src/i18n/ca/tours.ts @@ -357,7 +357,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Aquesta ruta té més parades de les que admet un passeig.', 'tours.provenance.takenOverFrom': 'Agafada de', 'tours.poi.title': 'Al llarg de la ruta', - 'tours.poi.intro': "Llocs per fer una parada, a prop de la ruta. No canvia res fins que n'afegeixis un.", 'tours.poi.categories': 'Què cercar', 'tours.poi.within': "Dins d'aquesta distància de la ruta", 'tours.poi.search': 'Cerca al llarg de la ruta', @@ -388,7 +387,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': "«{name}» ara és una parada; s'està tornant a planificar la ruta.", 'tours.poi.outcome.measured': '«{name}» ara és una parada. La ruta canvia en:', 'tours.poi.outcome.unmeasured': "«{name}» ara és una parada. No s'ha pogut calcular quant hi afegeix.", - 'tours.poi.undoHint': "Desfer a l'editor ho anul·la.", 'tours.poi.cat.viewpoint': 'Miradors', 'tours.poi.cat.peak': 'Cims', 'tours.poi.cat.shelter': 'Refugis i aixoplucs', @@ -527,7 +525,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Buit vol dir encara desconegut; 0 per a un punt pel qual només passes.', 'tours.walk.station.stayInvalid': 'Introdueix minuts sencers, fins a un dia.', 'tours.walk.station.stayUnknown': 'Encara desconegut', - 'tours.walk.station.stayUnknownShort': 'Estada oberta', 'tours.walk.station.passThrough': 'Només de pas', 'tours.walk.station.importance': 'Importància', 'tours.walk.station.protected': 'Protegida', @@ -573,7 +570,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parcs, jardins i miradors al llarg de la ruta.', 'tours.walk.themeHint.food': 'Cafeteries, llocs per menjar i mercats al llarg de la ruta.', 'tours.walk.themeHint.mixed': "Una mica de cada: llocs d'interès, museus, parcs, vistes, cafeteries i mercats.", - 'tours.walk.themeHint.own': 'La teva pròpia tria de categories a sota; un tema te les defineix.', 'tours.walk.themeReason': 'Suggerit per a {theme}', 'tours.walk.arrival': 'Arribada cap a les {time}', 'tours.walk.arrivalAtLeast': 'Arribada com a aviat a les {time}', diff --git a/shared/src/i18n/cs/tours.ts b/shared/src/i18n/cs/tours.ts index 3f450c854..91dc27370 100644 --- a/shared/src/i18n/cs/tours.ts +++ b/shared/src/i18n/cs/tours.ts @@ -350,7 +350,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Tato trasa má víc zastávek, než procházka pojme.', 'tours.provenance.takenOverFrom': 'Převzato z', 'tours.poi.title': 'Podél túry', - 'tours.poi.intro': 'Místa na zastávku blízko trasy. Nic se nezmění, dokud nějaké nepřidáš.', 'tours.poi.categories': 'Co hledat', 'tours.poi.within': 'Do této vzdálenosti od trasy', 'tours.poi.search': 'Hledat podél túry', @@ -380,7 +379,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '„{name}“ je teď zastávka; trasa se plánuje znovu.', 'tours.poi.outcome.measured': '„{name}“ je teď zastávka. Túra se mění o:', 'tours.poi.outcome.unmeasured': '„{name}“ je teď zastávka. Kolik přidává, se nepodařilo spočítat.', - 'tours.poi.undoHint': 'Zpět v editoru to vrátí.', 'tours.poi.cat.viewpoint': 'Vyhlídky', 'tours.poi.cat.peak': 'Vrcholy', 'tours.poi.cat.shelter': 'Chaty a přístřešky', @@ -516,7 +514,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Prázdné znamená zatím neznámé; 0 pro místo, kterým jen projdeš.', 'tours.walk.station.stayInvalid': 'Zadej celé minuty, nejvýše jeden den.', 'tours.walk.station.stayUnknown': 'Zatím neznámé', - 'tours.walk.station.stayUnknownShort': 'Pobyt neznámý', 'tours.walk.station.passThrough': 'Jen průchod', 'tours.walk.station.importance': 'Důležitost', 'tours.walk.station.protected': 'Chráněná', @@ -562,7 +559,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parky, zahrady a vyhlídky podél trasy.', 'tours.walk.themeHint.food': 'Kavárny, místa k jídlu a trhy podél trasy.', 'tours.walk.themeHint.mixed': 'Od všeho trochu: památky, muzea, parky, výhledy, kavárny a trhy.', - 'tours.walk.themeHint.own': 'Tvůj vlastní výběr kategorií níže; téma je nastaví za tebe.', 'tours.walk.themeReason': 'Navrženo pro {theme}', 'tours.walk.arrival': 'Příchod kolem {time}', 'tours.walk.arrivalAtLeast': 'Příchod nejdříve {time}', diff --git a/shared/src/i18n/de/tours.ts b/shared/src/i18n/de/tours.ts index b4b9253e6..d110e9ee7 100644 --- a/shared/src/i18n/de/tours.ts +++ b/shared/src/i18n/de/tours.ts @@ -359,7 +359,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Diese Route hat mehr Stationen, als ein Walk aufnehmen kann.', 'tours.provenance.takenOverFrom': 'Übernommen aus', 'tours.poi.title': 'Entlang der Tour', - 'tours.poi.intro': 'Orte für einen Stopp, nah an der Route. Es ändert sich nichts, bis du einen hinzufügst.', 'tours.poi.categories': 'Wonach suchen', 'tours.poi.within': 'In diesem Abstand zur Route', 'tours.poi.search': 'Entlang der Tour suchen', @@ -390,7 +389,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '„{name}“ ist jetzt ein Stopp; die Route wird neu geplant.', 'tours.poi.outcome.measured': '„{name}“ ist jetzt ein Stopp. Die Tour ändert sich um:', 'tours.poi.outcome.unmeasured': '„{name}“ ist jetzt ein Stopp. Wie viel er hinzufügt, ließ sich nicht berechnen.', - 'tours.poi.undoHint': 'Rückgängig im Editor nimmt es zurück.', 'tours.poi.cat.viewpoint': 'Aussichtspunkte', 'tours.poi.cat.peak': 'Gipfel', 'tours.poi.cat.shelter': 'Hütten und Unterstände', @@ -529,7 +527,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Leer heißt noch unbekannt; 0 für einen Punkt, den du nur passierst.', 'tours.walk.station.stayInvalid': 'Ganze Minuten eingeben, höchstens einen Tag.', 'tours.walk.station.stayUnknown': 'Noch unbekannt', - 'tours.walk.station.stayUnknownShort': 'Aufenthalt offen', 'tours.walk.station.passThrough': 'Durchgang', 'tours.walk.station.importance': 'Wichtigkeit', 'tours.walk.station.protected': 'Geschützt', @@ -577,7 +574,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parks, Gärten und Aussichtspunkte entlang der Route.', 'tours.walk.themeHint.food': 'Cafés, Gaststätten und Märkte entlang der Route.', 'tours.walk.themeHint.mixed': 'Von allem etwas: Sehenswürdigkeiten, Museen, Parks, Aussicht, Cafés und Märkte.', - 'tours.walk.themeHint.own': 'Deine eigene Auswahl darunter; ein Thema stellt sie für dich zusammen.', 'tours.walk.themeReason': 'Vorgeschlagen für {theme}', 'tours.walk.arrival': 'Ankunft ca. {time}', 'tours.walk.arrivalAtLeast': 'Ankunft frühestens {time}', diff --git a/shared/src/i18n/en/tours.ts b/shared/src/i18n/en/tours.ts index 962ffd4af..d250c93e1 100644 --- a/shared/src/i18n/en/tours.ts +++ b/shared/src/i18n/en/tours.ts @@ -349,7 +349,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'That route has more stops than a walk can take.', 'tours.provenance.takenOverFrom': 'Taken over from', 'tours.poi.title': 'Along the tour', - 'tours.poi.intro': 'Places for a stop, close to the route. Nothing changes until you add one.', 'tours.poi.categories': 'What to look for', 'tours.poi.within': 'Within this distance of the route', 'tours.poi.search': 'Search along the tour', @@ -380,7 +379,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” is a stop now; the route is being planned again.', 'tours.poi.outcome.measured': '“{name}” is a stop now. The tour changes by:', 'tours.poi.outcome.unmeasured': '“{name}” is a stop now. How much it adds could not be worked out.', - 'tours.poi.undoHint': 'Undo in the editor takes it back.', 'tours.poi.cat.viewpoint': 'Viewpoints', 'tours.poi.cat.peak': 'Summits', 'tours.poi.cat.shelter': 'Huts and shelters', @@ -517,7 +515,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Empty means not known yet; 0 for a point you only pass.', 'tours.walk.station.stayInvalid': 'Enter whole minutes, up to a day.', 'tours.walk.station.stayUnknown': 'Not known yet', - 'tours.walk.station.stayUnknownShort': 'Stay unknown', 'tours.walk.station.passThrough': 'Passing through', 'tours.walk.station.importance': 'Importance', 'tours.walk.station.protected': 'Protected', @@ -563,7 +560,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parks, gardens and viewpoints along the route.', 'tours.walk.themeHint.food': 'Coffee shops, places to eat and markets along the route.', 'tours.walk.themeHint.mixed': 'Some of each: sights, museums, parks, views, coffee shops and markets.', - 'tours.walk.themeHint.own': 'Your own pick of categories below; a theme sets them for you.', 'tours.walk.themeReason': 'Suggested for {theme}', 'tours.walk.arrival': 'Arrival about {time}', 'tours.walk.arrivalAtLeast': 'Arrival {time} at the earliest', diff --git a/shared/src/i18n/es/tours.ts b/shared/src/i18n/es/tours.ts index 89cbc8f76..13858566f 100644 --- a/shared/src/i18n/es/tours.ts +++ b/shared/src/i18n/es/tours.ts @@ -356,7 +356,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Esa ruta tiene más paradas de las que admite un paseo.', 'tours.provenance.takenOverFrom': 'Traída de', 'tours.poi.title': 'A lo largo de la ruta', - 'tours.poi.intro': 'Lugares para una parada, cerca de la ruta. Nada cambia hasta que añadas uno.', 'tours.poi.categories': 'Qué buscar', 'tours.poi.within': 'A esta distancia de la ruta', 'tours.poi.search': 'Buscar a lo largo de la ruta', @@ -387,7 +386,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '«{name}» ya es una parada; la ruta se está planificando de nuevo.', 'tours.poi.outcome.measured': '«{name}» ya es una parada. La ruta cambia en:', 'tours.poi.outcome.unmeasured': '«{name}» ya es una parada. No se pudo calcular cuánto añade.', - 'tours.poi.undoHint': 'Deshacer en el editor lo revierte.', 'tours.poi.cat.viewpoint': 'Miradores', 'tours.poi.cat.peak': 'Cumbres', 'tours.poi.cat.shelter': 'Refugios y cobertizos', @@ -525,7 +523,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Vacío significa aún desconocido; 0 para un punto por el que solo pasas.', 'tours.walk.station.stayInvalid': 'Introduce minutos enteros, hasta un día.', 'tours.walk.station.stayUnknown': 'Aún desconocido', - 'tours.walk.station.stayUnknownShort': 'Estancia abierta', 'tours.walk.station.passThrough': 'Solo de paso', 'tours.walk.station.importance': 'Importancia', 'tours.walk.station.protected': 'Protegida', @@ -571,7 +568,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parques, jardines y miradores a lo largo de la ruta.', 'tours.walk.themeHint.food': 'Cafeterías, sitios para comer y mercados a lo largo de la ruta.', 'tours.walk.themeHint.mixed': 'Un poco de cada: lugares de interés, museos, parques, vistas, cafeterías y mercados.', - 'tours.walk.themeHint.own': 'Tu propia selección de categorías abajo; un tema las elige por ti.', 'tours.walk.themeReason': 'Sugerido para {theme}', 'tours.walk.arrival': 'Llegada hacia las {time}', 'tours.walk.arrivalAtLeast': 'Llegada no antes de las {time}', diff --git a/shared/src/i18n/et/tours.ts b/shared/src/i18n/et/tours.ts index 2a9a3dc8c..2d96dc290 100644 --- a/shared/src/i18n/et/tours.ts +++ b/shared/src/i18n/et/tours.ts @@ -351,7 +351,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Sellel marsruudil on rohkem peatusi, kui jalutuskäiku mahub.', 'tours.provenance.takenOverFrom': 'Üle võetud', 'tours.poi.title': 'Matka ääres', - 'tours.poi.intro': 'Peatuskohad marsruudi lähedal. Midagi ei muutu enne, kui mõne lisad.', 'tours.poi.categories': 'Mida otsida', 'tours.poi.within': 'Selle kaugusega marsruudist', 'tours.poi.search': 'Otsi matka äärest', @@ -382,7 +381,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '„{name}“ on nüüd peatus; marsruuti planeeritakse uuesti.', 'tours.poi.outcome.measured': '„{name}“ on nüüd peatus. Matk muutub nii palju:', 'tours.poi.outcome.unmeasured': '„{name}“ on nüüd peatus. Kui palju see lisab, ei õnnestunud arvutada.', - 'tours.poi.undoHint': 'Võta toimeta redaktoris tühistab selle.', 'tours.poi.cat.viewpoint': 'Vaatekohad', 'tours.poi.cat.peak': 'Tipud', 'tours.poi.cat.shelter': 'Onnid ja varjualused', @@ -518,7 +516,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Tühi tähendab veel teadmata; 0 punkti jaoks, millest ainult möödud.', 'tours.walk.station.stayInvalid': 'Sisesta täisminutid, kuni üks päev.', 'tours.walk.station.stayUnknown': 'Veel teadmata', - 'tours.walk.station.stayUnknownShort': 'Peatus teadmata', 'tours.walk.station.passThrough': 'Läbisõit', 'tours.walk.station.importance': 'Tähtsus', 'tours.walk.station.protected': 'Kaitstud', @@ -564,7 +561,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Pargid, aiad ja vaatekohad marsruudi ääres.', 'tours.walk.themeHint.food': 'Kohvikud, söögikohad ja turud marsruudi ääres.', 'tours.walk.themeHint.mixed': 'Igaühest natuke: vaatamisväärsused, muuseumid, pargid, vaated, kohvikud ja turud.', - 'tours.walk.themeHint.own': 'Sinu enda kategooriate valik allpool; teema seab need sinu eest.', 'tours.walk.themeReason': 'Soovitatud teemale {theme}', 'tours.walk.arrival': 'Saabumine umbes {time}', 'tours.walk.arrivalAtLeast': 'Saabumine kõige varem {time}', diff --git a/shared/src/i18n/fr/tours.ts b/shared/src/i18n/fr/tours.ts index c7fd825dd..81fe08a99 100644 --- a/shared/src/i18n/fr/tours.ts +++ b/shared/src/i18n/fr/tours.ts @@ -362,7 +362,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': "Cet itinéraire compte plus d'étapes qu'une balade ne peut en prendre.", 'tours.provenance.takenOverFrom': 'Repris de', 'tours.poi.title': "Le long de l'itinéraire", - 'tours.poi.intro': "Des lieux où s'arrêter, près de l'itinéraire. Rien ne change tant que tu n'en ajoutes pas un.", 'tours.poi.categories': 'Quoi chercher', 'tours.poi.within': "À cette distance de l'itinéraire", 'tours.poi.search': "Chercher le long de l'itinéraire", @@ -393,7 +392,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': "« {name} » est maintenant un arrêt ; l'itinéraire est recalculé.", 'tours.poi.outcome.measured': "« {name} » est maintenant un arrêt. L'itinéraire change de :", 'tours.poi.outcome.unmeasured': "« {name} » est maintenant un arrêt. Ce qu'il ajoute n'a pas pu être calculé.", - 'tours.poi.undoHint': "Annuler dans l'éditeur le retire.", 'tours.poi.cat.viewpoint': 'Points de vue', 'tours.poi.cat.peak': 'Sommets', 'tours.poi.cat.shelter': 'Refuges et abris', @@ -533,7 +531,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Vide signifie encore inconnu ; 0 pour un point où tu ne fais que passer.', 'tours.walk.station.stayInvalid': "Saisis des minutes entières, jusqu'à une journée.", 'tours.walk.station.stayUnknown': 'Encore inconnu', - 'tours.walk.station.stayUnknownShort': 'Arrêt inconnu', 'tours.walk.station.passThrough': 'Simple passage', 'tours.walk.station.importance': "Importance de l'étape", 'tours.walk.station.protected': 'Protégée', @@ -580,7 +577,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parcs, jardins et points de vue le long du parcours.', 'tours.walk.themeHint.food': 'Cafés, restaurants et marchés le long du parcours.', 'tours.walk.themeHint.mixed': 'Un peu de chaque : sites, musées, parcs, points de vue, cafés et marchés.', - 'tours.walk.themeHint.own': 'Ta propre sélection de catégories ci-dessous ; un thème les choisit pour toi.', 'tours.walk.themeReason': 'Suggéré pour {theme}', 'tours.walk.arrival': 'Arrivée vers {time}', 'tours.walk.arrivalAtLeast': 'Arrivée au plus tôt à {time}', diff --git a/shared/src/i18n/gr/tours.ts b/shared/src/i18n/gr/tours.ts index 914597ea1..c588c2f40 100644 --- a/shared/src/i18n/gr/tours.ts +++ b/shared/src/i18n/gr/tours.ts @@ -364,7 +364,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Αυτή η διαδρομή έχει περισσότερες στάσεις απ’ όσες χωράει μια βόλτα.', 'tours.provenance.takenOverFrom': 'Μεταφέρθηκε από', 'tours.poi.title': 'Κατά μήκος της διαδρομής', - 'tours.poi.intro': 'Μέρη για στάση κοντά στη διαδρομή. Τίποτα δεν αλλάζει μέχρι να προσθέσεις κάποιο.', 'tours.poi.categories': 'Τι να αναζητηθεί', 'tours.poi.within': 'Σε αυτή την απόσταση από τη διαδρομή', 'tours.poi.search': 'Αναζήτηση κατά μήκος της διαδρομής', @@ -395,7 +394,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': 'Το «{name}» είναι τώρα στάση· η διαδρομή σχεδιάζεται ξανά.', 'tours.poi.outcome.measured': 'Το «{name}» είναι τώρα στάση. Η διαδρομή αλλάζει κατά:', 'tours.poi.outcome.unmeasured': 'Το «{name}» είναι τώρα στάση. Δεν ήταν δυνατό να υπολογιστεί πόσο προσθέτει.', - 'tours.poi.undoHint': 'Η αναίρεση στον επεξεργαστή το επαναφέρει.', 'tours.poi.cat.viewpoint': 'Σημεία θέας', 'tours.poi.cat.peak': 'Κορυφές', 'tours.poi.cat.shelter': 'Καταφύγια και στέγαστρα', @@ -535,7 +533,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Κενό σημαίνει άγνωστο ακόμη· 0 για σημείο από το οποίο απλώς περνάς.', 'tours.walk.station.stayInvalid': 'Δώσε ακέραια λεπτά, έως μία ημέρα.', 'tours.walk.station.stayUnknown': 'Άγνωστο ακόμη', - 'tours.walk.station.stayUnknownShort': 'Παραμονή άγνωστη', 'tours.walk.station.passThrough': 'Απλή διέλευση', 'tours.walk.station.importance': 'Σημασία', 'tours.walk.station.protected': 'Προστατευμένη', @@ -582,7 +579,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Πάρκα, κήποι και σημεία θέας κατά μήκος της διαδρομής.', 'tours.walk.themeHint.food': 'Καφετέριες, μέρη για φαγητό και αγορές κατά μήκος της διαδρομής.', 'tours.walk.themeHint.mixed': "Λίγο απ' όλα: αξιοθέατα, μουσεία, πάρκα, θέα, καφετέριες και αγορές.", - 'tours.walk.themeHint.own': 'Η δική σου επιλογή κατηγοριών παρακάτω· ένα θέμα τις ορίζει για σένα.', 'tours.walk.themeReason': 'Προτείνεται για {theme}', 'tours.walk.arrival': 'Άφιξη περίπου {time}', 'tours.walk.arrivalAtLeast': 'Άφιξη το νωρίτερο {time}', diff --git a/shared/src/i18n/hu/tours.ts b/shared/src/i18n/hu/tours.ts index faa6d162a..31dd12a41 100644 --- a/shared/src/i18n/hu/tours.ts +++ b/shared/src/i18n/hu/tours.ts @@ -357,7 +357,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Ennek az útvonalnak több megállója van, mint amennyit egy séta elbír.', 'tours.provenance.takenOverFrom': 'Átvéve innen', 'tours.poi.title': 'A túra mentén', - 'tours.poi.intro': 'Megállóhelyek az útvonal közelében. Semmi sem változik, amíg hozzá nem adsz egyet.', 'tours.poi.categories': 'Mit keress', 'tours.poi.within': 'Ezen a távolságon belül az útvonaltól', 'tours.poi.search': 'Keresés a túra mentén', @@ -387,7 +386,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '„{name}” most már megálló; az útvonal újratervezése folyik.', 'tours.poi.outcome.measured': '„{name}” most már megálló. A túra ennyivel változik:', 'tours.poi.outcome.unmeasured': '„{name}” most már megálló. Nem sikerült kiszámolni, mennyit tesz hozzá.', - 'tours.poi.undoHint': 'A szerkesztőben a visszavonás visszaállítja.', 'tours.poi.cat.viewpoint': 'Kilátópontok', 'tours.poi.cat.peak': 'Csúcsok', 'tours.poi.cat.shelter': 'Kunyhók és menedékek', @@ -525,7 +523,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Üres: még nem ismert; 0 egy pontnál, amelyen csak áthaladsz.', 'tours.walk.station.stayInvalid': 'Egész perceket adj meg, legfeljebb egy napot.', 'tours.walk.station.stayUnknown': 'Még nem ismert', - 'tours.walk.station.stayUnknownShort': 'Tartózkodás nyitott', 'tours.walk.station.passThrough': 'Csak áthaladás', 'tours.walk.station.importance': 'Fontosság', 'tours.walk.station.protected': 'Védett', @@ -572,7 +569,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parkok, kertek és kilátópontok az útvonal mentén.', 'tours.walk.themeHint.food': 'Kávézók, étkezőhelyek és piacok az útvonal mentén.', 'tours.walk.themeHint.mixed': 'Mindenből egy kicsit: látnivalók, múzeumok, parkok, kilátás, kávézók és piacok.', - 'tours.walk.themeHint.own': 'Saját kategóriaválasztásod lent; egy téma beállítja helyetted.', 'tours.walk.themeReason': 'Javasolva: {theme}', 'tours.walk.arrival': 'Érkezés kb. {time}', 'tours.walk.arrivalAtLeast': 'Érkezés legkorábban {time}', diff --git a/shared/src/i18n/id/tours.ts b/shared/src/i18n/id/tours.ts index 81c5cc981..3898b6591 100644 --- a/shared/src/i18n/id/tours.ts +++ b/shared/src/i18n/id/tours.ts @@ -351,7 +351,6 @@ const tours: TranslationStrings = { 'Rute ini punya lebih banyak titik singgah daripada yang muat dalam satu rute jalan kaki.', 'tours.provenance.takenOverFrom': 'Diambil dari', 'tours.poi.title': 'Di sepanjang tur', - 'tours.poi.intro': 'Tempat untuk berhenti, dekat rute. Tidak ada yang berubah sampai kamu menambahkannya.', 'tours.poi.categories': 'Apa yang dicari', 'tours.poi.within': 'Dalam jarak ini dari rute', 'tours.poi.search': 'Cari di sepanjang tur', @@ -382,7 +381,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” kini jadi pemberhentian; rute sedang direncanakan ulang.', 'tours.poi.outcome.measured': '“{name}” kini jadi pemberhentian. Tur berubah sebesar:', 'tours.poi.outcome.unmeasured': '“{name}” kini jadi pemberhentian. Seberapa banyak tambahannya tidak dapat dihitung.', - 'tours.poi.undoHint': 'Urungkan di editor akan membatalkannya.', 'tours.poi.cat.viewpoint': 'Titik pandang', 'tours.poi.cat.peak': 'Puncak', 'tours.poi.cat.shelter': 'Pondok dan tempat berteduh', @@ -519,7 +517,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Kosong berarti belum diketahui; 0 untuk titik yang hanya kamu lewati.', 'tours.walk.station.stayInvalid': 'Masukkan menit bulat, paling lama satu hari.', 'tours.walk.station.stayUnknown': 'Belum diketahui', - 'tours.walk.station.stayUnknownShort': 'Lama singgah belum ada', 'tours.walk.station.passThrough': 'Hanya lewat', 'tours.walk.station.importance': 'Tingkat penting', 'tours.walk.station.protected': 'Dilindungi', @@ -567,7 +564,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.food': 'Kedai kopi, tempat makan, dan pasar di sepanjang rute.', 'tours.walk.themeHint.mixed': 'Sedikit dari semuanya: tempat wisata, museum, taman, pemandangan, kedai kopi, dan pasar.', - 'tours.walk.themeHint.own': 'Pilihan kategorimu sendiri di bawah; tema mengaturnya untukmu.', 'tours.walk.themeReason': 'Disarankan untuk {theme}', 'tours.walk.arrival': 'Tiba sekitar {time}', 'tours.walk.arrivalAtLeast': 'Tiba paling cepat {time}', diff --git a/shared/src/i18n/it/tours.ts b/shared/src/i18n/it/tours.ts index c8c69d103..460eac51c 100644 --- a/shared/src/i18n/it/tours.ts +++ b/shared/src/i18n/it/tours.ts @@ -359,7 +359,6 @@ const tours: TranslationStrings = { 'Questo percorso ha più tappe di quante una passeggiata ne possa contenere.', 'tours.provenance.takenOverFrom': 'Ripreso da', 'tours.poi.title': 'Lungo il percorso', - 'tours.poi.intro': 'Luoghi per una sosta, vicino al percorso. Nulla cambia finché non ne aggiungi uno.', 'tours.poi.categories': 'Cosa cercare', 'tours.poi.within': 'Entro questa distanza dal percorso', 'tours.poi.search': 'Cerca lungo il percorso', @@ -390,7 +389,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” ora è una sosta; il percorso viene ripianificato.', 'tours.poi.outcome.measured': '“{name}” ora è una sosta. Il percorso cambia di:', 'tours.poi.outcome.unmeasured': '“{name}” ora è una sosta. Non è stato possibile calcolare quanto aggiunge.', - 'tours.poi.undoHint': "Annulla nell'editor lo ripristina.", 'tours.poi.cat.viewpoint': 'Punti panoramici', 'tours.poi.cat.peak': 'Vette', 'tours.poi.cat.shelter': 'Rifugi e ripari', @@ -527,7 +525,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Vuoto significa ancora sconosciuto; 0 per un punto da cui passi soltanto.', 'tours.walk.station.stayInvalid': 'Inserisci minuti interi, fino a un giorno.', 'tours.walk.station.stayUnknown': 'Ancora sconosciuto', - 'tours.walk.station.stayUnknownShort': 'Sosta da definire', 'tours.walk.station.passThrough': 'Solo di passaggio', 'tours.walk.station.importance': 'Importanza', 'tours.walk.station.protected': 'Protetta', @@ -573,7 +570,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parchi, giardini e punti panoramici lungo il percorso.', 'tours.walk.themeHint.food': 'Caffetterie, posti dove mangiare e mercati lungo il percorso.', 'tours.walk.themeHint.mixed': "Un po' di tutto: attrazioni, musei, parchi, panorami, caffetterie e mercati.", - 'tours.walk.themeHint.own': 'La tua scelta di categorie qui sotto; un tema le imposta per te.', 'tours.walk.themeReason': 'Suggerito per {theme}', 'tours.walk.arrival': 'Arrivo verso le {time}', 'tours.walk.arrivalAtLeast': 'Arrivo non prima delle {time}', diff --git a/shared/src/i18n/ja/tours.ts b/shared/src/i18n/ja/tours.ts index 0188c2815..889c92703 100644 --- a/shared/src/i18n/ja/tours.ts +++ b/shared/src/i18n/ja/tours.ts @@ -341,7 +341,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'このルートの立ち寄り先は、街歩きに入る数を超えています。', 'tours.provenance.takenOverFrom': '取り込み元', 'tours.poi.title': 'ツアー沿い', - 'tours.poi.intro': 'ルートの近くにある立ち寄り先です。追加するまで何も変わりません。', 'tours.poi.categories': '探すもの', 'tours.poi.within': 'ルートからこの距離以内', 'tours.poi.search': 'ツアー沿いを検索', @@ -372,7 +371,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '「{name}」を立ち寄り地点にしました。ルートを再計算しています。', 'tours.poi.outcome.measured': '「{name}」を立ち寄り地点にしました。ツアーの変化:', 'tours.poi.outcome.unmeasured': '「{name}」を立ち寄り地点にしました。どれだけ増えるかは計算できませんでした。', - 'tours.poi.undoHint': 'エディターの「元に戻す」で取り消せます。', 'tours.poi.cat.viewpoint': '展望地', 'tours.poi.cat.peak': '山頂', 'tours.poi.cat.shelter': '山小屋・避難小屋', @@ -507,7 +505,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': '空欄はまだ不明、通過するだけの地点は 0。', 'tours.walk.station.stayInvalid': '整数の分で入力してください(最大1日)。', 'tours.walk.station.stayUnknown': 'まだ不明', - 'tours.walk.station.stayUnknownShort': '滞在時間未定', 'tours.walk.station.passThrough': '通過のみ', 'tours.walk.station.importance': '重要度', 'tours.walk.station.protected': '保護', @@ -554,7 +551,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'ルート沿いの公園、庭園、展望スポット。', 'tours.walk.themeHint.food': 'ルート沿いのカフェ、食事処、市場。', 'tours.walk.themeHint.mixed': '観光名所、美術館・博物館、公園、眺望、カフェ、市場を少しずつ。', - 'tours.walk.themeHint.own': '下のカテゴリーは自分で選択中。テーマを選ぶと自動で設定されます。', 'tours.walk.themeReason': '{theme} のおすすめ', 'tours.walk.arrival': '到着 {time} ごろ', 'tours.walk.arrivalAtLeast': '到着は早くても {time}', diff --git a/shared/src/i18n/ko/tours.ts b/shared/src/i18n/ko/tours.ts index 12dda37b2..6fcadc5a1 100644 --- a/shared/src/i18n/ko/tours.ts +++ b/shared/src/i18n/ko/tours.ts @@ -340,7 +340,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': '이 경로에는 산책 하나에 담을 수 있는 것보다 많은 정류지가 있습니다.', 'tours.provenance.takenOverFrom': '가져온 곳', 'tours.poi.title': '투어 경로 주변', - 'tours.poi.intro': '경로 근처에 머물 만한 곳입니다. 추가하기 전에는 아무것도 바뀌지 않습니다.', 'tours.poi.categories': '찾을 항목', 'tours.poi.within': '경로에서 이 거리 이내', 'tours.poi.search': '투어 경로 주변 검색', @@ -370,7 +369,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}”이(가) 정차 지점이 되었습니다. 경로를 다시 계산하는 중입니다.', 'tours.poi.outcome.measured': '“{name}”이(가) 정차 지점이 되었습니다. 투어 변화:', 'tours.poi.outcome.unmeasured': '“{name}”이(가) 정차 지점이 되었습니다. 얼마나 늘어나는지 계산하지 못했습니다.', - 'tours.poi.undoHint': '편집기에서 실행 취소하면 되돌릴 수 있습니다.', 'tours.poi.cat.viewpoint': '전망대', 'tours.poi.cat.peak': '정상', 'tours.poi.cat.shelter': '산장과 대피소', @@ -505,7 +503,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': '비워 두면 아직 모름, 지나가기만 하는 곳은 0.', 'tours.walk.station.stayInvalid': '정수 분으로 입력하세요(최대 하루).', 'tours.walk.station.stayUnknown': '아직 모름', - 'tours.walk.station.stayUnknownShort': '머무는 시간 미정', 'tours.walk.station.passThrough': '지나가기만', 'tours.walk.station.importance': '중요도', 'tours.walk.station.protected': '보호됨', @@ -550,7 +547,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': '경로를 따라 있는 공원, 정원, 전망 명소.', 'tours.walk.themeHint.food': '경로를 따라 있는 카페, 식당, 시장.', 'tours.walk.themeHint.mixed': '명소, 박물관, 공원, 전망, 카페, 시장을 골고루.', - 'tours.walk.themeHint.own': '아래 카테고리는 직접 고른 것입니다. 테마를 고르면 자동으로 설정됩니다.', 'tours.walk.themeReason': '{theme} 추천', 'tours.walk.arrival': '도착 약 {time}', 'tours.walk.arrivalAtLeast': '도착 빨라야 {time}', diff --git a/shared/src/i18n/nl/tours.ts b/shared/src/i18n/nl/tours.ts index 242d67308..38b82a0fa 100644 --- a/shared/src/i18n/nl/tours.ts +++ b/shared/src/i18n/nl/tours.ts @@ -358,7 +358,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Deze route heeft meer haltes dan een wandeling kan bevatten.', 'tours.provenance.takenOverFrom': 'Overgenomen uit', 'tours.poi.title': 'Langs de tocht', - 'tours.poi.intro': 'Plekken voor een stop, dicht bij de route. Er verandert niets tot je er een toevoegt.', 'tours.poi.categories': 'Waarnaar zoeken', 'tours.poi.within': 'Binnen deze afstand van de route', 'tours.poi.search': 'Zoeken langs de tocht', @@ -389,7 +388,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '‘{name}’ is nu een stop; de route wordt opnieuw gepland.', 'tours.poi.outcome.measured': '‘{name}’ is nu een stop. De tocht verandert met:', 'tours.poi.outcome.unmeasured': '‘{name}’ is nu een stop. Hoeveel het toevoegt, kon niet worden berekend.', - 'tours.poi.undoHint': 'Ongedaan maken in de editor draait het terug.', 'tours.poi.cat.viewpoint': 'Uitzichtpunten', 'tours.poi.cat.peak': 'Toppen', 'tours.poi.cat.shelter': 'Hutten en schuilplaatsen', @@ -527,7 +525,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Leeg betekent nog onbekend; 0 voor een punt waar je alleen langs loopt.', 'tours.walk.station.stayInvalid': 'Vul hele minuten in, tot een dag.', 'tours.walk.station.stayUnknown': 'Nog onbekend', - 'tours.walk.station.stayUnknownShort': 'Verblijf open', 'tours.walk.station.passThrough': 'Alleen doorgaan', 'tours.walk.station.importance': 'Belang', 'tours.walk.station.protected': 'Beschermd', @@ -574,7 +571,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parken, tuinen en uitkijkpunten langs de route.', 'tours.walk.themeHint.food': 'Koffiezaken, eetgelegenheden en markten langs de route.', 'tours.walk.themeHint.mixed': 'Van alles wat: bezienswaardigheden, musea, parken, uitzicht, koffiezaken en markten.', - 'tours.walk.themeHint.own': 'Je eigen keuze van categorieën hieronder; een thema stelt ze voor je in.', 'tours.walk.themeReason': 'Voorgesteld voor {theme}', 'tours.walk.arrival': 'Aankomst rond {time}', 'tours.walk.arrivalAtLeast': 'Aankomst op zijn vroegst {time}', diff --git a/shared/src/i18n/pl/tours.ts b/shared/src/i18n/pl/tours.ts index 0141f349b..c0550f294 100644 --- a/shared/src/i18n/pl/tours.ts +++ b/shared/src/i18n/pl/tours.ts @@ -366,7 +366,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Ta trasa ma więcej przystanków, niż zmieści spacer.', 'tours.provenance.takenOverFrom': 'Przejęto z', 'tours.poi.title': 'Wzdłuż trasy', - 'tours.poi.intro': 'Miejsca na postój blisko trasy. Nic się nie zmienia, dopóki nie dodasz któregoś.', 'tours.poi.categories': 'Czego szukać', 'tours.poi.within': 'W tej odległości od trasy', 'tours.poi.search': 'Szukaj wzdłuż trasy', @@ -396,7 +395,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '„{name}” jest teraz postojem; trasa jest wyznaczana od nowa.', 'tours.poi.outcome.measured': '„{name}” jest teraz postojem. Trasa zmienia się o:', 'tours.poi.outcome.unmeasured': '„{name}” jest teraz postojem. Nie udało się obliczyć, ile dodaje.', - 'tours.poi.undoHint': 'Cofnij w edytorze to wycofa.', 'tours.poi.cat.viewpoint': 'Punkty widokowe', 'tours.poi.cat.peak': 'Szczyty', 'tours.poi.cat.shelter': 'Schroniska i wiaty', @@ -534,7 +532,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Puste oznacza jeszcze nieznane; 0 dla punktu, przez który tylko przechodzisz.', 'tours.walk.station.stayInvalid': 'Wpisz pełne minuty, maksymalnie jeden dzień.', 'tours.walk.station.stayUnknown': 'Jeszcze nieznane', - 'tours.walk.station.stayUnknownShort': 'Pobyt nieznany', 'tours.walk.station.passThrough': 'Tylko przejście', 'tours.walk.station.importance': 'Ważność', 'tours.walk.station.protected': 'Chroniony', @@ -581,7 +578,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parki, ogrody i punkty widokowe wzdłuż trasy.', 'tours.walk.themeHint.food': 'Kawiarnie, miejsca do jedzenia i targi wzdłuż trasy.', 'tours.walk.themeHint.mixed': 'Po trochu wszystkiego: atrakcje, muzea, parki, widoki, kawiarnie i targi.', - 'tours.walk.themeHint.own': 'Twój własny wybór kategorii poniżej; motyw ustawi je za ciebie.', 'tours.walk.themeReason': 'Proponowane dla: {theme}', 'tours.walk.arrival': 'Przyjście około {time}', 'tours.walk.arrivalAtLeast': 'Przyjście najwcześniej {time}', diff --git a/shared/src/i18n/ru/tours.ts b/shared/src/i18n/ru/tours.ts index 5c4e0c68b..225d1f160 100644 --- a/shared/src/i18n/ru/tours.ts +++ b/shared/src/i18n/ru/tours.ts @@ -369,7 +369,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'В этом маршруте больше остановок, чем вмещает прогулка.', 'tours.provenance.takenOverFrom': 'Взят из', 'tours.poi.title': 'Вдоль маршрута', - 'tours.poi.intro': 'Места для остановки рядом с маршрутом. Ничего не меняется, пока вы не добавите одно из них.', 'tours.poi.categories': 'Что искать', 'tours.poi.within': 'В пределах этого расстояния от маршрута', 'tours.poi.search': 'Искать вдоль маршрута', @@ -400,7 +399,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '«{name}» теперь остановка; маршрут строится заново.', 'tours.poi.outcome.measured': '«{name}» теперь остановка. Маршрут меняется на:', 'tours.poi.outcome.unmeasured': '«{name}» теперь остановка. Не удалось рассчитать, сколько она добавляет.', - 'tours.poi.undoHint': 'Отмена в редакторе вернёт всё назад.', 'tours.poi.cat.viewpoint': 'Смотровые площадки', 'tours.poi.cat.peak': 'Вершины', 'tours.poi.cat.shelter': 'Хижины и укрытия', @@ -536,7 +534,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Пусто значит пока неизвестно; 0 для точки, которую ты только проходишь.', 'tours.walk.station.stayInvalid': 'Введи целые минуты, не больше суток.', 'tours.walk.station.stayUnknown': 'Пока неизвестно', - 'tours.walk.station.stayUnknownShort': 'Остановка неизвестна', 'tours.walk.station.passThrough': 'Только мимо', 'tours.walk.station.importance': 'Важность', 'tours.walk.station.protected': 'Защищена', @@ -583,7 +580,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Парки, сады и смотровые площадки вдоль маршрута.', 'tours.walk.themeHint.food': 'Кафе, места, где поесть, и рынки вдоль маршрута.', 'tours.walk.themeHint.mixed': 'Всего понемногу: достопримечательности, музеи, парки, виды, кафе и рынки.', - 'tours.walk.themeHint.own': 'Твой собственный выбор категорий ниже; тема задаст их за тебя.', 'tours.walk.themeReason': 'Предложено для темы {theme}', 'tours.walk.arrival': 'Прибытие около {time}', 'tours.walk.arrivalAtLeast': 'Прибытие не раньше {time}', diff --git a/shared/src/i18n/sk/tours.ts b/shared/src/i18n/sk/tours.ts index 5d5ab8c66..35e6098a9 100644 --- a/shared/src/i18n/sk/tours.ts +++ b/shared/src/i18n/sk/tours.ts @@ -351,7 +351,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Táto trasa má viac zastávok, než prechádzka unesie.', 'tours.provenance.takenOverFrom': 'Prevzaté z', 'tours.poi.title': 'Pozdĺž túry', - 'tours.poi.intro': 'Miesta na zastávku blízko trasy. Nič sa nezmení, kým nejaké nepridáš.', 'tours.poi.categories': 'Čo hľadať', 'tours.poi.within': 'Do tejto vzdialenosti od trasy', 'tours.poi.search': 'Hľadať pozdĺž túry', @@ -381,7 +380,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '„{name}“ je teraz zastávka; trasa sa plánuje znova.', 'tours.poi.outcome.measured': '„{name}“ je teraz zastávka. Túra sa mení o:', 'tours.poi.outcome.unmeasured': '„{name}“ je teraz zastávka. Koľko pridáva, sa nepodarilo vypočítať.', - 'tours.poi.undoHint': 'Späť v editore to vráti.', 'tours.poi.cat.viewpoint': 'Vyhliadky', 'tours.poi.cat.peak': 'Vrcholy', 'tours.poi.cat.shelter': 'Chaty a prístrešky', @@ -518,7 +516,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Prázdne znamená zatiaľ neznáme; 0 pre miesto, ktorým len prejdeš.', 'tours.walk.station.stayInvalid': 'Zadaj celé minúty, najviac jeden deň.', 'tours.walk.station.stayUnknown': 'Zatiaľ neznáme', - 'tours.walk.station.stayUnknownShort': 'Pobyt neznámy', 'tours.walk.station.passThrough': 'Len prechod', 'tours.walk.station.importance': 'Dôležitosť', 'tours.walk.station.protected': 'Chránená', @@ -564,7 +561,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parky, záhrady a vyhliadky pozdĺž trasy.', 'tours.walk.themeHint.food': 'Kaviarne, miesta na jedlo a trhy pozdĺž trasy.', 'tours.walk.themeHint.mixed': 'Od všetkého trochu: pamiatky, múzeá, parky, výhľady, kaviarne a trhy.', - 'tours.walk.themeHint.own': 'Tvoj vlastný výber kategórií nižšie; téma ich nastaví za teba.', 'tours.walk.themeReason': 'Navrhnuté pre {theme}', 'tours.walk.arrival': 'Príchod okolo {time}', 'tours.walk.arrivalAtLeast': 'Príchod najskôr {time}', diff --git a/shared/src/i18n/sv/tours.ts b/shared/src/i18n/sv/tours.ts index 7b8d81087..06ccd739c 100644 --- a/shared/src/i18n/sv/tours.ts +++ b/shared/src/i18n/sv/tours.ts @@ -348,7 +348,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Den rutten har fler stopp än en promenad rymmer.', 'tours.provenance.takenOverFrom': 'Hämtad från', 'tours.poi.title': 'Längs turen', - 'tours.poi.intro': 'Platser för ett stopp nära rutten. Inget ändras förrän du lägger till en.', 'tours.poi.categories': 'Vad att leta efter', 'tours.poi.within': 'Inom det här avståndet från rutten', 'tours.poi.search': 'Sök längs turen', @@ -379,7 +378,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '”{name}” är nu ett stopp; rutten planeras om.', 'tours.poi.outcome.measured': '”{name}” är nu ett stopp. Turen ändras med:', 'tours.poi.outcome.unmeasured': '”{name}” är nu ett stopp. Hur mycket det lägger till gick inte att räkna ut.', - 'tours.poi.undoHint': 'Ångra i redigeraren tar tillbaka det.', 'tours.poi.cat.viewpoint': 'Utsiktspunkter', 'tours.poi.cat.peak': 'Toppar', 'tours.poi.cat.shelter': 'Stugor och vindskydd', @@ -516,7 +514,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Tomt betyder ännu okänt; 0 för en punkt du bara passerar.', 'tours.walk.station.stayInvalid': 'Ange hela minuter, högst ett dygn.', 'tours.walk.station.stayUnknown': 'Ännu okänt', - 'tours.walk.station.stayUnknownShort': 'Vistelse okänd', 'tours.walk.station.passThrough': 'Bara förbi', 'tours.walk.station.importance': 'Vikt', 'tours.walk.station.protected': 'Skyddad', @@ -563,7 +560,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Parker, trädgårdar och utsiktspunkter längs rutten.', 'tours.walk.themeHint.food': 'Kaféer, matställen och torghandel längs rutten.', 'tours.walk.themeHint.mixed': 'Lite av allt: sevärdheter, museer, parker, utsikter, kaféer och torghandel.', - 'tours.walk.themeHint.own': 'Ditt eget urval av kategorier nedan; ett tema väljer dem åt dig.', 'tours.walk.themeReason': 'Föreslaget för {theme}', 'tours.walk.arrival': 'Ankomst runt {time}', 'tours.walk.arrivalAtLeast': 'Ankomst tidigast {time}', diff --git a/shared/src/i18n/th/tours.ts b/shared/src/i18n/th/tours.ts index b89535c5e..e998cdaa0 100644 --- a/shared/src/i18n/th/tours.ts +++ b/shared/src/i18n/th/tours.ts @@ -332,7 +332,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'เส้นทางนี้มีจุดแวะมากกว่าที่ทริปเดินเท้าหนึ่งทริปรับได้', 'tours.provenance.takenOverFrom': 'นำมาจาก', 'tours.poi.title': 'ตามเส้นทางทัวร์', - 'tours.poi.intro': 'สถานที่แวะพักใกล้เส้นทาง จะไม่มีอะไรเปลี่ยนจนกว่าคุณจะเพิ่ม', 'tours.poi.categories': 'สิ่งที่ต้องการค้นหา', 'tours.poi.within': 'ภายในระยะนี้จากเส้นทาง', 'tours.poi.search': 'ค้นหาตามเส้นทางทัวร์', @@ -361,7 +360,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” เป็นจุดแวะแล้ว กำลังวางแผนเส้นทางใหม่', 'tours.poi.outcome.measured': '“{name}” เป็นจุดแวะแล้ว ทัวร์เปลี่ยนไป:', 'tours.poi.outcome.unmeasured': '“{name}” เป็นจุดแวะแล้ว คำนวณไม่ได้ว่าเพิ่มขึ้นเท่าไร', - 'tours.poi.undoHint': 'เลิกทำในตัวแก้ไขเพื่อย้อนกลับได้', 'tours.poi.cat.viewpoint': 'จุดชมวิว', 'tours.poi.cat.peak': 'ยอดเขา', 'tours.poi.cat.shelter': 'กระท่อมและที่พักหลบ', @@ -496,7 +494,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'เว้นว่างหมายถึงยังไม่ทราบ ใส่ 0 สำหรับจุดที่แค่เดินผ่าน', 'tours.walk.station.stayInvalid': 'กรอกเป็นนาทีเต็ม ไม่เกินหนึ่งวัน', 'tours.walk.station.stayUnknown': 'ยังไม่ทราบ', - 'tours.walk.station.stayUnknownShort': 'ยังไม่ระบุเวลาแวะ', 'tours.walk.station.passThrough': 'แค่เดินผ่าน', 'tours.walk.station.importance': 'ความสำคัญ', 'tours.walk.station.protected': 'ป้องกันไว้', @@ -538,7 +535,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'สวนสาธารณะ สวน และจุดชมวิวตามเส้นทาง', 'tours.walk.themeHint.food': 'ร้านกาแฟ ร้านอาหาร และตลาดตามเส้นทาง', 'tours.walk.themeHint.mixed': 'มีทุกอย่าง: สถานที่น่าสนใจ พิพิธภัณฑ์ สวน จุดชมวิว ร้านกาแฟ และตลาด', - 'tours.walk.themeHint.own': 'หมวดหมู่ด้านล่างเลือกเอง ธีมจะตั้งค่าให้อัตโนมัติ', 'tours.walk.themeReason': 'แนะนำสำหรับ {theme}', 'tours.walk.arrival': 'ถึงประมาณ {time}', 'tours.walk.arrivalAtLeast': 'ถึงเร็วที่สุด {time}', diff --git a/shared/src/i18n/tr/tours.ts b/shared/src/i18n/tr/tours.ts index dae962abd..0759e4847 100644 --- a/shared/src/i18n/tr/tours.ts +++ b/shared/src/i18n/tr/tours.ts @@ -350,7 +350,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Bu rotada bir yürüyüşün alabileceğinden fazla durak var.', 'tours.provenance.takenOverFrom': 'Alındığı yer', 'tours.poi.title': 'Tur boyunca', - 'tours.poi.intro': 'Rotaya yakın mola yerleri. Birini ekleyene kadar hiçbir şey değişmez.', 'tours.poi.categories': 'Ne aranacak', 'tours.poi.within': 'Rotaya bu mesafe içinde', 'tours.poi.search': 'Tur boyunca ara', @@ -381,7 +380,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” artık bir mola; rota yeniden planlanıyor.', 'tours.poi.outcome.measured': '“{name}” artık bir mola. Tur şu kadar değişiyor:', 'tours.poi.outcome.unmeasured': '“{name}” artık bir mola. Ne kadar eklediği hesaplanamadı.', - 'tours.poi.undoHint': 'Düzenleyicide geri al bunu geri alır.', 'tours.poi.cat.viewpoint': 'Seyir noktaları', 'tours.poi.cat.peak': 'Zirveler', 'tours.poi.cat.shelter': 'Kulübeler ve sığınaklar', @@ -519,7 +517,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Boş, henüz bilinmiyor demektir; yalnızca geçtiğin bir nokta için 0.', 'tours.walk.station.stayInvalid': 'Tam dakika gir, en fazla bir gün.', 'tours.walk.station.stayUnknown': 'Henüz bilinmiyor', - 'tours.walk.station.stayUnknownShort': 'Kalış belirsiz', 'tours.walk.station.passThrough': 'Sadece geçiş', 'tours.walk.station.importance': 'Önem', 'tours.walk.station.protected': 'Korunuyor', @@ -567,7 +564,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.food': 'Rota boyunca kafeler, yemek yerleri ve pazarlar.', 'tours.walk.themeHint.mixed': 'Her şeyden biraz: görülecek yerler, müzeler, parklar, manzaralar, kafeler ve pazarlar.', - 'tours.walk.themeHint.own': 'Aşağıdaki kategorileri kendin seçtin; bir tema onları senin için ayarlar.', 'tours.walk.themeReason': '{theme} için önerildi', 'tours.walk.arrival': 'Varış yaklaşık {time}', 'tours.walk.arrivalAtLeast': 'En erken varış {time}', diff --git a/shared/src/i18n/uk/tours.ts b/shared/src/i18n/uk/tours.ts index d687ad1e4..f77b7cd43 100644 --- a/shared/src/i18n/uk/tours.ts +++ b/shared/src/i18n/uk/tours.ts @@ -364,7 +364,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'У цьому маршруті більше зупинок, ніж уміщує прогулянка.', 'tours.provenance.takenOverFrom': 'Взято з', 'tours.poi.title': 'Уздовж маршруту', - 'tours.poi.intro': 'Місця для зупинки поруч із маршрутом. Нічого не змінюється, доки ви не додасте одне з них.', 'tours.poi.categories': 'Що шукати', 'tours.poi.within': 'У межах цієї відстані від маршруту', 'tours.poi.search': 'Шукати вздовж маршруту', @@ -395,7 +394,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '«{name}» тепер зупинка; маршрут будується заново.', 'tours.poi.outcome.measured': '«{name}» тепер зупинка. Маршрут змінюється на:', 'tours.poi.outcome.unmeasured': '«{name}» тепер зупинка. Не вдалося розрахувати, скільки вона додає.', - 'tours.poi.undoHint': 'Скасування в редакторі поверне все назад.', 'tours.poi.cat.viewpoint': 'Оглядові майданчики', 'tours.poi.cat.peak': 'Вершини', 'tours.poi.cat.shelter': 'Хатини й укриття', @@ -532,7 +530,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Порожньо означає поки невідомо; 0 для точки, яку ти лише проходиш.', 'tours.walk.station.stayInvalid': 'Введи цілі хвилини, не більше доби.', 'tours.walk.station.stayUnknown': 'Поки невідомо', - 'tours.walk.station.stayUnknownShort': 'Перебування невідоме', 'tours.walk.station.passThrough': 'Лише мимо', 'tours.walk.station.importance': 'Важливість', 'tours.walk.station.protected': 'Захищена', @@ -579,7 +576,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Парки, сади та оглядові майданчики вздовж маршруту.', 'tours.walk.themeHint.food': "Кав'ярні, місця, де поїсти, та ринки вздовж маршруту.", 'tours.walk.themeHint.mixed': "Усього потроху: пам'ятки, музеї, парки, краєвиди, кав'ярні та ринки.", - 'tours.walk.themeHint.own': 'Твій власний вибір категорій нижче; тема задасть їх за тебе.', 'tours.walk.themeReason': 'Запропоновано для теми {theme}', 'tours.walk.arrival': 'Прибуття близько {time}', 'tours.walk.arrivalAtLeast': 'Прибуття не раніше {time}', diff --git a/shared/src/i18n/vi/tours.ts b/shared/src/i18n/vi/tours.ts index 1c1474bb9..f03d8076b 100644 --- a/shared/src/i18n/vi/tours.ts +++ b/shared/src/i18n/vi/tours.ts @@ -349,7 +349,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': 'Tuyến này có nhiều điểm dừng hơn mức một lộ trình đi bộ chứa được.', 'tours.provenance.takenOverFrom': 'Lấy từ', 'tours.poi.title': 'Dọc chuyến đi', - 'tours.poi.intro': 'Những nơi để dừng chân gần lộ trình. Không có gì thay đổi cho đến khi bạn thêm một nơi.', 'tours.poi.categories': 'Tìm gì', 'tours.poi.within': 'Trong khoảng cách này tới lộ trình', 'tours.poi.search': 'Tìm dọc chuyến đi', @@ -379,7 +378,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}” giờ là điểm dừng; lộ trình đang được lập lại.', 'tours.poi.outcome.measured': '“{name}” giờ là điểm dừng. Chuyến đi thay đổi:', 'tours.poi.outcome.unmeasured': '“{name}” giờ là điểm dừng. Không tính được nó thêm bao nhiêu.', - 'tours.poi.undoHint': 'Hoàn tác trong trình chỉnh sửa sẽ bỏ thao tác này.', 'tours.poi.cat.viewpoint': 'Điểm ngắm cảnh', 'tours.poi.cat.peak': 'Đỉnh núi', 'tours.poi.cat.shelter': 'Lán và chỗ trú', @@ -516,7 +514,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': 'Để trống nghĩa là chưa biết; nhập 0 cho điểm chỉ đi ngang qua.', 'tours.walk.station.stayInvalid': 'Nhập số phút nguyên, tối đa một ngày.', 'tours.walk.station.stayUnknown': 'Chưa biết', - 'tours.walk.station.stayUnknownShort': 'Chưa rõ thời gian dừng', 'tours.walk.station.passThrough': 'Chỉ đi qua', 'tours.walk.station.importance': 'Mức độ quan trọng', 'tours.walk.station.protected': 'Được bảo vệ', @@ -562,7 +559,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': 'Công viên, khu vườn và điểm ngắm cảnh dọc tuyến đường.', 'tours.walk.themeHint.food': 'Quán cà phê, chỗ ăn uống và chợ dọc tuyến đường.', 'tours.walk.themeHint.mixed': 'Mỗi thứ một chút: điểm tham quan, bảo tàng, công viên, cảnh đẹp, quán cà phê và chợ.', - 'tours.walk.themeHint.own': 'Bạn đang tự chọn danh mục bên dưới; chủ đề sẽ chọn giúp bạn.', 'tours.walk.themeReason': 'Gợi ý cho {theme}', 'tours.walk.arrival': 'Đến khoảng {time}', 'tours.walk.arrivalAtLeast': 'Đến sớm nhất lúc {time}', diff --git a/shared/src/i18n/zh-TW/tours.ts b/shared/src/i18n/zh-TW/tours.ts index 523f99bc2..f29bc7664 100644 --- a/shared/src/i18n/zh-TW/tours.ts +++ b/shared/src/i18n/zh-TW/tours.ts @@ -317,7 +317,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': '這條路線的站點比一次漫步能容納的更多。', 'tours.provenance.takenOverFrom': '引入自', 'tours.poi.title': '沿途', - 'tours.poi.intro': '靠近路線的停留地點。在你新增之前,不會有任何變動。', 'tours.poi.categories': '要找什麼', 'tours.poi.within': '距路線的範圍', 'tours.poi.search': '沿途搜尋', @@ -346,7 +345,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '「{name}」已成為停留點,正在重新規劃路線。', 'tours.poi.outcome.measured': '「{name}」已成為停留點。路線變化:', 'tours.poi.outcome.unmeasured': '「{name}」已成為停留點。無法計算它增加了多少。', - 'tours.poi.undoHint': '在編輯器中復原即可恢復。', 'tours.poi.cat.viewpoint': '觀景點', 'tours.poi.cat.peak': '山頂', 'tours.poi.cat.shelter': '山屋與避難處', @@ -476,7 +474,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': '留空表示尚不清楚;只經過的地點填 0。', 'tours.walk.station.stayInvalid': '請輸入整數分鐘,最多一天。', 'tours.walk.station.stayUnknown': '尚不清楚', - 'tours.walk.station.stayUnknownShort': '停留時間未定', 'tours.walk.station.passThrough': '僅經過', 'tours.walk.station.importance': '重要程度', 'tours.walk.station.protected': '受保護', @@ -518,7 +515,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': '沿途的公園、花園和觀景點。', 'tours.walk.themeHint.food': '沿途的咖啡館、餐館和市集。', 'tours.walk.themeHint.mixed': '各類都有一些:景點、博物館、公園、景觀、咖啡館和市集。', - 'tours.walk.themeHint.own': '下方類別為你自選;選擇主題會自動設定。', 'tours.walk.themeReason': '推薦用於{theme}', 'tours.walk.arrival': '約 {time} 抵達', 'tours.walk.arrivalAtLeast': '最早 {time} 抵達', diff --git a/shared/src/i18n/zh/tours.ts b/shared/src/i18n/zh/tours.ts index 8331787d5..da99ca533 100644 --- a/shared/src/i18n/zh/tours.ts +++ b/shared/src/i18n/zh/tours.ts @@ -318,7 +318,6 @@ const tours: TranslationStrings = { 'tours.walkImport.refusal.too_many_stops': '这条路线的站点比一次漫步能容纳的更多。', 'tours.provenance.takenOverFrom': '引入自', 'tours.poi.title': '沿途', - 'tours.poi.intro': '靠近路线的停留地点。在你添加之前,不会有任何改动。', 'tours.poi.categories': '要找什么', 'tours.poi.within': '距路线的范围', 'tours.poi.search': '沿途搜索', @@ -347,7 +346,6 @@ const tours: TranslationStrings = { 'tours.poi.outcome.measuring': '“{name}”已成为停留点,正在重新规划路线。', 'tours.poi.outcome.measured': '“{name}”已成为停留点。路线变化:', 'tours.poi.outcome.unmeasured': '“{name}”已成为停留点。无法计算它增加了多少。', - 'tours.poi.undoHint': '在编辑器中撤销即可恢复。', 'tours.poi.cat.viewpoint': '观景点', 'tours.poi.cat.peak': '山顶', 'tours.poi.cat.shelter': '小屋与避难所', @@ -477,7 +475,6 @@ const tours: TranslationStrings = { 'tours.walk.station.stayHint': '留空表示尚不清楚;只经过的地点填 0。', 'tours.walk.station.stayInvalid': '请输入整数分钟,最多一天。', 'tours.walk.station.stayUnknown': '尚不清楚', - 'tours.walk.station.stayUnknownShort': '停留时间未定', 'tours.walk.station.passThrough': '仅经过', 'tours.walk.station.importance': '重要程度', 'tours.walk.station.protected': '受保护', @@ -519,7 +516,6 @@ const tours: TranslationStrings = { 'tours.walk.themeHint.parks': '沿途的公园、花园和观景点。', 'tours.walk.themeHint.food': '沿途的咖啡馆、餐馆和集市。', 'tours.walk.themeHint.mixed': '各类都有一些:景点、博物馆、公园、景观、咖啡馆和集市。', - 'tours.walk.themeHint.own': '下方类别为你自选;选择主题会自动设置。', 'tours.walk.themeReason': '推荐用于{theme}', 'tours.walk.arrival': '约 {time} 到达', 'tours.walk.arrivalAtLeast': '最早 {time} 到达', diff --git a/shared/src/tours/tour-poi-category.ts b/shared/src/tours/tour-poi-category.ts new file mode 100644 index 000000000..3b4e03b13 --- /dev/null +++ b/shared/src/tours/tour-poi-category.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +/** What can be looked for along a tour on foot, by bike or on the water. */ +export const TOUR_POI_CATEGORIES = [ + 'viewpoint', + 'peak', + 'shelter', + 'drinking_water', + 'food', + 'toilets', + 'picnic', + 'campsite', + 'sights', + 'slipway', + 'boat_rental', + 'bike_repair', + 'museum', + 'park', + 'cafe', + 'restaurant', + 'market', +] as const; +export const tourPoiCategorySchema = z.enum(TOUR_POI_CATEGORIES); +export type TourPoiCategory = z.infer; diff --git a/shared/src/tours/tour-poi-search.schema.ts b/shared/src/tours/tour-poi-search.schema.ts index 2781dc4ce..d68ac70ee 100644 --- a/shared/src/tours/tour-poi-search.schema.ts +++ b/shared/src/tours/tour-poi-search.schema.ts @@ -1,3 +1,4 @@ +import { tourPoiCategorySchema, type TourPoiCategory } from './tour-poi-category'; import { plannableTourTypeSchema, type PlannableTourType } from './tours.schema'; import { z } from 'zod'; @@ -8,28 +9,9 @@ import { z } from 'zod'; * the line as drawn, never a detour someone walked or paddled. */ -/** What can be looked for along a tour on foot, by bike or on the water. */ -export const TOUR_POI_CATEGORIES = [ - 'viewpoint', - 'peak', - 'shelter', - 'drinking_water', - 'food', - 'toilets', - 'picnic', - 'campsite', - 'sights', - 'slipway', - 'boat_rental', - 'bike_repair', - 'museum', - 'park', - 'cafe', - 'restaurant', - 'market', -] as const; -export const tourPoiCategorySchema = z.enum(TOUR_POI_CATEGORIES); -export type TourPoiCategory = z.infer; +// The categories live in a module of their own, so the tour contract can name +// one on a waypoint without importing this one, which imports it. +export { TOUR_POI_CATEGORIES, tourPoiCategorySchema, type TourPoiCategory } from './tour-poi-category'; /** The categories each planned tour type offers, the most asked first. */ export const TOUR_POI_CATEGORIES_BY_TYPE: Record = { diff --git a/shared/src/tours/tours.schema.ts b/shared/src/tours/tours.schema.ts index 6133835ac..5466227c8 100644 --- a/shared/src/tours/tours.schema.ts +++ b/shared/src/tours/tours.schema.ts @@ -5,6 +5,7 @@ import { portageSpeedKmhSchema, trackMarksSchema, } from './paddle-plan.schema'; +import { tourPoiCategorySchema } from './tour-poi-category'; import { tourOriginSchema, tourProvenanceSchema } from './tour-provenance.schema'; import { z } from 'zod'; @@ -360,6 +361,12 @@ export const tourWaypointSchema = z.object({ protected: z.boolean().optional(), /** CW-04: the station's opening hours as OpenStreetMap writes them, where it was found there. */ opening_hours: z.string().trim().max(TOUR_STATION_HOURS_MAX).nullable().optional(), + /** + * PLN-03: the kind of place the point was taken in from, found along the + * tour (a restaurant, a museum), so its row carries that kind's icon as the + * map drew the place. Null on a point set by hand. + */ + poi_category: tourPoiCategorySchema.nullable().optional(), }); export type TourWaypoint = z.infer;