Add API client, /me bootstrap and useJob polling hook
web/src/api provides createApiClient().apiFetch: same-origin JSON with
the cached /api/v1/me CSRF token on non-GET requests and a typed
ApiError from the server's error body. A 401 navigates to
/auth/login?return={path}; a csrf 403 refreshes /me once and retries
once; any other 403 shows the "not in this household" screen.
App loads /me before the routes, showing the Skeleton meanwhile. The
QueryClient retries only ApiErrors the server marks retryable. useJob
polls GET /api/v1/jobs/{id} backing off 500 ms to 2 s, exposing status,
partial result and error until a terminal status, and stops on unmount.
Maestro task 21051318-15ae-4d0e-a9a1-2143f87de759.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+31
-14
@@ -1,6 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Route, Routes } from 'react-router'
|
||||
import { ApiProvider, createQueryClient, type ApiClient } from './api/index.ts'
|
||||
import { AuthBootstrap } from './AuthBootstrap.tsx'
|
||||
import { PlaceholderPage } from './pages/PlaceholderPage.tsx'
|
||||
import { AppShell } from './shell/AppShell.tsx'
|
||||
|
||||
@@ -23,19 +25,34 @@ export const routes = [
|
||||
{ path: '/settings/calendar', title: 'Calendar' },
|
||||
] as const
|
||||
|
||||
// App expects to be rendered inside a router (BrowserRouter in main.tsx,
|
||||
// MemoryRouter in tests).
|
||||
export default function App() {
|
||||
const [queryClient] = useState(() => new QueryClient())
|
||||
// AppRoutes is the shell and its pages, shown once /api/v1/me has loaded.
|
||||
export function AppRoutes() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route element={<AppShell />}>
|
||||
{routes.map(({ path, title }) => (
|
||||
<Route key={path} path={path} element={<PlaceholderPage title={title} />} />
|
||||
))}
|
||||
</Route>
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
<Routes>
|
||||
<Route element={<AppShell />}>
|
||||
{routes.map(({ path, title }) => (
|
||||
<Route key={path} path={path} element={<PlaceholderPage title={title} />} />
|
||||
))}
|
||||
</Route>
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export type AppProps = {
|
||||
api: ApiClient
|
||||
}
|
||||
|
||||
// App expects to be rendered inside a router (BrowserRouter in main.tsx,
|
||||
// MemoryRouter in tests).
|
||||
export default function App({ api }: AppProps) {
|
||||
const [queryClient] = useState(createQueryClient)
|
||||
return (
|
||||
<ApiProvider client={api}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthBootstrap>
|
||||
<AppRoutes />
|
||||
</AuthBootstrap>
|
||||
</QueryClientProvider>
|
||||
</ApiProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user