diff --git a/client/src/components/Collections/CollectionFilterBar.test.tsx b/client/src/components/Collections/CollectionFilterBar.test.tsx index da13a5a21..bc63e3c55 100644 --- a/client/src/components/Collections/CollectionFilterBar.test.tsx +++ b/client/src/components/Collections/CollectionFilterBar.test.tsx @@ -131,4 +131,16 @@ describe('CollectionFilterBar', () => { render(); expect(screen.queryByRole('button', { name: 'Select' })).not.toBeInTheDocument(); }); + + it('FE-COMP-COLFILTERBAR-009: the active Select button marks itself with `on`, not `open`', () => { + // `.open` belongs to the dropdowns, where it means the panel is down and the + // label stays dark. Sharing that class made the active button paint a dark + // icon on its dark accent fill, because the dropdown rule sits later in the + // stylesheet and both have the same specificity. + render(); + const selectBtn = screen.getByRole('button', { name: 'Select' }); + expect(selectBtn).toHaveClass('on'); + expect(selectBtn).not.toHaveClass('open'); + expect(selectBtn).toHaveAttribute('aria-pressed', 'true'); + }); }); diff --git a/client/src/components/Collections/CollectionFilterBar.tsx b/client/src/components/Collections/CollectionFilterBar.tsx index 2dec032b7..21b262ff4 100644 --- a/client/src/components/Collections/CollectionFilterBar.tsx +++ b/client/src/components/Collections/CollectionFilterBar.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useRef, useState } from 'react' -import { ChevronDown, Check, Layers, Tag, Tags, CheckSquare, Star, Plus, ArrowDownUp } from 'lucide-react' +import { ChevronDown, Check, Layers, Tag, CheckSquare, Star, Plus, ArrowDownUp } from 'lucide-react' import type { StatusFilter, CollectionSortMode } from '../../store/collectionStore' import type { TranslationFn } from '../../types' import { getCategoryIcon } from '../shared/categoryIcons' import { STATUS_META, STATUS_ORDER } from '../../pages/collections/collectionsModel' import type { CategoryOption, LabelOption } from '../../pages/collections/collectionsModel' +import CollectionLabelFilter from './CollectionLabelFilter' interface Opt { key: string | number @@ -148,36 +149,26 @@ export default function CollectionFilterBar({ onRatingFilter(k as number | 'all')} lead={} /> onSortMode(k as CollectionSortMode)} lead={} /> {showSelect && ( - )} {showLabels && (labelOptions.length > 0 || canManageLabels) && ( -
- {labelOptions.map(l => { - const on = labelFilter.includes(l.id) - return ( - - ) - })} - {canManageLabels && ( - - )} -
+ )} ) diff --git a/client/src/components/Collections/CollectionLabelFilter.tsx b/client/src/components/Collections/CollectionLabelFilter.tsx new file mode 100644 index 000000000..f124b078d --- /dev/null +++ b/client/src/components/Collections/CollectionLabelFilter.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import { Plus, Settings2, Tags } from 'lucide-react' +import type { LabelOption } from '../../pages/collections/collectionsModel' +import type { TranslationFn } from '../../types' + +export type { LabelOption } + +interface CollectionLabelFilterProps { + labelOptions: LabelOption[] + labelFilter: number[] + onLabelFilter: (ids: number[]) => void + canManageLabels?: boolean + onManageLabels?: () => void + /** + * 'bar' is the filter row above the list, 'map' the strip floating over the + * map, which needs the same glass treatment as the buttons beside it. + */ + variant?: 'bar' | 'map' + t: TranslationFn +} + +/** + * The label filter, in one place because it has two homes. + * + * Labels are an axis of their own next to status, category and rating, so they + * are grouped into a tray instead of joining the row as more loose buttons. On + * a desktop it rides in the map's top bar, where there is room to spare; the + * filter row keeps it whenever no map is on screen, so it never becomes + * unreachable. + */ +export default function CollectionLabelFilter({ + labelOptions, labelFilter, onLabelFilter, canManageLabels = false, onManageLabels, + variant = 'bar', t, +}: CollectionLabelFilterProps): React.ReactElement { + return ( +
+
+ ) +} diff --git a/client/src/components/Collections/CollectionMapPanel.tsx b/client/src/components/Collections/CollectionMapPanel.tsx index 718f09de5..4658da62a 100644 --- a/client/src/components/Collections/CollectionMapPanel.tsx +++ b/client/src/components/Collections/CollectionMapPanel.tsx @@ -3,6 +3,7 @@ import { PanelLeftClose, PanelLeftOpen, Search } from 'lucide-react' import type { CollectionPlace } from '@trek/shared' import type { TranslationFn } from '../../types' import CollectionMap from './CollectionMap' +import CollectionLabelFilter, { type LabelOption } from './CollectionLabelFilter' interface CollectionMapPanelProps { places: CollectionPlace[] @@ -17,6 +18,16 @@ interface CollectionMapPanelProps { onToggleView: () => void search: string onSearch: (v: string) => void + /** + * The label filter rides along in the map's top bar, where there is room for + * it. The filter row keeps it whenever no map is on screen, so it can never + * become unreachable. + */ + labelOptions?: LabelOption[] + labelFilter?: number[] + onLabelFilter?: (ids: number[]) => void + canManageLabels?: boolean + onManageLabels?: () => void t: TranslationFn } @@ -27,8 +38,10 @@ interface CollectionMapPanelProps { */ export default function CollectionMapPanel({ places, selectedPlaceId, onSelect, onDeselect, dark, overlay, view, onToggleView, - search, onSearch, t, + search, onSearch, labelOptions = [], labelFilter = [], onLabelFilter, canManageLabels = false, + onManageLabels, t, }: CollectionMapPanelProps): React.ReactElement { + const showLabels = onLabelFilter != null && (labelOptions.length > 0 || canManageLabels) return (
{view === 'map' ? : } + {showLabels && ( + + )}
diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 6a1a9d04d..06b6d137b 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -43,7 +43,7 @@ import { DayPlanSidebarTransportDetailModal } from './DayPlanSidebarTransportDet import { TransitTitle, TransitLegChips, TransitItineraryInline } from './transitDisplay' import { DayPlanSidebarFooter } from './DayPlanSidebarFooter' import type { Trip, Day, Place, Category, Assignment, Accommodation, Reservation, AssignmentsMap, RouteResult, RouteSegment, DayNote } from '../../types' -import { getGoogleMapsUrlForPlace } from './placeGoogleMaps' +import { getNavigationTargets, openNavigationTarget } from './placeNavigation' interface DayPlanSidebarProps { tripId: number @@ -1701,12 +1701,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP onDragEnd={() => { setDraggingId(null); setDragOverDayId(null); setDropTargetKey(null); dragDataRef.current = null }} onClick={() => { onPlaceClick(isPlaceSelected ? null : place.id, isPlaceSelected ? null : assignment.id); if (!isPlaceSelected) onSelectDay(day.id, true) }} onContextMenu={e => { - const googleMapsUrl = getGoogleMapsUrlForPlace(place) + // Flat entries, one per app: the menu has room to + // grow and a submenu would need a component that + // does not exist here. + const navTargets = getNavigationTargets(place) ctxMenu.open(e, [ canEditDays && onEditPlace && { label: t('common.edit'), icon: Pencil, onClick: () => onEditPlace(place, assignment.id) }, canEditDays && onRemoveAssignment && { label: t('planner.removeFromDay'), icon: Trash2, onClick: () => onRemoveAssignment(day.id, assignment.id) }, place.website && { label: t('inspector.website'), icon: ExternalLink, onClick: () => window.open(place.website, '_blank') }, - googleMapsUrl && { label: t('inspector.google'), icon: Navigation, onClick: () => window.open(googleMapsUrl, '_blank') }, + ...navTargets.map(target => ({ label: target.label, icon: Navigation, onClick: () => openNavigationTarget(target) })), collectionsEnabled && { label: t('inspector.saveToCollection'), icon: Bookmark, onClick: () => useSaveToCollectionStore.getState().open(placeToSaveTarget(place)) }, { divider: true }, canEditDays && onDeletePlace && { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => onDeletePlace(place.id) }, diff --git a/client/src/components/Planner/PlaceInspector.test.tsx b/client/src/components/Planner/PlaceInspector.test.tsx index 8790ca3e8..0dcf4cc0a 100644 --- a/client/src/components/Planner/PlaceInspector.test.tsx +++ b/client/src/components/Planner/PlaceInspector.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor, fireEvent, act } from '../../../tests/helpers/render'; +import { render, screen, waitFor, fireEvent, act, within } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { buildUser, buildTrip, buildPlace, buildCategory, buildReservation } from '../../../tests/helpers/factories'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; @@ -632,13 +632,18 @@ describe('PlaceInspector', () => { // ── Google Maps URL action ───────────────────────────────────────────────── - it('FE-PLANNER-INSPECTOR-043: Google Maps lat/lng button visible when no google_maps_url', () => { + it('FE-PLANNER-INSPECTOR-043: the navigation button offers every app the place can be opened in', async () => { + const user = userEvent.setup(); render(); - // place has lat/lng so Google Maps button should appear with Navigation icon - const allButtons = screen.getAllByRole('button'); - // Find button containing "Google Maps" text - const mapsBtn = allButtons.find(btn => btn.textContent?.includes('Google Maps')); - expect(mapsBtn).toBeTruthy(); + // A place with coordinates reaches Google Maps and Waze (Apple Maps only on + // Apple platforms), so the button collects them behind one entry. + const navBtn = screen.getAllByRole('button').find(btn => btn.textContent?.includes('Navigation'))!; + expect(navBtn).toBeTruthy(); + + await user.click(navBtn); + const menu = await screen.findByRole('menu'); + expect(within(menu).getByRole('menuitem', { name: 'Google Maps' })).toBeInTheDocument(); + expect(within(menu).getByRole('menuitem', { name: 'Waze' })).toBeInTheDocument(); }); it('FE-PLANNER-INSPECTOR-043b: Google Maps action uses google_ftid over coordinates', async () => { @@ -651,9 +656,10 @@ describe('PlaceInspector', () => { lng: -80.5542617, google_ftid: '0x882bf179e806d471:0x8591dde29c821a93', })} />); - const mapsBtn = screen.getAllByRole('button').find(btn => btn.textContent?.includes('Google Maps'))!; - await user.click(mapsBtn); - expect(openSpy).toHaveBeenCalledWith(mapsUrl, '_blank'); + const navBtn = screen.getAllByRole('button').find(btn => btn.textContent?.includes('Navigation'))!; + await user.click(navBtn); + await user.click(await screen.findByRole('menuitem', { name: 'Google Maps' })); + expect(openSpy).toHaveBeenCalledWith(mapsUrl, '_blank', 'noopener,noreferrer'); openSpy.mockRestore(); }); @@ -934,12 +940,17 @@ describe('PlaceInspector', () => { // ── Footer actions ─────────────────────────────────────────────────────────── - it('FE-PLANNER-INSPECTOR-069: the OpenStreetMap and website buttons open their targets in a new tab', () => { + it('FE-PLANNER-INSPECTOR-069: OpenStreetMap sits in the navigation menu, the website keeps its own button', async () => { + const user = userEvent.setup(); const open = vi.fn(); vi.stubGlobal('open', open); const p = buildPlace({ id: 705, name: 'Linked Place', osm_id: 'node/12345', website: 'https://example.org' }); render(); - fireEvent.click(screen.getByText('OpenStreetMap').closest('button')!); + + // OSM moved in with the other map apps rather than standing beside them. + await user.click(screen.getAllByRole('button').find(b => b.textContent?.includes('Navigation'))!); + await user.click(await screen.findByRole('menuitem', { name: 'OpenStreetMap' })); + fireEvent.click(screen.getByText('Open Website').closest('button')!); expect(open).toHaveBeenCalledTimes(2); expect(open.mock.calls[1]).toEqual(['https://example.org', '_blank']); diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx index d7ad08ffe..8d53e9567 100644 --- a/client/src/components/Planner/PlaceInspector.tsx +++ b/client/src/components/Planner/PlaceInspector.tsx @@ -27,8 +27,8 @@ import type { CollectionStatus } from '@trek/shared' import { splitReservationDateTime, formatTime, formatMoney } from '../../utils/formatters' import { useTripStore } from '../../store/tripStore' import { formatDistance, formatElevation } from '../../utils/units' -import { getGoogleMapsUrlForPlace } from './placeGoogleMaps' -import { getOpenStreetMapUrlForPlace } from './placeOpenStreetMap' +import { getNavigationTargets, openNavigationTarget } from './placeNavigation' +import { NavigationMenu } from '../shared/NavigationMenu' import { resolveOpenNow, resolvePlaceTimeZone, placeWeekdayIndex } from './placeOpenState' import { convertHoursLine } from './placeHoursFormat' @@ -178,6 +178,8 @@ export default function PlaceInspector({ // Extra native rows contributed by placeDetailProvider plugins (#1429). Fail-safe: // any provider error/timeout is dropped server-side, so this only ever adds rows. const [providerDetails, setProviderDetails] = useState }>>([]) + const [navOpen, setNavOpen] = useState(false) + const navBtnRef = useRef(null) const placeIdForDetails = mode === 'trip' ? place?.id : undefined useEffect(() => { if (placeIdForDetails == null) { setProviderDetails([]); return } @@ -316,11 +318,10 @@ export default function PlaceInspector({ googleDetails?.open_now, ) // Prefer the place's stored ftid; if it has none yet, use the one just fetched from Google. - const googleMapsUrl = getGoogleMapsUrlForPlace( + const navigationTargets = getNavigationTargets( place ? { ...place, google_ftid: place.google_ftid || googleDetails?.google_ftid || null } : null, googleDetails?.google_maps_url, ) - const openStreetMapUrl = getOpenStreetMapUrlForPlace(place) const selectedDay = days?.find(d => d.id === selectedDayId) const weekdayIndex = getWeekdayIndex(selectedDay?.date, placeTimeZone) @@ -484,13 +485,32 @@ export default function PlaceInspector({ icon={savedInCollection ? : } label={{savedInCollection ? t('inspector.savedToCollection') : t('inspector.saveToCollection')}} /> )} - {googleMapsUrl && ( - window.open(googleMapsUrl, '_blank')} variant="ghost" icon={} - label={{t('inspector.google')}} /> - )} - {openStreetMapUrl && ( - window.open(openStreetMapUrl, '_blank')} variant="ghost" icon={} - label={{t('inspector.openStreetMap')}} /> + {navigationTargets.length > 0 && ( + <> + {/* One target left (a place without coordinates) opens straight + away, exactly as this button always did. */} + { + if (navigationTargets.length === 1) openNavigationTarget(navigationTargets[0]) + else setNavOpen(o => !o) + }} + variant="ghost" + icon={} + label={ + + {navigationTargets.length === 1 ? navigationTargets[0].label : t('inspector.navigation')} + + } + /> + {navOpen && ( + setNavOpen(false)} + /> + )} + )} {(place.website || googleDetails?.website) && ( window.open(place.website || googleDetails?.website, '_blank')} variant="ghost" icon={} @@ -535,9 +555,11 @@ interface ActionButtonProps { variant: 'primary' | 'ghost' | 'danger' icon: React.ReactNode label: React.ReactNode + /** For callers that anchor a popup to the button. React 19 passes it through. */ + ref?: React.Ref } -export function ActionButton({ onClick, variant, icon, label }: ActionButtonProps) { +export function ActionButton({ onClick, variant, icon, label, ref }: ActionButtonProps) { const base = { primary: { background: 'var(--accent)', color: 'var(--accent-text)', border: 'none', hoverBg: 'var(--text-secondary)' }, ghost: { background: 'var(--bg-hover)', color: 'var(--text-secondary)', border: 'none', hoverBg: 'var(--bg-tertiary)' }, @@ -546,6 +568,7 @@ export function ActionButton({ onClick, variant, icon, label }: ActionButtonProp const s = base[variant] || base.ghost return ( - {/* Star sort (#1435): order the list by average rating, best first. */} - + } + {/* Minimum-stars filter (#1435), same floors as the collections bar. */} + + {starDropOpen && ( +
+ {(['all', 5, 4, 3, 2, 1] as const).map(opt => { + const active = ratingFilter === opt + return ( + + ) + })} +
+ )} {canEditPlaces && ( + ))} +
, + document.body, + ) +} diff --git a/client/src/mobile/screens/trip/sheets/MPlaceSheet.tsx b/client/src/mobile/screens/trip/sheets/MPlaceSheet.tsx index 3d8cb10e6..544a70b3b 100644 --- a/client/src/mobile/screens/trip/sheets/MPlaceSheet.tsx +++ b/client/src/mobile/screens/trip/sheets/MPlaceSheet.tsx @@ -18,7 +18,8 @@ import TrackColorPicker from '../../../../components/shared/TrackColorPicker' import { resolveTrackColor, inheritedTrackColor } from '../../../../components/Map/trackColors' import { avatarSrc } from '../../../../utils/avatarSrc' import { openFile } from '../../../../utils/fileDownload' -import { getGoogleMapsUrlForPlace } from '../../../../components/Planner/placeGoogleMaps' +import { getNavigationTargets, openNavigationTarget } from '../../../../components/Planner/placeNavigation' +import { NavigationMenu } from '../../../../components/shared/NavigationMenu' import type { Assignment, Day, TripMember } from '../../../../types' import { ActionCircle, Eyebrow, INNER_CLS } from './MTripSheetUi' @@ -43,6 +44,8 @@ export default function MPlaceSheet({ planner, shell }: MTripSheetsProps) { const [dayPickerOpen, setDayPickerOpen] = useState(false) const [participantPickerOpen, setParticipantPickerOpen] = useState(false) const [uploading, setUploading] = useState(false) + const [navOpen, setNavOpen] = useState(false) + const navBtnRef = useRef(null) const [imgBusy, setImgBusy] = useState(false) const [colorPickerOpen, setColorPickerOpen] = useState(false) const fileInputRef = useRef(null) @@ -199,9 +202,13 @@ export default function MPlaceSheet({ planner, shell }: MTripSheetsProps) { planner.setFitKey(k => k + 1) } + // One app installed-and-offered means no picker: the tap opens it, which is + // what this button has always done. The sheet only appears when there is an + // actual choice to make. + const navTargets = getNavigationTargets(place) const openDirections = () => { - const url = getGoogleMapsUrlForPlace(place) - if (url) window.open(url, '_blank', 'noopener,noreferrer') + if (navTargets.length === 1) openNavigationTarget(navTargets[0]) + else if (navTargets.length > 1) setNavOpen(true) } return ( @@ -496,10 +503,24 @@ export default function MPlaceSheet({ planner, shell }: MTripSheetsProps) { )} - {place.lat != null && place.lng != null && ( - - - + {navTargets.length > 0 && ( + <> + + + + {navOpen && ( + setNavOpen(false)} + title={t('inspector.openWith')} + /> + )} + )} {place.lat != null && place.lng != null && ( diff --git a/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx b/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx index 156587a5e..6ce637da2 100644 --- a/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx +++ b/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react' +import { ReactNode, Ref } from 'react' import { X } from 'lucide-react' import MIconBtn from '../../../components/MIconBtn' import { formatTime } from '../../../../utils/formatters' @@ -88,12 +88,15 @@ interface ActionCircleProps { danger?: boolean className?: string children: ReactNode + /** For callers that anchor a popup to the button. React 19 passes it through. */ + ref?: Ref } /** 38px round action button of the inspector footer rows. */ -export function ActionCircle({ onClick, label, primary = false, danger = false, className = '', children }: ActionCircleProps) { +export function ActionCircle({ onClick, label, primary = false, danger = false, className = '', children, ref }: ActionCircleProps) { return ( - ) - })} +
+ + {label} + +
+ {members.map(m => { + const on = active.has(m.id) + return ( + + ) + })} +
+ {filtering && allLabel && onClear && ( + + )}
) } diff --git a/client/src/pages/CollectionsPage.test.tsx b/client/src/pages/CollectionsPage.test.tsx index c67620953..1e7634f63 100644 --- a/client/src/pages/CollectionsPage.test.tsx +++ b/client/src/pages/CollectionsPage.test.tsx @@ -69,6 +69,7 @@ function makeHook(overrides: Record = {}) { places: [{ id: 10 }], visiblePlaces: [{ id: 10 }], mappable: [{ id: 10 }], + hasMappable: true, members: [], incomingInvites: [], counts: { all: 1, idea: 0, want: 1, visited: 0 }, @@ -124,8 +125,25 @@ describe('CollectionsPage — Add place button reachability (#1485)', () => { }) it('shows the Add button on an empty collection', () => { - mockUseCollections.mockReturnValue(makeHook({ places: [], visiblePlaces: [], mappable: [] })) + mockUseCollections.mockReturnValue(makeHook({ places: [], visiblePlaces: [], mappable: [], hasMappable: false })) render() expect(screen.getAllByRole('button', { name: 'collections.addPlace' })).toHaveLength(1) }) }) + +describe('CollectionsPage — the map survives a filter that matches nothing', () => { + it('keeps the map when a label filter empties the list', () => { + // Clicking a label with no places behind it left visiblePlaces empty, and + // the layout hung off exactly that: the map was torn out and the page + // reflowed to full width. A filter should empty the map, not remove it. + mockUseCollections.mockReturnValue(makeHook({ visiblePlaces: [], mappable: [], hasMappable: true })) + render() + expect(screen.getByTestId('map')).toBeInTheDocument() + }) + + it('still hides the map for a list with nothing mappable in it at all', () => { + mockUseCollections.mockReturnValue(makeHook({ mappable: [], hasMappable: false })) + render() + expect(screen.queryByTestId('map')).toBeNull() + }) +}) diff --git a/client/src/pages/CollectionsPage.tsx b/client/src/pages/CollectionsPage.tsx index ccbe9c8a9..5ac3015f9 100644 --- a/client/src/pages/CollectionsPage.tsx +++ b/client/src/pages/CollectionsPage.tsx @@ -59,8 +59,8 @@ function CollectionsPageDesktop(): React.ReactElement { // + detail come into view alongside the map. const onMapSelect = (id: number) => { openPlace(id); if (c.view === 'map') c.setView('list') } - const desktopSplit = c.isWide && mappable.length > 0 - const mapShown = mappable.length > 0 && (c.view === 'map' || c.isWide) + const desktopSplit = c.isWide && c.hasMappable + const mapShown = c.hasMappable && (c.view === 'map' || c.isWide) const mapOverlay = c.isWide && mapShown // the map carries the toggle + search const canAddPlace = typeof c.activeId === 'number' && c.canEdit // a real list you can edit @@ -79,6 +79,11 @@ function CollectionsPageDesktop(): React.ReactElement { ) const mapPanel = (overlay: boolean) => ( c.setShowLabelManager(true)} places={mappable} selectedPlaceId={c.selectedPlaceId} onSelect={onMapSelect} @@ -109,7 +114,7 @@ function CollectionsPageDesktop(): React.ReactElement { onSortMode={c.setSortMode} canAddPlace={canAddPlace} onAddPlace={() => c.setShowAddPlace(true)} - showLabels={isRealList} + showLabels={isRealList && !mapShown} labelOptions={c.labelOptions} labelFilter={c.labelFilter} onLabelFilter={c.setLabelFilter} @@ -151,7 +156,7 @@ function CollectionsPageDesktop(): React.ReactElement {
{mapPanel(true)}
) - } else if (c.view === 'map' && mappable.length > 0) { + } else if (c.view === 'map' && c.hasMappable) { body =
{mapPanel(false)}
} else { body = listColumn @@ -212,7 +217,7 @@ function CollectionsPageDesktop(): React.ReactElement { - {!c.isWide && mappable.length > 0 && ( + {!c.isWide && c.hasMappable && (
} @@ -206,6 +208,7 @@ function makeHook(overrides: Hook = {}): Hook { places: [{ id: 10 }], visiblePlaces: [{ id: 10 }], mappable: [{ id: 10 }], + hasMappable: true, members: [], incomingInvites: [], counts: { all: 1, idea: 0, want: 1, visited: 0 }, @@ -340,7 +343,7 @@ describe('CollectionsPage — shell', () => { }) it('FE-PAGE-COLLPAGE-007: shows a spinner while the first places load', () => { - renderPage({ places: [], visiblePlaces: [], mappable: [], placesLoading: true }) + renderPage({ places: [], visiblePlaces: [], mappable: [], hasMappable: false, placesLoading: true }) expect(document.querySelector('.col-spinner')).not.toBeNull() expect(screen.queryByTestId('list')).toBeNull() }) @@ -348,7 +351,7 @@ describe('CollectionsPage — shell', () => { describe('CollectionsPage — body branches', () => { it('FE-PAGE-COLLPAGE-008: an empty list shows the empty state plus the add CTA', () => { - const hook = renderPage({ places: [], visiblePlaces: [], mappable: [] }) + const hook = renderPage({ places: [], visiblePlaces: [], mappable: [], hasMappable: false }) expect(screen.getByTestId('empty-collections')).toHaveTextContent('collections.empty.title') fireEvent.click(screen.getByRole('button', { name: /collections.addPlace/ })) @@ -356,7 +359,7 @@ describe('CollectionsPage — body branches', () => { }) it('FE-PAGE-COLLPAGE-009: a viewer on an empty list gets no add CTA', () => { - renderPage({ places: [], visiblePlaces: [], mappable: [], canEdit: false }) + renderPage({ places: [], visiblePlaces: [], mappable: [], hasMappable: false, canEdit: false }) expect(screen.getByTestId('empty-collections')).toBeInTheDocument() expect(screen.queryByRole('button', { name: /collections.addPlace/ })).toBeNull() }) @@ -384,13 +387,13 @@ describe('CollectionsPage — body branches', () => { }) it('FE-PAGE-COLLPAGE-013: places without coordinates keep the list column even on a wide layout', () => { - renderPage({ mappable: [] }) + renderPage({ mappable: [], hasMappable: false }) expect(screen.getByTestId('list')).toBeInTheDocument() expect(screen.queryByTestId('map-overlay')).toBeNull() }) it('FE-PAGE-COLLPAGE-014: a filter that matches nothing keeps the filter bar and shows the no-match state', () => { - renderPage({ visiblePlaces: [], mappable: [] }) + renderPage({ visiblePlaces: [], mappable: [], hasMappable: false }) expect(screen.getByTestId('filterbar')).toBeInTheDocument() expect(screen.getByTestId('empty-search')).toHaveTextContent('collections.empty.noMatchTitle') expect(screen.queryByTestId('list')).toBeNull() @@ -555,20 +558,32 @@ describe('CollectionsPage — child wiring', () => { expect(hook.setSelectedPlaceId).not.toHaveBeenCalled() }) - it('FE-PAGE-COLLPAGE-030: the filter bar drives add-place, label management and select mode', () => { + it('FE-PAGE-COLLPAGE-030: the filter bar drives add-place and select mode, the map takes the labels', () => { const hook = renderPage() - expect(screen.getByTestId('filter-flags')).toHaveTextContent('true|true|true') + // Labels ride in the map's top bar while a map is on screen, so the filter + // row is told not to draw them a second time. + expect(screen.getByTestId('filter-flags')).toHaveTextContent('false|true|true') + expect(screen.getByTestId('map-has-labels')).toHaveTextContent('true') fireEvent.click(screen.getByText('filter-add')) expect(hook.setShowAddPlace).toHaveBeenCalledWith(true) - fireEvent.click(screen.getByText('filter-labels')) + fireEvent.click(screen.getByText('map-labels')) expect(hook.setShowLabelManager).toHaveBeenCalledWith(true) fireEvent.click(screen.getByText('filter-select')) expect(hook.setSelectMode).toHaveBeenCalledWith(true) }) + it('FE-PAGE-COLLPAGE-030b: without a map the filter row keeps the labels, so they stay reachable', () => { + const hook = renderPage({ mappable: [], hasMappable: false }) + expect(screen.getByTestId('filter-flags')).toHaveTextContent('true|true|true') + expect(screen.queryByTestId('map-overlay')).toBeNull() + + fireEvent.click(screen.getByText('filter-labels')) + expect(hook.setShowLabelManager).toHaveBeenCalledWith(true) + }) + it('FE-PAGE-COLLPAGE-031: the All-saved union has no labels and no add button in the filter row', () => { renderPage({ isAllSaved: true, activeId: 'all', activeCollection: null }) expect(screen.getByTestId('filter-flags')).toHaveTextContent('false|false|true') diff --git a/client/src/pages/collections/useCollections.ts b/client/src/pages/collections/useCollections.ts index 86d53760c..0b11dc110 100644 --- a/client/src/pages/collections/useCollections.ts +++ b/client/src/pages/collections/useCollections.ts @@ -177,6 +177,11 @@ export function useCollections() { // Stable reference so the map doesn't tear down + rebuild every marker on each // unrelated re-render (which would swallow marker clicks mid-rebuild). const mappable = useMemo(() => mappablePlaces(visiblePlaces), [visiblePlaces]) + // Whether this list has anything mappable AT ALL, filters aside. The layout + // hangs off this rather than off `mappable`: a label with no places left the + // filtered set empty, which tore the map out and reflowed the page to full + // width. The filter should empty the map, not remove it. + const hasMappable = useMemo(() => mappablePlaces(places).length > 0, [places]) const counts = useMemo(() => statusCounts(places), [places]) // ── Handlers ──────────────────────────────────────────────────────── @@ -385,7 +390,7 @@ export function useCollections() { collections, ownedLists, sharedLists, activeCollection, isAllSaved, isOwner, myRole, canEdit, canDelete, canShare, shareMemberCount, - activeId, places, visiblePlaces, mappable, members, incomingInvites, counts, + activeId, places, visiblePlaces, mappable, hasMappable, members, incomingInvites, counts, view, statusFilter, categoryFilter, categoryOptions, ratingFilter, sortMode, search, selectedPlaceId, selectMode, selectedIds, labels, labelFilter, labelOptions, loading, placesLoading, diff --git a/client/src/styles/collections.css b/client/src/styles/collections.css index dc26384ce..0f98169a6 100644 --- a/client/src/styles/collections.css +++ b/client/src/styles/collections.css @@ -274,9 +274,31 @@ .trek-dash .col-lrow-label { display: inline-flex; align-items: center; gap: 5px; max-width: 120px; padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; color: var(--label, var(--accent)); background: color-mix(in oklch, var(--label, var(--accent)) 14%, transparent); } .trek-dash .col-lrow-label.more { color: var(--ink-3); background: var(--surface-2); } .trek-dash .col-labelchip-dot { width: 8px; height: 8px; border-radius: 999px; flex-shrink: 0; background: var(--label, var(--accent)); } -.trek-dash .col-labelfilter { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; } -.trek-dash .col-labelchip { display: inline-flex; align-items: center; gap: 6px; max-width: 180px; padding: 6px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap; color: var(--ink-2); background: var(--surface); border: 1px solid var(--line-2); transition: background .12s, border-color .12s, color .12s; } -.trek-dash .col-labelchip:hover { background: var(--surface-2); color: var(--ink); } +/* Labels are their own axis of filtering, so they read as one tray rather than + as more loose buttons: a recessed strip, led by a tag glyph, holding chips + that share the filter buttons' radius and type. The chips carry no border of + their own — a frame inside a frame is what made this look like a pile. */ +.trek-dash .col-labelfilter { + display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; + min-height: var(--col-ctl-h); padding: 3px 6px 3px 9px; border-radius: 11px; + background: color-mix(in oklch, var(--surface-2) 70%, transparent); + border: 1px solid var(--line-2); +} +.trek-dash .col-labelfilter-lead { color: var(--ink-3); flex-shrink: 0; margin-right: 2px; } +/* On the map the tray joins the floating controls, so it takes their glass, + their radius and their shadow instead of the row's flat surface. */ +.trek-dash .col-labelfilter.on-map { + pointer-events: auto; max-width: min(52ch, 100%); flex-wrap: nowrap; overflow-x: auto; + border-radius: 11px; border-color: var(--glass-border); box-shadow: var(--sh-md); + background: color-mix(in oklch, var(--surface) 92%, transparent); + backdrop-filter: blur(8px) saturate(1.2); -webkit-backdrop-filter: blur(8px) saturate(1.2); + scrollbar-width: none; +} +.trek-dash .col-labelfilter.on-map::-webkit-scrollbar { display: none; } +.trek-dash .col-labelfilter.on-map .col-labelchip { flex-shrink: 0; } +.trek-dash .col-labelfilter.on-map .col-labelchip:hover { background: var(--surface-2); } +.trek-dash .col-labelchip { display: inline-flex; align-items: center; gap: 6px; max-width: 180px; padding: 5px 9px; border-radius: 8px; font-size: 12.5px; font-weight: 600; white-space: nowrap; color: var(--ink-2); background: transparent; border: 1px solid transparent; transition: background .12s, border-color .12s, color .12s; } +.trek-dash .col-labelchip:hover { background: var(--surface); color: var(--ink); } .trek-dash .col-labelchip .col-filter-lbl { overflow: hidden; text-overflow: ellipsis; } .trek-dash .col-labelchip.on { color: var(--label, var(--accent)); background: color-mix(in oklch, var(--label, var(--accent)) 16%, transparent); border-color: color-mix(in oklch, var(--label, var(--accent)) 42%, transparent); } .trek-dash .col-labelchip.on .col-labelchip-dot { background: var(--label, var(--accent)); } @@ -284,14 +306,35 @@ .trek-dash .col-detail-labels { display: flex; flex-wrap: wrap; gap: 7px; margin: 2px 0 10px; } /* ----------------- filter bar (status + category dropdowns) ----------------- */ -.trek-dash .col-filterbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; } +/* One control height for the whole page: the filter row and the buttons floating + over the map are read as a single band when the page is scrolled to the top, + so they share a height and a top offset instead of each having their own. */ +.trek-dash { --col-ctl-h: 38px; } +.trek-dash .col-filterbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; } +/* Beside the map, the row starts where the map's own top bar does. */ +.trek-dash .col-split-list .col-filterbar { margin-top: 12px; } .trek-dash .col-filter { position: relative; } /* Icon-only add-place button: square it off so it matches the dropdowns' height. */ -.trek-dash .col-filter-add { padding: 7px; } +/* Square icon-only buttons: as wide as they are tall, so they sit in the row + without being the one element that is a few pixels off. */ +.trek-dash .col-filter-add, +.trek-dash .col-filter-icon { width: var(--col-ctl-h); padding: 0; justify-content: center; } +/* Manage/add labels sits inside the label tray, so it stays quiet until hovered. */ +.trek-dash .col-filter-addlabel { + display: inline-flex; align-items: center; justify-content: center; + padding: 5px; border-radius: 8px; color: var(--ink-3); + background: transparent; border: 1px solid transparent; + transition: background .12s, color .12s; +} +.trek-dash .col-filter-addlabel:hover { background: var(--surface); color: var(--ink); } .trek-dash .col-filter-select { margin-left: auto; } -.trek-dash .col-filter-select.open { background: var(--accent); border-color: var(--accent); color: var(--accent-text); } -.trek-dash .col-filter-select.open:hover { background: var(--accent); color: var(--accent-text); } -.trek-dash .col-filter-btn { display: inline-flex; align-items: center; gap: 7px; padding: 7px 11px; border-radius: 10px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); background: var(--surface); border: 1px solid var(--line-2); transition: background .12s, border-color .12s, color .12s; } +/* `.on`, not `.open`: on a dropdown "open" means the panel is down and the text + stays dark, and that rule sits later in this file, so sharing the name left + the active select button drawing a dark icon on its dark accent fill. The + other toggles in this page (col-map-btn, col-iconbtn) already use `.on`. */ +.trek-dash .col-filter-select.on { background: var(--accent); border-color: var(--accent); color: var(--accent-text); } +.trek-dash .col-filter-select.on:hover { background: var(--accent); color: var(--accent-text); } +.trek-dash .col-filter-btn { display: inline-flex; align-items: center; gap: 7px; height: var(--col-ctl-h); padding: 0 11px; border-radius: 10px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); background: var(--surface); border: 1px solid var(--line-2); transition: background .12s, border-color .12s, color .12s; } .trek-dash .col-filter-btn:hover { background: var(--surface-2); color: var(--ink); } .trek-dash .col-filter-btn.open { border-color: var(--accent); color: var(--ink); } .trek-dash .col-filter-btn .col-filter-chev { color: var(--ink-3); transition: transform .15s; } @@ -461,9 +504,10 @@ .trek-dash .col-search { flex: 1; } .trek-dash .col-mapwrap { height: calc(100dvh - 260px); min-height: 320px; } - /* bigger touch targets on phones (visuals unchanged on desktop) */ + /* bigger touch targets on phones (visuals unchanged on desktop). Raising the + shared height keeps the square icon buttons square. */ + .trek-dash { --col-ctl-h: 40px; } .trek-dash .col-viewseg button { width: 44px; height: 38px; } - .trek-dash .col-filter-btn { min-height: 40px; } .trek-dash .col-filter-opt { min-height: 40px; } .trek-dash .col-selbar-btn { min-height: 40px; } .trek-dash .col-detail-close { width: 40px; height: 40px; } diff --git a/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx b/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx index cde8129d1..946244266 100644 --- a/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx +++ b/client/tests/unit/mobile/trip/MPlaceSheet.test.tsx @@ -368,9 +368,14 @@ describe('MPlaceSheet', () => { const shell = makeShell({ trTab: 'dateien', view: 'plan' }) const { planner } = renderSheet(makePlanner(), shell) - fireEvent.click(screen.getByRole('button', { name: 'Google Maps' })) + // A place with coordinates reaches more than one map app, so the circle + // opens the picker instead of guessing which one was meant. + fireEvent.click(screen.getByRole('button', { name: 'Navigation' })) + fireEvent.click(screen.getByRole('menuitem', { name: 'Google Maps' })) + // Name and address rather than bare coordinates (#1278): the place has both, + // so Google opens the entry instead of an unlabelled pin. expect(window.open).toHaveBeenCalledWith( - 'https://www.google.com/maps/search/?api=1&query=48.2038,16.3616', '_blank', 'noopener,noreferrer', + 'https://www.google.com/maps/search/?api=1&query=Kunsthistorisches%20Museum%2C%20Maria-Theresien-Platz', '_blank', 'noopener,noreferrer', ) fireEvent.click(screen.getByRole('button', { name: 'Open Website' })) @@ -405,7 +410,7 @@ describe('MPlaceSheet', () => { expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: 'Save to Collection' })).not.toBeInTheDocument() // Read-only members still get the navigation actions. - expect(screen.getByRole('button', { name: 'Google Maps' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Navigation' })).toBeInTheDocument() }) it('FE-MOB-PLSH-026: collapses the track colour picker again when the sheet closes', () => { diff --git a/client/tests/unit/mobile/trip/tabChrome.test.tsx b/client/tests/unit/mobile/trip/tabChrome.test.tsx index 8ab8f67a6..bd2c464bf 100644 --- a/client/tests/unit/mobile/trip/tabChrome.test.tsx +++ b/client/tests/unit/mobile/trip/tabChrome.test.tsx @@ -148,4 +148,54 @@ describe('tabChrome', () => { expect(screen.getByTitle('Ada').className).toContain('opacity-100') expect(screen.getByTitle('Bob').className).toContain('opacity-100') }) + + it('FE-MOB-TABCHR-015: TravelerFilterRow names itself, so the avatars are not left unexplained', () => { + render( + , + ) + expect(screen.getByRole('group', { name: 'Travelers' })).toBeInTheDocument() + expect(screen.getByText('Travelers')).toBeInTheDocument() + }) + + it('FE-MOB-TABCHR-016: the way out of a filter only appears once one is set', () => { + const onClear = vi.fn() + const { rerender } = render( + , + ) + expect(screen.queryByRole('button', { name: 'All' })).not.toBeInTheDocument() + + rerender( + , + ) + fireEvent.click(screen.getByRole('button', { name: 'All' })) + expect(onClear).toHaveBeenCalledTimes(1) + }) + + it('FE-MOB-TABCHR-017: SectionHeader wraps its count exactly once', () => { + // The callers used to pass a into a prop that wraps one itself, + // which put a pill inside a pill and showed two stacked backgrounds. + render() + const pill = screen.getByText('2') + expect(pill.querySelector('span')).toBeNull() + expect(pill.parentElement?.tagName).toBe('BUTTON') + }) }) diff --git a/shared/src/i18n/ar/inspector.ts b/shared/src/i18n/ar/inspector.ts index ed125bd92..413aad04a 100644 --- a/shared/src/i18n/ar/inspector.ts +++ b/shared/src/i18n/ar/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'حجز مؤكد', 'inspector.pendingRes': 'حجز قيد الانتظار', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'التنقل', + 'inspector.openWith': 'الفتح باستخدام', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'فتح الموقع الإلكتروني', 'inspector.saveToCollection': 'حفظ في مجموعة', diff --git a/shared/src/i18n/ar/places.ts b/shared/src/i18n/ar/places.ts index cd2fe539f..197457b30 100644 --- a/shared/src/i18n/ar/places.ts +++ b/shared/src/i18n/ar/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'غير مخطط', 'places.planned': 'مخطط لها', 'places.filterTracks': 'المسارات', - 'places.sortByRating': 'الترتيب حسب التقييم', + 'places.filterByRating': 'تصفية حسب التقييم', 'places.yourRating': 'تقييمك', 'places.notRated': 'لم يُقيَّم بعد', 'places.search': 'ابحث عن أماكن...', diff --git a/shared/src/i18n/br/inspector.ts b/shared/src/i18n/br/inspector.ts index ea27087f6..531528444 100644 --- a/shared/src/i18n/br/inspector.ts +++ b/shared/src/i18n/br/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Reserva confirmada', 'inspector.pendingRes': 'Reserva pendente', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navegação', + 'inspector.openWith': 'Abrir com', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.saveToCollection': 'Salvar na Coleção', 'inspector.savedToCollection': 'Salvo', diff --git a/shared/src/i18n/br/places.ts b/shared/src/i18n/br/places.ts index 9a51323f7..c92fd76aa 100644 --- a/shared/src/i18n/br/places.ts +++ b/shared/src/i18n/br/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Não planejados', 'places.planned': 'Planejados', 'places.filterTracks': 'Trilhas', - 'places.sortByRating': 'Ordenar por avaliação', + 'places.filterByRating': 'Filtrar por avaliação', 'places.yourRating': 'Sua avaliação', 'places.notRated': 'Ainda sem avaliações', 'places.search': 'Buscar lugares...', diff --git a/shared/src/i18n/ca/inspector.ts b/shared/src/i18n/ca/inspector.ts index 9c27e5f96..df1893253 100644 --- a/shared/src/i18n/ca/inspector.ts +++ b/shared/src/i18n/ca/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Reserva confirmada', 'inspector.pendingRes': 'Reserva pendent', 'inspector.google': 'Obre a Google Maps', + 'inspector.navigation': 'Navegació', + 'inspector.openWith': 'Obre amb', 'inspector.website': 'Obre el lloc web', 'inspector.addRes': 'Reserva', 'inspector.editRes': 'Edita la reserva', diff --git a/shared/src/i18n/ca/places.ts b/shared/src/i18n/ca/places.ts index 7056bf151..56d3280f1 100644 --- a/shared/src/i18n/ca/places.ts +++ b/shared/src/i18n/ca/places.ts @@ -43,7 +43,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Sense planificar', 'places.planned': 'Planificats', 'places.filterTracks': 'Rutes', - 'places.sortByRating': 'Ordena per valoració', + 'places.filterByRating': 'Filtra per valoració', 'places.yourRating': 'La teva valoració', 'places.notRated': 'Encara sense valoració', 'places.search': 'Cerca llocs...', diff --git a/shared/src/i18n/cs/inspector.ts b/shared/src/i18n/cs/inspector.ts index f06d35e40..0ca346de8 100644 --- a/shared/src/i18n/cs/inspector.ts +++ b/shared/src/i18n/cs/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Potvrzená rezervace', 'inspector.pendingRes': 'Čekající rezervace', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigace', + 'inspector.openWith': 'Otevřít v', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Otevřít webové stránky', 'inspector.saveToCollection': 'Uložit do sbírky', diff --git a/shared/src/i18n/cs/places.ts b/shared/src/i18n/cs/places.ts index 5cd2df76d..e8073085d 100644 --- a/shared/src/i18n/cs/places.ts +++ b/shared/src/i18n/cs/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Nezařazené', 'places.planned': 'Naplánované', 'places.filterTracks': 'Trasy', - 'places.sortByRating': 'Seřadit podle hodnocení', + 'places.filterByRating': 'Filtrovat podle hodnocení', 'places.yourRating': 'Tvé hodnocení', 'places.notRated': 'Zatím bez hodnocení', 'places.search': 'Hledat místa...', diff --git a/shared/src/i18n/de/inspector.ts b/shared/src/i18n/de/inspector.ts index 3cb723e69..0dcb72c28 100644 --- a/shared/src/i18n/de/inspector.ts +++ b/shared/src/i18n/de/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Bestätigte Reservierung', 'inspector.pendingRes': 'Ausstehende Reservierung', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigation', + 'inspector.openWith': 'Öffnen mit', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Webseite öffnen', 'inspector.saveToCollection': 'In Sammlung speichern', diff --git a/shared/src/i18n/de/places.ts b/shared/src/i18n/de/places.ts index 90c2173ca..46f655dd8 100644 --- a/shared/src/i18n/de/places.ts +++ b/shared/src/i18n/de/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Ungeplant', 'places.planned': 'Geplant', 'places.filterTracks': 'Tracks', - 'places.sortByRating': 'Nach Bewertung sortieren', + 'places.filterByRating': 'Nach Bewertung filtern', 'places.yourRating': 'Deine Bewertung', 'places.notRated': 'Noch nicht bewertet', 'places.search': 'Orte suchen...', diff --git a/shared/src/i18n/en/inspector.ts b/shared/src/i18n/en/inspector.ts index a0d456eee..a70884d6a 100644 --- a/shared/src/i18n/en/inspector.ts +++ b/shared/src/i18n/en/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Confirmed Reservation', 'inspector.pendingRes': 'Pending Reservation', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigation', + 'inspector.openWith': 'Open with', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Open Website', 'inspector.saveToCollection': 'Save to Collection', diff --git a/shared/src/i18n/en/places.ts b/shared/src/i18n/en/places.ts index 90ea728c0..7795ee8a5 100644 --- a/shared/src/i18n/en/places.ts +++ b/shared/src/i18n/en/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Unplanned', 'places.planned': 'Planned', 'places.filterTracks': 'Tracks', - 'places.sortByRating': 'Sort by rating', + 'places.filterByRating': 'Filter by rating', 'places.yourRating': 'Your rating', 'places.notRated': 'Not rated yet', 'places.search': 'Search places...', diff --git a/shared/src/i18n/es/inspector.ts b/shared/src/i18n/es/inspector.ts index bd16e2e5e..f8dfaa26d 100644 --- a/shared/src/i18n/es/inspector.ts +++ b/shared/src/i18n/es/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Reserva confirmada', 'inspector.pendingRes': 'Reserva pendiente', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navegación', + 'inspector.openWith': 'Abrir con', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Abrir la web', 'inspector.saveToCollection': 'Guardar en colección', diff --git a/shared/src/i18n/es/places.ts b/shared/src/i18n/es/places.ts index 458f7774f..b405f2905 100644 --- a/shared/src/i18n/es/places.ts +++ b/shared/src/i18n/es/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Sin planificar', 'places.planned': 'Planificados', 'places.filterTracks': 'Rutas', - 'places.sortByRating': 'Ordenar por valoración', + 'places.filterByRating': 'Filtrar por valoración', 'places.yourRating': 'Tu valoración', 'places.notRated': 'Sin valorar todavía', 'places.search': 'Buscar lugares...', diff --git a/shared/src/i18n/fr/inspector.ts b/shared/src/i18n/fr/inspector.ts index 30ef5b160..f30c148df 100644 --- a/shared/src/i18n/fr/inspector.ts +++ b/shared/src/i18n/fr/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Réservation confirmée', 'inspector.pendingRes': 'Réservation en attente', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigation', + 'inspector.openWith': 'Ouvrir avec', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Ouvrir le site web', 'inspector.saveToCollection': 'Enregistrer dans une collection', diff --git a/shared/src/i18n/fr/places.ts b/shared/src/i18n/fr/places.ts index 190dd7a63..1b1c71851 100644 --- a/shared/src/i18n/fr/places.ts +++ b/shared/src/i18n/fr/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Non planifiés', 'places.planned': 'Planifiés', 'places.filterTracks': 'Traces', - 'places.sortByRating': 'Trier par note', + 'places.filterByRating': 'Filtrer par note', 'places.yourRating': 'Ta note', 'places.notRated': 'Pas encore noté', 'places.search': 'Rechercher des lieux…', diff --git a/shared/src/i18n/gr/inspector.ts b/shared/src/i18n/gr/inspector.ts index 409ba2dc8..b81a6c617 100644 --- a/shared/src/i18n/gr/inspector.ts +++ b/shared/src/i18n/gr/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Επιβεβαιωμένη Κράτηση', 'inspector.pendingRes': 'Εκκρεμής Κράτηση', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Πλοήγηση', + 'inspector.openWith': 'Άνοιγμα με', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.saveToCollection': 'Αποθήκευση στη Συλλογή', 'inspector.savedToCollection': 'Αποθηκεύτηκε', diff --git a/shared/src/i18n/gr/places.ts b/shared/src/i18n/gr/places.ts index d7cf13cbc..d721161c2 100644 --- a/shared/src/i18n/gr/places.ts +++ b/shared/src/i18n/gr/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Μη προγραμματισμένα', 'places.planned': 'Προγραμματισμένα', 'places.filterTracks': 'Ίχνη', - 'places.sortByRating': 'Ταξινόμηση κατά βαθμολογία', + 'places.filterByRating': 'Φιλτράρισμα κατά βαθμολογία', 'places.yourRating': 'Η βαθμολογία σου', 'places.notRated': 'Χωρίς βαθμολογία ακόμη', 'places.search': 'Αναζήτηση μερών...', diff --git a/shared/src/i18n/hu/inspector.ts b/shared/src/i18n/hu/inspector.ts index df5236f2c..6c4004134 100644 --- a/shared/src/i18n/hu/inspector.ts +++ b/shared/src/i18n/hu/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Megerősített foglalás', 'inspector.pendingRes': 'Függőben lévő foglalás', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigáció', + 'inspector.openWith': 'Megnyitás ezzel', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Weboldal megnyitása', 'inspector.saveToCollection': 'Mentés gyűjteménybe', diff --git a/shared/src/i18n/hu/places.ts b/shared/src/i18n/hu/places.ts index b9c736ea5..3cb41240c 100644 --- a/shared/src/i18n/hu/places.ts +++ b/shared/src/i18n/hu/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Nem tervezett', 'places.planned': 'Tervezett', 'places.filterTracks': 'Nyomvonalak', - 'places.sortByRating': 'Rendezés értékelés szerint', + 'places.filterByRating': 'Szűrés értékelés szerint', 'places.yourRating': 'Az értékelésed', 'places.notRated': 'Még nincs értékelve', 'places.search': 'Helyek keresése...', diff --git a/shared/src/i18n/id/inspector.ts b/shared/src/i18n/id/inspector.ts index 1df45f455..0a88e0146 100644 --- a/shared/src/i18n/id/inspector.ts +++ b/shared/src/i18n/id/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Reservasi Dikonfirmasi', 'inspector.pendingRes': 'Reservasi Menunggu', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigasi', + 'inspector.openWith': 'Buka dengan', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.saveToCollection': 'Simpan ke Koleksi', 'inspector.savedToCollection': 'Tersimpan', diff --git a/shared/src/i18n/id/places.ts b/shared/src/i18n/id/places.ts index 59170197b..ae57bb480 100644 --- a/shared/src/i18n/id/places.ts +++ b/shared/src/i18n/id/places.ts @@ -44,7 +44,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Belum direncanakan', 'places.planned': 'Direncanakan', 'places.filterTracks': 'Trek', - 'places.sortByRating': 'Urutkan berdasarkan rating', + 'places.filterByRating': 'Saring menurut penilaian', 'places.yourRating': 'Rating kamu', 'places.notRated': 'Belum dinilai', 'places.search': 'Cari tempat...', diff --git a/shared/src/i18n/it/inspector.ts b/shared/src/i18n/it/inspector.ts index 5fe161cd9..d8f914b6b 100644 --- a/shared/src/i18n/it/inspector.ts +++ b/shared/src/i18n/it/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Prenotazione confermata', 'inspector.pendingRes': 'Prenotazione in attesa', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigazione', + 'inspector.openWith': 'Apri con', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Apri sito web', 'inspector.saveToCollection': 'Salva nella raccolta', diff --git a/shared/src/i18n/it/places.ts b/shared/src/i18n/it/places.ts index b5e537728..812bb1272 100644 --- a/shared/src/i18n/it/places.ts +++ b/shared/src/i18n/it/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Non pianificati', 'places.planned': 'Pianificati', 'places.filterTracks': 'Tracce', - 'places.sortByRating': 'Ordina per valutazione', + 'places.filterByRating': 'Filtra per valutazione', 'places.yourRating': 'La tua valutazione', 'places.notRated': 'Non ancora valutato', 'places.search': 'Cerca luoghi...', diff --git a/shared/src/i18n/ja/inspector.ts b/shared/src/i18n/ja/inspector.ts index 86f467e7e..13380f861 100644 --- a/shared/src/i18n/ja/inspector.ts +++ b/shared/src/i18n/ja/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': '確定済み予約', 'inspector.pendingRes': '保留中の予約', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'ナビゲーション', + 'inspector.openWith': 'アプリで開く', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Webサイトを開く', 'inspector.saveToCollection': 'コレクションに保存', diff --git a/shared/src/i18n/ja/places.ts b/shared/src/i18n/ja/places.ts index ddc2ad4c2..c358d1d1c 100644 --- a/shared/src/i18n/ja/places.ts +++ b/shared/src/i18n/ja/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': '未計画', 'places.planned': '計画済み', 'places.filterTracks': 'トラック', - 'places.sortByRating': '評価順に並べ替え', + 'places.filterByRating': '評価で絞り込む', 'places.yourRating': 'あなたの評価', 'places.notRated': '未評価', 'places.search': '場所を検索…', diff --git a/shared/src/i18n/ko/inspector.ts b/shared/src/i18n/ko/inspector.ts index fb00c9f7f..0a2f3a706 100644 --- a/shared/src/i18n/ko/inspector.ts +++ b/shared/src/i18n/ko/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': '확정된 예약', 'inspector.pendingRes': '대기 중인 예약', 'inspector.google': 'Google Maps', + 'inspector.navigation': '내비게이션', + 'inspector.openWith': '다음으로 열기', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': '웹사이트 열기', 'inspector.saveToCollection': '컬렉션에 저장', diff --git a/shared/src/i18n/ko/places.ts b/shared/src/i18n/ko/places.ts index 81b40c34a..03139024f 100644 --- a/shared/src/i18n/ko/places.ts +++ b/shared/src/i18n/ko/places.ts @@ -44,7 +44,7 @@ const places: TranslationStrings = { 'places.unplanned': '미계획', 'places.planned': '계획됨', 'places.filterTracks': '트랙', - 'places.sortByRating': '평점순 정렬', + 'places.filterByRating': '평점으로 필터링', 'places.yourRating': '내 평점', 'places.notRated': '아직 평점 없음', 'places.search': '장소 검색...', diff --git a/shared/src/i18n/nl/inspector.ts b/shared/src/i18n/nl/inspector.ts index 6cd2a3479..838afd9b2 100644 --- a/shared/src/i18n/nl/inspector.ts +++ b/shared/src/i18n/nl/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Bevestigde reservering', 'inspector.pendingRes': 'Reservering in behandeling', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigatie', + 'inspector.openWith': 'Openen met', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Website openen', 'inspector.saveToCollection': 'In collectie opslaan', diff --git a/shared/src/i18n/nl/places.ts b/shared/src/i18n/nl/places.ts index b8fa93959..6c0c6f008 100644 --- a/shared/src/i18n/nl/places.ts +++ b/shared/src/i18n/nl/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Ongepland', 'places.planned': 'Gepland', 'places.filterTracks': 'Tracks', - 'places.sortByRating': 'Sorteren op beoordeling', + 'places.filterByRating': 'Filteren op beoordeling', 'places.yourRating': 'Jouw beoordeling', 'places.notRated': 'Nog niet beoordeeld', 'places.search': 'Plaatsen zoeken...', diff --git a/shared/src/i18n/pl/inspector.ts b/shared/src/i18n/pl/inspector.ts index b0fe05a29..b5dafafe2 100644 --- a/shared/src/i18n/pl/inspector.ts +++ b/shared/src/i18n/pl/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Potwierdzona rezerwacja', 'inspector.pendingRes': 'Oczekująca rezerwacja', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Nawigacja', + 'inspector.openWith': 'Otwórz w', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Otwórz stronę internetową', 'inspector.saveToCollection': 'Zapisz w kolekcji', diff --git a/shared/src/i18n/pl/places.ts b/shared/src/i18n/pl/places.ts index 21a103ca1..224be55ab 100644 --- a/shared/src/i18n/pl/places.ts +++ b/shared/src/i18n/pl/places.ts @@ -36,7 +36,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Niezaplanowane', 'places.planned': 'Zaplanowane', 'places.filterTracks': 'Trasy', - 'places.sortByRating': 'Sortuj według oceny', + 'places.filterByRating': 'Filtruj według oceny', 'places.yourRating': 'Twoja ocena', 'places.notRated': 'Jeszcze nie oceniono', 'places.search': 'Szukaj miejsc...', diff --git a/shared/src/i18n/ru/inspector.ts b/shared/src/i18n/ru/inspector.ts index bb95d7d86..1f5a28940 100644 --- a/shared/src/i18n/ru/inspector.ts +++ b/shared/src/i18n/ru/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Подтверждённое бронирование', 'inspector.pendingRes': 'Ожидающее бронирование', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Навигация', + 'inspector.openWith': 'Открыть в', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Открыть сайт', 'inspector.saveToCollection': 'Сохранить в коллекцию', diff --git a/shared/src/i18n/ru/places.ts b/shared/src/i18n/ru/places.ts index e2392bf37..d154d1eb2 100644 --- a/shared/src/i18n/ru/places.ts +++ b/shared/src/i18n/ru/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Незапланированные', 'places.planned': 'Запланированные', 'places.filterTracks': 'Треки', - 'places.sortByRating': 'Сортировать по рейтингу', + 'places.filterByRating': 'Фильтр по оценке', 'places.yourRating': 'Твоя оценка', 'places.notRated': 'Ещё нет оценок', 'places.search': 'Поиск мест...', diff --git a/shared/src/i18n/sv/inspector.ts b/shared/src/i18n/sv/inspector.ts index ec94f53a6..d578fa6de 100644 --- a/shared/src/i18n/sv/inspector.ts +++ b/shared/src/i18n/sv/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Bekräftad bokning', 'inspector.pendingRes': 'Pendlande bokning', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Navigation', + 'inspector.openWith': 'Öppna med', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Öppna hemsida', 'inspector.saveToCollection': 'Spara i samling', diff --git a/shared/src/i18n/sv/places.ts b/shared/src/i18n/sv/places.ts index b03db9923..4e29e62c8 100644 --- a/shared/src/i18n/sv/places.ts +++ b/shared/src/i18n/sv/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Oplanerat', 'places.planned': 'Planerat', 'places.filterTracks': 'Spår', - 'places.sortByRating': 'Sortera efter betyg', + 'places.filterByRating': 'Filtrera efter betyg', 'places.yourRating': 'Ditt betyg', 'places.notRated': 'Inte betygsatt ännu', 'places.search': 'Sök efter platser...', diff --git a/shared/src/i18n/tr/inspector.ts b/shared/src/i18n/tr/inspector.ts index 9ca66ae1b..d3067940c 100644 --- a/shared/src/i18n/tr/inspector.ts +++ b/shared/src/i18n/tr/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Onaylanmış Rezervasyon', 'inspector.pendingRes': 'Bekleyen Rezervasyon', 'inspector.google': "Google Haritalar'da aç", + 'inspector.navigation': 'Navigasyon', + 'inspector.openWith': 'Şununla aç', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.saveToCollection': 'Koleksiyona kaydet', 'inspector.savedToCollection': 'Kaydedildi', diff --git a/shared/src/i18n/tr/places.ts b/shared/src/i18n/tr/places.ts index cd58fd6d3..b6dc3cfe6 100644 --- a/shared/src/i18n/tr/places.ts +++ b/shared/src/i18n/tr/places.ts @@ -47,7 +47,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Planlanmamış', 'places.planned': 'Planlanmış', 'places.filterTracks': 'Parçalar', - 'places.sortByRating': 'Puana göre sırala', + 'places.filterByRating': 'Puana göre filtrele', 'places.yourRating': 'Senin puanın', 'places.notRated': 'Henüz puanlanmadı', 'places.search': 'Yer ara...', diff --git a/shared/src/i18n/uk/inspector.ts b/shared/src/i18n/uk/inspector.ts index 04b16ee23..d59e79bf9 100644 --- a/shared/src/i18n/uk/inspector.ts +++ b/shared/src/i18n/uk/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Підтверджене бронювання', 'inspector.pendingRes': 'Очікуване бронювання', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Навігація', + 'inspector.openWith': 'Відкрити в', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': 'Відкрити сайт', 'inspector.saveToCollection': 'Зберегти в колекцію', diff --git a/shared/src/i18n/uk/places.ts b/shared/src/i18n/uk/places.ts index 1f569f769..efde4db2e 100644 --- a/shared/src/i18n/uk/places.ts +++ b/shared/src/i18n/uk/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Незаплановані', 'places.planned': 'Заплановані', 'places.filterTracks': 'Треки', - 'places.sortByRating': 'Сортувати за рейтингом', + 'places.filterByRating': 'Фільтр за оцінкою', 'places.yourRating': 'Твоя оцінка', 'places.notRated': 'Ще не оцінено', 'places.search': 'Пошук місць...', diff --git a/shared/src/i18n/vi/inspector.ts b/shared/src/i18n/vi/inspector.ts index 377cbb99e..e63cb2b75 100644 --- a/shared/src/i18n/vi/inspector.ts +++ b/shared/src/i18n/vi/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': 'Đã xác nhận đặt chỗ', 'inspector.pendingRes': 'Đang chờ đặt chỗ', 'inspector.google': 'Google Maps', + 'inspector.navigation': 'Điều hướng', + 'inspector.openWith': 'Mở bằng', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.saveToCollection': 'Lưu vào Bộ sưu tập', 'inspector.savedToCollection': 'Đã lưu', diff --git a/shared/src/i18n/vi/places.ts b/shared/src/i18n/vi/places.ts index f0f4b174b..6cbee89bf 100644 --- a/shared/src/i18n/vi/places.ts +++ b/shared/src/i18n/vi/places.ts @@ -45,7 +45,7 @@ const places: TranslationStrings = { 'places.unplanned': 'Không có kế hoạch', 'places.planned': 'Có kế hoạch', 'places.filterTracks': 'Bài hát', - 'places.sortByRating': 'Sắp xếp theo đánh giá', + 'places.filterByRating': 'Lọc theo đánh giá', 'places.yourRating': 'Đánh giá của bạn', 'places.notRated': 'Chưa có đánh giá', 'places.search': 'Tìm kiếm địa điểm...', diff --git a/shared/src/i18n/zh-TW/inspector.ts b/shared/src/i18n/zh-TW/inspector.ts index 2fc9be63b..ad4a2444c 100644 --- a/shared/src/i18n/zh-TW/inspector.ts +++ b/shared/src/i18n/zh-TW/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': '已確認預訂', 'inspector.pendingRes': '待確認預訂', 'inspector.google': 'Google Maps', + 'inspector.navigation': '導航', + 'inspector.openWith': '開啟方式', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': '開啟網站', 'inspector.saveToCollection': '儲存到收藏', diff --git a/shared/src/i18n/zh-TW/places.ts b/shared/src/i18n/zh-TW/places.ts index b4cf00fba..b05b4dfcd 100644 --- a/shared/src/i18n/zh-TW/places.ts +++ b/shared/src/i18n/zh-TW/places.ts @@ -44,7 +44,7 @@ const places: TranslationStrings = { 'places.unplanned': '未規劃', 'places.planned': '已規劃', 'places.filterTracks': '路線', - 'places.sortByRating': '依評分排序', + 'places.filterByRating': '依評分篩選', 'places.yourRating': '你的評分', 'places.notRated': '尚未評分', 'places.search': '搜尋地點...', diff --git a/shared/src/i18n/zh/inspector.ts b/shared/src/i18n/zh/inspector.ts index daae1d81a..052298667 100644 --- a/shared/src/i18n/zh/inspector.ts +++ b/shared/src/i18n/zh/inspector.ts @@ -13,6 +13,8 @@ const inspector: TranslationStrings = { 'inspector.confirmedRes': '已确认预订', 'inspector.pendingRes': '待确认预订', 'inspector.google': 'Google 地图', + 'inspector.navigation': '导航', + 'inspector.openWith': '打开方式', 'inspector.openStreetMap': 'OpenStreetMap', 'inspector.website': '打开网站', 'inspector.saveToCollection': '保存到收藏', diff --git a/shared/src/i18n/zh/places.ts b/shared/src/i18n/zh/places.ts index df5478bc2..acc141c99 100644 --- a/shared/src/i18n/zh/places.ts +++ b/shared/src/i18n/zh/places.ts @@ -44,7 +44,7 @@ const places: TranslationStrings = { 'places.unplanned': '未规划', 'places.planned': '已规划', 'places.filterTracks': '路线', - 'places.sortByRating': '按评分排序', + 'places.filterByRating': '按评分筛选', 'places.yourRating': '你的评分', 'places.notRated': '暂无评分', 'places.search': '搜索地点...',