168 lines
5.5 KiB
TypeScript
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,
|
|
})
|