diff --git a/.gitignore b/.gitignore index bfaa629a..14638f00 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ client/public/icons/*.png # User data server/data/* -!server/data/airports.json server/uploads/ # Environment diff --git a/client/src/pages/DashboardPage.test.tsx b/client/src/pages/DashboardPage.test.tsx index 5effca39..4aac3121 100644 --- a/client/src/pages/DashboardPage.test.tsx +++ b/client/src/pages/DashboardPage.test.tsx @@ -416,15 +416,10 @@ describe('DashboardPage', () => { expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0); }); - // Find settings button — it's the gear icon button without title or text + // Find settings button — the gear icon button (icon-only, no visible label) const allBtns = screen.getAllByRole('button'); - const settingsButton = allBtns.find( - btn => { - const title = btn.getAttribute('title'); - const text = btn.textContent?.trim() || ''; - // Settings gear: no title, no meaningful text, not the notification bell - return !title && !text && btn.querySelector('.lucide-settings'); - } + const settingsButton = allBtns.find(btn => + btn.querySelector('.lucide-settings') && !btn.textContent?.trim() ); expect(settingsButton).toBeDefined(); @@ -646,14 +641,10 @@ describe('DashboardPage', () => { expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0); }); - // Open widget settings + // Open widget settings — gear icon button (icon-only, no visible label) const allBtns = screen.getAllByRole('button'); - const settingsButton = allBtns.find( - btn => { - const title = btn.getAttribute('title'); - const text = btn.textContent?.trim() || ''; - return !title && !text && btn.querySelector('.lucide-settings'); - } + const settingsButton = allBtns.find(btn => + btn.querySelector('.lucide-settings') && !btn.textContent?.trim() ); expect(settingsButton).toBeDefined(); diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 96c8375f..47063034 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -897,61 +897,76 @@ export default function DashboardPage(): React.ReactElement { - {/* Desktop header */} -
-
-

{t('dashboard.title')}

-

+ {/* Desktop header — unified toolbar */} +

+
+

+ {t('dashboard.title')} +

+
+ {isLoading ? t('common.loading') : trips.length > 0 ? `${t(trips.length !== 1 ? 'dashboard.subtitle.activeMany' : 'dashboard.subtitle.activeOne', { count: trips.length })}${archivedTrips.length > 0 ? t('dashboard.subtitle.archivedSuffix', { count: archivedTrips.length }) : ''}` : t('dashboard.subtitle.empty')} -

-
-
- {/* View mode toggle */} - - {/* Widget settings */} - - {can('trip_create') && } + + +
+ + + {can('trip_create') && ( + + )} +
diff --git a/client/src/pages/JourneyPage.tsx b/client/src/pages/JourneyPage.tsx index dbd6d818..31da4b9a 100644 --- a/client/src/pages/JourneyPage.tsx +++ b/client/src/pages/JourneyPage.tsx @@ -150,39 +150,41 @@ export default function JourneyPage() { )}
- {/* Header — desktop */} -
-
-

{t('journey.title')}

-

{t("journey.frontpage.subtitle")}

-
-
- {searchOpen && ( - setSearchQuery(e.target.value)} - onKeyDown={e => { if (e.key === 'Escape') { setSearchQuery(''); setSearchOpen(false) } }} - placeholder={t('journey.search.placeholder')} - autoFocus - className="w-52 px-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-[10px] text-[13px] bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white focus:border-zinc-400 focus:outline-none" - /> - )} - - + {/* Header — desktop (unified toolbar) */} +
+
+

+ {t('journey.title')} +

+
+ + {t('journey.frontpage.subtitle')} + + +
+ +
diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index 685092a0..27aa4966 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -233,10 +233,20 @@ export default function TripPlannerPage(): React.ReactElement | null { const [showReservationModal, setShowReservationModal] = useState(false) const [editingReservation, setEditingReservation] = useState(null) const [fitKey, setFitKey] = useState(0) + const initialFitTripId = useRef(null) const [mobileSidebarOpen, setMobileSidebarOpen] = useState<'left' | 'right' | null>(null) const [deletePlaceId, setDeletePlaceId] = useState(null) const [deletePlaceIds, setDeletePlaceIds] = useState(null) + useEffect(() => { + if (!trip) return + if (initialFitTripId.current === trip.id) return + const hasGeoPlaces = places.some(p => p.lat != null && p.lng != null) + if (!hasGeoPlaces) return + initialFitTripId.current = trip.id + setFitKey(k => k + 1) + }, [trip, places]) + const connectionsStorageKey = tripId ? `trek:visible-connections:${tripId}` : null const [visibleConnections, setVisibleConnections] = useState(() => { if (typeof window === 'undefined' || !connectionsStorageKey) return [] diff --git a/client/src/pages/VacayPage.tsx b/client/src/pages/VacayPage.tsx index b3a524ed..d0d50689 100644 --- a/client/src/pages/VacayPage.tsx +++ b/client/src/pages/VacayPage.tsx @@ -138,19 +138,15 @@ export default function VacayPage(): React.ReactElement {
- {/* Header */} -
+ {/* Mobile + tablet header (filter toggle lives here) */} +
-
-

{t('admin.addons.catalog.vacay.name')}

-

{t('vacay.subtitle')}

-
+

{t('admin.addons.catalog.vacay.name')}

- {/* Mobile sidebar toggle */}
+ {/* Desktop header — unified toolbar (sidebar is always visible at this width) */} +
+
+

+ {t('admin.addons.catalog.vacay.name')} +

+
+ + {t('vacay.subtitle')} + +
+ +
+
+
+ {/* Main layout */}
{/* Desktop Sidebar */} diff --git a/client/src/types.ts b/client/src/types.ts index 4bcfa22a..5d841c64 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -212,6 +212,7 @@ export interface Settings { show_place_description: boolean route_calculation?: boolean blur_booking_codes?: boolean + map_booking_labels?: boolean } export interface AssignmentsMap { diff --git a/server/data/airports.json b/server/assets/airports.json similarity index 100% rename from server/data/airports.json rename to server/assets/airports.json diff --git a/server/scripts/build-airports.mjs b/server/scripts/build-airports.mjs index d7c00465..153fe88f 100644 --- a/server/scripts/build-airports.mjs +++ b/server/scripts/build-airports.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Build server/data/airports.json from OurAirports (davidmegginson.github.io/ourairports-data). +// Build server/assets/airports.json from OurAirports (davidmegginson.github.io/ourairports-data). // License: Public Domain. Keeps large/medium airports with an IATA code; timezone derived from coords via tz-lookup. import fs from 'node:fs' @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url' import tzLookup from 'tz-lookup' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const OUT = path.join(__dirname, '..', 'data', 'airports.json') +const OUT = path.join(__dirname, '..', 'assets', 'airports.json') const SRC = 'https://davidmegginson.github.io/ourairports-data/airports.csv' function fetchText(url) { diff --git a/server/src/services/airportService.ts b/server/src/services/airportService.ts index c3250b11..a5745d31 100644 --- a/server/src/services/airportService.ts +++ b/server/src/services/airportService.ts @@ -18,7 +18,7 @@ let byIata: Map | null = null; function load(): Airport[] { if (cache) return cache; - const file = path.join(__dirname, '..', '..', 'data', 'airports.json'); + const file = path.join(__dirname, '..', '..', 'assets', 'airports.json'); if (!fs.existsSync(file)) { console.warn('[airports] airports.json missing — run `node scripts/build-airports.mjs`'); cache = [];