Replace order-filter chip wall with 3 compact selects; subgrid-align trust badges

OrderFilters.tsx: filter chips (7 status + 5 payment-status + N year
options as pills) read as cluttered and ate a lot of vertical space.
Replaced with 3 native <select>s in a row (a small Client Component
just for the onChange→router.push navigation) — same URL-search-param
filtering underneath, just a much more compact control.

TrustRow.tsx: replaced the hardcoded min-h-[3.2rem] title reservation
(a guess, and it visibly over-reserved space for single-line titles)
with a real CSS Grid + `grid-template-rows: subgrid` — every badge's
icon+title box shares the same row-track height (auto-sized to
whichever title actually needs 2 lines), so descriptions align
without any hardcoded value. The two structurally different layouts
(icon-above-title below lg vs. icon-beside-a-title/description-stack
at lg) now render as two separate blocks instead of fighting one
shared shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-30 22:11:12 +00:00
parent c8cf6ae10d
commit 475ee3fa16
3 changed files with 133 additions and 109 deletions
@@ -0,0 +1,82 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
// Three native <select>s in a row instead of a wall of filter chips (tried
// first, reverted 2026-07-30 — with 7 status options + 5 payment-status
// options + N years, a chip per option read as cluttered and ate a lot of
// vertical space). Client Component only for the onChange→navigate wiring;
// the actual filtering still happens server-side in page.tsx via the same
// URL search params, so this stays a plain GET-style filter (shareable/
// bookmarkable/back-button-safe), not client-side state.
export function OrderFilters({
statusOptions,
paymentStatusOptions,
years,
}: {
statusOptions: { value: string; label: string }[];
paymentStatusOptions: { value: string; label: string }[];
years: string[];
}) {
const router = useRouter();
const searchParams = useSearchParams();
const status = searchParams.get("status") ?? "";
const paymentStatus = searchParams.get("paymentStatus") ?? "";
const year = searchParams.get("year") ?? "";
const hasAnyFilter = Boolean(status || paymentStatus || year);
function setParam(key: string, value: string) {
const params = new URLSearchParams(searchParams.toString());
if (value) params.set(key, value);
else params.delete(key);
const qs = params.toString();
router.push(qs ? `/konto/bestellungen?${qs}` : "/konto/bestellungen");
}
const selectClass =
"w-full sm:w-auto min-w-0 border border-border rounded-sm px-3 py-2 text-body-sm text-text-primary bg-bg-base outline-none focus:border-brand transition-colors";
return (
<div className="flex flex-col sm:flex-row sm:items-center gap-3 w-full">
<select aria-label="Nach Fulfillment-Status filtern" className={selectClass} value={status} onChange={(e) => setParam("status", e.target.value)}>
<option value="">Alle Status</option>
{statusOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<select
aria-label="Nach Zahlungsstatus filtern"
className={selectClass}
value={paymentStatus}
onChange={(e) => setParam("paymentStatus", e.target.value)}
>
<option value="">Alle Zahlungsstatus</option>
{paymentStatusOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<select aria-label="Nach Jahr filtern" className={selectClass} value={year} onChange={(e) => setParam("year", e.target.value)}>
<option value="">Alle Jahre</option>
{years.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
{hasAnyFilter && (
<button
type="button"
onClick={() => router.push("/konto/bestellungen")}
className="text-body-sm font-semibold text-text-muted underline hover:text-brand transition-colors self-start sm:self-auto"
>
Zurücksetzen
</button>
)}
</div>
);
}
+7 -77
View File
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { redirect } from "next/navigation";
import Link from "next/link";
import { Reveal } from "../../components/Reveal";
@@ -13,6 +14,7 @@ import {
import { OrderStatusBadge } from "../components/OrderStatusBadge";
import { PaymentStatusBadge } from "../components/PaymentStatusBadge";
import { LogoutButton } from "../components/LogoutButton";
import { OrderFilters } from "./components/OrderFilters";
// robots: noindex — account area, same reasoning as /checkout.
export const metadata: Metadata = {
@@ -34,45 +36,8 @@ const PAYMENT_STATUS_FILTER_LABEL: Record<string, string> = {
partially_refunded: "Teilweise erstattet",
};
function FilterChip({
href,
active,
children,
}: {
href: string;
active: boolean;
children: React.ReactNode;
}) {
return (
<Link
href={href}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-body-sm font-semibold whitespace-nowrap border transition-colors ${
active
? "bg-brand border-brand text-text-primary"
: "bg-bg-base border-border text-text-muted hover:border-brand hover:text-brand"
}`}
>
{children}
</Link>
);
}
// Every filter is expressed as plain URL search params (?status=…&paymentStatus=…&year=…)
// rather than client-side state — this page stays a Server Component, each
// chip is just a <Link> to the same route with one param changed/removed,
// and the filtered result is shareable/bookmarkable/back-button-safe for
// free. buildFilterHref() only ever changes ONE param at a time, keeping
// the other active filters intact (a status filter + a year filter can
// both be active together).
function buildFilterHref(current: Record<string, string | undefined>, key: string, value: string | undefined): string {
const params = new URLSearchParams();
const next = { ...current, [key]: value };
for (const [k, v] of Object.entries(next)) {
if (v) params.set(k, v);
}
const qs = params.toString();
return qs ? `/konto/bestellungen?${qs}` : "/konto/bestellungen";
}
const STATUS_OPTIONS = Object.entries(ORDER_STATUS_LABEL).map(([value, label]) => ({ value, label }));
const PAYMENT_STATUS_OPTIONS = Object.entries(PAYMENT_STATUS_FILTER_LABEL).map(([value, label]) => ({ value, label }));
export default async function KontoBestellungenPage({
searchParams,
@@ -83,13 +48,11 @@ export default async function KontoBestellungenPage({
if (!session) redirect("/konto/login");
const { status, paymentStatus, year } = await searchParams;
const activeFilters = { status, paymentStatus, year };
const [orders, availableYears] = await Promise.all([
getCustomerOrders(session.token, session.customer.id, true, { status, paymentStatus, year }),
getCustomerOrderYears(session.token, session.customer.id),
]);
const hasAnyFilter = Boolean(status || paymentStatus || year);
return (
<>
@@ -103,42 +66,9 @@ export default async function KontoBestellungenPage({
</p>
{availableYears.length > 0 && (
<div className="flex flex-wrap gap-2 w-full">
<FilterChip href={buildFilterHref(activeFilters, "status", undefined)} active={!status}>
Alle Status
</FilterChip>
{Object.entries(ORDER_STATUS_LABEL).map(([value, label]) => (
<FilterChip key={value} href={buildFilterHref(activeFilters, "status", value)} active={status === value}>
{label}
</FilterChip>
))}
<div className="hidden sm:block w-px h-6 self-center bg-border" />
<FilterChip href={buildFilterHref(activeFilters, "paymentStatus", undefined)} active={!paymentStatus}>
Alle Zahlungsstatus
</FilterChip>
{Object.entries(PAYMENT_STATUS_FILTER_LABEL).map(([value, label]) => (
<FilterChip key={value} href={buildFilterHref(activeFilters, "paymentStatus", value)} active={paymentStatus === value}>
{label}
</FilterChip>
))}
<div className="hidden sm:block w-px h-6 self-center bg-border" />
<FilterChip href={buildFilterHref(activeFilters, "year", undefined)} active={!year}>
Alle Jahre
</FilterChip>
{availableYears.map((y) => (
<FilterChip key={y} href={buildFilterHref(activeFilters, "year", y)} active={year === y}>
{y}
</FilterChip>
))}
{hasAnyFilter && (
<Link
href="/konto/bestellungen"
className="inline-flex items-center px-3 py-1.5 text-body-sm font-semibold text-text-muted underline hover:text-brand transition-colors"
>
Filter zurücksetzen
</Link>
)}
</div>
<Suspense fallback={null}>
<OrderFilters statusOptions={STATUS_OPTIONS} paymentStatusOptions={PAYMENT_STATUS_OPTIONS} years={availableYears} />
</Suspense>
)}
{availableYears.length === 0 ? (