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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user