Files
gutscheintracker/app/components/overview-toolbar.tsx
T

223 lines
7.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
export type OverviewParams = {
filter: string;
q: string;
kat: string;
typ: string;
sort: string;
ansicht: string;
gruppe: string;
bestand: string;
householdName: string | null;
};
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;
function toUrl(p: OverviewParams) {
const q = new URLSearchParams();
if (p.filter) q.set("filter", p.filter);
if (p.q) q.set("q", p.q);
if (p.kat) q.set("kat", p.kat);
if (p.typ) q.set("typ", p.typ);
if (p.sort !== "neu") q.set("sort", p.sort);
if (p.ansicht !== "liste") q.set("ansicht", p.ansicht);
if (p.gruppe !== "keine") q.set("gruppe", p.gruppe);
if (p.bestand && p.bestand !== "household") q.set("bestand", p.bestand);
const s = q.toString();
return s ? `/?${s}` : "/";
}
const selectClass =
"h-10 w-full rounded-field border border-line bg-white px-2.5 text-[13px] font-medium";
export default function OverviewToolbar({
current,
kategorien,
}: {
current: OverviewParams;
kategorien: string[];
}) {
const router = useRouter();
const [q, setQ] = useState(current.q);
useEffect(() => {
setQ(current.q);
}, [current.q]);
function update(overrides: Partial<OverviewParams>) {
router.replace(toUrl({ ...current, ...overrides }), { scroll: false });
}
useEffect(() => {
if (q === current.q) return;
const t = setTimeout(() => update({ q }), 400);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [q]);
const bestandDefault = current.householdName !== null ? "household" : "private";
const hasActive =
current.filter !== "" ||
current.q !== "" ||
current.kat !== "" ||
current.typ !== "" ||
current.sort !== "neu" ||
current.gruppe !== "keine" ||
(current.bestand !== "" && current.bestand !== bestandDefault);
return (
<div className="mb-4 grid gap-4 rounded-card border border-line bg-white p-4">
{current.householdName !== null && (
<div className="flex flex-wrap gap-2" role="group" aria-label="Bestand">
{(
[
{ key: "household", label: current.householdName },
{ key: "private", label: "Privat" },
] as const
).map((b) => (
<button
key={b.key}
type="button"
onClick={() => update({ bestand: b.key })}
aria-current={current.bestand === b.key ? "page" : undefined}
className={`rounded-full border px-4 py-2 text-[13px] font-semibold transition-colors ${
current.bestand === b.key
? "border-brand-700 bg-brand-600 text-white"
: "border-line bg-white text-ink hover:border-brand-600 hover:text-brand-700"
}`}
>
{b.label}
</button>
))}
</div>
)}
<div>
<label htmlFor="q" className="sr-only">
Suchen
</label>
<input
id="q"
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Name oder Anbieter suchen …"
autoComplete="off"
className="h-11 w-full rounded-field border border-line bg-white px-3.5 text-sm text-ink"
/>
</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" value={current.kat} onChange={(e) => update({ kat: e.target.value })} className={`${selectClass} mt-1.5`}>
<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" value={current.typ} onChange={(e) => update({ typ: e.target.value })} className={`${selectClass} mt-1.5`}>
<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" value={current.sort} onChange={(e) => update({ sort: e.target.value })} className={`${selectClass} mt-1.5`}>
<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" value={current.gruppe} onChange={(e) => update({ gruppe: e.target.value })} className={`${selectClass} mt-1.5`}>
<option value="keine">Keine</option>
<option value="anbieter">Anbieter</option>
<option value="kategorie">Kategorie</option>
<option value="ablauf">Ablauf</option>
</select>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-2">
<nav className="flex flex-wrap gap-2" aria-label="Filter">
{FILTERS.map((f) => (
<button
key={f.key}
type="button"
onClick={() => update({ 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}
</button>
))}
</nav>
<div className="flex gap-2" role="group" aria-label="Ansicht">
{(
[
{ key: "liste", label: "Liste" },
{ key: "kachel", label: "Kacheln" },
] as const
).map((a) => (
<button
key={a.key}
type="button"
onClick={() => update({ 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}
</button>
))}
</div>
</div>
{hasActive && (
<div>
<button
type="button"
onClick={() => {
setQ("");
router.replace("/", { scroll: false });
}}
className="text-sm font-medium text-muted underline hover:text-ink"
>
Zurücksetzen
</button>
</div>
)}
</div>
);
}