Workstream 1: Projekt-Fundament & Design-System (Phase 0)
Greenfield-Next.js-15-App-Router-Gerüst (TS strict) mit: - Route-Groups (auth)/(app) inkl. loading/error/not-found je Group; Guard-Slot-Kommentar im (app)/layout.tsx (vom Auth-WS zu füllen). - "Amtlich"/Netzknoten-Designsystem: Tailwind-Tokens (Navy #1B3A5B, Signalrot #E2231A, Anthrazit, Nebelgrau, bereit/Wartung), tabular-nums, Serif-Display/Inter-Sans via CSS-Variablen, Inline-SVG-Logo. - Radix-Basiskomponenten (button/input/label/badge/tabs/dialog/select/ switch/slider); StatusBadge entspricht asset_status. - Kanonisches src/lib/env.ts (Zod, Fail-Fast) mit ALLEN DB-/Auth-/Geo-Slots inkl. AUTH_URL; isHttps-Ableitung. Zentrale i18n-Tabelle de.ts + t(). - Drizzle-Setup: client.ts (Pool-Singleton), leeres schema/index.ts-Barrel (KEIN Migrations-Eigentümer), drizzle.config.ts, .env.example. - next.config.ts: output:standalone, experimental.authInterrupts, Security-Header. Vitest + Fail-Fast-Env-Test (TDD, 5/5 grün). Bewusst KEINE Auth-Logik und KEINE fachlichen Tabellen. Verifikation: typecheck/lint/test grün; npm run build erzeugt .next/standalone/server.js; curl /anmelden -> lang="de" + FlorianNetz. next/font/google durch CSS-Variablen ersetzt (air-gapped-Build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
src/app/(app)/error.tsx
Normal file
15
src/app/(app)/error.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
export default function AppError({ reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="mx-auto max-w-md py-16 text-center">
|
||||
<p className="text-anthrazit">{t("fehler.allgemein")}</p>
|
||||
<Button onClick={reset} className="mt-4">
|
||||
{t("aktion.erneutVersuchen")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/app/(app)/layout.tsx
Normal file
21
src/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AppShell } from "@/components/layout/app-shell";
|
||||
|
||||
// Gated App-Shell.
|
||||
//
|
||||
// GUARD-SLOT (Default-deny, dreifach — Querschnittsstandard 1):
|
||||
// Der Auth-Workstream (Workstream 3) fügt hier als ALLERERSTE Anweisung den
|
||||
// serverseitigen Session-Guard ein, z. B.:
|
||||
//
|
||||
// import { requireSession } from "@/lib/auth/guards";
|
||||
// ...
|
||||
// await requireSession(); // leitet anonyme Aufrufe auf /anmelden um
|
||||
//
|
||||
// Lese-Seiten dürfen sich NICHT allein auf die Middleware verlassen.
|
||||
export default async function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// await requireSession(); // <- vom Auth-Workstream zu aktivieren
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
9
src/app/(app)/loading.tsx
Normal file
9
src/app/(app)/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
export default function AppLoading() {
|
||||
return (
|
||||
<div className="py-16 text-center text-sm text-anthrazit/70">
|
||||
{t("aktion.laden")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
src/app/(app)/not-found.tsx
Normal file
14
src/app/(app)/not-found.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
export default function AppNotFound() {
|
||||
return (
|
||||
<div className="mx-auto max-w-md py-16 text-center">
|
||||
<p className="text-anthrazit">{t("fehler.nichtGefunden")}</p>
|
||||
<Button asChild className="mt-4">
|
||||
<Link href="/start">{t("aktion.zurueckZurStartseite")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src/app/(app)/start/page.tsx
Normal file
24
src/app/(app)/start/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { StatusBadge } from "@/components/ui/badge";
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
export default function StartPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header>
|
||||
<h1 className="font-display text-2xl text-navy">{t("app.name")}</h1>
|
||||
<p className="mt-1 text-sm text-anthrazit/70">
|
||||
Vernetzte Übersicht der Fahrzeuge und Geräte der Feuerwehren.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="rounded-md border border-rand bg-white p-6">
|
||||
<h2 className="font-display text-lg text-navy">Statusübersicht</h2>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<StatusBadge status="einsatzbereit" />
|
||||
<StatusBadge status="wartung" />
|
||||
<StatusBadge status="ausser_dienst" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
src/app/(auth)/anmelden/page.tsx
Normal file
33
src/app/(auth)/anmelden/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
// Platzhalter-Anmeldeseite. Der Auth-Workstream (Workstream 3) ersetzt die
|
||||
// Logik durch Authentik-OIDC bzw. lokalen Login.
|
||||
export default function AnmeldenPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="font-display text-xl text-navy">{t("auth.anmelden")}</h1>
|
||||
<p className="mt-1 text-sm text-anthrazit/70">{t("auth.erforderlich")}</p>
|
||||
<form className="mt-6 space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="email">E-Mail</Label>
|
||||
<Input id="email" name="email" type="email" autoComplete="username" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="passwort">Passwort</Label>
|
||||
<Input
|
||||
id="passwort"
|
||||
name="passwort"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled>
|
||||
{t("auth.anmelden")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/app/(auth)/error.tsx
Normal file
15
src/app/(auth)/error.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
export default function AuthError({ reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-anthrazit">{t("fehler.allgemein")}</p>
|
||||
<Button onClick={reset} className="mt-4">
|
||||
{t("aktion.erneutVersuchen")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/app/(auth)/layout.tsx
Normal file
17
src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NetzknotenLogo } from "@/components/brand/netzknoten-logo";
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
// Schlankes Login-Layout ohne App-Chrome (keine Topbar/Navigation).
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-nebel px-4">
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<NetzknotenLogo size={32} />
|
||||
<span className="font-display text-xl text-navy">{t("app.name")}</span>
|
||||
</div>
|
||||
<div className="w-full max-w-sm rounded-md border border-rand bg-white p-6 shadow-sm">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
src/app/(auth)/loading.tsx
Normal file
7
src/app/(auth)/loading.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
export default function AuthLoading() {
|
||||
return (
|
||||
<div className="py-8 text-center text-sm text-anthrazit/70">{t("aktion.laden")}</div>
|
||||
);
|
||||
}
|
||||
25
src/app/layout.tsx
Normal file
25
src/app/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from "next";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "FlorianNetz",
|
||||
description:
|
||||
"FlorianNetz — vernetzte Übersicht der Fahrzeuge und Geräte der Feuerwehren.",
|
||||
};
|
||||
|
||||
// Schriften werden über CSS-Variablen (--font-sans / --font-display) in
|
||||
// globals.css gesetzt. Bewusst KEIN next/font/google: das würde zur Build-Zeit
|
||||
// Google-Fonts laden und in abgeschotteten (air-gapped) Deployments/CI brechen.
|
||||
// Die Stacks fallen sauber auf Inter→system-ui bzw. Source Serif 4→Georgia
|
||||
// zurück; selbst gehostete Schriften können später ergänzt werden.
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<body className="font-sans">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
7
src/app/page.tsx
Normal file
7
src/app/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
// Root-Redirect. Der Auth-Workstream verfeinert dies (Login vs. /start je
|
||||
// nach Session). Im Fundament leiten wir auf die Startseite weiter.
|
||||
export default function RootPage() {
|
||||
redirect("/start");
|
||||
}
|
||||
Reference in New Issue
Block a user