Add persistent account navigation (sidebar/tabs); fix CTA arrow on Android

Konto-Aktionen links (profile, wishlist, logout) used to live at the bottom of the orders page's own content — unreachable once the order list got long enough. New KontoShell + AccountNav give every /konto/* page a persistent sidebar (sm+) / tab bar (mobile) instead, always reachable regardless of list length. The order filters also collapse behind a "Filter" toggle on mobile now, since the tab bar above them left little room for 3 full-width dropdowns.

Also: replaced the Unicode "→" arrow in CTA links (Tools.tsx, Blog.tsx, ProductGrid.tsx) with an SVG icon — the glyph isn't covered by the site's custom fonts, so browsers fall back to a system font per-platform; confirmed sitting visibly low relative to the label text on Android/Chrome (Galaxy S22), not reproducible in desktop Chromium. An SVG has no font-fallback path, so it renders identically everywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-31 21:58:04 +00:00
parent 92727ff22a
commit 6f033d52b5
11 changed files with 218 additions and 115 deletions
+93
View File
@@ -0,0 +1,93 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { clearCart } from "../../lib/cart";
import { dispatchAuthChanged } from "../../lib/auth";
// Persistent account navigation — a fixed sidebar (sm+) / horizontal tab
// bar (below sm) shown on every logged-in /konto/* page via KontoShell.
// Replaces the "Profil & Adresse / Weiter einkaufen / Abmelden" links that
// used to live at the bottom of the orders page's content — those became
// 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.
const NAV_ITEMS = [
{ href: "/konto/bestellungen", label: "Bestellungen", wishlistOnly: false },
{ href: "/konto/merkliste", label: "Merkliste", wishlistOnly: true },
{ href: "/konto/profil", label: "Profil", wishlistOnly: false },
] as const;
export function AccountNav({ wishlistEnabled }: { wishlistEnabled: boolean }) {
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
// CartSync, so safe to drop — the next login's mergeServerCartIntoLocal()
// restores it), tell already-mounted Client Components (Navbar's
// AccountLink) the auth state changed, leave the account area, then
// force a fresh Server Component render.
async function handleLogout() {
await fetch("/api/account/logout", { method: "POST" });
clearCart();
dispatchAuthChanged();
router.push("/");
router.refresh();
}
return (
<>
{/* Desktop/tablet — sidebar, sm+ (640px). Sits to the left of the
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) => (
<Link
key={item.href}
href={item.href}
className={`px-3 py-2 rounded-sm text-body-sm font-semibold transition-colors ${
pathname.startsWith(item.href) ? "bg-bg-muted text-text-primary" : "text-text-muted hover:text-text-primary"
}`}
>
{item.label}
</Link>
))}
<button
type="button"
onClick={handleLogout}
className="mt-4 px-3 py-2 text-left rounded-sm text-body-sm font-semibold text-red-600 hover:bg-red-50 transition-colors"
>
Abmelden
</button>
</nav>
{/* Mobile — horizontal tab bar, below sm. overflow-x-auto rather
than wrapping: 4 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) => (
<Link
key={item.href}
href={item.href}
className={`shrink-0 px-4 py-2 rounded-full text-body-sm font-bold whitespace-nowrap transition-colors ${
pathname.startsWith(item.href) ? "bg-brand text-text-primary" : "text-text-muted"
}`}
>
{item.label}
</Link>
))}
<button
type="button"
onClick={handleLogout}
className="shrink-0 px-4 py-2 rounded-full text-body-sm font-bold whitespace-nowrap text-red-600"
>
Abmelden
</button>
</nav>
</>
);
}
+29
View File
@@ -0,0 +1,29 @@
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,
// profil) — the <main>/<Footer> wrapping plus the persistent AccountNav
// were previously duplicated per page, with the equivalent of the nav
// buried as plain links at the bottom of the orders page's own content
// (unreachable once the order list got long enough to push it below the
// fold). Each page keeps its own auth-check/redirect and data-fetching
// 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();
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>
</div>
</main>
<Footer />
</>
);
}
-29
View File
@@ -1,29 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { dispatchAuthChanged } from "../../lib/auth";
import { clearCart } from "../../lib/cart";
export function LogoutButton() {
const router = useRouter();
async function handleLogout() {
await fetch("/api/account/logout", { method: "POST" });
// The local cart is already mirrored server-side by CartSync, so it's
// safe to clear it here — the next login's mergeServerCartIntoLocal()
// restores it from the server. Without this, the local cart survived
// logout untouched, and mergeServerCartIntoLocal()'s additive merge
// (existing.qty += qty) would add the already-synced server quantities
// on top of it on every login, doubling every logout/login cycle.
clearCart();
dispatchAuthChanged();
router.push("/");
router.refresh();
}
return (
<button type="button" onClick={handleLogout} className="underline text-body-sm text-text-primary hover:text-brand transition-colors">
Abmelden
</button>
);
}