From dcd3a8ce71a2211ffcbff4e4ff32f8953c775ccb Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 22 Aug 2026 01:54:56 +0200 Subject: [PATCH] feat: Oberflaechenbausteine, Anwendungsrahmen und Konventionen Basisbausteine im shadcn/ui-Stil, aber mit drei bewussten Abweichungen: - Bedienelemente sind 44px hoch statt 36px. Ich bediene die App oft am Handy im Heim oder auf dem Amt; kleiner ist dort nicht treffsicher. - Auswahlfelder benutzen das native + + {zustand.fehler && ( + + + )} + + + + + + + + + + + + + + + + ) +} diff --git a/src/app/(auth)/anmelden/page.tsx b/src/app/(auth)/anmelden/page.tsx new file mode 100644 index 0000000..1a94be9 --- /dev/null +++ b/src/app/(auth)/anmelden/page.tsx @@ -0,0 +1,51 @@ +import type { Metadata } from 'next' +import { redirect } from 'next/navigation' +import { ShieldCheck } from 'lucide-react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { aktuellerBenutzer } from '@/lib/auth/sitzung' +import { db } from '@/lib/db' +import { AnmeldeFormular } from './formular' + +export const metadata: Metadata = { title: 'Anmelden' } + +/** Fachdaten dürfen nie aus einem Cache kommen. */ +export const dynamic = 'force-dynamic' + +export default async function AnmeldenSeite({ + searchParams, +}: { + searchParams: Promise<{ weiter?: string }> +}) { + // Wer schon angemeldet ist, soll nicht am Anmeldeformular hängen bleiben. + if (await aktuellerBenutzer()) redirect('/heute') + + // Gibt es überhaupt schon einen Zugang? Beim allerersten Start nicht. + const anzahlBenutzer = await db.user.count({ where: { geloeschtAm: null } }) + if (anzahlBenutzer === 0) redirect('/ersteinrichtung') + + const { weiter = '/heute' } = await searchParams + + return ( + <> + + + Anmelden + + E-Mail, Passwort und der Code aus deiner Authenticator-App — alle drei zusammen. + + + + + + + +
+
+ + ) +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..9ac2c3b --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,38 @@ +import Link from 'next/link' + +/** + * Rahmen fuer Anmeldung, Ersteinrichtung und Zweitfaktor. + * Bewusst schmal und ruhig - hier gibt es genau eine Aufgabe. + */ +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return ( +
+
+
+
+ + Betreuungs-Cockpit +
+ {children} +
+
+ +
+
+ + Datenschutz + + + Daten bleiben in der EU + + Keine Rechtsberatung +
+
+
+ ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..4351ff7 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,65 @@ +import type { Metadata, Viewport } from 'next' +import { headers } from 'next/headers' +import './globals.css' + +export const metadata: Metadata = { + title: { + default: 'Betreuungs-Cockpit', + template: '%s · Betreuungs-Cockpit', + }, + description: + 'Arbeits- und Dokumentationswerkzeug für die ehrenamtliche rechtliche Betreuung: ' + + 'Fristen, Vorgänge, Belege, Vermögen, Sozialhilfe.', + // Kein Indexieren - hier stehen Gesundheits- und Finanzdaten. + robots: { index: false, follow: false, nocache: true }, + applicationName: 'Betreuungs-Cockpit', + formatDetection: { telephone: false }, +} + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + // Zoomen bleibt erlaubt. Wer schlecht sieht, muss heranzoomen können. + maximumScale: 5, + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#ffffff' }, + { media: '(prefers-color-scheme: dark)', color: '#0b1220' }, + ], +} + +/** + * Setzt das Farbschema, BEVOR die Seite gezeichnet wird. + * + * Ohne dieses Skript blitzt beim Laden kurz das helle Thema auf, auch wenn + * dunkel eingestellt ist. Es laeuft synchron im und traegt die + * Nonce der Content-Security-Policy - Inline-Skripte ohne Nonce werden + * vom Browser blockiert. + */ +const THEMA_SKRIPT = ` +(function () { + try { + var gespeichert = localStorage.getItem('bc-thema'); + var dunkel = + gespeichert === 'dunkel' || + ((!gespeichert || gespeichert === 'system') && + window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList.toggle('dark', dunkel); + document.documentElement.style.colorScheme = dunkel ? 'dark' : 'light'; + } catch (e) { + /* localStorage kann gesperrt sein - dann bleibt es beim hellen Thema. */ + } +})(); +` + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const nonce = (await headers()).get('x-nonce') ?? undefined + + return ( + + +