Fix account-nav feedback, blog filter position, and small icon polish
- Remove Merkliste from AccountNav — the Navbar's own wishlist icon already covers it, a second nav entry was redundant. - Fix duplicate "Mein Profil" heading (ProfileForm already renders its own; KontoShell no longer adds a second one). - "Eingeloggt als ..." now renders once in KontoShell, consistently on every /konto/* page, instead of only on the orders page. - Blog category filter chips moved into the hero's own text column (was a separate bar below the entire hero including the photo) — visible immediately on every width instead of requiring a scroll past the hero image first. - ArrowRightIcon's arrowhead wings shortened (looked like a generic oversized chevron at full length). - Search overlay's "Esc" text button replaced with an X icon; the actual Escape keyboard shortcut is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+40
-52
@@ -66,6 +66,46 @@ export default async function BlogOverviewPage({
|
||||
<p className="text-body text-text-muted">
|
||||
Gedanken, Methoden und Impulse für einen leichteren und klareren Alltag.
|
||||
</p>
|
||||
{/* Moved into the hero's own text column (was a separate full-
|
||||
width bar below the entire hero, including the photo) — on
|
||||
mobile especially (text stacks above the photo here), that
|
||||
pushed the filters below a full extra screen's worth of
|
||||
hero image before they were even visible. Living right
|
||||
under the description keeps them in view immediately, on
|
||||
every width, no scrolling past the photo needed. */}
|
||||
{allCategories.length > 1 && (
|
||||
<div className="flex flex-wrap gap-2 w-full">
|
||||
{allCategories.map((category) => {
|
||||
const active = activeCategories.includes(category);
|
||||
return (
|
||||
<Link
|
||||
key={category}
|
||||
href={buildCategoryHref(activeCategories, category)}
|
||||
// bg-bg-muted, not bg-bg-base — border-border (#e5e0d8)
|
||||
// on bg-base (#f8f5f1) is a ~2% lightness difference,
|
||||
// nearly invisible as a pill outline; a filled muted
|
||||
// background makes the chip read as a discrete control
|
||||
// regardless of the border's own low contrast.
|
||||
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-muted border-border text-text-muted hover:border-brand hover:text-brand"
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</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>
|
||||
<div className="relative w-full sm:absolute sm:inset-y-0 sm:right-0 sm:w-[68%] h-56 sm:h-full">
|
||||
<Image alt="" src="/hero.png" fill sizes="(min-width: 640px) 68vw, 100vw" className="object-cover object-top" />
|
||||
@@ -74,58 +114,6 @@ export default async function BlogOverviewPage({
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Plain div, not <Reveal> — this bar sits right at/just past the
|
||||
hero's bottom edge, exactly the "already near the initial
|
||||
viewport top" position Reveal.tsx's own comment documents as a
|
||||
whileInView(margin:"-80px") trap: the shrunk viewport can
|
||||
permanently miss triggering "entered view" for an element
|
||||
that's already visible without any further scroll, since
|
||||
`once: true` never gets a second chance. The Hero brand dot
|
||||
hit the identical bug and switched to a plain animate — this
|
||||
filter bar doesn't need a scroll-reveal animation at all, so
|
||||
it's simplest to just not wrap it in Reveal in the first place. */}
|
||||
{/* relative z-20 — the featured-post card right below pulls itself
|
||||
up by -mt-8 with its own z-10 to overlap the *hero's* bottom
|
||||
edge (its original, intended design). Inserting this filter
|
||||
bar between the hero and that card meant the same -mt-8 pull
|
||||
now overlapped THIS bar instead, and the card's higher/equal
|
||||
stacking rendered on top of it, visually hiding the chips
|
||||
behind the card. z-20 keeps this bar above that overlap
|
||||
regardless. */}
|
||||
{allCategories.length > 1 && (
|
||||
<div className="relative z-20 flex flex-wrap gap-2 w-full max-w-[80rem] mx-auto px-[var(--layout-padding-x)] pt-6">
|
||||
{allCategories.map((category) => {
|
||||
const active = activeCategories.includes(category);
|
||||
return (
|
||||
<Link
|
||||
key={category}
|
||||
href={buildCategoryHref(activeCategories, category)}
|
||||
// bg-bg-muted, not bg-bg-base — border-border (#e5e0d8)
|
||||
// on bg-base (#f8f5f1) is a ~2% lightness difference,
|
||||
// nearly invisible as a pill outline; a filled muted
|
||||
// background makes the chip read as a discrete control
|
||||
// regardless of the border's own low contrast.
|
||||
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-muted border-border text-text-muted hover:border-brand hover:text-brand"
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{posts.length === 0 && (
|
||||
<p className="text-body text-text-muted w-full max-w-[80rem] mx-auto px-[var(--layout-padding-x)] pt-10">
|
||||
Keine Beiträge in dieser Kategorie gefunden.
|
||||
|
||||
@@ -8,10 +8,14 @@
|
||||
// devices. An SVG has no font-fallback path — it renders identically
|
||||
// everywhere. `currentColor` stroke follows the parent Link's own
|
||||
// text/hover color, same as every other icon in this codebase.
|
||||
// Arrowhead wings are deliberately short relative to the shaft (4.2 units
|
||||
// vs. a 14-unit shaft) — the first version used full-length 45° wings
|
||||
// (7 units), which read as a generic, oversized chevron next to the small
|
||||
// bold label text it sits beside.
|
||||
export function ArrowRightIcon() {
|
||||
return (
|
||||
<svg aria-hidden width="16" height="12" viewBox="0 0 16 12" fill="none" className="shrink-0">
|
||||
<path d="M1 6H15M15 6L10 1M15 6L10 11" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M1 6H15M15 6L12 3M15 6L12 9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,8 +90,14 @@ function SearchOverlay({ onClose }: { onClose: () => void }) {
|
||||
placeholder="Produkte, Blogbeiträge…"
|
||||
className="flex-1 min-w-0 bg-transparent outline-none text-h4 text-text-primary placeholder:text-text-muted"
|
||||
/>
|
||||
<button type="button" onClick={onClose} aria-label="Suche schließen" className="shrink-0 text-body-sm text-text-muted hover:text-brand transition-colors">
|
||||
Esc
|
||||
{/* X icon, not the old "Esc" text — the keyboard shortcut still
|
||||
works (see the Escape keydown handler above), this button is
|
||||
just the mouse/touch affordance, and a close icon reads
|
||||
faster than a text label at a glance. */}
|
||||
<button type="button" onClick={onClose} aria-label="Suche schließen" className="shrink-0 h-8 w-8 flex items-center justify-center text-text-muted hover:text-brand transition-colors">
|
||||
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" aria-hidden="true">
|
||||
<path d="M2 2L14 14M14 2L2 14" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -59,14 +59,11 @@ export default async function KontoBestellungenPage({
|
||||
]);
|
||||
|
||||
return (
|
||||
<KontoShell>
|
||||
<KontoShell customer={session.customer}>
|
||||
<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>
|
||||
<p className="text-body text-text-muted">
|
||||
Eingeloggt als {session.customer.email} (Kundennummer {session.customer.customerNumber})
|
||||
</p>
|
||||
|
||||
{orderFilterEnabled && availableYears.length > 0 && (
|
||||
<Suspense fallback={null}>
|
||||
|
||||
@@ -12,16 +12,18 @@ import { dispatchAuthChanged } from "../../lib/auth";
|
||||
// unreachable without scrolling past an arbitrarily long order list. Being
|
||||
// part of the shell now (not page content), reachability no longer depends
|
||||
// on how much is above it.
|
||||
//
|
||||
// No Merkliste entry — the Navbar's own wishlist heart icon already covers
|
||||
// that (visible at every width, on every page, not just inside /konto/*),
|
||||
// so a second entry here would just be a redundant path to the same page.
|
||||
const NAV_ITEMS = [
|
||||
{ href: "/konto/bestellungen", label: "Bestellungen", wishlistOnly: false },
|
||||
{ href: "/konto/merkliste", label: "Merkliste", wishlistOnly: true },
|
||||
{ href: "/konto/profil", label: "Profil", wishlistOnly: false },
|
||||
{ href: "/konto/bestellungen", label: "Bestellungen" },
|
||||
{ href: "/konto/profil", label: "Profil" },
|
||||
] as const;
|
||||
|
||||
export function AccountNav({ wishlistEnabled }: { wishlistEnabled: boolean }) {
|
||||
export function AccountNav() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const items = NAV_ITEMS.filter((item) => !item.wishlistOnly || wishlistEnabled);
|
||||
|
||||
// Same sequence as the old LogoutButton (now folded in here, its one
|
||||
// call site): clear the local cart (already mirrored server-side by
|
||||
@@ -43,7 +45,7 @@ export function AccountNav({ wishlistEnabled }: { wishlistEnabled: boolean }) {
|
||||
page content inside KontoShell's flex row. */}
|
||||
<nav className="hidden sm:flex sm:flex-col sm:w-48 shrink-0 gap-1">
|
||||
<p className="text-label font-bold text-text-muted uppercase tracking-wide px-3 pb-2">Mein Konto</p>
|
||||
{items.map((item) => (
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
@@ -64,12 +66,12 @@ export function AccountNav({ wishlistEnabled }: { wishlistEnabled: boolean }) {
|
||||
</nav>
|
||||
|
||||
{/* Mobile — horizontal tab bar, below sm. overflow-x-auto rather
|
||||
than wrapping: 4 items at once already fits most phones, and a
|
||||
than wrapping: 3 items at once already fits most phones, and a
|
||||
scrollable single row reads clearly as "more tabs this way"
|
||||
rather than a wrapped second line competing for attention with
|
||||
the page content right below it. */}
|
||||
<nav className="sm:hidden flex items-center gap-2 overflow-x-auto pb-1 -mx-[var(--layout-padding-x)] px-[var(--layout-padding-x)]">
|
||||
{items.map((item) => (
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Footer } from "../../components/Footer";
|
||||
import { getWishlistEnabled } from "../../lib/payload";
|
||||
import { AccountNav } from "./AccountNav";
|
||||
|
||||
// Shared shell for every logged-in /konto/* page (bestellungen, merkliste,
|
||||
@@ -12,15 +11,29 @@ import { AccountNav } from "./AccountNav";
|
||||
// exactly as before — this only replaces the outer chrome, not the
|
||||
// page-specific logic each page still needs (e.g. merkliste's return-URL
|
||||
// login redirect, profil's second profile-fetch redirect).
|
||||
export async function KontoShell({ children }: { children: ReactNode }) {
|
||||
const wishlistEnabled = await getWishlistEnabled();
|
||||
|
||||
//
|
||||
// `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 };
|
||||
}) {
|
||||
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 wishlistEnabled={wishlistEnabled} />
|
||||
<div className="flex-1 min-w-0 flex flex-col">{children}</div>
|
||||
<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>
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -40,7 +40,7 @@ export default async function KontoMerklistePage() {
|
||||
const initialProducts = await getProductsByIds(wishlistItems.map((i) => i.productId));
|
||||
|
||||
return (
|
||||
<KontoShell>
|
||||
<KontoShell customer={session.customer}>
|
||||
<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
|
||||
|
||||
@@ -28,11 +28,8 @@ export default async function KontoProfilPage({
|
||||
const { verified } = await searchParams;
|
||||
|
||||
return (
|
||||
<KontoShell>
|
||||
<KontoShell customer={{ email: profile.email, customerNumber: session.customer.customerNumber }}>
|
||||
<div className="flex flex-col gap-10 items-start w-full max-w-[40rem]">
|
||||
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
|
||||
Mein Profil
|
||||
</p>
|
||||
<VerificationBanner emailVerified={profile.emailVerified} justVerified={verified === "1" || verified === "0" ? verified : undefined} />
|
||||
<ProfileForm profile={profile} shippingCountries={shippingCountries} />
|
||||
<PasswordForm email={profile.email} />
|
||||
|
||||
Reference in New Issue
Block a user