feat: UI polish — responsive, loading/empty/error states, German fixes (Phase 3Q)
- Responsive sidebar: collapses on mobile with hamburger menu, slide-in animation - Skeleton loaders: dashboard cards, case table, case detail page - Empty states: friendly messages with icons for cases, deadlines, parties, documents - Error states: retry button on dashboard, proper error message on case not found - Form validation: inline error messages on case creation form - German language: fix all missing umlauts (Zurück, wählen, Anhängig, Verfügung, etc.) - Status labels: display German translations instead of raw status values - Transitions: fade-in animations on page load, hover/transition-colors on all interactive elements - Focus states: focus-visible ring for keyboard accessibility - Mobile layout: stacking for filters, forms, tabs; horizontal scroll for tables - Extraction results: card layout on mobile, table on desktop - Missing types: add DashboardData, DeadlineSummary, CaseSummary, ExtractedDeadline etc. - Fix QuickActions links to use correct routes (/cases/new, /ai/extract) - Consistent input focus styles across all forms
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
{ value: "", label: "-- Typ wahlen --" },
|
||||
{ value: "", label: "-- Typ wählen --" },
|
||||
{ value: "INF", label: "Verletzungsklage (INF)" },
|
||||
{ value: "REV", label: "Widerruf (REV)" },
|
||||
{ value: "CCR", label: "Einstweilige Verfugung (CCR)" },
|
||||
{ value: "CCR", label: "Einstweilige Verfügung (CCR)" },
|
||||
{ value: "APP", label: "Berufung (APP)" },
|
||||
{ value: "PI", label: "Vorlaufiger Rechtsschutz (PI)" },
|
||||
{ value: "PI", label: "Vorläufiger Rechtsschutz (PI)" },
|
||||
{ value: "ZPO_CIVIL", label: "ZPO Zivilverfahren" },
|
||||
];
|
||||
|
||||
@@ -43,8 +43,23 @@ export function CaseForm({
|
||||
status: initialData?.status ?? "active",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof CaseFormData, string>>>({});
|
||||
|
||||
function validate(): boolean {
|
||||
const newErrors: Partial<Record<keyof CaseFormData, string>> = {};
|
||||
if (!form.case_number.trim()) {
|
||||
newErrors.case_number = "Aktenzeichen ist erforderlich";
|
||||
}
|
||||
if (!form.title.trim()) {
|
||||
newErrors.title = "Titel ist erforderlich";
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!validate()) return;
|
||||
const data: CaseFormData = {
|
||||
...form,
|
||||
case_type: form.case_type || undefined,
|
||||
@@ -56,26 +71,31 @@ export function CaseForm({
|
||||
|
||||
function update(field: keyof CaseFormData, value: string) {
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
|
||||
"w-full rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-sm outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-neutral-700">
|
||||
Aktenzeichen *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={form.case_number}
|
||||
onChange={(e) => update("case_number", e.target.value)}
|
||||
placeholder="z.B. 2026/001"
|
||||
className={inputClass}
|
||||
className={`${inputClass} ${errors.case_number ? "border-red-300 focus:border-red-400 focus:ring-red-400" : ""}`}
|
||||
/>
|
||||
{errors.case_number && (
|
||||
<p className="mt-1 text-xs text-red-600">{errors.case_number}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-neutral-700">
|
||||
@@ -87,7 +107,7 @@ export function CaseForm({
|
||||
className={inputClass}
|
||||
>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="pending">Anhangig</option>
|
||||
<option value="pending">Anhängig</option>
|
||||
<option value="closed">Geschlossen</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -99,15 +119,17 @@ export function CaseForm({
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={form.title}
|
||||
onChange={(e) => update("title", e.target.value)}
|
||||
placeholder="Bezeichnung der Akte"
|
||||
className={inputClass}
|
||||
className={`${inputClass} ${errors.title ? "border-red-300 focus:border-red-400 focus:ring-red-400" : ""}`}
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-xs text-red-600">{errors.title}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-neutral-700">
|
||||
Verfahrensart
|
||||
@@ -132,7 +154,7 @@ export function CaseForm({
|
||||
type="text"
|
||||
value={form.court}
|
||||
onChange={(e) => update("court", e.target.value)}
|
||||
placeholder="z.B. UPC Munich Central Division"
|
||||
placeholder="z.B. UPC München Zentralkammer"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
@@ -155,7 +177,7 @@ export function CaseForm({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="rounded-md bg-neutral-900 px-4 py-1.5 text-sm font-medium text-white hover:bg-neutral-800 disabled:opacity-50"
|
||||
className="rounded-md bg-neutral-900 px-4 py-1.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? "Speichern..." : submitLabel}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user