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

+61 -26
View File
@@ -1,7 +1,8 @@
import Link from "next/link";
import { Suspense } from "react";
import Shell from "./components/shell";
import { getDb } from "@/lib/db";
import { getVoucherStatus } from "@/lib/voucher-status";
import { getVoucherStatus, type VoucherStatus } from "@/lib/voucher-status";
import { formatEuro, isExpiringSoon } from "@/lib/vouchers";
const FILTERS = [
@@ -12,13 +13,25 @@ const FILTERS = [
{ key: "eingeloest", label: "Eingelöst" },
] as const;
const STATUS_STYLE: Record<VoucherStatus, string> = {
aktiv: "bg-brand-50 text-brand-800",
abgelaufen: "bg-danger/10 text-danger",
eingeloest: "bg-wash text-muted",
};
function statusLabel(s: VoucherStatus) {
if (s === "aktiv") return "Aktiv";
if (s === "abgelaufen") return "Abgelaufen";
return "Eingelöst";
}
function Skeleton() {
return (
<ul aria-label="lädt">
<ul aria-label="lädt" className="grid gap-3">
{[0, 1, 2].map((i) => (
<li key={i} className="animate-pulse rounded border p-4">
<div className="h-4 w-1/3 bg-gray-200" />
<div className="mt-2 h-3 w-1/4 bg-gray-100" />
<li key={i} className="animate-pulse rounded-card border border-line bg-white p-5">
<div className="h-4 w-1/3 rounded bg-line" />
<div className="mt-2 h-3 w-1/4 rounded bg-wash" />
</li>
))}
</ul>
@@ -38,26 +51,38 @@ async function VoucherList({ filter }: { filter: string }) {
});
if (filtered.length === 0) {
return (
<div className="rounded border p-8 text-center">
<p>Keine Gutscheine gefunden.</p>
<Link href="/vouchers/new" className="mt-4 inline-block rounded bg-black px-4 py-2 text-white">
<div className="rounded-card border border-line bg-white p-10 text-center">
<p className="text-sm text-muted">Keine Gutscheine gefunden.</p>
<Link
href="/vouchers/new"
className="mt-4 inline-block rounded-field bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-700"
>
Gutschein anlegen
</Link>
</div>
);
}
return (
<ul className="space-y-2">
{filtered.map((v) => (
<li key={v.id} className="rounded border p-4">
<Link href={`/vouchers/${v.id}`} className="font-medium underline">
{v.name}
</Link>
<p className="text-sm text-gray-600">
{v.anbieter} · {formatEuro(v.restCents)} / {formatEuro(v.wertCents)} · {getVoucherStatus(v)}
</p>
</li>
))}
<ul className="grid gap-3">
{filtered.map((v) => {
const s = getVoucherStatus(v);
return (
<li key={v.id} className="rounded-card border border-line bg-white p-5 transition-shadow hover:shadow-[0_8px_24px_rgba(32,55,39,0.08)]">
<div className="flex items-start justify-between gap-3">
<Link href={`/vouchers/${v.id}`} className="text-[15px] font-semibold tracking-tight hover:text-brand-700">
{v.name}
</Link>
<span className={`shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold ${STATUS_STYLE[s]}`}>
{statusLabel(s)}
</span>
</div>
<p className="mt-1.5 text-sm text-muted">
{v.anbieter} · {formatEuro(v.restCents)} von {formatEuro(v.wertCents)}
{v.ablaufdatum ? ` · bis ${v.ablaufdatum.toLocaleDateString("de-DE")}` : ""}
</p>
</li>
);
})}
</ul>
);
}
@@ -65,20 +90,30 @@ async function VoucherList({ filter }: { filter: string }) {
export default async function Home({ searchParams }: { searchParams: Promise<{ filter?: string }> }) {
const { filter = "" } = await searchParams;
return (
<main className="mx-auto max-w-2xl space-y-4 p-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold">Gutscheine</h1>
<Link href="/vouchers/new" className="rounded bg-black px-4 py-2 text-white">
<Shell>
<div className="mb-5 flex items-end justify-between gap-3">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Gutscheine</h1>
<p className="mt-1 text-sm text-muted">Alle Gutscheine eures Haushalts im Blick.</p>
</div>
<Link
href="/vouchers/new"
className="shrink-0 rounded-field bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-700"
>
Neu
</Link>
</div>
<nav className="flex gap-2" aria-label="Filter">
<nav className="mb-5 flex flex-wrap gap-2" aria-label="Filter">
{FILTERS.map((f) => (
<Link
key={f.key}
href={f.key ? `/?filter=${f.key}` : "/"}
aria-current={filter === f.key ? "page" : undefined}
className={`rounded border px-3 py-1 text-sm ${filter === f.key ? "bg-black text-white" : ""}`}
className={`rounded-full border px-3.5 py-1.5 text-[13px] font-medium transition-colors ${
filter === f.key
? "border-brand-700 bg-brand-600 text-white"
: "border-line bg-white text-ink hover:border-brand-600 hover:text-brand-700"
}`}
>
{f.label}
</Link>
@@ -87,6 +122,6 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ f
<Suspense fallback={<Skeleton />}>
<VoucherList filter={filter} />
</Suspense>
</main>
</Shell>
);
}