feat: modernize main list pages from tables to card-based rows

New shared ListItemCard component: icon avatar, title/subtitle, trailing
badges/actions, chevron affordance when linked to a detail page. Applied
to the 9 primary data-list pages (Bienenstaende, Voelker, Koeniginnen,
Honigchargen, Aufgaben, Lager, Produkte, Inventar, Wiederkehrende
Aufgaben), replacing the plain HTML tables with the same rounded-card
list aesthetic already used elsewhere in the app (inspections,
treatments, varroa, etc.), for a consistent look across the whole
product. Admin/settings tables (Benutzer, Webhooks, Audit-Log, Import)
intentionally left as tables per user's scoping choice.
This commit is contained in:
Nick Szaunig committed 2026-08-25 23:49:31 +02:00
1 parent faefd47e4b
commit c586bff18f
10 files changed
+185 -305

No files matched your search

@@ -11,6 +11,7 @@ import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { ListItemCard } from "@/components/list-item-card";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -18,14 +19,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function ApiariesPage() { export default function ApiariesPage() {
const [status, setStatus] = useState<ApiaryStatus | "">(""); const [status, setStatus] = useState<ApiaryStatus | "">("");
@@ -92,32 +85,22 @@ export default function ApiariesPage() {
) : null} ) : null}
{apiaries && apiaries.length > 0 ? ( {apiaries && apiaries.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Standort</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiaries.map((apiary) => ( {apiaries.map((apiary) => (
<TableRow key={apiary.id}> <ListItemCard
<TableCell> key={apiary.id}
<Link className="underline" href={`/apiaries/${apiary.id}`}> href={`/apiaries/${apiary.id}`}
{apiary.name} icon={MapPin}
</Link> title={apiary.name}
</TableCell> subtitle={apiary.location ?? "—"}
<TableCell>{apiary.location ?? "—"}</TableCell> trailing={
<TableCell>
<Badge variant={apiary.status === "ACTIVE" ? "default" : "secondary"}> <Badge variant={apiary.status === "ACTIVE" ? "default" : "secondary"}>
{apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"} {apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"}
</Badge> </Badge>
</TableCell> }
</TableRow> />
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</main> </main>
); );
@@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { ListItemCard } from "@/components/list-item-card";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -19,14 +20,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const STATUS_OPTIONS: ColonyStatus[] = [ const STATUS_OPTIONS: ColonyStatus[] = [
"ACTIVE", "ACTIVE",
@@ -104,33 +97,18 @@ export default function ColoniesPage() {
) : null} ) : null}
{colonies && colonies.length > 0 ? ( {colonies && colonies.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Code</TableHead>
<TableHead>Typ</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{colonies.map((colony) => ( {colonies.map((colony) => (
<TableRow key={colony.id}> <ListItemCard
<TableCell> key={colony.id}
<Link
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
href={`/colonies/${colony.id}`} href={`/colonies/${colony.id}`}
> icon={Layers}
{colony.code} title={<span className="font-mono">{colony.code}</span>}
</Link> subtitle={colony.colonyType}
</TableCell> trailing={<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>}
<TableCell>{colony.colonyType}</TableCell> />
<TableCell>
<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>
</TableCell>
</TableRow>
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</main> </main>
); );
@@ -9,14 +9,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { import { ListItemCard } from "@/components/list-item-card";
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function EquipmentPage() { export default function EquipmentPage() {
const { data: equipmentList, isLoading, isError } = useQuery({ const { data: equipmentList, isLoading, isError } = useQuery({
@@ -54,35 +47,19 @@ export default function EquipmentPage() {
) : null} ) : null}
{equipmentList && equipmentList.length > 0 ? ( {equipmentList && equipmentList.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Inventarnummer</TableHead>
<TableHead>Bezeichnung</TableHead>
<TableHead>Standort</TableHead>
<TableHead>Zustand</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{equipmentList.map((item) => ( {equipmentList.map((item) => (
<TableRow key={item.id}> <ListItemCard
<TableCell> key={item.id}
<Link
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
href={`/equipment/${item.id}`} href={`/equipment/${item.id}`}
> icon={Wrench}
{item.inventoryNumber} gradient="var(--gradient-sage)"
</Link> title={item.name}
</TableCell> subtitle={`${item.inventoryNumber} · ${item.location ?? "—"}`}
<TableCell>{item.name}</TableCell> trailing={<Badge variant={statusTone(item.condition)}>{item.condition}</Badge>}
<TableCell>{item.location ?? "—"}</TableCell> />
<TableCell>
<Badge variant={statusTone(item.condition)}>{item.condition}</Badge>
</TableCell>
</TableRow>
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</main> </main>
); );
@@ -1,7 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link";
import { Droplet } from "lucide-react"; import { Droplet } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { fetchHoneyBatches } from "@/lib/honey-batches/api"; import { fetchHoneyBatches } from "@/lib/honey-batches/api";
@@ -12,6 +11,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { ListItemCard } from "@/components/list-item-card";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -19,14 +19,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CreateHoneyBatchDialog } from "./create-honey-batch-dialog"; import { CreateHoneyBatchDialog } from "./create-honey-batch-dialog";
const STATUS_OPTIONS: HoneyBatchStatus[] = [ const STATUS_OPTIONS: HoneyBatchStatus[] = [
@@ -105,37 +97,22 @@ export default function HoneyBatchesPage() {
) : null} ) : null}
{honeyBatches && honeyBatches.length > 0 ? ( {honeyBatches && honeyBatches.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Chargennummer</TableHead>
<TableHead>Sorte</TableHead>
<TableHead>Menge</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{honeyBatches.map((honeyBatch) => ( {honeyBatches.map((honeyBatch) => (
<TableRow key={honeyBatch.id}> <ListItemCard
<TableCell> key={honeyBatch.id}
<Link
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
href={`/honey-batches/${honeyBatch.id}`} href={`/honey-batches/${honeyBatch.id}`}
> icon={Droplet}
{honeyBatch.batchNumber} title={<span className="font-mono">{honeyBatch.batchNumber}</span>}
</Link> subtitle={`${honeyBatch.honeyType ?? "—"} · ${
</TableCell> honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"
<TableCell>{honeyBatch.honeyType ?? "—"}</TableCell> }`}
<TableCell> trailing={
{honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"}
</TableCell>
<TableCell>
<Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge> <Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge>
</TableCell> }
</TableRow> />
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</main> </main>
); );
@@ -8,14 +8,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { import { ListItemCard } from "@/components/list-item-card";
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function ProductsPage() { export default function ProductsPage() {
const { data: products, isLoading, isError } = useQuery({ const { data: products, isLoading, isError } = useQuery({
@@ -52,30 +45,21 @@ export default function ProductsPage() {
) : null} ) : null}
{products && products.length > 0 ? ( {products && products.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>SKU</TableHead>
<TableHead>Verkaufseinheit</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products.map((product) => ( {products.map((product) => (
<TableRow key={product.id}> <ListItemCard
<TableCell className="font-medium">{product.name}</TableCell> key={product.id}
<TableCell className="font-mono">{product.sku ?? "—"}</TableCell> icon={ShoppingBag}
<TableCell>{product.salesUnit ?? "—"}</TableCell> title={product.name}
<TableCell> subtitle={`${product.sku ?? "—"} · ${product.salesUnit ?? "—"}`}
trailing={
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}> <Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
{product.status} {product.status}
</Badge> </Badge>
</TableCell> }
</TableRow> />
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</main> </main>
); );
+13 -29
View File
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { ListItemCard } from "@/components/list-item-card";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -17,14 +18,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const STATUS_OPTIONS: QueenStatus[] = [ const STATUS_OPTIONS: QueenStatus[] = [
"ACTIVE", "ACTIVE",
@@ -92,32 +85,23 @@ export default function QueensPage() {
) : null} ) : null}
{queens && queens.length > 0 ? ( {queens && queens.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Code</TableHead>
<TableHead>Zucht</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{queens.map((queen) => ( {queens.map((queen) => (
<TableRow key={queen.id}> <ListItemCard
<TableCell> key={queen.id}
<Link className="underline" href={`/queens/${queen.id}`}> href={`/queens/${queen.id}`}
{queen.code} icon={Crown}
</Link> gradient="var(--gradient-sage)"
</TableCell> title={queen.code}
<TableCell>{queen.breed ?? "—"}</TableCell> subtitle={queen.breed ?? "—"}
<TableCell> trailing={
<Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}> <Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
{queen.status} {queen.status}
</Badge> </Badge>
</TableCell> }
</TableRow> />
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</main> </main>
); );
@@ -8,14 +8,7 @@ import { TaskTemplate, TaskTemplateRecurrenceType } from "@/lib/task-templates/t
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { import { ListItemCard } from "@/components/list-item-card";
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CreateTaskTemplateDialog } from "./create-task-template-dialog"; import { CreateTaskTemplateDialog } from "./create-task-template-dialog";
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = { const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
@@ -76,29 +69,19 @@ export default function TaskTemplatesPage() {
) : null} ) : null}
{templates && templates.length > 0 ? ( {templates && templates.length > 0 ? (
<div className="overflow-x-auto"> <div className="flex flex-col gap-2">
<Table>
<TableHeader>
<TableRow>
<TableHead>Titel</TableHead>
<TableHead>Wiederholung</TableHead>
<TableHead>Nächste Fälligkeit</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Aktion</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.map((template) => ( {templates.map((template) => (
<TableRow key={template.id}> <ListItemCard
<TableCell className="font-medium">{template.title}</TableCell> key={template.id}
<TableCell>{describeRecurrence(template)}</TableCell> icon={Repeat}
<TableCell>{formatDate(template.nextDueDate)}</TableCell> gradient="var(--gradient-sky)"
<TableCell> title={template.title}
subtitle={`${describeRecurrence(template)} · Nächste Fälligkeit: ${formatDate(template.nextDueDate)}`}
trailing={
<>
<Badge variant={template.active ? "default" : "outline"}> <Badge variant={template.active ? "default" : "outline"}>
{template.active ? "Aktiv" : "Inaktiv"} {template.active ? "Aktiv" : "Inaktiv"}
</Badge> </Badge>
</TableCell>
<TableCell className="text-right">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -108,11 +91,10 @@ export default function TaskTemplatesPage() {
> >
Deaktivieren Deaktivieren
</Button> </Button>
</TableCell> </>
</TableRow> }
/>
))} ))}
</TableBody>
</Table>
</div> </div>
) : null} ) : null}
</main> </main>
+30 -53
View File
@@ -6,15 +6,8 @@ import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
import { QrCodeDialog } from "@/components/qr-code-dialog"; import { QrCodeDialog } from "@/components/qr-code-dialog";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { ListItemCard } from "@/components/list-item-card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CreateStorageContainerDialog } from "./create-storage-container-dialog"; import { CreateStorageContainerDialog } from "./create-storage-container-dialog";
import { CreateStockItemDialog } from "./create-stock-item-dialog"; import { CreateStockItemDialog } from "./create-stock-item-dialog";
import { RecordMovementDialog } from "./record-movement-dialog"; import { RecordMovementDialog } from "./record-movement-dialog";
@@ -50,34 +43,25 @@ export default function StoragePage() {
) : null} ) : null}
{containers && containers.length > 0 ? ( {containers && containers.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Code</TableHead>
<TableHead>Typ</TableHead>
<TableHead>Standort</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">QR-Code</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{containers.map((container) => ( {containers.map((container) => (
<TableRow key={container.id}> <ListItemCard
<TableCell className="font-mono font-medium">{container.code}</TableCell> key={container.id}
<TableCell>{container.containerType ?? "—"}</TableCell> icon={Warehouse}
<TableCell>{container.location ?? "—"}</TableCell> gradient="var(--gradient-sky)"
<TableCell> title={<span className="font-mono">{container.code}</span>}
subtitle={`${container.containerType ?? "—"} · ${container.location ?? "—"}`}
trailing={
<>
<Badge variant={container.status === "FILLED" ? "default" : "secondary"}> <Badge variant={container.status === "FILLED" ? "default" : "secondary"}>
{container.status} {container.status}
</Badge> </Badge>
</TableCell>
<TableCell className="text-right">
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" /> <QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
</TableCell> </>
</TableRow> }
/>
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</section> </section>
@@ -96,35 +80,28 @@ export default function StoragePage() {
) : null} ) : null}
{stockItems && stockItems.length > 0 ? ( {stockItems && stockItems.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Kategorie</TableHead>
<TableHead>Bestand</TableHead>
<TableHead>Aktion</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stockItems.map((item) => ( {stockItems.map((item) => (
<TableRow key={item.id}> <ListItemCard
<TableCell>{item.name}</TableCell> key={item.id}
<TableCell>{item.category}</TableCell> icon={Package}
<TableCell> gradient="var(--gradient-sky)"
title={item.name}
subtitle={item.category}
trailing={
<>
<span className="text-sm tabular-nums">
{item.quantity} {item.unit} {item.quantity} {item.unit}
</span>
{item.minThreshold !== null && item.quantity < item.minThreshold ? ( {item.minThreshold !== null && item.quantity < item.minThreshold ? (
<Badge variant="destructive" className="ml-2"> <Badge variant="destructive">Niedrig</Badge>
Niedrig
</Badge>
) : null} ) : null}
</TableCell>
<TableCell>
<RecordMovementDialog stockItemId={item.id} /> <RecordMovementDialog stockItemId={item.id} />
</TableCell> </>
</TableRow> }
/>
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</section> </section>
</main> </main>
+16 -34
View File
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header"; import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { ListItemCard } from "@/components/list-item-card";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -17,14 +18,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TaskEditDialog } from "./task-edit-dialog"; import { TaskEditDialog } from "./task-edit-dialog";
const STATUS_OPTIONS: TaskStatus[] = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"]; const STATUS_OPTIONS: TaskStatus[] = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"];
@@ -137,31 +130,21 @@ export default function TasksPage() {
) : null} ) : null}
{tasks && visibleTasks.length > 0 ? ( {tasks && visibleTasks.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader>
<TableRow>
<TableHead>Titel</TableHead>
<TableHead>Priorität</TableHead>
<TableHead>Fällig</TableHead>
<TableHead>Status</TableHead>
<TableHead>Aktion</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visibleTasks.map((task) => ( {visibleTasks.map((task) => (
<TableRow key={task.id}> <ListItemCard
<TableCell>{task.title}</TableCell> key={task.id}
<TableCell>{task.priority}</TableCell> icon={ListTodo}
<TableCell> gradient="var(--gradient-sky)"
{task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—"} title={task.title}
</TableCell> subtitle={`${task.priority} · Fällig: ${
<TableCell> task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—"
}`}
trailing={
<>
<Badge variant={task.status === "DONE" ? "secondary" : "default"}> <Badge variant={task.status === "DONE" ? "secondary" : "default"}>
{task.status} {task.status}
</Badge> </Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<TaskEditDialog task={task} /> <TaskEditDialog task={task} />
{task.status !== "DONE" && task.status !== "CANCELLED" ? ( {task.status !== "DONE" && task.status !== "CANCELLED" ? (
<Button <Button
@@ -173,12 +156,11 @@ export default function TasksPage() {
Erledigen Erledigen
</Button> </Button>
) : null} ) : null}
</div> </>
</TableCell> }
</TableRow> />
))} ))}
</TableBody> </div>
</Table>
) : null} ) : null}
</main> </main>
); );
@@ -0,0 +1,56 @@
import { ReactNode } from "react";
import Link from "next/link";
import { ChevronRight, type LucideIcon } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
interface ListItemCardProps {
href?: string;
icon: LucideIcon;
gradient?: string;
title: ReactNode;
subtitle?: ReactNode;
trailing?: ReactNode;
className?: string;
}
export function ListItemCard({
href,
icon: Icon,
gradient = "var(--gradient-honey)",
title,
subtitle,
trailing,
className,
}: ListItemCardProps) {
const body = (
<Card interactive={Boolean(href)} className={className}>
<CardContent className="flex items-center gap-3">
<div
className="flex size-10 shrink-0 items-center justify-center rounded-xl text-white shadow-[var(--shadow-soft)]"
style={{ background: gradient }}
>
<Icon className="size-5" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium">{title}</p>
{subtitle ? (
<p className="truncate text-sm text-muted-foreground">{subtitle}</p>
) : null}
</div>
{trailing ? <div className="flex shrink-0 items-center gap-2">{trailing}</div> : null}
{href ? (
<ChevronRight className="size-4 shrink-0 text-muted-foreground/60" />
) : null}
</CardContent>
</Card>
);
if (href) {
return (
<Link href={href} className="block">
{body}
</Link>
);
}
return body;
}