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
+242 -362

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> {apiaries.map((apiary) => (
<TableRow> <ListItemCard
<TableHead>Name</TableHead> key={apiary.id}
<TableHead>Standort</TableHead> href={`/apiaries/${apiary.id}`}
<TableHead>Status</TableHead> icon={MapPin}
</TableRow> title={apiary.name}
</TableHeader> subtitle={apiary.location ?? "—"}
<TableBody> trailing={
{apiaries.map((apiary) => ( <Badge variant={apiary.status === "ACTIVE" ? "default" : "secondary"}>
<TableRow key={apiary.id}> {apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"}
<TableCell> </Badge>
<Link className="underline" href={`/apiaries/${apiary.id}`}> }
{apiary.name} />
</Link> ))}
</TableCell> </div>
<TableCell>{apiary.location ?? "—"}</TableCell>
<TableCell>
<Badge variant={apiary.status === "ACTIVE" ? "default" : "secondary"}>
{apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</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> {colonies.map((colony) => (
<TableRow> <ListItemCard
<TableHead>Code</TableHead> key={colony.id}
<TableHead>Typ</TableHead> href={`/colonies/${colony.id}`}
<TableHead>Status</TableHead> icon={Layers}
</TableRow> title={<span className="font-mono">{colony.code}</span>}
</TableHeader> subtitle={colony.colonyType}
<TableBody> trailing={<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>}
{colonies.map((colony) => ( />
<TableRow key={colony.id}> ))}
<TableCell> </div>
<Link
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
href={`/colonies/${colony.id}`}
>
{colony.code}
</Link>
</TableCell>
<TableCell>{colony.colonyType}</TableCell>
<TableCell>
<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</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> {equipmentList.map((item) => (
<TableRow> <ListItemCard
<TableHead>Inventarnummer</TableHead> key={item.id}
<TableHead>Bezeichnung</TableHead> href={`/equipment/${item.id}`}
<TableHead>Standort</TableHead> icon={Wrench}
<TableHead>Zustand</TableHead> gradient="var(--gradient-sage)"
</TableRow> title={item.name}
</TableHeader> subtitle={`${item.inventoryNumber} · ${item.location ?? "—"}`}
<TableBody> trailing={<Badge variant={statusTone(item.condition)}>{item.condition}</Badge>}
{equipmentList.map((item) => ( />
<TableRow key={item.id}> ))}
<TableCell> </div>
<Link
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
href={`/equipment/${item.id}`}
>
{item.inventoryNumber}
</Link>
</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell>{item.location ?? "—"}</TableCell>
<TableCell>
<Badge variant={statusTone(item.condition)}>{item.condition}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</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> {honeyBatches.map((honeyBatch) => (
<TableRow> <ListItemCard
<TableHead>Chargennummer</TableHead> key={honeyBatch.id}
<TableHead>Sorte</TableHead> href={`/honey-batches/${honeyBatch.id}`}
<TableHead>Menge</TableHead> icon={Droplet}
<TableHead>Status</TableHead> title={<span className="font-mono">{honeyBatch.batchNumber}</span>}
</TableRow> subtitle={`${honeyBatch.honeyType ?? "—"} · ${
</TableHeader> honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"
<TableBody> }`}
{honeyBatches.map((honeyBatch) => ( trailing={
<TableRow key={honeyBatch.id}> <Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge>
<TableCell> }
<Link />
className="font-mono font-medium text-primary underline-offset-4 hover:underline" ))}
href={`/honey-batches/${honeyBatch.id}`} </div>
>
{honeyBatch.batchNumber}
</Link>
</TableCell>
<TableCell>{honeyBatch.honeyType ?? "—"}</TableCell>
<TableCell>
{honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"}
</TableCell>
<TableCell>
<Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</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> {products.map((product) => (
<TableRow> <ListItemCard
<TableHead>Name</TableHead> key={product.id}
<TableHead>SKU</TableHead> icon={ShoppingBag}
<TableHead>Verkaufseinheit</TableHead> title={product.name}
<TableHead>Status</TableHead> subtitle={`${product.sku ?? "—"} · ${product.salesUnit ?? "—"}`}
</TableRow> trailing={
</TableHeader> <Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
<TableBody> {product.status}
{products.map((product) => ( </Badge>
<TableRow key={product.id}> }
<TableCell className="font-medium">{product.name}</TableCell> />
<TableCell className="font-mono">{product.sku ?? "—"}</TableCell> ))}
<TableCell>{product.salesUnit ?? "—"}</TableCell> </div>
<TableCell>
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
{product.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : null} ) : null}
</main> </main>
); );
+18 -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";
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> {queens.map((queen) => (
<TableRow> <ListItemCard
<TableHead>Code</TableHead> key={queen.id}
<TableHead>Zucht</TableHead> href={`/queens/${queen.id}`}
<TableHead>Status</TableHead> icon={Crown}
</TableRow> gradient="var(--gradient-sage)"
</TableHeader> title={queen.code}
<TableBody> subtitle={queen.breed ?? "—"}
{queens.map((queen) => ( trailing={
<TableRow key={queen.id}> <Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
<TableCell> {queen.status}
<Link className="underline" href={`/queens/${queen.id}`}> </Badge>
{queen.code} }
</Link> />
</TableCell> ))}
<TableCell>{queen.breed ?? "—"}</TableCell> </div>
<TableCell>
<Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
{queen.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</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,43 +69,32 @@ 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> {templates.map((template) => (
<TableHeader> <ListItemCard
<TableRow> key={template.id}
<TableHead>Titel</TableHead> icon={Repeat}
<TableHead>Wiederholung</TableHead> gradient="var(--gradient-sky)"
<TableHead>Nächste Fälligkeit</TableHead> title={template.title}
<TableHead>Status</TableHead> subtitle={`${describeRecurrence(template)} · Nächste Fälligkeit: ${formatDate(template.nextDueDate)}`}
<TableHead className="text-right">Aktion</TableHead> trailing={
</TableRow> <>
</TableHeader> <Badge variant={template.active ? "default" : "outline"}>
<TableBody> {template.active ? "Aktiv" : "Inaktiv"}
{templates.map((template) => ( </Badge>
<TableRow key={template.id}> <Button
<TableCell className="font-medium">{template.title}</TableCell> type="button"
<TableCell>{describeRecurrence(template)}</TableCell> variant="ghost"
<TableCell>{formatDate(template.nextDueDate)}</TableCell> size="sm"
<TableCell> disabled={!template.active || deactivateMutation.isPending}
<Badge variant={template.active ? "default" : "outline"}> onClick={() => deactivateMutation.mutate(template.id)}
{template.active ? "Aktiv" : "Inaktiv"} >
</Badge> Deaktivieren
</TableCell> </Button>
<TableCell className="text-right"> </>
<Button }
type="button" />
variant="ghost" ))}
size="sm"
disabled={!template.active || deactivateMutation.isPending}
onClick={() => deactivateMutation.mutate(template.id)}
>
Deaktivieren
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div> </div>
) : null} ) : null}
</main> </main>
+35 -58
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> {containers.map((container) => (
<TableRow> <ListItemCard
<TableHead>Code</TableHead> key={container.id}
<TableHead>Typ</TableHead> icon={Warehouse}
<TableHead>Standort</TableHead> gradient="var(--gradient-sky)"
<TableHead>Status</TableHead> title={<span className="font-mono">{container.code}</span>}
<TableHead className="text-right">QR-Code</TableHead> subtitle={`${container.containerType ?? "—"} · ${container.location ?? "—"}`}
</TableRow> trailing={
</TableHeader> <>
<TableBody>
{containers.map((container) => (
<TableRow key={container.id}>
<TableCell className="font-mono font-medium">{container.code}</TableCell>
<TableCell>{container.containerType ?? "—"}</TableCell>
<TableCell>{container.location ?? "—"}</TableCell>
<TableCell>
<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> ))}
</Table> </div>
) : 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> {stockItems.map((item) => (
<TableRow> <ListItemCard
<TableHead>Name</TableHead> key={item.id}
<TableHead>Kategorie</TableHead> icon={Package}
<TableHead>Bestand</TableHead> gradient="var(--gradient-sky)"
<TableHead>Aktion</TableHead> title={item.name}
</TableRow> subtitle={item.category}
</TableHeader> trailing={
<TableBody> <>
{stockItems.map((item) => ( <span className="text-sm tabular-nums">
<TableRow key={item.id}> {item.quantity} {item.unit}
<TableCell>{item.name}</TableCell> </span>
<TableCell>{item.category}</TableCell>
<TableCell>
{item.quantity} {item.unit}
{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> ))}
</Table> </div>
) : null} ) : null}
</section> </section>
</main> </main>
+29 -47
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,48 +130,37 @@ export default function TasksPage() {
) : null} ) : null}
{tasks && visibleTasks.length > 0 ? ( {tasks && visibleTasks.length > 0 ? (
<Table> <div className="flex flex-col gap-2">
<TableHeader> {visibleTasks.map((task) => (
<TableRow> <ListItemCard
<TableHead>Titel</TableHead> key={task.id}
<TableHead>Priorität</TableHead> icon={ListTodo}
<TableHead>Fällig</TableHead> gradient="var(--gradient-sky)"
<TableHead>Status</TableHead> title={task.title}
<TableHead>Aktion</TableHead> subtitle={`${task.priority} · Fällig: ${
</TableRow> task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—"
</TableHeader> }`}
<TableBody> trailing={
{visibleTasks.map((task) => ( <>
<TableRow key={task.id}>
<TableCell>{task.title}</TableCell>
<TableCell>{task.priority}</TableCell>
<TableCell>
{task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—"}
</TableCell>
<TableCell>
<Badge variant={task.status === "DONE" ? "secondary" : "default"}> <Badge variant={task.status === "DONE" ? "secondary" : "default"}>
{task.status} {task.status}
</Badge> </Badge>
</TableCell> <TaskEditDialog task={task} />
<TableCell> {task.status !== "DONE" && task.status !== "CANCELLED" ? (
<div className="flex items-center gap-2"> <Button
<TaskEditDialog task={task} /> size="sm"
{task.status !== "DONE" && task.status !== "CANCELLED" ? ( variant="outline"
<Button disabled={completeMutation.isPending}
size="sm" onClick={() => completeMutation.mutate(task.id)}
variant="outline" >
disabled={completeMutation.isPending} Erledigen
onClick={() => completeMutation.mutate(task.id)} </Button>
> ) : null}
Erledigen </>
</Button> }
) : null} />
</div> ))}
</TableCell> </div>
</TableRow>
))}
</TableBody>
</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;
}