fix(06): make AppNav persistent across routes so Lists keeps the nav
- Lift AppNav from CalendarShell to App.tsx as a sibling of <Routes> - App.tsx fetches /api/me (same query key as CalendarShell — deduplicated by TanStack Query) - App.tsx provides the outer layout (phone: column, desktop: row) with AppNav always rendered - CalendarShell simplified: no longer manages AppNav, outer flex layout stays in App.tsx - AuthSplash gains overlay prop (position:fixed inset:0 z-index:999) so it covers AppNav when needed - CalendarShell uses AuthSplash with overlay=true so auth splashes cover full viewport - Remove onOpenSettings prop from CalendarShell (wired directly in App.tsx to SettingsSheet) - Desktop sidebar nav (FamilySync brand, Calendar/Lists links) now persists on /lists route
This commit is contained in:
+97
-13
@@ -7,10 +7,21 @@
|
||||
* /lists → ListsIndex
|
||||
* /lists/:listId → ListDetail (placeholder for Plan 04-04)
|
||||
*
|
||||
* BottomTabBar is rendered as a sibling of <Routes> so it persists across route
|
||||
* changes. On desktop (≥768px) AppNav sidebar handles navigation — BottomTabBar
|
||||
* is only visible on phone via CSS, but we render it in the tree at all sizes so
|
||||
* the tab state remains consistent.
|
||||
* Layout:
|
||||
* AppNav is rendered as a PERSISTENT sibling of <Routes> (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 <Routes> 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
|
||||
@@ -19,36 +30,109 @@
|
||||
* 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 } from 'react'
|
||||
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 { BottomTabBar } from './components/BottomTabBar.js'
|
||||
import { AppNav } from './components/AppNav.js'
|
||||
import { PushPermissionPrompt } from './components/PushPermissionPrompt.js'
|
||||
import { PermissionDeniedBanner } from './components/PermissionDeniedBanner.js'
|
||||
import { SettingsSheet } from './components/SettingsSheet.js'
|
||||
import { fetchMe } from './api/client.js'
|
||||
|
||||
function isPhone(): boolean {
|
||||
return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const phone = isPhone()
|
||||
|
||||
// Fetch current user once at the app shell level so AppNav has member data on
|
||||
// ALL routes. This is the same query key (['me']) used by CalendarShell, so
|
||||
// TanStack Query deduplicates the request — no double fetch.
|
||||
const meQuery = useQuery({
|
||||
queryKey: ['me'],
|
||||
queryFn: fetchMe,
|
||||
retry: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
// Derive members for AppNav from the shared /api/me response
|
||||
const members = useMemo(() => {
|
||||
if (!meQuery.data?.user) return []
|
||||
return [
|
||||
{
|
||||
id: String(meQuery.data.user.id),
|
||||
name: meQuery.data.user.displayName ?? 'Member',
|
||||
color: meQuery.data.user.color,
|
||||
},
|
||||
]
|
||||
}, [meQuery.data])
|
||||
|
||||
// Outer layout: phone = column, desktop = row (AppNav sidebar + content)
|
||||
const outerStyle: React.CSSProperties = {
|
||||
height: '100dvh',
|
||||
display: 'flex',
|
||||
flexDirection: phone ? 'column' : 'row',
|
||||
background: 'var(--color-surface)',
|
||||
color: 'var(--color-text-primary)',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
// Content area: fills remaining space next to / below AppNav
|
||||
const contentStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
{/* Permission-denied banner: shown when OS revoked and user had notifications on (D-10) */}
|
||||
<PermissionDeniedBanner />
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/calendar" replace />} />
|
||||
<Route
|
||||
path="/calendar"
|
||||
element={<CalendarShell onOpenSettings={() => setSettingsOpen(true)} />}
|
||||
<div style={outerStyle}>
|
||||
{/* Persistent AppNav — phone: top bar; desktop: left sidebar.
|
||||
Renders on ALL routes so nav chrome survives route transitions (FIX 3). */}
|
||||
<AppNav
|
||||
members={members}
|
||||
currentUserColor={meQuery.data?.user.color}
|
||||
currentUserName={meQuery.data?.user.displayName ?? undefined}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
/>
|
||||
<Route path="/lists" element={<ListsIndex />} />
|
||||
<Route path="/lists/:listId" element={<ListDetail />} />
|
||||
</Routes>
|
||||
|
||||
{/* Main content area — all routes render here */}
|
||||
<div style={contentStyle}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/calendar" replace />} />
|
||||
<Route
|
||||
path="/calendar"
|
||||
element={<CalendarShell />}
|
||||
/>
|
||||
<Route path="/lists" element={<ListsIndex />} />
|
||||
<Route path="/lists/:listId" element={<ListDetail />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BottomTabBar — phone-only (hidden ≥768px via CSS, FIX 4) */}
|
||||
<BottomTabBar />
|
||||
|
||||
{/* Post-install permission prompt (D-08): renders only when isInstalled() is true
|
||||
and Notification.permission === 'default' and not dismissed */}
|
||||
<PushPermissionPrompt />
|
||||
|
||||
Reference in New Issue
Block a user