119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
/**
|
||||
|
|
* App — top-level provider stack and route tree.
|
|||
|
|
*
|
|||
|
|
* Provider order (outermost first):
|
|||
|
|
* MantineProvider → QueryClientProvider → BrowserRouter → SessionProvider → routes
|
|||
|
|
*
|
|||
|
|
* Route tree:
|
|||
|
|
* /login → LoginPage (public, T07 will build the real form)
|
|||
|
|
* / → ProtectedRoute → AppLayout → HomePage (T09)
|
|||
|
|
* /config → ProtectedRoute → AppLayout → ConfigPage (T08)
|
|||
|
|
*
|
|||
|
|
* AppLayout renders a minimal shell with a gear-icon nav entry for /config (§5#10).
|
|||
|
|
* T07–T10 slot their real pages in without touching the provider/router setup.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { MantineProvider } from '@mantine/core'
|
|||
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|||
|
|
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom'
|
|||
|
|
|
|||
|
|
// Mantine requires its CSS to be imported once.
|
|||
|
|
import '@mantine/core/styles.css'
|
|||
|
|
|
|||
|
|
import { SessionProvider } from './auth/SessionProvider'
|
|||
|
|
import { ProtectedRoute } from './auth/ProtectedRoute'
|
|||
|
|
import { LoginPage } from './pages/LoginPage'
|
|||
|
|
import { HomePage } from './pages/HomePage'
|
|||
|
|
import { ConfigPage } from './pages/ConfigPage'
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// TanStack Query client (singleton, created outside render to avoid re-creation)
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
const queryClient = new QueryClient({
|
|||
|
|
defaultOptions: {
|
|||
|
|
queries: {
|
|||
|
|
// Don't retry on 4xx — we handle 401 in the middleware
|
|||
|
|
retry: (failureCount, error) => {
|
|||
|
|
if (error instanceof Error && 'status' in error) {
|
|||
|
|
const status = (error as unknown as { status: number }).status
|
|||
|
|
if (status >= 400 && status < 500) return false
|
|||
|
|
}
|
|||
|
|
return failureCount < 2
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// App shell layout (used by all protected pages)
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
function AppLayout() {
|
|||
|
|
return (
|
|||
|
|
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|||
|
|
{/* Minimal top nav — T07–T10 can enhance this with Mantine AppShell */}
|
|||
|
|
<nav
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
padding: '0.5rem 1rem',
|
|||
|
|
borderBottom: '1px solid #eee',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Link to="/" style={{ fontWeight: 600, textDecoration: 'none' }}>
|
|||
|
|
Home Automation
|
|||
|
|
</Link>
|
|||
|
|
{/* Gear icon nav slot — links to config page (§5#10) */}
|
|||
|
|
<Link
|
|||
|
|
to="/config"
|
|||
|
|
aria-label="Configuration"
|
|||
|
|
style={{ fontSize: '1.25rem', textDecoration: 'none' }}
|
|||
|
|
title="Configuration"
|
|||
|
|
>
|
|||
|
|
⚙
|
|||
|
|
</Link>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
{/* Page content */}
|
|||
|
|
<main style={{ flex: 1 }}>
|
|||
|
|
<Outlet />
|
|||
|
|
</main>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Root app
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
export default function App() {
|
|||
|
|
return (
|
|||
|
|
<MantineProvider>
|
|||
|
|
<QueryClientProvider client={queryClient}>
|
|||
|
|
<BrowserRouter>
|
|||
|
|
<SessionProvider>
|
|||
|
|
<Routes>
|
|||
|
|
{/* Public routes */}
|
|||
|
|
<Route path="/login" element={<LoginPage />} />
|
|||
|
|
|
|||
|
|
{/* Protected routes — all nested under AppLayout */}
|
|||
|
|
<Route
|
|||
|
|
element={
|
|||
|
|
<ProtectedRoute>
|
|||
|
|
<AppLayout />
|
|||
|
|
</ProtectedRoute>
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<Route index element={<HomePage />} />
|
|||
|
|
<Route path="/config" element={<ConfigPage />} />
|
|||
|
|
</Route>
|
|||
|
|
</Routes>
|
|||
|
|
</SessionProvider>
|
|||
|
|
</BrowserRouter>
|
|||
|
|
</QueryClientProvider>
|
|||
|
|
</MantineProvider>
|
|||
|
|
)
|
|||
|
|
}
|