feat: gemeinsames App-Design mit Seitenleiste, globalem Theme und neuem Login
This commit is contained in:
1 parent
be6aac86ce
commit
fa1e8c2c33
15 files changed
+1248
-189
No files matched your search
+61
-26
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user