mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
feat(places): planned pool filter + hotel-transfer day routing (#1666)
* fix(dayplan): expose route tools on a hotel-to-hotel transfer day (#1297) * feat(places): add a Planned filter to the places pool (sidebar, map, mobile) * fix(places): stretch the mobile pool filter chips to fill the row
This commit is contained in:
@@ -210,6 +210,48 @@ describe('DayPlanSidebar', () => {
|
||||
expect(screen.queryByRole('button', { name: 'Open in Google Maps' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ── #1297: route tools for a hotel-to-hotel transfer day with no stops ───────
|
||||
it('FE-PLANNER-DAYPLAN-005d: route tools show for a two-hotel transfer day with no places (#1297)', () => {
|
||||
const dayA = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
|
||||
const dayTransfer = buildDay({ id: 11, date: '2025-06-02', title: 'Day 2' })
|
||||
const dayB = buildDay({ id: 12, date: '2025-06-03', title: 'Day 3' })
|
||||
// Check out of hotel A (slept there) and into hotel B on the same day — two distinct
|
||||
// located hotels, zero places. The map draws A → B, so the tools must be reachable.
|
||||
const accommodations = [
|
||||
{ id: 1, start_day_id: 10, end_day_id: 11, place_lat: 48.85, place_lng: 2.35 },
|
||||
{ id: 2, start_day_id: 11, end_day_id: 12, place_lat: 51.50, place_lng: -0.12 },
|
||||
]
|
||||
render(<DayPlanSidebar {...makeDefaultProps({
|
||||
days: [dayA, dayTransfer, dayB], accommodations: accommodations as any, selectedDayId: 11,
|
||||
})} />)
|
||||
expect(screen.getByRole('button', { name: 'Open in Google Maps' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-PLANNER-DAYPLAN-005e: no route tools on a same-hotel rest day with no places (#1297 guard)', () => {
|
||||
const dayA = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
|
||||
const dayRest = buildDay({ id: 11, date: '2025-06-02', title: 'Day 2' })
|
||||
const dayC = buildDay({ id: 12, date: '2025-06-03', title: 'Day 3' })
|
||||
// One hotel spanning all three days: the middle day has morning === evening, so there is
|
||||
// no transfer leg to draw and the tools stay hidden.
|
||||
const accommodations = [{ id: 1, start_day_id: 10, end_day_id: 12, place_lat: 48.85, place_lng: 2.35 }]
|
||||
render(<DayPlanSidebar {...makeDefaultProps({
|
||||
days: [dayA, dayRest, dayC], accommodations: accommodations as any, selectedDayId: 11,
|
||||
})} />)
|
||||
expect(screen.queryByRole('button', { name: 'Open in Google Maps' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-PLANNER-DAYPLAN-005f: no route tools on a plain arrival day with no places (#1297 guard)', () => {
|
||||
const dayArrive = buildDay({ id: 11, date: '2025-06-02', title: 'Day 1' })
|
||||
const dayNext = buildDay({ id: 12, date: '2025-06-03', title: 'Day 2' })
|
||||
// Only a check-in today (no hotel slept in last night, no places): morning === evening,
|
||||
// so there is no hotel → hotel leg and the tools stay hidden.
|
||||
const accommodations = [{ id: 2, start_day_id: 11, end_day_id: 12, place_lat: 51.50, place_lng: -0.12 }]
|
||||
render(<DayPlanSidebar {...makeDefaultProps({
|
||||
days: [dayArrive, dayNext], accommodations: accommodations as any, selectedDayId: 11,
|
||||
})} />)
|
||||
expect(screen.queryByRole('button', { name: 'Open in Google Maps' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ── Day expansion/collapse ──────────────────────────────────────────────
|
||||
|
||||
it('FE-PLANNER-DAYPLAN-006: days are expanded by default', () => {
|
||||
|
||||
@@ -1448,7 +1448,21 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
(routeBookends?.morning?.place_lat != null && routeBookends?.morning?.place_lng != null) ||
|
||||
(routeBookends?.evening?.place_lat != null && routeBookends?.evening?.place_lng != null)
|
||||
)
|
||||
const routeToolsRoutable = da.length >= 2 || (loc != null && hasRouteBookend)
|
||||
// Transfer day with no stops at all: you check out of one hotel and into another, so
|
||||
// the map already draws a hotel → hotel leg (see the transfer branch in
|
||||
// useRouteCalculation) even with zero places. Mirror that exact gate here — two
|
||||
// distinct bookend hotels you actually slept in / sleep in tonight — so the route
|
||||
// tools appear when you click the day (#1297). A same-hotel rest day or a plain
|
||||
// arrival/departure day has morning === evening and stays excluded.
|
||||
const transferMorning = routeBookends?.morning
|
||||
const transferEvening = routeBookends?.evening
|
||||
const hasHotelTransfer = !!(
|
||||
routeBookends?.morningIsSleptHere && routeBookends?.eveningIsOvernight &&
|
||||
transferMorning?.place_lat != null && transferMorning?.place_lng != null &&
|
||||
transferEvening?.place_lat != null && transferEvening?.place_lng != null &&
|
||||
(transferMorning.place_lat !== transferEvening.place_lat || transferMorning.place_lng !== transferEvening.place_lng)
|
||||
)
|
||||
const routeToolsRoutable = da.length >= 2 || (loc != null && hasRouteBookend) || hasHotelTransfer
|
||||
// Is this day's inline route currently on? Mobile toggles it per day (its
|
||||
// own expandedRouteDayIds entry); desktop uses the global Route toggle on
|
||||
// the selected day (#1374).
|
||||
@@ -2444,7 +2458,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Routen-Werkzeuge (ausgewählter Tag, 2+ Orte — oder 1 Ort mit Hotel-Bookend, #1330) */}
|
||||
{/* Routen-Werkzeuge (ausgewählter Tag, 2+ Orte — oder 1 Ort mit Hotel-Bookend #1330 — oder Hotel-zu-Hotel-Transfertag ohne Orte #1297) */}
|
||||
{(isSelected || (showRouteToolsWhenExpanded && isExpanded)) && routeToolsRoutable && (
|
||||
<div style={{ padding: '10px 16px 12px', borderTop: '1px solid var(--border-faint)', display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
|
||||
|
||||
@@ -242,6 +242,18 @@ describe('Filter tabs', () => {
|
||||
await user.click(screen.getByRole('button', { name: /Unplanned/i }));
|
||||
expect(screen.getByText(/All places are planned/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-SIDEBAR-019b: "Planned" filter shows only planned places', () => {
|
||||
const planned = buildPlace({ name: 'Planned Place' });
|
||||
const unplanned = buildPlace({ name: 'Unplanned Place' });
|
||||
const assignments = { '1': [buildAssignment({ place: planned, day_id: 1 })] };
|
||||
// Seed the pool filter directly: the tab label resolves from @trek/shared, but the
|
||||
// filter behaviour (only day-assigned places survive) is what this guards.
|
||||
seedStore(useTripStore, { placesFilter: 'planned' });
|
||||
render(<PlacesSidebar {...defaultProps} places={[planned, unplanned]} assignments={assignments} />);
|
||||
expect(screen.getByText('Planned Place')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Unplanned Place')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Search ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -86,13 +86,15 @@ export function PlacesHeader(S: SidebarState) {
|
||||
const counts = {
|
||||
all: baseFiltered.length,
|
||||
unplanned: baseFiltered.filter(p => !plannedIds.has(p.id)).length,
|
||||
planned: baseFiltered.filter(p => plannedIds.has(p.id)).length,
|
||||
tracks: baseFiltered.filter(p => p.route_geometry).length,
|
||||
}
|
||||
const tabs = ([
|
||||
{ id: 'all', label: t('places.all') },
|
||||
{ id: 'unplanned', label: t('places.unplanned') },
|
||||
{ id: 'planned', label: t('places.planned') },
|
||||
hasTracks ? { id: 'tracks', label: t('places.filterTracks') } : null,
|
||||
] as const).filter(Boolean) as Array<{ id: 'all' | 'unplanned' | 'tracks'; label: string }>
|
||||
] as const).filter(Boolean) as Array<{ id: 'all' | 'unplanned' | 'planned' | 'tracks'; label: string }>
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 8, flexWrap: 'wrap' }}>
|
||||
{tabs.map(f => {
|
||||
|
||||
@@ -201,6 +201,7 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
||||
const filtered = useMemo(() => {
|
||||
const list = places.filter(p => {
|
||||
if (filter === 'unplanned' && plannedIds.has(p.id)) return false
|
||||
if (filter === 'planned' && !plannedIds.has(p.id)) return false
|
||||
if (filter === 'tracks' && !p.route_geometry) return false
|
||||
if (categoryFilters.size > 0) {
|
||||
if (p.category_id == null) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { MPlacesBrowserProps } from '../MTripShell'
|
||||
import type { Place } from '../../../../types'
|
||||
import MPlacesBulkCategorySheet from './MPlacesBulkCategorySheet'
|
||||
import MPlacesSaveToCollectionSheet from './MPlacesSaveToCollectionSheet'
|
||||
import { filterPool, firstPlannedDayNumbers, plannedPlaceIds, poolCounts } from './placesBrowserModel'
|
||||
import { filterPool, firstPlannedDayNumbers, plannedPlaceIds } from './placesBrowserModel'
|
||||
|
||||
/**
|
||||
* Fullscreen places pool (mode === 'browse'): All/Unplanned/Tracks filter
|
||||
@@ -56,10 +56,6 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
|
||||
|
||||
const plannedIds = useMemo(() => plannedPlaceIds(assignments), [assignments])
|
||||
const dayNumberByPlace = useMemo(() => firstPlannedDayNumbers(assignments, days), [assignments, days])
|
||||
const counts = useMemo(
|
||||
() => poolCounts(places, categoryFilters, search, plannedIds),
|
||||
[places, categoryFilters, search, plannedIds],
|
||||
)
|
||||
const filtered = useMemo(
|
||||
() => filterPool(places, { filter, categoryFilters, search, plannedIds }),
|
||||
[places, filter, categoryFilters, search, plannedIds],
|
||||
@@ -121,38 +117,30 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-[calc(var(--bottom-nav-h,84px)+22px)] pt-[calc(var(--m-safe-top,12px)+58px)]">
|
||||
{/* ── Filter chips + Add + Import ── */}
|
||||
{/* ── Filter chips + Import (Add sits in the search row below to free space) ── */}
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<FilterChip
|
||||
active={filter === 'all'}
|
||||
label={t('places.all')}
|
||||
count={counts.all}
|
||||
onClick={() => { setFilter('all'); setSelectedIds(new Set()) }}
|
||||
/>
|
||||
<FilterChip
|
||||
active={filter === 'unplanned'}
|
||||
label={t('places.unplanned')}
|
||||
count={counts.unplanned}
|
||||
onClick={() => { setFilter('unplanned'); setSelectedIds(new Set()) }}
|
||||
/>
|
||||
<FilterChip
|
||||
active={filter === 'planned'}
|
||||
label={t('places.planned')}
|
||||
onClick={() => { setFilter('planned'); setSelectedIds(new Set()) }}
|
||||
/>
|
||||
{hasTracks && (
|
||||
<FilterChip
|
||||
active={filter === 'tracks'}
|
||||
label={t('places.filterTracks')}
|
||||
count={counts.tracks}
|
||||
onClick={() => { setFilter('tracks'); setSelectedIds(new Set()) }}
|
||||
/>
|
||||
)}
|
||||
{canEditPlaces && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openAddPlace}
|
||||
className="flex h-10 min-w-0 flex-1 items-center justify-center gap-[6px] whitespace-nowrap rounded-full bg-m-act text-[0.8125rem] font-semibold text-m-actfg"
|
||||
>
|
||||
<Plus size={14} strokeWidth={2.2} />
|
||||
{t('common.add')}
|
||||
</button>
|
||||
)}
|
||||
{canEditPlaces && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -202,6 +190,16 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
|
||||
{selectMode ? <X size={15} strokeWidth={2} /> : <ListChecks size={15} strokeWidth={2} />}
|
||||
</button>
|
||||
)}
|
||||
{canEditPlaces && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openAddPlace}
|
||||
aria-label={t('common.add')}
|
||||
className="flex w-[42px] flex-none items-center justify-center rounded-full bg-m-act text-m-actfg"
|
||||
>
|
||||
<Plus size={18} strokeWidth={2.2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Selection toolbar ── */}
|
||||
@@ -359,18 +357,18 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
|
||||
)
|
||||
}
|
||||
|
||||
/** All / Unplanned / Tracks pool chip with its Geist count. */
|
||||
function FilterChip({ active, label, count, onClick }: { active: boolean; label: string; count: number; onClick: () => void }) {
|
||||
/** All / Unplanned / Planned / Tracks pool chip. Counts are omitted on mobile to save row
|
||||
* space; chips flex to share the row's full width up to the import button on the right. */
|
||||
function FilterChip({ active, label, onClick }: { active: boolean; label: string; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`flex h-10 flex-none items-center gap-[5px] whitespace-nowrap rounded-full px-[15px] text-[0.75rem] font-semibold ${
|
||||
className={`flex h-10 min-w-0 flex-1 items-center justify-center whitespace-nowrap rounded-full px-[12px] text-[0.75rem] font-semibold ${
|
||||
active ? 'bg-m-act text-m-actfg' : 'bg-[color:var(--m-ic)] text-m-ink'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
<span className="font-geist text-[0.625rem] opacity-75">{count}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { filterPool, poolCounts } from './placesBrowserModel'
|
||||
import type { Place } from '../../../../types'
|
||||
|
||||
const mkPlace = (id: number, over: Partial<Place> = {}): Place =>
|
||||
({ id, name: `Place ${id}`, lat: 0, lng: 0, address: null, category_id: null, ...over } as Place)
|
||||
|
||||
describe('placesBrowserModel — planned pool', () => {
|
||||
const places = [mkPlace(1), mkPlace(2), mkPlace(3)]
|
||||
const plannedIds = new Set([1, 3]) // 1 and 3 are assigned to a day
|
||||
const noCats = new Set<string>()
|
||||
|
||||
it('MOBILE-POOL-planned: keeps only planned places', () => {
|
||||
const out = filterPool(places, { filter: 'planned', categoryFilters: noCats, search: '', plannedIds })
|
||||
expect(out.map(p => p.id)).toEqual([1, 3])
|
||||
})
|
||||
|
||||
it('MOBILE-POOL-unplanned: keeps only unplanned places (unchanged)', () => {
|
||||
const out = filterPool(places, { filter: 'unplanned', categoryFilters: noCats, search: '', plannedIds })
|
||||
expect(out.map(p => p.id)).toEqual([2])
|
||||
})
|
||||
|
||||
it('MOBILE-POOL-all: keeps everything', () => {
|
||||
const out = filterPool(places, { filter: 'all', categoryFilters: noCats, search: '', plannedIds })
|
||||
expect(out.map(p => p.id)).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
it('MOBILE-POOL-planned+search: planned pool still honors the search box', () => {
|
||||
const named = [mkPlace(1, { name: 'Louvre' }), mkPlace(3, { name: 'Eiffel' })]
|
||||
const out = filterPool(named, { filter: 'planned', categoryFilters: noCats, search: 'eiff', plannedIds })
|
||||
expect(out.map(p => p.id)).toEqual([3])
|
||||
})
|
||||
|
||||
it('MOBILE-POOL-counts: poolCounts reports planned alongside all/unplanned', () => {
|
||||
const c = poolCounts(places, noCats, '', plannedIds)
|
||||
expect(c.all).toBe(3)
|
||||
expect(c.planned).toBe(2)
|
||||
expect(c.unplanned).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -57,6 +57,7 @@ interface PoolFilterArgs {
|
||||
export function filterPool(places: Place[], { filter, categoryFilters, search, plannedIds }: PoolFilterArgs): Place[] {
|
||||
return places.filter(p => {
|
||||
if (filter === 'unplanned' && plannedIds.has(p.id)) return false
|
||||
if (filter === 'planned' && !plannedIds.has(p.id)) return false
|
||||
if (filter === 'tracks' && !p.route_geometry) return false
|
||||
return matchesCategoryFilter(p, categoryFilters) && matchesSearch(p, search)
|
||||
})
|
||||
@@ -68,6 +69,7 @@ export function poolCounts(places: Place[], categoryFilters: Set<string>, search
|
||||
return {
|
||||
all: base.length,
|
||||
unplanned: base.filter(p => !plannedIds.has(p.id)).length,
|
||||
planned: base.filter(p => plannedIds.has(p.id)).length,
|
||||
tracks: base.filter(p => p.route_geometry).length,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,8 +371,9 @@ export function useTripPlanner() {
|
||||
}
|
||||
}
|
||||
|
||||
// Build set of planned place IDs for unplanned filter
|
||||
const plannedIds = placesFilter === 'unplanned'
|
||||
// Planned place IDs — needed by both the 'unplanned' filter (exclude them) and
|
||||
// the new 'planned' filter (keep only them).
|
||||
const plannedIds = placesFilter === 'unplanned' || placesFilter === 'planned'
|
||||
? new Set(Object.values(assignments).flatMap(da => da.map(a => a.place?.id).filter(Boolean)))
|
||||
: null
|
||||
|
||||
@@ -384,8 +385,12 @@ export function useTripPlanner() {
|
||||
if (!placesCategoryFilter.has('uncategorized')) return false
|
||||
} else if (!placesCategoryFilter.has(String(p.category_id))) return false
|
||||
}
|
||||
if (hiddenPlaceIds.has(p.id)) return false
|
||||
if (plannedIds && plannedIds.has(p.id)) return false
|
||||
// Collapsed-day declutter hides a day's stops on every filter EXCEPT 'planned':
|
||||
// there the user asked to see the whole plan on the map, so a collapsed day
|
||||
// must not drop its planned places.
|
||||
if (placesFilter !== 'planned' && hiddenPlaceIds.has(p.id)) return false
|
||||
if (placesFilter === 'unplanned' && plannedIds && plannedIds.has(p.id)) return false
|
||||
if (placesFilter === 'planned' && plannedIds && !plannedIds.has(p.id)) return false
|
||||
return true
|
||||
})
|
||||
}, [places, placesCategoryFilter, placesFilter, assignments, expandedDayIds])
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'إلى أي يوم تريد الإضافة؟',
|
||||
'places.all': 'الكل',
|
||||
'places.unplanned': 'غير مخطط',
|
||||
'places.planned': 'مخطط لها',
|
||||
'places.filterTracks': 'المسارات',
|
||||
'places.sortByRating': 'الترتيب حسب التقييم',
|
||||
'places.yourRating': 'تقييمك',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Adicionar a qual dia?',
|
||||
'places.all': 'Todos',
|
||||
'places.unplanned': 'Não planejados',
|
||||
'places.planned': 'Planejados',
|
||||
'places.filterTracks': 'Trilhas',
|
||||
'places.sortByRating': 'Ordenar por avaliação',
|
||||
'places.yourRating': 'Sua avaliação',
|
||||
|
||||
@@ -40,6 +40,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': "A quin dia l'afegeixo?",
|
||||
'places.all': 'Tot',
|
||||
'places.unplanned': 'Sense planificar',
|
||||
'places.planned': 'Planificats',
|
||||
'places.filterTracks': 'Rutes',
|
||||
'places.sortByRating': 'Ordena per valoració',
|
||||
'places.yourRating': 'La teva valoració',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Přidat do kterého dne?',
|
||||
'places.all': 'Vše',
|
||||
'places.unplanned': 'Nezařazené',
|
||||
'places.planned': 'Naplánované',
|
||||
'places.filterTracks': 'Trasy',
|
||||
'places.sortByRating': 'Seřadit podle hodnocení',
|
||||
'places.yourRating': 'Tvé hodnocení',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Zu welchem Tag hinzufügen?',
|
||||
'places.all': 'Alle',
|
||||
'places.unplanned': 'Ungeplant',
|
||||
'places.planned': 'Geplant',
|
||||
'places.filterTracks': 'Tracks',
|
||||
'places.sortByRating': 'Nach Bewertung sortieren',
|
||||
'places.yourRating': 'Deine Bewertung',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Add to which day?',
|
||||
'places.all': 'All',
|
||||
'places.unplanned': 'Unplanned',
|
||||
'places.planned': 'Planned',
|
||||
'places.filterTracks': 'Tracks',
|
||||
'places.sortByRating': 'Sort by rating',
|
||||
'places.yourRating': 'Your rating',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': '¿A qué día añadirlo?',
|
||||
'places.all': 'Todo',
|
||||
'places.unplanned': 'Sin planificar',
|
||||
'places.planned': 'Planificados',
|
||||
'places.filterTracks': 'Rutas',
|
||||
'places.sortByRating': 'Ordenar por valoración',
|
||||
'places.yourRating': 'Tu valoración',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Ajouter à quel jour ?',
|
||||
'places.all': 'Tous',
|
||||
'places.unplanned': 'Non planifiés',
|
||||
'places.planned': 'Planifiés',
|
||||
'places.filterTracks': 'Traces',
|
||||
'places.sortByRating': 'Trier par note',
|
||||
'places.yourRating': 'Ta note',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Σε ποια ημέρα να προστεθεί;',
|
||||
'places.all': 'Όλα',
|
||||
'places.unplanned': 'Μη προγραμματισμένα',
|
||||
'places.planned': 'Προγραμματισμένα',
|
||||
'places.filterTracks': 'Ίχνη',
|
||||
'places.sortByRating': 'Ταξινόμηση κατά βαθμολογία',
|
||||
'places.yourRating': 'Η βαθμολογία σου',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Melyik naphoz adod?',
|
||||
'places.all': 'Összes',
|
||||
'places.unplanned': 'Nem tervezett',
|
||||
'places.planned': 'Tervezett',
|
||||
'places.filterTracks': 'Nyomvonalak',
|
||||
'places.sortByRating': 'Rendezés értékelés szerint',
|
||||
'places.yourRating': 'Az értékelésed',
|
||||
|
||||
@@ -41,6 +41,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Tambah ke hari mana?',
|
||||
'places.all': 'Semua',
|
||||
'places.unplanned': 'Belum direncanakan',
|
||||
'places.planned': 'Direncanakan',
|
||||
'places.filterTracks': 'Trek',
|
||||
'places.sortByRating': 'Urutkan berdasarkan rating',
|
||||
'places.yourRating': 'Rating kamu',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'A quale giorno aggiungere?',
|
||||
'places.all': 'Tutti',
|
||||
'places.unplanned': 'Non pianificati',
|
||||
'places.planned': 'Pianificati',
|
||||
'places.filterTracks': 'Tracce',
|
||||
'places.sortByRating': 'Ordina per valutazione',
|
||||
'places.yourRating': 'La tua valutazione',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'どの日に追加しますか?',
|
||||
'places.all': 'すべて',
|
||||
'places.unplanned': '未計画',
|
||||
'places.planned': '計画済み',
|
||||
'places.filterTracks': 'トラック',
|
||||
'places.sortByRating': '評価順に並べ替え',
|
||||
'places.yourRating': 'あなたの評価',
|
||||
|
||||
@@ -41,6 +41,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': '어느 날에 추가할까요?',
|
||||
'places.all': '전체',
|
||||
'places.unplanned': '미계획',
|
||||
'places.planned': '계획됨',
|
||||
'places.filterTracks': '트랙',
|
||||
'places.sortByRating': '평점순 정렬',
|
||||
'places.yourRating': '내 평점',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Aan welke dag toevoegen?',
|
||||
'places.all': 'Alle',
|
||||
'places.unplanned': 'Ongepland',
|
||||
'places.planned': 'Gepland',
|
||||
'places.filterTracks': 'Tracks',
|
||||
'places.sortByRating': 'Sorteren op beoordeling',
|
||||
'places.yourRating': 'Jouw beoordeling',
|
||||
|
||||
@@ -33,6 +33,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Do którego dnia dodać?',
|
||||
'places.all': 'Wszystkie',
|
||||
'places.unplanned': 'Niezaplanowane',
|
||||
'places.planned': 'Zaplanowane',
|
||||
'places.filterTracks': 'Trasy',
|
||||
'places.sortByRating': 'Sortuj według oceny',
|
||||
'places.yourRating': 'Twoja ocena',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Добавить в какой день?',
|
||||
'places.all': 'Все',
|
||||
'places.unplanned': 'Незапланированные',
|
||||
'places.planned': 'Запланированные',
|
||||
'places.filterTracks': 'Треки',
|
||||
'places.sortByRating': 'Сортировать по рейтингу',
|
||||
'places.yourRating': 'Твоя оценка',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Lägg till vilken dag?',
|
||||
'places.all': 'Alla',
|
||||
'places.unplanned': 'Oplanerat',
|
||||
'places.planned': 'Planerat',
|
||||
'places.filterTracks': 'Spår',
|
||||
'places.sortByRating': 'Sortera efter betyg',
|
||||
'places.yourRating': 'Ditt betyg',
|
||||
|
||||
@@ -44,6 +44,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Hangi güne eklensin?',
|
||||
'places.all': 'Tüm',
|
||||
'places.unplanned': 'Planlanmamış',
|
||||
'places.planned': 'Planlanmış',
|
||||
'places.filterTracks': 'Parçalar',
|
||||
'places.sortByRating': 'Puana göre sırala',
|
||||
'places.yourRating': 'Senin puanın',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Додати на який день?',
|
||||
'places.all': 'Усі',
|
||||
'places.unplanned': 'Незаплановані',
|
||||
'places.planned': 'Заплановані',
|
||||
'places.filterTracks': 'Треки',
|
||||
'places.sortByRating': 'Сортувати за рейтингом',
|
||||
'places.yourRating': 'Твоя оцінка',
|
||||
|
||||
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': 'Thêm vào ngày nào?',
|
||||
'places.all': 'Tất cả',
|
||||
'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.yourRating': 'Đánh giá của bạn',
|
||||
|
||||
@@ -41,6 +41,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': '新增到哪一天?',
|
||||
'places.all': '全部',
|
||||
'places.unplanned': '未規劃',
|
||||
'places.planned': '已規劃',
|
||||
'places.filterTracks': '路線',
|
||||
'places.sortByRating': '依評分排序',
|
||||
'places.yourRating': '你的評分',
|
||||
|
||||
@@ -41,6 +41,7 @@ const places: TranslationStrings = {
|
||||
'places.assignToDay': '添加到哪一天?',
|
||||
'places.all': '全部',
|
||||
'places.unplanned': '未规划',
|
||||
'places.planned': '已规划',
|
||||
'places.filterTracks': '路线',
|
||||
'places.sortByRating': '按评分排序',
|
||||
'places.yourRating': '你的评分',
|
||||
|
||||
Reference in New Issue
Block a user