feat: Sofort-Filter ohne Anwenden-Button; Neu-Button unter den Filtern; Bearbeiten-Link in Übersicht und Kacheln

This commit is contained in:
opencode committed 2026-10-18 15:15:47 +02:00
1 parent b11c036dce
commit 540af1f77c
1 file changed
+9 -157
+9 -157
View File
@@ -2,6 +2,7 @@ import Link from "next/link";
import { Suspense } from "react";
import Shell from "./components/shell";
import LogoImg from "./components/logo-img";
import OverviewToolbar from "./components/overview-toolbar";
import { withLogoUrl } from "@/lib/logo";
import type { Voucher as DbVoucher } from "@prisma/client";
import { getDb } from "@/lib/db";
@@ -9,14 +10,6 @@ import { getVoucherStatus, type VoucherStatus } from "@/lib/voucher-status";
import { formatEuro, formatMindestbestellwert, formatWert } from "@/lib/vouchers";
import { filterVouchers, sortVouchers, groupVouchers, listCategories } from "@/lib/voucher-view";
const FILTERS = [
{ key: "", label: "Alle" },
{ key: "aktiv", label: "Aktiv" },
{ key: "bald", label: "Bald ablaufend" },
{ key: "abgelaufen", label: "Abgelaufen" },
{ 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",
@@ -29,23 +22,6 @@ function statusLabel(s: VoucherStatus) {
return "Eingelöst";
}
function hrefWith(
current: { filter: string; q: string; kat: string; typ: string; sort: string; ansicht: string; gruppe: string },
overrides: Partial<{ filter: string; q: string; kat: string; typ: string; sort: string; ansicht: string; gruppe: string }>
) {
const p = new URLSearchParams();
const merged = { ...current, ...overrides };
if (merged.filter) p.set("filter", merged.filter);
if (merged.q) p.set("q", merged.q);
if (merged.kat) p.set("kat", merged.kat);
if (merged.typ) p.set("typ", merged.typ);
if (merged.sort && merged.sort !== "neu") p.set("sort", merged.sort);
if (merged.ansicht && merged.ansicht !== "liste") p.set("ansicht", merged.ansicht);
if (merged.gruppe && merged.gruppe !== "keine") p.set("gruppe", merged.gruppe);
const s = p.toString();
return s ? `/?${s}` : "/";
}
type VoucherRow = DbVoucher & { logoUrl: string | null };
function StatusChip({ s }: { s: VoucherStatus }) {
@@ -133,9 +109,6 @@ function Skeleton() {
);
}
const selectClass =
"h-10 rounded-field border border-line bg-white px-2.5 text-[13px] font-medium";
async function VoucherOverview({
params,
}: {
@@ -207,140 +180,19 @@ export default async function Home({
const kategorien = listCategories(await db.voucher.findMany({ select: { kategorie: true } }));
return (
<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>
<div className="mb-5">
<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>
<OverviewToolbar current={current} kategorien={kategorien} />
<div className="mb-5">
<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"
className="block h-12 rounded-field bg-brand-600 text-center text-sm font-semibold leading-[3rem] text-white transition-colors hover:bg-brand-700 sm:inline-block sm:h-auto sm:px-6 sm:py-2.5 sm:leading-normal"
>
Neu
Neuer Gutschein
</Link>
</div>
<form method="get" action="/" className="mb-4 grid gap-3 rounded-card border border-line bg-white p-4">
<div className="flex gap-2">
<label htmlFor="q" className="sr-only">
Suchen
</label>
<input
id="q"
name="q"
defaultValue={current.q}
placeholder="Name oder Anbieter suchen …"
className="h-11 min-w-0 flex-1 rounded-field border border-line bg-white px-3.5 text-sm text-ink"
/>
<button
type="submit"
className="h-11 shrink-0 rounded-field bg-brand-600 px-5 text-sm font-semibold text-white transition-colors hover:bg-brand-700"
>
Suchen
</button>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div>
<label htmlFor="kat" className="block text-[13px] font-semibold">
Kategorie
</label>
<select id="kat" name="kat" defaultValue={current.kat} className={`${selectClass} mt-1.5 w-full`}>
<option value="">Alle</option>
{kategorien.map((k) => (
<option key={k} value={k}>
{k}
</option>
))}
<option value="ohne">Ohne Kategorie</option>
</select>
</div>
<div>
<label htmlFor="typ" className="block text-[13px] font-semibold">
Gutscheinart
</label>
<select id="typ" name="typ" defaultValue={current.typ} className={`${selectClass} mt-1.5 w-full`}>
<option value="">Alle</option>
<option value="euro">Geldwert</option>
<option value="prozent">Rabatt</option>
</select>
</div>
<div>
<label htmlFor="sort" className="block text-[13px] font-semibold">
Sortierung
</label>
<select id="sort" name="sort" defaultValue={current.sort} className={`${selectClass} mt-1.5 w-full`}>
<option value="neu">Neueste zuerst</option>
<option value="ablauf">Ablaufdatum</option>
<option value="name">Name</option>
<option value="anbieter">Anbieter</option>
</select>
</div>
<div>
<label htmlFor="gruppe" className="block text-[13px] font-semibold">
Gruppierung
</label>
<select id="gruppe" name="gruppe" defaultValue={current.gruppe} className={`${selectClass} mt-1.5 w-full`}>
<option value="keine">Keine</option>
<option value="anbieter">Anbieter</option>
<option value="kategorie">Kategorie</option>
<option value="ablauf">Ablauf</option>
</select>
</div>
</div>
{current.filter && <input type="hidden" name="filter" value={current.filter} />}
{current.ansicht !== "liste" && <input type="hidden" name="ansicht" value={current.ansicht} />}
<div>
<button
type="submit"
className="h-10 rounded-field border border-line px-5 text-sm font-semibold transition-colors hover:border-brand-600 hover:text-brand-700"
>
Anwenden
</button>
{(current.q || current.kat || current.typ || current.sort !== "neu" || current.gruppe !== "keine" || current.filter) && (
<Link href={current.ansicht === "kachel" ? "/?ansicht=kachel" : "/"} className="ml-3 text-sm font-medium text-muted underline hover:text-ink">
Zurücksetzen
</Link>
)}
</div>
</form>
<div className="mb-5 flex flex-wrap items-center justify-between gap-2">
<nav className="flex flex-wrap gap-2" aria-label="Filter">
{FILTERS.map((f) => (
<Link
key={f.key}
href={hrefWith(current, { filter: f.key })}
aria-current={current.filter === f.key ? "page" : undefined}
className={`rounded-full border px-3.5 py-1.5 text-[13px] font-medium transition-colors ${
current.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>
))}
</nav>
<div className="flex gap-2" role="group" aria-label="Ansicht">
{(
[
{ key: "liste", label: "Liste" },
{ key: "kachel", label: "Kacheln" },
] as const
).map((a) => (
<Link
key={a.key}
href={hrefWith(current, { ansicht: a.key })}
aria-current={current.ansicht === a.key ? "page" : undefined}
className={`rounded-full border px-3.5 py-1.5 text-[13px] font-medium transition-colors ${
current.ansicht === a.key
? "border-brand-700 bg-brand-600 text-white"
: "border-line bg-white text-ink hover:border-brand-600 hover:text-brand-700"
}`}
>
{a.label}
</Link>
))}
</div>
</div>
<Suspense fallback={<Skeleton />}>
<VoucherOverview params={current} />
</Suspense>