Files
agw/src/routes/app/route.tsx

168 lines
5.5 KiB
TypeScript

import { DbProvider } from "@/shared/db/provider"
import { Link, Outlet, createFileRoute, useMatches, useNavigate } from "@tanstack/react-router"
import { Suspense } from "react"
interface TabDef {
to: string
label: string
icon: string
}
const TABS: TabDef[] = [
{
to: "/app/home",
label: "Home",
icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1m-2 0h2",
},
{
to: "/app/bundestag",
label: "Bundestag",
icon: "M3 7h18M3 12h18M3 17h18",
},
{
to: "/app/landtag",
label: "Landtag",
icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z",
},
{
to: "/app/settings",
label: "Einstellungen",
icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z",
},
]
function TabIcon({ d }: { d: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d={d} />
</svg>
)
}
function AppLayout() {
const navigate = useNavigate()
const matches = useMatches()
const currentPath = matches.at(-1)?.pathname ?? "/app/home"
const currentTab = TABS.find((t) => currentPath.startsWith(t.to)) ?? TABS[0]
const isConfigureRoute = currentPath.endsWith("/configure")
const isBundestag = currentPath.startsWith("/app/bundestag")
const isLandtag = currentPath.startsWith("/app/landtag")
// Determine parent path for back navigation from configure routes
const parentPath = isConfigureRoute ? currentPath.replace(/\/configure$/, "") : null
// Determine configure link target for typed navigation
const configureTarget = isBundestag ? "/app/bundestag/configure" : isLandtag ? "/app/landtag/configure" : null
return (
<div className="flex flex-col h-dvh max-w-lg mx-auto">
<header className="flex items-center px-4 py-3 bg-card border-b border-border shadow-sm safe-area-top">
{isConfigureRoute && parentPath ? (
<button
type="button"
onClick={() => navigate({ to: parentPath })}
className="flex items-center gap-1 text-primary"
aria-label="Zurück"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm">Zurück</span>
</button>
) : null}
<h1 className={`text-base font-semibold text-card-foreground ${isConfigureRoute ? "ml-2" : ""}`}>
{isConfigureRoute ? `${currentTab.label} konfigurieren` : currentTab.label}
</h1>
{!isConfigureRoute && configureTarget && (
<Link
to={configureTarget}
className="ml-auto p-1.5 rounded-md hover:bg-muted transition-colors"
aria-label={`${currentTab.label} konfigurieren`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5 text-muted-foreground"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
/>
</svg>
</Link>
)}
</header>
<Suspense
fallback={
<div className="flex-1 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
</div>
}
>
<DbProvider>
<div className="flex-1 overflow-y-auto">
<Outlet />
</div>
</DbProvider>
</Suspense>
<nav className="flex bg-card border-t border-border safe-area-bottom" role="tablist" aria-label="Hauptnavigation">
{TABS.map((tab) => {
const active = currentPath.startsWith(tab.to)
return (
<Link
key={tab.to}
to={tab.to}
role="tab"
aria-selected={active}
aria-label={tab.label}
onClick={(e) => {
if (active) return
e.preventDefault()
const go = () => navigate({ to: tab.to })
if (document.startViewTransition && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
document.startViewTransition(go)
} else {
go()
}
}}
className={`flex flex-col items-center justify-center flex-1 py-2 gap-0.5 transition-colors no-underline ${
active ? "text-primary" : "text-muted-foreground hover:text-foreground"
}`}
>
<TabIcon d={tab.icon} />
<span className="text-[10px] font-medium">{tab.label}</span>
</Link>
)
})}
</nav>
</div>
)
}
export const Route = createFileRoute("/app")({
component: AppLayout,
})