syncova-policies/components/site-pages-form.tsx
Weapie 3633e8a4cf
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m33s
Responsiv machen und Barrierefreiheit nach WCAG 2.1 AA herstellen
Rechtsrahmen: § 13 Abs. 3 LBGG SH i. V. m. §§ 3 und 4 BITV 2.0. Eine
Erklaerung "nicht barrierefrei" ersetzt die Herstellungspflicht nicht,
sie dokumentiert den Stand nur voruebergehend.

Farben, alle Werte gegen die WCAG-Formel nachgerechnet:
- Sekundaertext 4.40:1 auf muted-Flaechen -> 4.55:1
- Rahmen von Eingabefeldern 1.24:1 -> 3:1 (WCAG 1.4.11); dekorative
  Trenner bleiben unveraendert
- Badge-Farben scheiterten auf der eigenen 10-Prozent-Flaeche
  (brand 4.20, success 4.18, destructive 3.88) -> jeweils ueber 4.5
- Warnfarbe im Admin 3.11:1 -> amber-700 mit 4.90:1

Fokus war faktisch unsichtbar: Der Ring wurde durchgaengig mit halber
Deckkraft gezeichnet (ring-ring/50), effektiv rund 1.5:1. Jetzt volle
Deckkraft in Markenfarbe, 12 Dateien betroffen.

Weiter: Sprungmarke zum Inhalt auf allen Seiten, Suchfelder beschriftet,
Druck-Schaltflaechen hatten als reine Icon-Buttons gar keinen
zugaenglichen Namen, Liste als echte Liste ausgezeichnet, doppelter Link
je Zeile entfernt, Trefferzahl als Live-Region.

Responsiv: Aussenabstaende und Kopfzeilen ab 320 Pixel, Listenzeilen
brechen um.

Neu nach § 4 BITV 2.0: Seiten fuer Leichte Sprache und Gebaerdensprache,
im Admin pflegbar. Leer gelassen und damit noch nicht online - die
Inhalte kommen vom Amt. In der Erklaerung als bekannte Luecke benannt.

Geprueft mit axe-core (WCAG 2.1 A und AA) ueber alle oeffentlichen Seiten
und den Adminbereich, hell und dunkel, dazu Tastaturbedienung, 200
Prozent Vergroesserung und 320 Pixel Breite: keine Verstoesse, kein
waagerechter Ueberlauf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:44:30 +02:00

293 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useState } from "react"
import { AlertCircle, ExternalLink } from "lucide-react"
import Link from "next/link"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { MarkdownInput } from "@/components/markdown-input"
import { updateSitePages, useSitePages } from "@/hooks/use-site-pages"
import { ApiError } from "@/lib/api"
import {
EMPTY_SITE_PAGES,
SITE_PAGES,
SITE_PAGE_META,
SitePageKey,
SitePagesContent,
} from "@/lib/site-pages"
/**
* Vorlagen als Platzhalter. Sie werden nicht gespeichert, sondern zeigen nur,
* welche Angaben die jeweilige Seite enthalten muss.
*/
const placeholders: Record<SitePageKey, string> = {
imprint: `## Angaben gemäß § 5 DDG
**Amt Musterstadt**
Musterstraße 1
12345 Musterstadt
**Vertreten durch:** Amtsvorsteher/in Vorname Nachname
**Kontakt**
Telefon: 01234 5678-0
E-Mail: info@amt-musterstadt.de
**Zuständige Aufsichtsbehörde**
Name und Anschrift der Rechtsaufsicht
**Verantwortlich für den Inhalt:** Vorname Nachname, Anschrift`,
privacy: `## Datenschutzerklärung für dieses Portal
Diese Erklärung betrifft die Verarbeitung Ihrer Daten beim **Besuch dieses
Portals**. Die Merkblätter zu den einzelnen Verwaltungsverfahren finden Sie in
der Übersicht.
### Verantwortlicher
Name, Anschrift, Kontakt
### Datenschutzbeauftragte/r
Name, Anschrift, E-Mail
### Server-Protokolldaten
Beim Aufruf werden IP-Adresse, Zeitpunkt, aufgerufene Seite, übertragene
Datenmenge und Browserkennung protokolliert.
*Zweck:* technischer Betrieb und Abwehr von Angriffen
*Rechtsgrundlage:* Art. 6 Abs. 1 lit. e DSGVO i. V. m. Landesrecht
*Speicherdauer:* z. B. 7 Tage
### Cookies
Ohne Anmeldung setzt das Portal keine Cookies. Im Admin-Bereich wird ein
technisch notwendiges Sitzungs-Cookie verwendet.
### Ihre Rechte
Auskunft (Art. 15), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung
(Art. 18), Widerspruch (Art. 21) sowie Beschwerde bei der Aufsichtsbehörde
(Art. 77 DSGVO).`,
accessibility: `## Erklärung zur Barrierefreiheit
Diese Erklärung gilt für die Website **datenschutz.beispiel.de**.
### Stand der Vereinbarkeit mit den Anforderungen
Diese Website ist mit der BITV 2.0 *[vollständig / teilweise / nicht]*
vereinbar.
### Nicht barrierefreie Inhalte
Hier die bekannten Einschränkungen und deren Begründung auflisten.
### Erstellung dieser Erklärung
Diese Erklärung wurde am TT.MM.JJJJ erstellt.
Grundlage war eine *[Selbstbewertung / Prüfung durch Dritte]*.
Letzte Überprüfung: TT.MM.JJJJ
### Barrieren melden – Kontakt
Sie können uns Mängel bei der Barrierefreiheit mitteilen und Informationen in
barrierefreier Form anfordern:
Name, E-Mail, Telefon
### Schlichtungsverfahren
Wenn wir auf Ihre Meldung nicht zufriedenstellend reagieren, können Sie die
Schlichtungsstelle nach § 16 BGG anrufen:
Anschrift und Kontakt der zuständigen Schlichtungsstelle`,
easyLanguage: `## Willkommen
Das hier ist die Internet-Seite vom Amt Musterstadt.
Hier steht: Was macht das Amt mit Ihren Daten?
## Was finden Sie auf dieser Seite?
Sie finden hier eine Liste.
In der Liste stehen viele Themen.
Zum Beispiel: Bau-Antrag. Oder: Wohn-Geld.
Zu jedem Thema gibt es einen Text.
In dem Text steht: Welche Daten braucht das Amt?
Und: Wie lange speichert das Amt die Daten?
## Wie finden Sie ein Thema?
Oben auf der Seite ist ein Such-Feld.
Dort können Sie ein Wort eintippen.
Dann zeigt die Seite nur noch passende Themen.
Sie können auch auf ein Thema in der Liste klicken.
## Barriere-Freiheit
Barriere-frei heißt: Alle Menschen können die Seite benutzen.
Wir arbeiten daran, dass die Seite barriere-frei wird.
Manche Sachen sind noch nicht barriere-frei.
Sie können uns das melden.
Schreiben Sie an: *[E-Mail-Adresse eintragen]*
*Dieser Text muss vor der Veröffentlichung von einer Prüf-Gruppe
gegengelesen werden. Das ist bei Leichter Sprache üblich.*`,
signLanguage: `## Informationen in Deutscher Gebärdensprache
Hier entsteht ein Video in Deutscher Gebärdensprache.
Das Video erklärt:
- die wesentlichen Inhalte dieser Website
- wie die Navigation funktioniert
- die wesentlichen Inhalte der Erklärung zur Barrierefreiheit
*[Video einbinden, sobald es vorliegt. Erforderlich nach § 4 BITV 2.0.
Ein Video braucht eine Produktion mit gehörlosen Dolmetscherinnen oder
Dolmetschern – Untertitel allein genügen nicht.]*
### Kontakt
Fragen zur Barrierefreiheit richten Sie bitte an:
*[Kontaktdaten eintragen]*`,
}
interface SitePagesFormProps {
open: boolean
onOpenChange: (open: boolean) => void
}
/**
* Pflegt Impressum, Datenschutzerklärung und Erklärung zur Barrierefreiheit.
* Ein leer gelassener Text nimmt die betreffende Seite vom Netz.
*/
export function SitePagesForm({ open, onOpenChange }: SitePagesFormProps) {
const { pages, isLoading } = useSitePages()
const [formData, setFormData] = useState<SitePagesContent>(EMPTY_SITE_PAGES)
const [errors, setErrors] = useState<string[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const [tab, setTab] = useState<SitePageKey>("imprint")
// Beim Öffnen den gespeicherten Stand laden
useEffect(() => {
if (!open || isLoading) return
setErrors([])
setTab("imprint")
setFormData({ ...pages })
// `pages` bewusst nicht als Abhängigkeit: sonst würden Eingaben beim
// Hintergrund-Refresh von SWR überschrieben.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, isLoading])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setErrors([])
setIsSubmitting(true)
try {
await updateSitePages(formData)
toast.success("Seiteninhalte gespeichert")
onOpenChange(false)
} catch (error) {
const details =
error instanceof ApiError && error.details.length > 0
? error.details
: [error instanceof Error ? error.message : "Fehler beim Speichern"]
setErrors(details)
} finally {
setIsSubmitting(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[88vh] sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Rechtliche Seiten</DialogTitle>
<DialogDescription>
Die eigenständigen Seiten des Portals: drei rechtliche Pflichtseiten
sowie Leichte Sprache und Gebärdensprache nach § 4 BITV 2.0. Sie
erscheinen in der Fußzeile der öffentlichen Seiten – ein leer
gelassener Text nimmt die betreffende Seite vom Netz.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col gap-4">
<Tabs
value={tab}
onValueChange={(value) => setTab(value as SitePageKey)}
className="min-h-0 flex-1 gap-3"
>
<TabsList className="h-auto flex-wrap">
{SITE_PAGES.map((key) => (
<TabsTrigger key={key} value={key}>
{SITE_PAGE_META[key].short}
</TabsTrigger>
))}
</TabsList>
{errors.length > 0 && (
<div className="flex gap-2.5 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
<AlertCircle className="mt-0.5 size-4 shrink-0" />
<ul className="space-y-1">
{errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}
<DialogBody className="pt-1">
{SITE_PAGES.map((key) => (
<TabsContent key={key} value={key} className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground">
<span>{SITE_PAGE_META[key].hint}</span>
{formData[key].trim() && (
<Button variant="ghost" size="sm" asChild>
<Link href={SITE_PAGE_META[key].path} target="_blank">
<ExternalLink />
{SITE_PAGE_META[key].path}
</Link>
</Button>
)}
</div>
<MarkdownInput
id={`site-page-${key}`}
label={SITE_PAGE_META[key].title}
value={formData[key]}
onChange={(value) =>
setFormData((prev) => ({ ...prev, [key]: value }))
}
placeholder={placeholders[key]}
rows={20}
/>
</TabsContent>
))}
</DialogBody>
</Tabs>
<DialogFooter className="border-t pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Abbrechen
</Button>
<Button type="submit" disabled={isSubmitting || isLoading}>
{isSubmitting ? "Speichern…" : "Seiteninhalte speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}