Fix cart-badge false pulse, blog hero-card misattribution, legal breadcrumb color

- Cart badge pulsed on every page load/reload, not just real cart
  changes — useCartCount's SSR snapshot is always 0, so hydration alone
  satisfied the "count increased" check. Skip the very first effect run.
- WishlistButton: touch-device smaller/subtler sizing now applies
  regardless of revealOnHover (e.g. the homepage spotlight heart), not
  just the shop grid's hover-reveal mode.
- /blog: the big featured-style hero card was applied to whatever post
  sorted first post-filter, even when that post isn't actually the
  `featured` one (a category filter can exclude the real featured post
  entirely). Now gated on the post's own `featured` flag.
- Legal pages' "Startseite" breadcrumb link now matches the same
  text-brand/hover:underline treatment already used elsewhere on these
  pages (e.g. Impressum's Anbieter email link), instead of staying muted
  until hover.

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:46:24 +00:00
parent bf6c5d079a
commit 7c26bf7b7c
7 changed files with 32 additions and 10 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ export default async function AgbPage() {
<main className="flex flex-col flex-1 bg-bg-base">
<Reveal className="flex flex-col gap-3 items-start pb-6 pt-10 px-[var(--layout-padding-x)] w-full">
<p className="flex items-center gap-2 text-body-sm text-text-muted">
<Link href="/" className="hover:text-brand transition-colors">Startseite</Link>
<Link href="/" className="text-brand hover:underline">Startseite</Link>
<span></span>
<span className="text-text-primary">AGB</span>
</p>
+10 -1
View File
@@ -47,7 +47,16 @@ export default async function BlogOverviewPage({
const allCategories = blogFilterEnabled ? distinctCategories(allPosts) : [];
const posts =
activeCategories.length === 0 ? allPosts : allPosts.filter((post) => post.categories.some((c) => activeCategories.includes(c)));
const [featured, ...rest] = posts;
// getBlogPosts sorts "-featured,-publishedAt", so index 0 IS the actual
// featured post when the unfiltered list is shown — but a category
// filter can (and often will) exclude it entirely, in which case index 0
// is just the newest matching post, not an editorially featured one. It
// still doesn't deserve the big hero-card treatment (implies "the
// featured post", not "whatever sorted first"), so that layout is gated
// on the post's own `featured` flag, not on array position.
const [first, ...restAll] = posts;
const featured = first?.featured ? first : undefined;
const rest = featured ? restAll : posts;
return (
<>
+11
View File
@@ -177,6 +177,12 @@ function CartLink() {
const anchorRef = useRef<HTMLAnchorElement>(null);
const [pulse, setPulse] = useState(false);
const prevVisibleRef = useRef(0);
// useCartCount's getServerSnapshot is always 0 (see cart.ts) so hydration
// always transitions 0 -> the real count on first render — without this
// guard that transition alone satisfied "visibleCount > prevVisibleRef"
// and pulsed the badge on every single page load/reload, not just an
// actual in-session cart change.
const hasMountedRef = useRef(false);
const pathname = usePathname();
// Held back by pendingCount while a ball is mid-flight, so the badge only
@@ -191,6 +197,11 @@ function CartLink() {
}, [registerCartIcon]);
useEffect(() => {
if (!hasMountedRef.current) {
hasMountedRef.current = true;
prevVisibleRef.current = visibleCount;
return;
}
if (visibleCount > prevVisibleRef.current) {
setPulse(true);
const t = setTimeout(() => setPulse(false), 350);
+7 -5
View File
@@ -39,7 +39,11 @@ export function WishlistButton({
* but visually quieter than the full-size default. Applies to
* already-wishlisted hearts too (not just the hover-revealed ones) —
* consistent sizing across every heart on a touch device, rather than
* only the not-yet-wishlisted ones shrinking. */
* only the not-yet-wishlisted ones shrinking. This smaller touch sizing
* is applied regardless of `revealOnHover` (see the className below) —
* a heart reading visually "quieter" on a touch screen is a general
* touch-device trait, not specific to the multi-card-grid use case it
* was first built for. */
revealOnHover?: boolean;
}) {
const { isWishlisted, toggle } = useWishlist();
@@ -70,9 +74,7 @@ export function WishlistButton({
aria-label={wishlisted ? "Von der Merkliste entfernen" : "Zur Merkliste hinzufügen"}
aria-pressed={wishlisted}
disabled={pending}
className={`flex h-9 w-9 items-center justify-center rounded-full bg-bg-base/90 backdrop-blur-sm transition-all active:scale-90 disabled:opacity-60 ${
revealOnHover ? "pointer-coarse:h-7 pointer-coarse:w-7 pointer-coarse:bg-bg-base/60" : ""
} ${
className={`flex h-9 w-9 items-center justify-center rounded-full bg-bg-base/90 backdrop-blur-sm transition-all active:scale-90 disabled:opacity-60 pointer-coarse:h-7 pointer-coarse:w-7 pointer-coarse:bg-bg-base/60 ${
revealOnHover && !wishlisted
? "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100"
: ""
@@ -83,7 +85,7 @@ export function WishlistButton({
height="18"
viewBox="0 0 20 18"
fill={wishlisted ? "currentColor" : "none"}
className={`${wishlisted ? "text-brand" : "text-text-primary"} ${revealOnHover ? "pointer-coarse:scale-75" : ""}`}
className={`${wishlisted ? "text-brand" : "text-text-primary"} pointer-coarse:scale-75`}
>
<path
d="M10 17S1 11.5 1 5.8C1 2.6 3.4 1 5.8 1c1.6 0 3.2.9 4.2 2.4C11 1.9 12.6 1 14.2 1 16.6 1 19 2.6 19 5.8 19 11.5 10 17 10 17Z"
+1 -1
View File
@@ -26,7 +26,7 @@ export default async function DatenschutzPage() {
<main className="flex flex-col flex-1 bg-bg-base">
<Reveal className="flex flex-col gap-3 items-start pb-6 pt-10 px-[var(--layout-padding-x)] w-full">
<p className="flex items-center gap-2 text-body-sm text-text-muted">
<Link href="/" className="hover:text-brand transition-colors">Startseite</Link>
<Link href="/" className="text-brand hover:underline">Startseite</Link>
<span></span>
<span className="text-text-primary">Datenschutz</span>
</p>
+1 -1
View File
@@ -29,7 +29,7 @@ export default async function ImpressumPage() {
<main className="flex flex-col flex-1 bg-bg-base">
<Reveal className="flex flex-col gap-3 items-start pb-6 pt-10 px-[var(--layout-padding-x)] w-full">
<p className="flex items-center gap-2 text-body-sm text-text-muted">
<Link href="/" className="hover:text-brand transition-colors">Startseite</Link>
<Link href="/" className="text-brand hover:underline">Startseite</Link>
<span></span>
<span className="text-text-primary">Impressum</span>
</p>
+1 -1
View File
@@ -27,7 +27,7 @@ export default async function WiderrufPage() {
<main className="flex flex-col flex-1 bg-bg-base">
<Reveal className="flex flex-col gap-3 items-start pb-6 pt-10 px-[var(--layout-padding-x)] w-full">
<p className="flex items-center gap-2 text-body-sm text-text-muted">
<Link href="/" className="hover:text-brand transition-colors">Startseite</Link>
<Link href="/" className="text-brand hover:underline">Startseite</Link>
<span></span>
<span className="text-text-primary">Widerruf</span>
</p>