chore: kanban template
This commit is contained in:
111
apps/kanban/src/routes/__root.tsx
Normal file
111
apps/kanban/src/routes/__root.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { createRootRoute, HeadContent, Link, Outlet, Scripts } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { defaultLocale, localeDir } from '../i18n/config'
|
||||
import { classifyHealth, sortCards, type Card } from '../lib/kanban'
|
||||
import { formatApiError, makeApi } from '../lib/api'
|
||||
import '../i18n/config'
|
||||
import '../index.css'
|
||||
|
||||
export interface BoardData {
|
||||
cards: Card[]
|
||||
error: string | null
|
||||
}
|
||||
|
||||
// Root route renders the full HTML document for SSR (owns <html lang dir> so the
|
||||
// tree mirrors for RTL locales) and the shared board chrome. Its loader fetches
|
||||
// the cards once on the server; child routes read this same data via
|
||||
// RootRoute.useLoaderData(), and mutations call router.invalidate() to re-run it.
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ title: 'Kanban Board' },
|
||||
],
|
||||
}),
|
||||
loader: async (): Promise<BoardData> => {
|
||||
try {
|
||||
const { cards } = await makeApi().invoke('listCards', {})
|
||||
return { cards: sortCards(cards as Card[]), error: null }
|
||||
} catch (thrown: unknown) {
|
||||
let message: string
|
||||
try {
|
||||
message = await formatApiError(thrown)
|
||||
} catch {
|
||||
message = 'Request failed'
|
||||
}
|
||||
return { cards: [], error: message }
|
||||
}
|
||||
},
|
||||
component: RootDocument,
|
||||
})
|
||||
|
||||
function RootDocument() {
|
||||
const locale = defaultLocale
|
||||
const { t } = useTranslation()
|
||||
const { cards, error } = Route.useLoaderData()
|
||||
const health = classifyHealth(cards)
|
||||
|
||||
return (
|
||||
<html lang={locale} dir={localeDir(locale)}>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<div className="app-shell">
|
||||
<header className="hero">
|
||||
<div className="hero-copy">
|
||||
<div className="eyebrow">{t('shell.eyebrow')}</div>
|
||||
<h1>{t('shell.title')}</h1>
|
||||
<p>{t('shell.subtitle')}</p>
|
||||
</div>
|
||||
<div className="hero-status">
|
||||
<div className="hero-stat">
|
||||
<span>{t('shell.stats.boardHealth')}</span>
|
||||
<strong data-tone={health}>{t(`health.${health}`)}</strong>
|
||||
</div>
|
||||
<div className="hero-stat">
|
||||
<span>{t('shell.stats.cardsTracked')}</span>
|
||||
<strong>{cards.length}</strong>
|
||||
</div>
|
||||
<div className="hero-stat">
|
||||
<span>{t('shell.stats.runtime')}</span>
|
||||
<strong>{error ? t('shell.runtimeDegraded') : t('shell.runtimeConnected')}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav className="top-nav">
|
||||
<Link
|
||||
to="/"
|
||||
className="nav-link"
|
||||
activeOptions={{ exact: true }}
|
||||
activeProps={{ className: 'nav-link is-active' }}
|
||||
>
|
||||
{t('shell.nav.overview')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/board"
|
||||
className="nav-link"
|
||||
activeProps={{ className: 'nav-link is-active' }}
|
||||
>
|
||||
{t('shell.nav.board')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/automations"
|
||||
className="nav-link"
|
||||
activeProps={{ className: 'nav-link is-active' }}
|
||||
>
|
||||
{t('shell.nav.automations')}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<main className="page-content">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user