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;