Files
pansy/web/src/router.tsx
T
steve b79bfcf7a9
Build image / build-and-push (push) Successful in 13s
Field editor: palette, select/move/resize/rotate, inspector, optimistic sync (#11) (#30)
Co-authored-by: Steve Dudenhoeffer <[email protected]>
2026-07-19 01:01:38 +00:00

128 lines
3.7 KiB
TypeScript

import {
createRootRouteWithContext,
createRoute,
createRouter,
redirect,
} from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
import { AppShell } from '@/components/layout/AppShell'
import { RouteError } from '@/components/RouteError'
import { LoginPage } from '@/pages/LoginPage'
import { RegisterPage } from '@/pages/RegisterPage'
import { GardensPage } from '@/pages/GardensPage'
import { GardenEditorPage } from '@/pages/GardenEditorPage'
import { PlantsPage } from '@/pages/PlantsPage'
import { meQueryOptions } from '@/lib/auth'
import { queryClient } from '@/lib/queryClient'
import { safeRedirectPath } from '@/lib/redirect'
interface RouterContext {
queryClient: QueryClient
}
const rootRoute = createRootRouteWithContext<RouterContext>()({
component: AppShell,
// A beforeLoad/loader failure that isn't a redirect (e.g. /auth/me errors with
// a 500 or the network drops) lands here instead of a blank screen.
errorComponent: RouteError,
})
// requireAuth: resolve the current user (shared cache with useMe); send anyone
// unauthenticated to /login, remembering the path they were headed to.
async function requireAuth(context: RouterContext, path: string) {
const me = await context.queryClient.ensureQueryData(meQueryOptions)
if (!me) {
throw redirect({ to: '/login', search: { redirect: path } })
}
}
// requireGuest: keep already-authenticated users off /login and /register.
async function requireGuest(context: RouterContext, redirectTo: string) {
const me = await context.queryClient.ensureQueryData(meQueryOptions)
if (me) {
throw redirect({ to: redirectTo })
}
}
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
beforeLoad: () => {
throw redirect({ to: '/gardens' })
},
})
interface LoginSearch {
redirect?: string
error?: string
}
const loginRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'login',
// Optional properties (not `key: undefined`) so links to /login don't have to
// pass a search object.
validateSearch: (search: Record<string, unknown>): LoginSearch => {
const out: LoginSearch = {}
if (typeof search.redirect === 'string') out.redirect = search.redirect
if (typeof search.error === 'string') out.error = search.error
return out
},
beforeLoad: ({ context, search }) => requireGuest(context, safeRedirectPath(search.redirect)),
component: LoginPage,
})
const registerRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'register',
beforeLoad: ({ context }) => requireGuest(context, '/gardens'),
component: RegisterPage,
})
const gardensRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'gardens',
beforeLoad: ({ context, location }) => requireAuth(context, location.href),
component: GardensPage,
})
const gardenEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'gardens/$gardenId',
// ?focus=<objectId> frames that object on load.
validateSearch: (search: Record<string, unknown>): { focus?: number } => {
const f = Number(search.focus)
return Number.isInteger(f) && f > 0 ? { focus: f } : {}
},
beforeLoad: ({ context, location }) => requireAuth(context, location.href),
component: GardenEditorPage,
})
const plantsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'plants',
beforeLoad: ({ context, location }) => requireAuth(context, location.href),
component: PlantsPage,
})
const routeTree = rootRoute.addChildren([
indexRoute,
loginRoute,
registerRoute,
gardensRoute,
gardenEditorRoute,
plantsRoute,
])
export const router = createRouter({
routeTree,
defaultPreload: 'intent',
context: { queryClient },
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}