feat: Sofort-Filter ohne Anwenden-Button; Neu-Button unter den Filtern; Bearbeiten-Link in Übersicht und Kacheln
This commit is contained in:
1 parent
b11c036dce
commit
540af1f77c
1 file changed
+9
-157
+9
-157
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user