diff --git a/app/components/TrustRow.tsx b/app/components/TrustRow.tsx index c97198a..6646db1 100644 --- a/app/components/TrustRow.tsx +++ b/app/components/TrustRow.tsx @@ -11,41 +11,53 @@ export async function TrustRow() { if (items.length === 0) return null; return ( - // Below lg (1024px): badges sit side by side as one row (never wrap, - // never stack to one-per-row) with icon ABOVE text, centered — icon - // beside text (the lg: layout below) is what made a full row of 3 - // badges too wide below ~1024px in the first place (see git history: - // originally lg:-gated for exactly this, then briefly tried flex-wrap, - // which put an odd 3rd item alone on its own wrapped line and read as - // disorganized). Putting the icon above the text instead shrinks each - // badge down to just its text column's width, and — combined with - // dropping whitespace-nowrap on the title/description below lg: so - // long copy wraps within its own narrow column instead of forcing - // overflow — a real row of 3 now fits without wrapping at all, so - // there's no odd-item-out case to worry about. lg:+ reverts to the - // original icon-beside-text row-with-dividers layout unchanged. -
-
- {items.map((item, i) => ( -
- {i > 0 &&
} -
+
+ {/* Below lg (1024px): a CSS Grid with `grid-template-rows: subgrid` — + not a fixed min-height guess — so every badge's "icon+title" box + shares the SAME row track height (auto-sized to whichever + badge's title actually needs 2 lines), and every description + starts exactly at that row's bottom edge regardless of how many + lines its own title happens to wrap to. Icon beside text (the + lg: layout below) is what made a full row of 3 badges too wide + below ~1024px in the first place (see git history: originally + lg:-gated for exactly this, then briefly tried flex-wrap, which + put an odd 3rd item alone on its own wrapped line and read as + disorganized) — icon above text instead shrinks each badge down + to just its text column's width, letting a real row of 3 fit + without wrapping at all. Two structurally different layouts + (icon-above-title here vs. icon-beside-a-title/description- + stack at lg:) don't share one flexible markup shape cleanly, so + this renders as two separate blocks (lg:hidden / hidden lg:flex) + rather than fighting one shape across both breakpoints. */} +
+ {items.map((item) => ( +
+
-
- {/* min-h reserves 2 lines worth of height (text-body's - line-height is 1.6rem) below lg: — titles are short - enough that some wrap to 2 lines in their narrow - stacked column and some don't, and without a reserved - height the description below started at a different - y-position from one badge to the next depending on - whether its own title happened to wrap. Reset to - min-h-0 at lg: (whitespace-nowrap there forces a - single line anyway, so the reservation would just add - unwanted empty space). */} -

{item.title}

-

{item.description}

+

{item.title}

+
+

{item.description}

+
+ ))} +
+ + {/* lg+: original icon-beside-text row with dividers, unchanged. */} +
+ {items.map((item, i) => ( +
+ {i > 0 &&
} +
+
+ +
+
+

{item.title}

+

{item.description}

diff --git a/app/konto/bestellungen/components/OrderFilters.tsx b/app/konto/bestellungen/components/OrderFilters.tsx new file mode 100644 index 0000000..f04cbc9 --- /dev/null +++ b/app/konto/bestellungen/components/OrderFilters.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; + +// Three native setParam("status", e.target.value)}> + + {statusOptions.map((o) => ( + + ))} + + + + {hasAnyFilter && ( + + )} +
+ ); +} diff --git a/app/konto/bestellungen/page.tsx b/app/konto/bestellungen/page.tsx index e6d00ad..73f6388 100644 --- a/app/konto/bestellungen/page.tsx +++ b/app/konto/bestellungen/page.tsx @@ -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 = { partially_refunded: "Teilweise erstattet", }; -function FilterChip({ - href, - active, - children, -}: { - href: string; - active: boolean; - children: React.ReactNode; -}) { - return ( - - {children} - - ); -} - -// 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 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, 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({

{availableYears.length > 0 && ( -
- - Alle Status - - {Object.entries(ORDER_STATUS_LABEL).map(([value, label]) => ( - - {label} - - ))} -
- - Alle Zahlungsstatus - - {Object.entries(PAYMENT_STATUS_FILTER_LABEL).map(([value, label]) => ( - - {label} - - ))} -
- - Alle Jahre - - {availableYears.map((y) => ( - - {y} - - ))} - {hasAnyFilter && ( - - Filter zurücksetzen - - )} -
+ + + )} {availableYears.length === 0 ? (