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:
Maurice
2026-07-23 21:28:00 +02:00
committed by GitHub
parent c5360be242
commit 8855b5581d
32 changed files with 169 additions and 30 deletions
@@ -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])
+1
View File
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
'places.assignToDay': 'إلى أي يوم تريد الإضافة؟',
'places.all': 'الكل',
'places.unplanned': 'غير مخطط',
'places.planned': 'مخطط لها',
'places.filterTracks': 'المسارات',
'places.sortByRating': 'الترتيب حسب التقييم',
'places.yourRating': 'تقييمك',
+1
View File
@@ -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',
+1
View File
@@ -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ó',
+1
View File
@@ -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í',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
'places.assignToDay': 'Σε ποια ημέρα να προστεθεί;',
'places.all': 'Όλα',
'places.unplanned': 'Μη προγραμματισμένα',
'places.planned': 'Προγραμματισμένα',
'places.filterTracks': 'Ίχνη',
'places.sortByRating': 'Ταξινόμηση κατά βαθμολογία',
'places.yourRating': 'Η βαθμολογία σου',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
'places.assignToDay': 'どの日に追加しますか?',
'places.all': 'すべて',
'places.unplanned': '未計画',
'places.planned': '計画済み',
'places.filterTracks': 'トラック',
'places.sortByRating': '評価順に並べ替え',
'places.yourRating': 'あなたの評価',
+1
View File
@@ -41,6 +41,7 @@ const places: TranslationStrings = {
'places.assignToDay': '어느 날에 추가할까요?',
'places.all': '전체',
'places.unplanned': '미계획',
'places.planned': '계획됨',
'places.filterTracks': '트랙',
'places.sortByRating': '평점순 정렬',
'places.yourRating': '내 평점',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
'places.assignToDay': 'Добавить в какой день?',
'places.all': 'Все',
'places.unplanned': 'Незапланированные',
'places.planned': 'Запланированные',
'places.filterTracks': 'Треки',
'places.sortByRating': 'Сортировать по рейтингу',
'places.yourRating': 'Твоя оценка',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -42,6 +42,7 @@ const places: TranslationStrings = {
'places.assignToDay': 'Додати на який день?',
'places.all': 'Усі',
'places.unplanned': 'Незаплановані',
'places.planned': 'Заплановані',
'places.filterTracks': 'Треки',
'places.sortByRating': 'Сортувати за рейтингом',
'places.yourRating': 'Твоя оцінка',
+1
View File
@@ -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',
+1
View File
@@ -41,6 +41,7 @@ const places: TranslationStrings = {
'places.assignToDay': '新增到哪一天?',
'places.all': '全部',
'places.unplanned': '未規劃',
'places.planned': '已規劃',
'places.filterTracks': '路線',
'places.sortByRating': '依評分排序',
'places.yourRating': '你的評分',
+1
View File
@@ -41,6 +41,7 @@ const places: TranslationStrings = {
'places.assignToDay': '添加到哪一天?',
'places.all': '全部',
'places.unplanned': '未规划',
'places.planned': '已规划',
'places.filterTracks': '路线',
'places.sortByRating': '按评分排序',
'places.yourRating': '你的评分',