/** * App — BrowserRouter shell with react-router declarative routing. * * Routes: * / → redirect to /calendar * /calendar → CalendarShell * /lists → ListsIndex * /lists/:listId → ListDetail * /setup → SetupPage (standalone wizard — no AppNav/BottomTabBar) * * Setup gate (Phase 12): * On app load, GET /api/setup/status is fetched with staleTime 0 (always fresh). * While loading: render nothing (prevent flash — mirrors the isAdmin loading gate). * When setupComplete === false: redirect all non-/setup routes to /setup via Navigate. * When setupComplete === true: normal app boot proceeds. * The /setup route renders standalone — AppNav/BottomTabBar are NOT rendered on wizard. * * Layout: * AppNav is rendered as a PERSISTENT sibling of (outside any Route), * so it survives route transitions (FIX 3). AppNav provides: * - Phone (≤767px): 48px top bar (the header above the main content area) * - Desktop (≥768px): 240px left sidebar with nav links + colour legend * * BottomTabBar is also a sibling of so the tab state remains consistent. * On desktop (≥768px) BottomTabBar is hidden via CSS (FIX 4). * * Auth flow: * /api/me is fetched once at the App level. While loading or on error the * full-screen AuthSplash overlay is shown (CalendarShell's AuthSplash is * positioned fixed with z-index so it covers AppNav too). AppNav renders * with empty / partial data until auth resolves — this is fine because the * AuthSplash overlay hides the AppNav during that window. * * navigateFallback ('/index.html') in vite.config.ts covers SPA deep-links to * /lists/* — the SW denylist only excludes /callback, /api/, and /health, so * /lists/* is served from cache correctly. * * Phase 5 additions: * - PermissionDeniedBanner: shown below AppNav when OS permission revoked (D-10) * - SettingsSheet: avatar-triggered bottom sheet with master notifications toggle (D-09) * * Phase 6 fix (FIX 3): * - AppNav lifted from CalendarShell to this level so /lists keeps the nav chrome. * - /api/me query shared so AppNav has user data on all routes. */ import { useState, useMemo } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { CalendarShell } from './components/CalendarShell.js'; import { ListsIndex } from './routes/ListsIndex.js'; import { ListDetail } from './routes/ListDetail.js'; import { AdminPage } from './routes/AdminPage.js'; import { SetupPage } from './routes/SetupPage.js'; import { LoginPage } from './routes/LoginPage.js'; import { BottomTabBar } from './components/BottomTabBar.js'; import { AppNav } from './components/AppNav.js'; import { PushPermissionPrompt } from './components/PushPermissionPrompt.js'; import { PermissionDeniedBanner } from './components/PermissionDeniedBanner.js'; import { SetupBanner } from './components/SetupBanner.js'; import { SettingsSheet } from './components/SettingsSheet.js'; import { fetchMe, fetchSetupStatus, fetchAuthMode } from './api/client.js'; function isPhone(): boolean { return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches; } /** * OidcRedirect — tiny helper that triggers a top-level navigation to /api/login. * * Used in the auth gate when localEnabled === false and oidcEnabled === true — * the OIDC-only mode that was the app's only auth path before Phase 19. * A top-level navigation (not a React Router navigate) is required because * /api/login responds with a 302 redirect to the external OIDC provider, * which browsers cannot follow as a fetch/XHR (T-07-04). */ function OidcRedirect() { window.location.replace('/api/login'); return