All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m33s
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>
293 lines
9.4 KiB
TypeScript
293 lines
9.4 KiB
TypeScript
"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>
|
||
)
|
||
}
|