feat(16-04): add TanStack Router file-based routes and app shell layout

Create route structure with __root, index (redirect to /initiatives),
initiatives/index (dashboard stub), initiatives/$id (detail stub with
type-safe params), and inbox (stub). Add AppLayout with persistent nav
header matching wireframe: title, New Initiative button, navigation tabs
with active highlighting. Disabled tabs for Agents/Tasks/Settings (out
of scope). Replace temporary health-check App with RouterProvider.
Route tree auto-generated by TanStack Router Vite plugin.
This commit is contained in:
Lukas May
2026-02-04 20:38:30 +01:00
parent 64d751d203
commit 6d2920d60f
9 changed files with 256 additions and 12 deletions

View File

@@ -1,16 +1,8 @@
import { trpc } from './lib/trpc';
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'
function App() {
const health = trpc.health.useQuery();
return (
<div className="p-8">
<h1 className="text-2xl font-bold">Codewalk District</h1>
<p className="text-muted-foreground mt-2">
Server: {health.data?.status ?? 'connecting...'}
</p>
</div>
);
return <RouterProvider router={router} />
}
export default App;
export default App

View File

@@ -0,0 +1,57 @@
import { Link } from '@tanstack/react-router'
import { Button } from '@/components/ui/button'
const navItems = [
{ label: 'Initiatives', to: '/initiatives', enabled: true },
{ label: 'Inbox', to: '/inbox', enabled: true },
{ label: 'Agents', to: '#', enabled: false },
{ label: 'Tasks', to: '#', enabled: false },
{ label: 'Settings', to: '#', enabled: false },
] as const
export function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-6">
<Link to="/initiatives" className="text-lg font-bold tracking-tight">
Codewalk District
</Link>
<Button size="sm" onClick={() => { /* placeholder - Phase 17 */ }}>
+ New Initiative
</Button>
</div>
{/* Navigation */}
<nav className="mx-auto flex max-w-7xl gap-1 px-6 pb-2">
{navItems.map((item) =>
item.enabled ? (
<Link
key={item.label}
to={item.to}
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
activeProps={{
className: 'rounded-md px-3 py-1.5 text-sm font-medium text-foreground bg-muted',
}}
>
{item.label}
</Link>
) : (
<span
key={item.label}
className="cursor-not-allowed rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground/50"
>
{item.label}
</span>
)
)}
</nav>
</header>
{/* Page content */}
<main className="mx-auto max-w-7xl px-6 py-6">
{children}
</main>
</div>
)
}

View File

@@ -0,0 +1,113 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as InboxRouteImport } from './routes/inbox'
import { Route as IndexRouteImport } from './routes/index'
import { Route as InitiativesIndexRouteImport } from './routes/initiatives/index'
import { Route as InitiativesIdRouteImport } from './routes/initiatives/$id'
const InboxRoute = InboxRouteImport.update({
id: '/inbox',
path: '/inbox',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const InitiativesIndexRoute = InitiativesIndexRouteImport.update({
id: '/initiatives/',
path: '/initiatives/',
getParentRoute: () => rootRouteImport,
} as any)
const InitiativesIdRoute = InitiativesIdRouteImport.update({
id: '/initiatives/$id',
path: '/initiatives/$id',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/inbox': typeof InboxRoute
'/initiatives/$id': typeof InitiativesIdRoute
'/initiatives/': typeof InitiativesIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/inbox': typeof InboxRoute
'/initiatives/$id': typeof InitiativesIdRoute
'/initiatives': typeof InitiativesIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/inbox': typeof InboxRoute
'/initiatives/$id': typeof InitiativesIdRoute
'/initiatives/': typeof InitiativesIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/inbox' | '/initiatives/$id' | '/initiatives/'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/inbox' | '/initiatives/$id' | '/initiatives'
id: '__root__' | '/' | '/inbox' | '/initiatives/$id' | '/initiatives/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
InboxRoute: typeof InboxRoute
InitiativesIdRoute: typeof InitiativesIdRoute
InitiativesIndexRoute: typeof InitiativesIndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/inbox': {
id: '/inbox'
path: '/inbox'
fullPath: '/inbox'
preLoaderRoute: typeof InboxRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/initiatives/': {
id: '/initiatives/'
path: '/initiatives'
fullPath: '/initiatives/'
preLoaderRoute: typeof InitiativesIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/initiatives/$id': {
id: '/initiatives/$id'
path: '/initiatives/$id'
fullPath: '/initiatives/$id'
preLoaderRoute: typeof InitiativesIdRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
InboxRoute: InboxRoute,
InitiativesIdRoute: InitiativesIdRoute,
InitiativesIndexRoute: InitiativesIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

View File

@@ -0,0 +1,10 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

View File

@@ -0,0 +1,16 @@
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { AppLayout } from '../layouts/AppLayout'
export const Route = createRootRoute({
component: () => (
<AppLayout>
<Outlet />
</AppLayout>
),
notFoundComponent: () => (
<div className="p-8 text-center">
<h1 className="text-2xl font-bold">Page not found</h1>
<p className="text-muted-foreground mt-2">The page you are looking for does not exist.</p>
</div>
),
})

View File

@@ -0,0 +1,14 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/inbox')({
component: InboxPage,
})
function InboxPage() {
return (
<div>
<h1 className="text-2xl font-bold">Agent Inbox</h1>
<p className="text-muted-foreground mt-1">Content coming in Phase 19</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
beforeLoad: () => {
throw redirect({ to: '/initiatives' })
},
})

View File

@@ -0,0 +1,16 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/initiatives/$id')({
component: InitiativeDetailPage,
})
function InitiativeDetailPage() {
const { id } = Route.useParams()
return (
<div>
<h1 className="text-2xl font-bold">Initiative Detail</h1>
<p className="text-muted-foreground mt-2">Initiative ID: {id}</p>
<p className="text-muted-foreground mt-1">Content coming in Phase 18</p>
</div>
)
}

View File

@@ -0,0 +1,19 @@
import { createFileRoute } from '@tanstack/react-router'
import { trpc } from '../../lib/trpc'
export const Route = createFileRoute('/initiatives/')({
component: DashboardPage,
})
function DashboardPage() {
const health = trpc.health.useQuery()
return (
<div>
<h1 className="text-2xl font-bold">Initiative Dashboard</h1>
<p className="text-muted-foreground mt-2">
Server: {health.data?.status ?? 'connecting...'}
</p>
<p className="text-muted-foreground mt-1">Content coming in Phase 17</p>
</div>
)
}