feat: gemeinsames App-Design mit Seitenleiste, globalem Theme und neuem Login

This commit is contained in:
opencode committed 2026-10-18 11:11:30 +02:00
1 parent be6aac86ce
commit fa1e8c2c33
15 files changed
+1248 -189

No files matched your search

+56 -22
View File
@@ -1,6 +1,7 @@
"use client";
import { use, useEffect, useState } from "react";
import Link from "next/link";
import Shell from "../../components/shell";
import { formatEuro } from "@/lib/vouchers";
import { getVoucherStatus } from "@/lib/voucher-status";
@@ -18,6 +19,15 @@ type Voucher = {
contacts?: { contact: { id: number; name: string; email: string | null; telegramChatId: string | null } }[];
};
function Row({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5">
<dt className="text-sm text-muted">{label}</dt>
<dd className="text-sm font-medium">{children}</dd>
</div>
);
}
export default function VoucherDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params);
const [v, setV] = useState<Voucher | null>(null);
@@ -41,37 +51,61 @@ export default function VoucherDetailPage({ params }: { params: Promise<{ id: st
if (res.ok) setV(await res.json());
}
if (loading) return <main className="mx-auto max-w-xl animate-pulse space-y-2 p-6"><div className="h-6 w-1/2 bg-gray-200" /><div className="h-4 w-1/3 bg-gray-100" /></main>;
if (loading)
return (
<Shell>
<div className="animate-pulse rounded-card border border-line bg-white p-6">
<div className="h-6 w-1/2 rounded bg-line" />
<div className="mt-2 h-4 w-1/3 rounded bg-wash" />
</div>
</Shell>
);
if (error || !v)
return (
<main className="mx-auto max-w-xl space-y-4 p-6">
<p>{error}</p>
<Link href="/" className="underline">Zurück zur Liste</Link>
</main>
<Shell>
<div className="rounded-card border border-line bg-white p-8 text-center">
<p className="text-sm text-muted">{error}</p>
<Link href="/" className="mt-4 inline-block text-sm font-semibold text-brand-700 underline">
Zurück zur Liste
</Link>
</div>
</Shell>
);
const status = getVoucherStatus({
status: v.status,
ablaufdatum: v.ablaufdatum ? new Date(v.ablaufdatum) : null,
});
return (
<main className="mx-auto max-w-xl space-y-3 p-6">
<Link href="/" className="text-sm underline">← Zurück</Link>
<h1 className="text-xl font-bold">{v.name}</h1>
<dl className="space-y-1 text-sm">
<div><dt className="font-medium">Anbieter</dt><dd>{v.anbieter}</dd></div>
<div><dt className="font-medium">Wert</dt><dd>{formatEuro(v.wertCents)}</dd></div>
<div><dt className="font-medium">Rest</dt><dd>{formatEuro(v.restCents)}</dd></div>
<div><dt className="font-medium">Status</dt><dd>{getVoucherStatus({ status: v.status, ablaufdatum: v.ablaufdatum ? new Date(v.ablaufdatum) : null })}</dd></div>
{v.ablaufdatum && <div><dt className="font-medium">Ablaufdatum</dt><dd>{new Date(v.ablaufdatum).toLocaleDateString("de-DE")}</dd></div>}
{v.codeText && <div><dt className="font-medium">Code</dt><dd>{v.codeText}</dd></div>}
{v.kategorie && <div><dt className="font-medium">Kategorie</dt><dd>{v.kategorie}</dd></div>}
{v.notiz && <div><dt className="font-medium">Notiz</dt><dd>{v.notiz}</dd></div>}
{v.contacts && v.contacts.length > 0 && (
<div><dt className="font-medium">Empfänger</dt><dd>{v.contacts.map((c) => c.contact.name).join(", ")}</dd></div>
<Shell>
<Link href="/" className="text-sm font-medium text-muted hover:text-ink">
← Zurück
</Link>
<h1 className="mt-2 text-2xl font-semibold tracking-tight">{v.name}</h1>
<section aria-label="Details" className="mt-4 divide-y divide-line rounded-card border border-line bg-white px-5 py-2">
<Row label="Anbieter">{v.anbieter}</Row>
<Row label="Wert">{formatEuro(v.wertCents)}</Row>
<Row label="Restguthaben">{formatEuro(v.restCents)}</Row>
<Row label="Status">{status === "aktiv" ? "Aktiv" : status === "abgelaufen" ? "Abgelaufen" : "Eingelöst"}</Row>
{v.ablaufdatum && (
<Row label="Ablaufdatum">{new Date(v.ablaufdatum).toLocaleDateString("de-DE")}</Row>
)}
</dl>
{v.codeText && <Row label="Code">{v.codeText}</Row>}
{v.kategorie && <Row label="Kategorie">{v.kategorie}</Row>}
{v.notiz && <Row label="Notiz">{v.notiz}</Row>}
{v.contacts && v.contacts.length > 0 && (
<Row label="Empfänger">{v.contacts.map((c) => c.contact.name).join(", ")}</Row>
)}
</section>
{v.status !== "eingeloest" && (
<button onClick={redeem} className="rounded bg-black px-4 py-2 text-white">
<button
onClick={redeem}
className="mt-4 h-12 w-full rounded-field bg-brand-600 text-sm font-semibold text-white transition-colors hover:bg-brand-700 active:scale-[0.99] sm:w-auto sm:px-8"
>
Einlösen
</button>
)}
</main>
</Shell>
);
}