Bisher liess sich der laufenden Anwendung nicht ansehen, welche Fassung sie ist. Nach einem Update war damit auch nicht zu erkennen, ob es angekommen ist — die haeufigste Frage im Betrieb. Die Fussleiste nennt jetzt die Version und fuehrt auf /version mit dem vollstaendigen Verlauf. Zusammengetragen ist er aus dem, was es schon gab: dem Abschnitt „Gebaut" der Roadmap und der Git-Historie. Gepflegt wird der Verlauf an genau einer Stelle, src/lib/changelog.ts. Daraus entsteht sowohl die Seite als auch die CHANGELOG.md (npm run changelog); eine Pruefung mit --check wacht darueber, dass beides zusammenpasst. Zwei gepflegte Listen liefen unweigerlich auseinander, und ein Changelog, der nicht stimmt, ist schlimmer als keiner. Die Historie hatte keine Tags. Die Eintraege sind deshalb nach Erscheinungstag zu Versionen zusammengefasst — die Nummern sind nachtraeglich vergeben, die Daten stammen aus den Commits. Zu jeder Version gibt es jetzt einen annotierten Tag im Repo; v1.6.0 folgt beim Merge. Die Roadmap fuehrt den Verlauf nicht mehr doppelt, sondern verweist darauf und behaelt den Blick nach vorn. Refs #9
151 lines
5.8 KiB
TypeScript
151 lines
5.8 KiB
TypeScript
import Link from "next/link";
|
|
|
|
import { LogoutButton } from "@/components/auth-forms";
|
|
import { Icon } from "@/components/icons";
|
|
import { ThemeToggle } from "@/components/theme-toggle";
|
|
import { actorLabel, getActor, isAuthEnabled } from "@/lib/auth/actor";
|
|
import { can } from "@/lib/auth/policy";
|
|
import { getBranding } from "@/lib/branding";
|
|
import { APP_VERSION } from "@/lib/changelog";
|
|
|
|
// Der Kopfbereich hängt von der Anmeldung ab und darf nicht vorgerendert
|
|
// werden.
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const NAV_LINK =
|
|
"rounded-lg px-3 py-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900";
|
|
|
|
/**
|
|
* Rahmen der eigentlichen Anwendung: Kopfzeile mit Navigation, Inhalt,
|
|
* Fußzeile. Die öffentlichen Landingpages liegen bewusst außerhalb dieser
|
|
* Gruppe und bekommen davon nichts ab.
|
|
*/
|
|
export default async function AppLayout({ children }: LayoutProps<"/">) {
|
|
const actor = await getActor();
|
|
const signedIn = actor.kind !== "anonymous";
|
|
const showAdmin = can(actor, "instance:manage");
|
|
const branding = await getBranding();
|
|
|
|
return (
|
|
<div className="flex flex-1 flex-col">
|
|
<header className="border-b border-slate-200 bg-surface">
|
|
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-3.5 sm:px-6">
|
|
<Link
|
|
href="/"
|
|
className="flex items-center gap-2.5 rounded-lg font-semibold text-slate-900"
|
|
>
|
|
{branding.logoUploadId ? (
|
|
/* eslint-disable-next-line @next/next/no-img-element -- aus der
|
|
Datenbank ausgeliefert, nicht aus dem Dateisystem */
|
|
<img
|
|
src={`/api/uploads/${branding.logoUploadId}`}
|
|
alt={branding.appName}
|
|
className="h-9 w-auto max-w-[12rem] object-contain"
|
|
/>
|
|
) : (
|
|
<span className="flex size-9 items-center justify-center rounded-lg bg-indigo-600 text-white">
|
|
<Icon name="qr" className="size-5" />
|
|
</span>
|
|
)}
|
|
{/* Auf schmalen Geräten reicht das Zeichen — sonst passt die
|
|
Navigation nicht mehr in eine Zeile. Mit eigenem Logo trägt
|
|
dieses schon den Namen. */}
|
|
{branding.logoUploadId ? null : (
|
|
<span className="hidden sm:inline">{branding.appName}</span>
|
|
)}
|
|
</Link>
|
|
|
|
<nav className="flex flex-wrap items-center gap-0.5 text-sm font-medium sm:gap-1">
|
|
<ThemeToggle />
|
|
<span
|
|
aria-hidden="true"
|
|
className="mx-1 hidden h-5 w-px bg-slate-200 sm:block"
|
|
/>
|
|
{signedIn ? (
|
|
<>
|
|
<Link href="/" className={NAV_LINK}>
|
|
Erstellen
|
|
</Link>
|
|
<Link href="/meine-codes" className={NAV_LINK}>
|
|
Meine Codes
|
|
</Link>
|
|
{showAdmin ? (
|
|
<Link href="/verwaltung" className={NAV_LINK}>
|
|
Verwaltung
|
|
</Link>
|
|
) : null}
|
|
</>
|
|
) : null}
|
|
|
|
{isAuthEnabled() && actor.kind === "user" ? (
|
|
<>
|
|
<span
|
|
aria-hidden="true"
|
|
className="mx-1 hidden h-5 w-px bg-slate-200 sm:block"
|
|
/>
|
|
<Link
|
|
href="/konto"
|
|
title={`Konto von ${actor.user.email}`}
|
|
className="flex max-w-[13rem] items-center gap-2 rounded-lg px-2.5 py-2 text-slate-700 transition-colors hover:bg-slate-100"
|
|
>
|
|
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-accent-soft text-[0.7rem] font-semibold text-accent-soft-fg uppercase">
|
|
{actorLabel(actor).slice(0, 2)}
|
|
</span>
|
|
<span className="hidden truncate sm:inline">
|
|
{actorLabel(actor)}
|
|
</span>
|
|
</Link>
|
|
<LogoutButton />
|
|
</>
|
|
) : null}
|
|
|
|
{isAuthEnabled() && actor.kind === "anonymous" ? (
|
|
<Link href="/anmelden" className={NAV_LINK}>
|
|
Anmelden
|
|
</Link>
|
|
) : null}
|
|
|
|
{/* Ohne Anmeldung gibt es kein Konto. Das gehört sichtbar
|
|
gesagt — sonst sucht man den Abmelden-Knopf vergeblich. */}
|
|
{!isAuthEnabled() ? (
|
|
<>
|
|
<span
|
|
aria-hidden="true"
|
|
className="mx-1 hidden h-5 w-px bg-slate-200 sm:block"
|
|
/>
|
|
<Link
|
|
href="/konto"
|
|
title="Die Anmeldung ist ausgeschaltet — hier steht, wie du sie einschaltest"
|
|
className="rounded-lg px-3 py-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
|
>
|
|
Ohne Anmeldung
|
|
</Link>
|
|
</>
|
|
) : null}
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex-1">{children}</main>
|
|
|
|
<footer className="border-t border-slate-200 bg-surface">
|
|
<div className="mx-auto flex max-w-6xl flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-4 py-6 text-sm text-slate-500 sm:px-6">
|
|
<span>
|
|
{branding.appName}
|
|
{branding.footerText ? ` · ${branding.footerText}` : null}
|
|
</span>
|
|
{/* Welche Fassung hier läuft — nach einem Update die schnellste
|
|
Antwort auf die Frage, ob das Update auch angekommen ist. */}
|
|
<Link
|
|
href="/version"
|
|
title="Version und Änderungen"
|
|
className="rounded font-mono text-xs tabular-nums transition-colors hover:text-slate-900"
|
|
>
|
|
v{APP_VERSION}
|
|
</Link>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|