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:
m
2026-03-25 14:16:30 +01:00
parent 2cf01073a3
commit f81a2492c6
23 changed files with 834 additions and 288 deletions

View File

@@ -13,6 +13,9 @@ interface ExtractionFormProps {
isLoading: boolean;
}
const inputClass =
"w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
export function ExtractionForm({
cases,
selectedCaseId,
@@ -63,10 +66,10 @@ export function ExtractionForm({
id="case-select"
value={selectedCaseId}
onChange={(e) => onCaseChange(e.target.value)}
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-500"
className={inputClass}
disabled={isLoading}
>
<option value="">Akte auswaehlen...</option>
<option value="">Akte auswählen...</option>
{cases.map((c) => (
<option key={c.id} value={c.id}>
{c.case_number} - {c.title}
@@ -95,7 +98,7 @@ export function ExtractionForm({
type="button"
onClick={removeFile}
disabled={isLoading}
className="rounded p-1 text-neutral-400 hover:bg-neutral-200 hover:text-neutral-600"
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-200 hover:text-neutral-600"
>
<X className="h-4 w-4" />
</button>
@@ -142,10 +145,10 @@ export function ExtractionForm({
setText(e.target.value);
if (e.target.value.trim()) setFile(null);
}}
placeholder="Gerichtsschriftsatz, Beschluss oder sonstigen Text hier einfuegen..."
placeholder="Gerichtsschriftsatz, Beschluss oder sonstigen Text hier einfügen..."
rows={6}
disabled={isLoading}
className="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-500 disabled:opacity-50"
className={`${inputClass} resize-y placeholder:text-neutral-400 disabled:opacity-50`}
/>
</div>

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Trash2, Check, Pencil, X, Loader2 } from "lucide-react";
import { Trash2, Check, Pencil, X, Loader2, Brain } from "lucide-react";
import type { ExtractedDeadline } from "@/lib/types";
interface ExtractionResultsProps {
@@ -22,6 +22,9 @@ function confidenceLabel(confidence: number): string {
return "Niedrig";
}
const editInputClass =
"w-full rounded border border-neutral-300 px-2 py-1 text-sm outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
export function ExtractionResults({
deadlines: initialDeadlines,
onAdopt,
@@ -56,8 +59,11 @@ export function ExtractionResults({
if (deadlines.length === 0) {
return (
<div className="rounded-md border border-neutral-200 bg-neutral-50 p-6 text-center">
<p className="text-sm text-neutral-500">
<div className="flex flex-col items-center py-8 text-center">
<div className="rounded-xl bg-neutral-100 p-3">
<Brain className="h-5 w-5 text-neutral-400" />
</div>
<p className="mt-2 text-sm text-neutral-500">
Keine Fristen gefunden. Alle extrahierten Fristen wurden entfernt.
</p>
</div>
@@ -66,7 +72,7 @@ export function ExtractionResults({
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h3 className="text-sm font-medium text-neutral-900">
{deadlines.length} Frist{deadlines.length !== 1 ? "en" : ""} erkannt
</h3>
@@ -78,18 +84,19 @@ export function ExtractionResults({
{isAdopting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Uebernehme...
Übernehme...
</>
) : (
<>
<Check className="h-4 w-4" />
Fristen uebernehmen
Fristen übernehmen
</>
)}
</button>
</div>
<div className="overflow-hidden rounded-md border border-neutral-200">
{/* Mobile: card layout, Desktop: table */}
<div className="hidden overflow-hidden rounded-md border border-neutral-200 sm:block">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-200 bg-neutral-50">
@@ -97,7 +104,7 @@ export function ExtractionResults({
Frist
</th>
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
Faelligkeitsdatum
Fälligkeitsdatum
</th>
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
Rechtsgrundlage
@@ -105,7 +112,7 @@ export function ExtractionResults({
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
Konfidenz
</th>
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
<th className="hidden px-4 py-2.5 text-left font-medium text-neutral-700 lg:table-cell">
Quellenangabe
</th>
<th className="px-4 py-2.5 text-right font-medium text-neutral-700">
@@ -117,7 +124,7 @@ export function ExtractionResults({
{deadlines.map((d, i) => (
<tr
key={i}
className="border-b border-neutral-100 last:border-b-0"
className="border-b border-neutral-100 transition-colors last:border-b-0 hover:bg-neutral-50"
>
{editingIndex === i && editForm ? (
<>
@@ -127,7 +134,7 @@ export function ExtractionResults({
onChange={(e) =>
setEditForm({ ...editForm, title: e.target.value })
}
className="w-full rounded border border-neutral-300 px-2 py-1 text-sm"
className={editInputClass}
/>
</td>
<td className="px-4 py-2">
@@ -140,7 +147,7 @@ export function ExtractionResults({
due_date: e.target.value || null,
})
}
className="rounded border border-neutral-300 px-2 py-1 text-sm"
className={editInputClass}
/>
</td>
<td className="px-4 py-2">
@@ -152,7 +159,7 @@ export function ExtractionResults({
rule_reference: e.target.value,
})
}
className="w-full rounded border border-neutral-300 px-2 py-1 text-sm"
className={editInputClass}
/>
</td>
<td className="px-4 py-2">
@@ -162,21 +169,21 @@ export function ExtractionResults({
{confidenceLabel(editForm.confidence)}
</span>
</td>
<td className="px-4 py-2 text-xs text-neutral-500">
<td className="hidden px-4 py-2 text-xs text-neutral-500 lg:table-cell">
{editForm.source_quote}
</td>
<td className="px-4 py-2 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={saveEdit}
className="rounded p-1 text-green-600 hover:bg-green-50"
className="rounded p-1 text-green-600 transition-colors hover:bg-green-50"
title="Speichern"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={cancelEdit}
className="rounded p-1 text-neutral-400 hover:bg-neutral-100"
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100"
title="Abbrechen"
>
<X className="h-4 w-4" />
@@ -205,21 +212,21 @@ export function ExtractionResults({
{Math.round(d.confidence * 100)}%
</span>
</td>
<td className="max-w-48 truncate px-4 py-2.5 text-xs text-neutral-500">
<td className="hidden max-w-48 truncate px-4 py-2.5 text-xs text-neutral-500 lg:table-cell">
{d.source_quote || "-"}
</td>
<td className="px-4 py-2.5 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => startEdit(i)}
className="rounded p-1 text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600"
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
title="Bearbeiten"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
onClick={() => removeDeadline(i)}
className="rounded p-1 text-neutral-400 hover:bg-red-50 hover:text-red-600"
className="rounded p-1 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-600"
title="Entfernen"
>
<Trash2 className="h-3.5 w-3.5" />
@@ -233,6 +240,53 @@ export function ExtractionResults({
</tbody>
</table>
</div>
{/* Mobile card layout */}
<div className="space-y-3 sm:hidden">
{deadlines.map((d, i) => (
<div
key={i}
className="rounded-md border border-neutral-200 bg-white p-4"
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-neutral-900">{d.title}</p>
<div className="flex shrink-0 items-center gap-1">
<button
onClick={() => startEdit(i)}
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
onClick={() => removeDeadline(i)}
className="rounded p-1 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-600"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className="mt-2 flex flex-wrap gap-2 text-xs text-neutral-500">
<span>
{d.due_date
? new Date(d.due_date).toLocaleDateString("de-DE")
: `${d.duration_value} ${d.duration_unit}`}
</span>
{d.rule_reference && (
<>
<span>·</span>
<span>{d.rule_reference}</span>
</>
)}
<span
className={`rounded-full px-2 py-0.5 font-medium ${confidenceColor(d.confidence)}`}
>
{confidenceLabel(d.confidence)} {Math.round(d.confidence * 100)}
%
</span>
</div>
</div>
))}
</div>
</div>
);
}