feat(web): management console — TanStack Start + orval + shadcn + Paraglide
ci / rust (push) Has been cancelled
ci / rust (push) Has been cancelled
Browser UI for the host's management REST API (mgmt.rs / docs/api/openapi.json). Stack, exactly as specified: - TanStack Start (Vite, SPA mode) — file-based routes, SSR shell + client hydration. - React Query via orval codegen from the checked-in OpenAPI spec: a custom fetch mutator (src/api/fetcher.ts) centralizes the base URL, the bearer token (Settings → localStorage), JSON, and a throwing ApiError; the query client skips retries on 4xx. orval returns the response body directly (includeHttpResponseReturnType:false) so a query's `.data` is the typed payload; GET→useQuery, POST/DELETE→useMutation by method. - shadcn/ui on Tailwind v4 (CSS-first tokens, dark-first) — button/card/badge/input/label/ table/skeleton primitives hand-authored from the canonical source. - Paraglide i18n (en + de) with a reactive useLocale() hook and a language switcher. Pages: dashboard (live status — video/audio/session/stream, stop-session + request-IDR, 2s polling), host (identity/codecs/ports), clients (paired list + unpair), pairing (PIN submit, polls pin_pending), settings (API token + language). Dev server proxies /api → 127.0.0.1:47990 (same-origin, no CORS; PUNKTFUNK_MGMT_URL to override). Generated code (orval client, paraglide runtime, routeTree) is gitignored and reproduced by `pnpm codegen` (prepare/pre* scripts). Validated live against `serve`: API shapes match, dev proxy works, SSR shell renders the localized nav, build + tsc green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Activity, Server, Users, KeyRound, Settings, Radio } from 'lucide-react'
|
||||
import { m } from '@/paraglide/messages'
|
||||
import { useLocale, changeLocale, locales, type Locale } from '@/lib/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const NAV = [
|
||||
{ to: '/', icon: Activity, label: () => m.nav_dashboard() },
|
||||
{ to: '/host', icon: Server, label: () => m.nav_host() },
|
||||
{ to: '/clients', icon: Users, label: () => m.nav_clients() },
|
||||
{ to: '/pairing', icon: KeyRound, label: () => m.nav_pairing() },
|
||||
{ to: '/settings', icon: Settings, label: () => m.nav_settings() },
|
||||
] as const
|
||||
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
// Read the locale so the whole shell re-renders on a language switch.
|
||||
useLocale()
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<aside className="hidden w-60 shrink-0 flex-col border-r bg-card/40 p-4 sm:flex">
|
||||
<div className="mb-6 flex items-center gap-2 px-2">
|
||||
<Radio className="size-5 text-[var(--success)]" />
|
||||
<div>
|
||||
<div className="font-semibold leading-tight">{m.app_name()}</div>
|
||||
<div className="text-xs text-muted-foreground">{m.app_tagline()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-1">
|
||||
{NAV.map(({ to, icon: Icon, label }) => (
|
||||
<Link
|
||||
key={to}
|
||||
to={to}
|
||||
activeOptions={{ exact: to === '/' }}
|
||||
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
activeProps={{ className: 'bg-accent text-foreground font-medium' }}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
{label()}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="mt-auto pt-4">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-x-hidden">
|
||||
<div className="mx-auto max-w-5xl p-6 sm:p-10">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LanguageSwitcher() {
|
||||
const current = useLocale()
|
||||
return (
|
||||
<div className="flex gap-1" role="group" aria-label="Language">
|
||||
{locales.map((l: Locale) => (
|
||||
<button
|
||||
key={l}
|
||||
onClick={() => changeLocale(l)}
|
||||
className={cn(
|
||||
'rounded px-2 py-1 text-xs uppercase transition-colors',
|
||||
l === current
|
||||
? 'bg-secondary text-secondary-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{l}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user