Fix order-filter dropdowns, restructure Konto identity line, drop blog reset link

- CustomSelect.tsx: mousedown on an option blurred the trigger button
  before the click landed, unmounting the list before anything could be
  selected — add preventDefault on the list's mousedown to stop that.
- KontoShell no longer renders "Eingeloggt als ..." above each page's
  content; each page renders its own title then AccountIdentity right
  below it, and ProfileForm's now-duplicate email/Kundennummer line is
  removed.
- Blog category filter: drop the separate "Zurücksetzen" link — clicking
  an active category badge again already deactivates it.
- Werkzeuge cards: more vertical gap between stacked cards below sm:.
- Legal pages' "Stand: ..." line is now derived from the LegalPages doc's
  own updatedAt instead of a hand-typed string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
This commit is contained in:
Marco
2026-07-31 22:29:36 +00:00
parent d018d87e25
commit bf6c5d079a
15 changed files with 80 additions and 50 deletions
+2 -1
View File
@@ -9,6 +9,7 @@ import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
import { getLegalPage } from "../lib/payload";
import { formatMonthYear } from "../lib/format";
export const metadata: Metadata = {
title: "AGB",
@@ -36,7 +37,7 @@ export default async function AgbPage() {
>
Allgemeine Geschäftsbedingungen
</p>
<p className="text-body text-text-muted">Stand: Juli 2026</p>
{page && <p className="text-body text-text-muted">Stand: {formatMonthYear(page.updatedAt)}</p>}
</Reveal>
{/* MobileSectionTOC — below lg: only, see SectionTOC.tsx's own
-8
View File
@@ -96,14 +96,6 @@ export default async function BlogOverviewPage({
</Link>
);
})}
{activeCategories.length > 0 && (
<Link
href="/blog"
className="inline-flex items-center px-3 py-1.5 text-body-sm font-semibold text-text-muted underline hover:text-brand transition-colors"
>
Zurücksetzen
</Link>
)}
</div>
)}
</div>
+5
View File
@@ -131,6 +131,11 @@ export function CustomSelect({
ref={listRef}
role="listbox"
aria-label={label}
// Without this, a mousedown on an <li> blurs the trigger button
// first (li isn't focusable) — the resulting onBlur closes and
// unmounts this list before the click event ever fires, so
// nothing is ever selectable by mouse/touch.
onMouseDown={(e) => e.preventDefault()}
className="absolute z-20 mt-1 w-full sm:min-w-[12rem] max-h-64 overflow-y-auto bg-bg-base border border-border rounded-sm shadow-lg py-1"
>
{allOptions.map((o, i) => (
+1 -1
View File
@@ -35,7 +35,7 @@ export async function Tools() {
arrives where the fluid floor also sits), stacked below sm;
internal icon+text layout stays horizontal at every size, only
the outer span changes */}
<RevealGroup className="grid grid-cols-1 sm:grid-cols-12 gap-10 sm:gap-[var(--layout-grid-gap)] px-[var(--layout-padding-x)] w-full">
<RevealGroup className="grid grid-cols-1 gap-y-14 sm:grid-cols-12 sm:gap-y-10 gap-x-10 sm:gap-x-[var(--layout-grid-gap)] px-[var(--layout-padding-x)] w-full">
{tools.map((tool) => (
<RevealItem
key={tool.id}
+2 -1
View File
@@ -8,6 +8,7 @@ import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
import { getLegalPage } from "../lib/payload";
import { formatMonthYear } from "../lib/format";
export const metadata: Metadata = {
title: "Datenschutzerklärung",
@@ -35,7 +36,7 @@ export default async function DatenschutzPage() {
>
Datenschutzerklärung
</p>
<p className="text-body text-text-muted">Stand: Juli 2026</p>
{page && <p className="text-body text-text-muted">Stand: {formatMonthYear(page.updatedAt)}</p>}
</Reveal>
{/* MobileSectionTOC — below lg: only, see SectionTOC.tsx's own
+8 -4
View File
@@ -14,6 +14,7 @@ import { getOrderFilterEnabled } from "../../lib/payload";
import { OrderStatusBadge } from "../components/OrderStatusBadge";
import { PaymentStatusBadge } from "../components/PaymentStatusBadge";
import { KontoShell } from "../components/KontoShell";
import { AccountIdentity } from "../components/AccountIdentity";
import { OrderFilters } from "./components/OrderFilters";
// robots: noindex — account area, same reasoning as /checkout.
@@ -59,11 +60,14 @@ export default async function KontoBestellungenPage({
]);
return (
<KontoShell customer={session.customer}>
<KontoShell>
<Reveal className="flex flex-col gap-6 items-start w-full">
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Meine Bestellungen
</p>
<div className="flex flex-col gap-1 items-start">
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Meine Bestellungen
</p>
<AccountIdentity email={session.customer.email} customerNumber={session.customer.customerNumber} />
</div>
{orderFilterEnabled && availableYears.length > 0 && (
<Suspense fallback={null}>
+7
View File
@@ -0,0 +1,7 @@
export function AccountIdentity({ email, customerNumber }: { email: string; customerNumber: string }) {
return (
<p className="text-body-sm text-text-muted">
Eingeloggt als {email} (Kundennummer {customerNumber})
</p>
);
}
+7 -17
View File
@@ -12,28 +12,18 @@ import { AccountNav } from "./AccountNav";
// page-specific logic each page still needs (e.g. merkliste's return-URL
// login redirect, profil's second profile-fetch redirect).
//
// `customer` is rendered here, once, so "Eingeloggt als …" looks and
// appears identically on every /konto/* page — it used to be hand-copied
// onto individual pages (bestellungen had it, merkliste/profil didn't),
// which read as inconsistent rather than a deliberate per-page choice.
export async function KontoShell({
children,
customer,
}: {
children: ReactNode;
customer: { email: string; customerNumber: string };
}) {
// "Eingeloggt als …" is NOT rendered here (even though `customer` used to
// be the only prop this shell needed) — it belongs directly under each
// page's own title so it reads as "page title, then who's logged in", not
// above the title before the page has even introduced itself. See
// AccountIdentity.tsx, rendered by each page right after its own heading.
export async function KontoShell({ children }: { children: ReactNode }) {
return (
<>
<main className="flex flex-col flex-1 bg-bg-base">
<div className="flex flex-col sm:flex-row gap-6 sm:gap-10 w-full max-w-[75rem] mx-auto px-[var(--layout-padding-x)] pt-8 sm:pt-10 pb-16">
<AccountNav />
<div className="flex-1 min-w-0 flex flex-col gap-6">
<p className="text-body-sm text-text-muted">
Eingeloggt als {customer.email} (Kundennummer {customer.customerNumber})
</p>
{children}
</div>
<div className="flex-1 min-w-0 flex flex-col gap-6">{children}</div>
</div>
</main>
<Footer />
+8 -4
View File
@@ -4,6 +4,7 @@ import { Reveal } from "../../components/Reveal";
import { getSessionCustomer, getWishlist } from "../../lib/customerAuth";
import { getProductsByIds, getWishlistEnabled, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload";
import { KontoShell } from "../components/KontoShell";
import { AccountIdentity } from "../components/AccountIdentity";
import { MerklisteGrid } from "./components/MerklisteGrid";
// robots: noindex — account area, same reasoning as /konto/bestellungen.
@@ -40,11 +41,14 @@ export default async function KontoMerklistePage() {
const initialProducts = await getProductsByIds(wishlistItems.map((i) => i.productId));
return (
<KontoShell customer={session.customer}>
<KontoShell>
<Reveal className="flex flex-col gap-6 items-start w-full">
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Meine Merkliste
</p>
<div className="flex flex-col gap-1 items-start">
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Meine Merkliste
</p>
<AccountIdentity email={session.customer.email} customerNumber={session.customer.customerNumber} />
</div>
<MerklisteGrid initialProducts={initialProducts} defaultTaxRate={defaultTaxRate} kleinunternehmer={kleinunternehmer} />
</Reveal>
@@ -103,13 +103,6 @@ export function ProfileForm({
return (
<Reveal className="flex flex-col gap-6 items-start w-full">
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Mein Profil
</p>
<p className="text-body-sm text-text-muted">
{profile.email} · Kundennummer {profile.customerNumber}
</p>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 items-start w-full">
{/* Explicit heading, matching /checkout's own "1. Rechnungsadresse"
card title — without it, this section and the "Lieferadresse"
+8 -1
View File
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { getSessionCustomer, getCustomerProfile } from "../../lib/customerAuth";
import { getShippingCountries } from "../../lib/payload";
import { KontoShell } from "../components/KontoShell";
import { AccountIdentity } from "../components/AccountIdentity";
import { ProfileForm } from "./components/ProfileForm";
import { PasswordForm } from "./components/PasswordForm";
import { VerificationBanner } from "./components/VerificationBanner";
@@ -28,8 +29,14 @@ export default async function KontoProfilPage({
const { verified } = await searchParams;
return (
<KontoShell customer={{ email: profile.email, customerNumber: session.customer.customerNumber }}>
<KontoShell>
<div className="flex flex-col gap-10 items-start w-full max-w-[40rem]">
<div className="flex flex-col gap-1 items-start w-full">
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Mein Profil
</p>
<AccountIdentity email={profile.email} customerNumber={session.customer.customerNumber} />
</div>
<VerificationBanner emailVerified={profile.emailVerified} justVerified={verified === "1" || verified === "0" ? verified : undefined} />
<ProfileForm profile={profile} shippingCountries={shippingCountries} />
<PasswordForm email={profile.email} />
+9
View File
@@ -31,3 +31,12 @@ export function formatDate(iso: string): string {
const day = String(d.getDate()).padStart(2, "0");
return `${day}. ${MONTHS_DE[d.getMonth()]} ${d.getFullYear()}`;
}
// "Juli 2026" — for legal pages' "Stand: …" line, derived from the
// LegalPages doc's own updatedAt so it always reflects when the template
// was actually last edited, instead of a hand-typed string an admin has
// to remember to bump on every content change.
export function formatMonthYear(iso: string): string {
const d = new Date(iso);
return `${MONTHS_DE[d.getMonth()]} ${d.getFullYear()}`;
}
+3
View File
@@ -838,6 +838,7 @@ export type LegalPage = {
title: string;
content: unknown;
attachment: { url: string; title: string } | null;
updatedAt: string;
};
type PayloadLegalPage = {
@@ -845,6 +846,7 @@ type PayloadLegalPage = {
title: string;
content: unknown;
attachment: { url: string; title: string } | number | null;
updatedAt: string;
};
export async function getLegalPage(type: LegalPageType, options?: { draft?: boolean }): Promise<LegalPage | null> {
@@ -872,6 +874,7 @@ export async function getLegalPage(type: LegalPageType, options?: { draft?: bool
typeof doc.attachment === "object" && doc.attachment
? { url: doc.attachment.url, title: doc.attachment.title }
: null,
updatedAt: doc.updatedAt,
};
}
+2 -1
View File
@@ -9,6 +9,7 @@ import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
import { getLegalPage } from "../lib/payload";
import { formatMonthYear } from "../lib/format";
export const metadata: Metadata = {
title: "Widerrufsrecht",
@@ -36,7 +37,7 @@ export default async function WiderrufPage() {
>
Widerrufsrecht
</p>
<p className="text-body text-text-muted">Stand: Juli 2026</p>
{page && <p className="text-body text-text-muted">Stand: {formatMonthYear(page.updatedAt)}</p>}
</Reveal>
{/* MobileSectionTOC — below lg: only, see SectionTOC.tsx's own