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 <select>. Android und iOS oeffnen
  dafuer ihre eigene Liste - schneller als jeder Nachbau, und mit
  Screenreader und Tastatur funktioniert es ohne Zutun.
- Eingabefelder haben mindestens 16px Schrift, sonst zoomt iOS-Safari
  beim Hineintippen das Formular heran.

<Feld> verdrahtet Beschriftung, Erklaerung und Fehler ueber
aria-describedby. Der Erklaerungstext ist kein Beiwerk: die Anwendung
benutzt Begriffe aus dem Betreuungsrecht, die niemand nachschlagen soll.

<Rechtshinweis> ist der stehende Vermerk, dass hier keine Rechtsberatung
stattfindet. Er steht an jeder Stelle mit Paragraphenbezug.

Der Anwendungsrahmen fuehrt drei Bedingungen zusammen, ohne die niemand
an Fachdaten kommt: angemeldet, zweiter Faktor eingerichtet, mindestens
eine betreute Person zugeordnet. Die Navigation zeigt Zaehler fuer
ueberfaellige Fristen und neuen Posteingang - die Frage "wo muss ich
hin?" wird schon im Menue beantwortet.

Das Farbschema wird per Inline-Skript mit CSP-Nonce gesetzt, bevor die
Seite gezeichnet wird - sonst blitzt beim Laden das helle Thema auf.

docs/KONVENTIONEN.md haelt fest, was in allen weiteren Etappen gilt:
Verzeichnisaufbau, Cent-Betraege, Umgang mit verschluesselten Feldern,
Zugriffspruefung, Audit-Pflicht, Ampelfarben, Commit-Trailer.

Etappe: 1
Phase: Fundament
This commit is contained in:
Florian Wilde
2026-08-22 01:54:56 +02:00
parent bd1c7b801f
commit dcd3a8ce71
30 changed files with 2151 additions and 12 deletions
+51
View File
@@ -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 (
<>
<Card>
<CardHeader>
<CardTitle>Anmelden</CardTitle>
<CardDescription>
E-Mail, Passwort und der Code aus deiner Authenticator-App — alle drei zusammen.
</CardDescription>
</CardHeader>
<CardContent>
<AnmeldeFormular weiter={weiter} />
</CardContent>
</Card>
<div className="flex items-start gap-2.5 rounded-md border bg-muted/50 p-3 text-sm text-muted-foreground">
<ShieldCheck className="mt-0.5 size-4 shrink-0" aria-hidden="true" />
<p>
Hier liegen Gesundheits- und Vermögensdaten einer betreuten Person. Der zweite Faktor ist
deshalb Pflicht und lässt sich nicht abschalten.
</p>
</div>
</>
)
}