Fix orange dot/hero image visibility, arrow alignment, and add a mobile legal-page TOC
- PopIn (Home Hero's brand dot) switched from whileInView to animate — its translate-based entrance could push the element off-screen on a narrow phone before the IntersectionObserver ever saw it as visible, leaving it stuck invisible permanently. - Hero image: no longer wrapped in Reveal below lg: — whileInView's margin meant it stayed at opacity:0 (a white gap above the fold) on short mobile viewports until scrolled. Reveal's fade-in kept from lg: up. - "→ Label" CTA links (Tools.tsx, Blog.tsx) now use a flex row with the arrow as its own span instead of a literal inline "→" character, which doesn't reliably align to the surrounding text's cap-height. - Replaced icon-arrow-connector.svg with a new shared StepArrow component (inline SVG) across all three step sections — the old asset's color couldn't be overridden from outside the SVG file, so it could never actually become brand-orange. Bigger and better-shaped below the structural breakpoint per feedback. - Added MobileSectionTOC (SectionTOC.tsx) — a <details> accordion shown below lg: on Impressum/Datenschutz/AGB/Widerruf/Versand, which previously had no on-page navigation aid at all below lg: (the sidebar TOC is `hidden` entirely there). - Updated the figma-to-nextjs skill with 8 new dated Gotchas from this mobile-responsive pass, and expanded Step 6's verification checklist. - README: new "Mobile responsive pass" section summarizing the above.
This commit is contained in:
@@ -1408,6 +1408,39 @@ session (low-stock digest, stale-unverified-accounts report, weekly revenue
|
||||
report, expired-discount-code cleanup) needed no monitor changes of their
|
||||
own; see the Payload README's "Jobs Queue" section.
|
||||
|
||||
## Mobile responsive pass (2026-07-24)
|
||||
|
||||
A full-site pass fixing concrete Mobile-width bugs across the Home Hero,
|
||||
Navbar, Newsletter, cart, `/challenge`, `/todo-cards`, blog detail, and the
|
||||
legal pages — found by testing on a real phone rather than just resizing a
|
||||
desktop browser to 768px. The detailed technical lessons (aspect-ratio
|
||||
distortion from mixing fluid/fixed sizing, `preserveAspectRatio="none"` SVGs,
|
||||
`whileInView` failing to trigger on short/narrow viewports, etc.) are written
|
||||
up as dated Gotchas in the `figma-to-nextjs` skill
|
||||
(`~/.claude/skills/figma-to-nextjs/SKILL.md`) rather than duplicated here —
|
||||
that's now the reference for "why" on any of this. Two changes are worth
|
||||
calling out specifically since they add new shared components:
|
||||
|
||||
- **`app/components/StepArrow.tsx`** — a small inline-SVG connector arrow,
|
||||
replacing `/icon-arrow-connector.svg` across all three "how it works" step
|
||||
sections (`/challenge`, `/todo-cards`, `/newsletter`). The old asset's
|
||||
color lived in a CSS custom property scoped to the SVG file itself, so it
|
||||
could never actually become the brand orange used everywhere else once
|
||||
loaded via `next/image` — converting it to a real component was the only
|
||||
fix, and having one shared component means a future color/shape/size
|
||||
tweak is one edit instead of three.
|
||||
- **`app/components/SectionTOC.tsx`'s `MobileSectionTOC`** — the
|
||||
Impressum/Datenschutz/AGB/Widerruf/Versand table-of-contents sidebar was
|
||||
`hidden` entirely below `lg:` (a 360px sidebar genuinely doesn't fit next
|
||||
to a readable content column below that), which meant Mobile/Tablet had no
|
||||
on-page navigation aid at all on these often-long pages — exactly where
|
||||
scanning a long legal document by scrolling is hardest. `MobileSectionTOC`
|
||||
is a `<details>/<summary>` accordion (no extra JS state needed) sharing the
|
||||
same scroll-spy "active section" logic (extracted into a `useActiveSection`
|
||||
hook) as the desktop sidebar nav, rendered as its own element right after
|
||||
each page's heading — not nested inside the sidebar's `hidden lg:...`
|
||||
wrapper, which would hide it too regardless of its own `lg:hidden` class.
|
||||
|
||||
## Tests
|
||||
|
||||
`npm run test:unit` (Vitest, `node` environment, no jsdom/Next.js runtime
|
||||
|
||||
+8
-1
@@ -7,7 +7,7 @@ import { Footer } from "../components/Footer";
|
||||
import { TrustRow } from "../components/TrustRow";
|
||||
import { RichText, extractHeadings } from "../components/RichText";
|
||||
import { LiveRichText } from "../components/LiveRichText";
|
||||
import { SectionTOC } from "../components/SectionTOC";
|
||||
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
|
||||
import { getLegalPage } from "../lib/payload";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -39,6 +39,13 @@ export default async function AgbPage() {
|
||||
<p className="text-body text-text-muted">Stand: Juli 2026</p>
|
||||
</Reveal>
|
||||
|
||||
{/* MobileSectionTOC — below lg: only, see SectionTOC.tsx's own
|
||||
comment. Outside the sidebar's `hidden lg:flex` wrapper below
|
||||
(that wrapper's `hidden` would hide this too otherwise). */}
|
||||
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
|
||||
<MobileSectionTOC sections={headings} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
|
||||
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
|
||||
<SectionTOC sections={headings} />
|
||||
|
||||
+8
-16
@@ -4,6 +4,7 @@ import Image from "next/image";
|
||||
import { draftMode } from "next/headers";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../components/Reveal";
|
||||
import { StepArrow } from "../components/StepArrow";
|
||||
import { TestimonialsGrid } from "../components/TestimonialsGrid";
|
||||
import { LiveTestimonialsGrid } from "../components/LiveTestimonialsGrid";
|
||||
import { getTestimonials } from "../lib/payload";
|
||||
@@ -251,23 +252,14 @@ export default async function ChallengePage() {
|
||||
</div>
|
||||
</RevealItem>,
|
||||
i < steps.length - 1 ? (
|
||||
// Same /icon-arrow-connector.svg asset and rotate-on-stack
|
||||
// pattern as todo-cards/newsletter's HowItWorks — this
|
||||
// used to be its own hand-drawn SVG arrow, inconsistent
|
||||
// with those two. Always visible (rotated 90° while
|
||||
// stacked below lg, this page's own structural
|
||||
// breakpoint) rather than hidden below lg like before.
|
||||
// Shared StepArrow component (see its own file) — same
|
||||
// rotate-on-stack pattern as todo-cards/newsletter's
|
||||
// HowItWorks. Always visible (rotated 90° while stacked
|
||||
// below lg, this page's own structural breakpoint) rather
|
||||
// than hidden below lg like before. Bigger below lg:
|
||||
// (w-8 h-8, was w-6 h-6) per explicit feedback.
|
||||
<div key={`arrow-${i}`} className="flex items-center justify-center shrink-0 lg:mt-5">
|
||||
{/* object-contain — same preserveAspectRatio="none" fix
|
||||
as todo-cards/newsletter's HowItWorks; the square
|
||||
mobile box was stretching this into a blob. */}
|
||||
<Image
|
||||
alt=""
|
||||
src="/icon-arrow-connector.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
className="w-6 h-6 object-contain rotate-90 lg:w-10 lg:h-3 lg:rotate-0"
|
||||
/>
|
||||
<StepArrow className="w-8 h-8 rotate-90 lg:w-10 lg:h-4 lg:rotate-0" />
|
||||
</div>
|
||||
) : null,
|
||||
])}
|
||||
|
||||
+10
-4
@@ -63,11 +63,14 @@ export async function Blog() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* flex + arrow as its own span — see Tools.tsx's comment on
|
||||
this same fix (→'s glyph baseline sits low next to text). */}
|
||||
<Link
|
||||
href={featured.href}
|
||||
className="font-bold text-body text-text-primary whitespace-nowrap hover:text-brand transition-colors"
|
||||
className="flex items-center gap-1 font-bold text-body text-text-primary whitespace-nowrap hover:text-brand transition-colors"
|
||||
>
|
||||
→ Zum Beitrag
|
||||
<span aria-hidden>→</span>
|
||||
<span>Zum Beitrag</span>
|
||||
</Link>
|
||||
</div>
|
||||
</RevealItem>
|
||||
@@ -109,11 +112,14 @@ export async function Blog() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* flex + arrow as its own span — see the featured post's
|
||||
own Link above / Tools.tsx's comment on this same fix. */}
|
||||
<Link
|
||||
href={post.href}
|
||||
className="font-bold text-body whitespace-nowrap hover:text-brand transition-colors"
|
||||
className="flex items-center gap-1 font-bold text-body whitespace-nowrap hover:text-brand transition-colors"
|
||||
>
|
||||
→ Zum Beitrag
|
||||
<span aria-hidden>→</span>
|
||||
<span>Zum Beitrag</span>
|
||||
</Link>
|
||||
</div>
|
||||
</RevealItem>
|
||||
|
||||
+37
-17
@@ -2,6 +2,29 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { PopIn, Reveal } from "./Reveal";
|
||||
|
||||
// Shared between the plain (below lg:) and Reveal-wrapped (lg:+) render —
|
||||
// see the two call sites' own comment on why this needs two wrappers.
|
||||
function HeroImage() {
|
||||
return (
|
||||
<Image
|
||||
src="/hero.png"
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
sizes="(min-width: 1024px) 58vw, 100vw"
|
||||
className="object-cover"
|
||||
style={{
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
|
||||
WebkitMaskComposite: "destination-in",
|
||||
maskImage:
|
||||
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
|
||||
maskComposite: "intersect",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="bg-bg-base w-full overflow-hidden">
|
||||
@@ -113,26 +136,23 @@ export function Hero() {
|
||||
plain box-shadow reads as a hard rectangular edge against the
|
||||
existing corner/right/bottom mask-gradient fade below, which
|
||||
looked worse than no shadow at all — tried and reverted. */}
|
||||
{/* No Reveal (fade-in-on-scroll) below lg: — whileInView's -80px
|
||||
viewport margin means the image doesn't fade in until scrolled
|
||||
that much further into view; on a short mobile viewport this
|
||||
image sits right at the initial fold, so it stayed at
|
||||
opacity:0 (a white gap, matching the section's own bg-bg-base)
|
||||
above the fold until the user scrolled (reported 2026-07-24).
|
||||
Plain, always-visible image below lg: instead; Reveal's fade
|
||||
kept from lg: up, where the image is beside the text with
|
||||
plenty of room and this was never an issue. */}
|
||||
<div className="order-2 lg:hidden relative w-full aspect-[887/583] max-h-[16rem] md:max-h-[22rem] mt-0 md:-mt-6">
|
||||
<HeroImage />
|
||||
</div>
|
||||
<Reveal
|
||||
className="order-2 lg:order-none lg:col-span-7 relative w-full aspect-[887/583] max-h-[16rem] md:max-h-[22rem] lg:max-h-none mt-0 md:-mt-6 lg:mt-0"
|
||||
className="hidden lg:block lg:col-span-7 relative w-full aspect-[887/583]"
|
||||
delay={0.15}
|
||||
>
|
||||
<Image
|
||||
src="/hero.png"
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
sizes="(min-width: 1024px) 58vw, 100vw"
|
||||
className="object-cover"
|
||||
style={{
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
|
||||
WebkitMaskComposite: "destination-in",
|
||||
maskImage:
|
||||
"linear-gradient(to right, transparent 0%, black 14%), linear-gradient(to bottom, transparent 0%, black 10%)",
|
||||
maskComposite: "intersect",
|
||||
}}
|
||||
/>
|
||||
<HeroImage />
|
||||
</Reveal>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -120,8 +120,17 @@ export function PopIn({ children, className, delay = 0 }: RevealProps) {
|
||||
<motion.span
|
||||
className={className}
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: "-80px" }}
|
||||
// animate, not whileInView — its only caller (Hero.tsx's brand dot)
|
||||
// sits above the fold, already visible on load, so there's no real
|
||||
// "scrolls into view" moment to gate on. whileInView's -80px viewport
|
||||
// margin also broke on some phones: the popIn variant's own "hidden"
|
||||
// state translates x:+140, and on a narrow mobile viewport that could
|
||||
// push the dot's pre-animation bounding box past the right edge —
|
||||
// IntersectionObserver then never reports it as visible, so
|
||||
// whileInView never fires and the dot stays stuck off-screen
|
||||
// (reported 2026-07-24: dot invisible on a real phone). Firing on
|
||||
// mount sidesteps that geometry entirely.
|
||||
animate="show"
|
||||
variants={popIn}
|
||||
transition={{ delay }}
|
||||
>
|
||||
|
||||
@@ -10,24 +10,12 @@ export type TOCSection = { id: string; title: string };
|
||||
// active state away from what was actually clicked.
|
||||
const CLICK_OVERRIDE_MS = 1000;
|
||||
|
||||
// lg:-only sidebar — same "wide fixed-width block next to content" shape
|
||||
// as the cart's order-summary sidebar (see figma-to-nextjs skill Gotcha
|
||||
// #5): a 360px TOC card plus a readable content column already exceeds
|
||||
// the 768px Tablet floor, so md: wouldn't leave room for a real 2-column
|
||||
// split at Tablet widths.
|
||||
//
|
||||
// Generic over `sections` — originally written just for /versand
|
||||
// (VersandTOC), generalized once /datenschutz needed the identical
|
||||
// scroll-spy sidebar but driven by CMS-authored headings instead of a
|
||||
// hardcoded array. Any future long legal/content page reuses this too.
|
||||
//
|
||||
// Not sticky itself — Impressum/Datenschutz put an extra card below this
|
||||
// in the same sidebar column, and if only this <nav> were sticky, the
|
||||
// card (a plain-flow sibling) would scroll away independently instead of
|
||||
// travelling with it. The caller wraps whatever the sidebar column
|
||||
// contains (this alone, or this + more) in `lg:sticky lg:top-32
|
||||
// lg:self-start` so the whole column moves as one unit.
|
||||
export function SectionTOC({ sections }: { sections: TOCSection[] }) {
|
||||
// Shared between SectionTOC (desktop sidebar nav) and MobileSectionTOC
|
||||
// (below lg: collapsible accordion, added 2026-07-24) — both need the same
|
||||
// scroll-spy "active" state and click-override handling, just render it
|
||||
// completely differently, so the logic lives here once instead of being
|
||||
// duplicated per component.
|
||||
function useActiveSection(sections: TOCSection[]) {
|
||||
const [active, setActive] = useState<string>(sections[0]?.id ?? "");
|
||||
// Not state — read inside the IntersectionObserver callback without
|
||||
// needing to re-subscribe it on every click, and cleared by its own
|
||||
@@ -67,6 +55,28 @@ export function SectionTOC({ sections }: { sections: TOCSection[] }) {
|
||||
}, CLICK_OVERRIDE_MS);
|
||||
}
|
||||
|
||||
return { active, handleClick };
|
||||
}
|
||||
|
||||
// lg:-only sidebar — same "wide fixed-width block next to content" shape
|
||||
// as the cart's order-summary sidebar (see figma-to-nextjs skill Gotcha
|
||||
// #5): a 360px TOC card plus a readable content column already exceeds
|
||||
// the 768px Tablet floor, so md: wouldn't leave room for a real 2-column
|
||||
// split at Tablet widths.
|
||||
//
|
||||
// Generic over `sections` — originally written just for /versand
|
||||
// (VersandTOC), generalized once /datenschutz needed the identical
|
||||
// scroll-spy sidebar but driven by CMS-authored headings instead of a
|
||||
// hardcoded array. Any future long legal/content page reuses this too.
|
||||
//
|
||||
// Not sticky itself — every caller wraps this in its own `hidden lg:flex
|
||||
// ... lg:sticky lg:top-32 lg:self-start` div (Impressum/Datenschutz also
|
||||
// stack a second "Nachhaltigkeit" card below this in that same wrapper, so
|
||||
// the sticky behavior has to live on the wrapper for the two to travel
|
||||
// together as one unit — putting it on this <nav> instead would leave
|
||||
// that card behind as a plain-flow sibling scrolling past a now-fixed nav).
|
||||
export function SectionTOC({ sections }: { sections: TOCSection[] }) {
|
||||
const { active, handleClick } = useActiveSection(sections);
|
||||
if (sections.length === 0) return null;
|
||||
|
||||
return (
|
||||
@@ -92,3 +102,44 @@ export function SectionTOC({ sections }: { sections: TOCSection[] }) {
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// Below lg: only — a collapsible accordion instead of the sidebar nav
|
||||
// (which is `hidden` entirely below lg:, see SectionTOC's own comment on
|
||||
// why a real 2-column split doesn't fit there). Added 2026-07-24: these
|
||||
// legal pages had no on-page navigation aid at all on Mobile/Tablet, which
|
||||
// is exactly where scanning a long legal document by scrolling is hardest.
|
||||
// Native <details>/<summary> — no extra open/close state needed, and it
|
||||
// stays open after a click so jumping between sections doesn't require
|
||||
// reopening it each time. Render this as its own element in the page
|
||||
// (typically right after the heading, before the two-column content row),
|
||||
// not nested inside a parent that's itself `hidden lg:...` — that would
|
||||
// hide this too regardless of its own lg:hidden class.
|
||||
export function MobileSectionTOC({ sections }: { sections: TOCSection[] }) {
|
||||
const { active, handleClick } = useActiveSection(sections);
|
||||
if (sections.length === 0) return null;
|
||||
|
||||
return (
|
||||
<details className="lg:hidden w-full bg-bg-base border border-border rounded-md p-4 open:pb-2">
|
||||
<summary className="text-label font-semibold text-text-muted uppercase tracking-wide cursor-pointer select-none">
|
||||
Inhaltsübersicht
|
||||
</summary>
|
||||
<div className="flex flex-col gap-1 mt-3">
|
||||
{sections.map(({ id, title }) => (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${id}`}
|
||||
onClick={() => handleClick(id)}
|
||||
className={
|
||||
"px-3 py-2 rounded-sm text-body-sm transition-colors border-l-2 " +
|
||||
(active === id
|
||||
? "border-toc-active-border bg-bg-muted text-text-primary font-semibold"
|
||||
: "border-transparent text-text-muted hover:text-text-primary")
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Shared "how it works" step connector — used by Challenge's, /todo-cards's,
|
||||
// and /newsletter's ("Impulse & Tipps") step sections. Used to be
|
||||
// /icon-arrow-connector.svg (a thin gray line+chevron) loaded via next/image;
|
||||
// replaced 2026-07-24 for two reasons that both needed an inline SVG to fix:
|
||||
// 1. It read as a faint gray line, not a real arrow, even after the
|
||||
// object-contain aspect-ratio fix — too thin/subtle at these sizes.
|
||||
// 2. Its color lives in a `var(--stroke-0, #C9C9C9)` CSS custom property
|
||||
// that's scoped to the SVG file's own document when loaded via <img
|
||||
// src>/next/image — un-recolorable from the host page's CSS, so it could
|
||||
// never actually become the brand orange used everywhere else (buttons,
|
||||
// checkmarks, icons). Inline SVG sidesteps that entirely.
|
||||
export function StepArrow({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg width="40" height="16" viewBox="0 0 40 16" fill="none" aria-hidden="true" className={className}>
|
||||
<path
|
||||
d="M1 8H33M26 14.5L34.5 8L26 1.5"
|
||||
stroke="#f6a701"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -71,11 +71,18 @@ export async function Tools() {
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
{/* flex items-center + arrow as its own span, not inline text
|
||||
— the → glyph sits low relative to the surrounding text's
|
||||
cap-height in the font used here, off-center against the
|
||||
label if it's just part of the same text node (fixed
|
||||
2026-07-24, same pattern ProductGrid.tsx's "Mehr
|
||||
erfahren" link already uses). */}
|
||||
<Link
|
||||
href={tool.ctaHref}
|
||||
className="font-bold leading-normal text-body whitespace-nowrap hover:text-brand transition-colors"
|
||||
className="flex items-center gap-1 font-bold leading-normal text-body whitespace-nowrap hover:text-brand transition-colors"
|
||||
>
|
||||
→ {tool.ctaLabel}
|
||||
<span aria-hidden>→</span>
|
||||
<span>{tool.ctaLabel}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</RevealItem>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { RichText, extractHeadings } from "../components/RichText";
|
||||
import { LiveRichText } from "../components/LiveRichText";
|
||||
import { SectionTOC } from "../components/SectionTOC";
|
||||
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
|
||||
import { getLegalPage } from "../lib/payload";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -38,6 +38,13 @@ export default async function DatenschutzPage() {
|
||||
<p className="text-body text-text-muted">Stand: Juli 2026</p>
|
||||
</Reveal>
|
||||
|
||||
{/* MobileSectionTOC — below lg: only, see SectionTOC.tsx's own
|
||||
comment. Outside the sidebar's `hidden lg:flex` wrapper below
|
||||
(that wrapper's `hidden` would hide this too otherwise). */}
|
||||
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
|
||||
<MobileSectionTOC sections={headings} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
|
||||
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
|
||||
<SectionTOC sections={headings} />
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { RichText, extractHeadings } from "../components/RichText";
|
||||
import { LiveRichText } from "../components/LiveRichText";
|
||||
import { SectionTOC } from "../components/SectionTOC";
|
||||
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
|
||||
import { getLegalPage, getCompanySettings } from "../lib/payload";
|
||||
import { AnbieterAngaben, anbieterAngabenHeadings } from "./components/AnbieterAngaben";
|
||||
|
||||
@@ -42,6 +42,13 @@ export default async function ImpressumPage() {
|
||||
<p className="text-body text-text-muted">Angaben gemäß § 5 DDG</p>
|
||||
</Reveal>
|
||||
|
||||
{/* MobileSectionTOC — below lg: only, see SectionTOC.tsx's own
|
||||
comment. Outside the sidebar's `hidden lg:flex` wrapper below
|
||||
(that wrapper's `hidden` would hide this too otherwise). */}
|
||||
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
|
||||
<MobileSectionTOC sections={headings} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-16 pt-2 px-[var(--layout-padding-x)] w-full">
|
||||
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
|
||||
<SectionTOC sections={headings} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Fragment } from "react";
|
||||
import Image from "next/image";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
|
||||
import { StepArrow } from "../../components/StepArrow";
|
||||
|
||||
// Each icon's own real pixel dimensions (not uniformly square) — needed so
|
||||
// the `h-16 w-auto` sizing below infers the correct aspect ratio instead of
|
||||
@@ -71,22 +72,15 @@ export function HowItWorks() {
|
||||
<p className="text-body-sm text-text-primary text-center">{step.desc}</p>
|
||||
</RevealItem>
|
||||
{i < steps.length - 1 && (
|
||||
// md:mt-[1.625rem] (26px) centers the arrow on the h-16
|
||||
// (64px) icon above it — same margin-based centering
|
||||
// technique as Challenge's step connector and todo-cards'
|
||||
// identical HowItWorks, not just the same icon asset.
|
||||
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
|
||||
{/* object-contain — see todo-cards/HowItWorks.tsx's own
|
||||
comment on this same fix: the SVG's preserveAspectRatio
|
||||
="none" stretched it to fill the square mobile box
|
||||
instead of keeping its thin-arrow ratio. */}
|
||||
<Image
|
||||
alt=""
|
||||
src="/icon-arrow-connector.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
className="w-6 h-6 object-contain rotate-90 md:w-10 md:h-3 md:rotate-0"
|
||||
/>
|
||||
// md:mt-[1.5rem] centers the arrow on the h-16 icon above it,
|
||||
// same technique as todo-cards'/Challenge's own step
|
||||
// connector. Below md: pulled up with a negative margin so it
|
||||
// sits nearer the icon row above it instead of dead-center in
|
||||
// the whole gap between steps (fixed 2026-07-24, consistency
|
||||
// with Challenge's icon-at-top layout). Bigger below md:
|
||||
// (w-8 h-8, was w-6 h-6) per explicit feedback.
|
||||
<div className="flex items-center justify-center shrink-0 -mt-2 md:mt-[1.5rem]">
|
||||
<StepArrow className="w-8 h-8 rotate-90 md:w-10 md:h-4 md:rotate-0" />
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Fragment } from "react";
|
||||
import Image from "next/image";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
|
||||
import { StepArrow } from "../../components/StepArrow";
|
||||
|
||||
// Each icon's own real pixel dimensions (not uniformly square, e.g.
|
||||
// icon-step-2 is 180x168) — needed so the `h-16 w-auto` sizing below infers
|
||||
@@ -79,23 +80,12 @@ export function HowItWorks() {
|
||||
</RevealItem>
|
||||
{i < steps.length - 1 && (
|
||||
// md:mt-[1.625rem] (26px) centers the arrow on the h-16
|
||||
// (64px) icon above it — (64 - arrow's own 12px height) / 2
|
||||
// (64px) icon above it — (64 - arrow's own 16px height) / 2
|
||||
// — same margin-based centering technique as Challenge's
|
||||
// step connector, not just the same icon asset.
|
||||
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
|
||||
{/* object-contain — the SVG has preserveAspectRatio="none"
|
||||
(viewBox 40x12) and stretches to fill whatever box
|
||||
it's given; the square w-6 h-6 mobile box squished it
|
||||
into a blob instead of a thin arrow. contain keeps its
|
||||
real ratio, letterboxed inside the (still square, so
|
||||
rotate-90 doesn't shift its footprint) box. */}
|
||||
<Image
|
||||
alt=""
|
||||
src="/icon-arrow-connector.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
className="w-6 h-6 object-contain rotate-90 md:w-10 md:h-3 md:rotate-0"
|
||||
/>
|
||||
// step connector, not just the same icon asset. Bigger below
|
||||
// md: (w-8 h-8, was w-6 h-6) per explicit feedback.
|
||||
<div className="flex items-center justify-center shrink-0 md:mt-[1.5rem]">
|
||||
<StepArrow className="w-8 h-8 rotate-90 md:w-10 md:h-4 md:rotate-0" />
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { SectionTOC } from "../../components/SectionTOC";
|
||||
import { SectionTOC, MobileSectionTOC } from "../../components/SectionTOC";
|
||||
import { VERSAND_SECTION_IDS } from "./VersandSections";
|
||||
|
||||
export function VersandTOC() {
|
||||
return <SectionTOC sections={[...VERSAND_SECTION_IDS]} />;
|
||||
}
|
||||
|
||||
// Below lg: accordion counterpart — see SectionTOC.tsx's own comment on
|
||||
// why this needs to be a separate element rather than nested inside
|
||||
// VersandTOC/the page's `hidden lg:block` sidebar wrapper.
|
||||
export function MobileVersandTOC() {
|
||||
return <MobileSectionTOC sections={[...VERSAND_SECTION_IDS]} />;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { VersandSections } from "./components/VersandSections";
|
||||
import { VersandTOC } from "./components/VersandTOC";
|
||||
import { VersandTOC, MobileVersandTOC } from "./components/VersandTOC";
|
||||
import { getShippingSettings } from "../lib/payload";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -36,6 +36,12 @@ export default async function VersandPage() {
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
{/* MobileSectionTOC counterpart — below lg: only, see
|
||||
SectionTOC.tsx's own comment. */}
|
||||
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
|
||||
<MobileVersandTOC />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-16 pt-2 px-[var(--layout-padding-x)] w-full">
|
||||
<div className="hidden lg:block lg:sticky lg:top-32 lg:self-start">
|
||||
<VersandTOC />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Footer } from "../components/Footer";
|
||||
import { TrustRow } from "../components/TrustRow";
|
||||
import { RichText, extractHeadings } from "../components/RichText";
|
||||
import { LiveRichText } from "../components/LiveRichText";
|
||||
import { SectionTOC } from "../components/SectionTOC";
|
||||
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
|
||||
import { getLegalPage } from "../lib/payload";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -39,6 +39,13 @@ export default async function WiderrufPage() {
|
||||
<p className="text-body text-text-muted">Stand: Juli 2026</p>
|
||||
</Reveal>
|
||||
|
||||
{/* MobileSectionTOC — below lg: only, see SectionTOC.tsx's own
|
||||
comment. Outside the sidebar's `hidden lg:flex` wrapper below
|
||||
(that wrapper's `hidden` would hide this too otherwise). */}
|
||||
<div className="lg:hidden px-[var(--layout-padding-x)] pb-4 w-full">
|
||||
<MobileSectionTOC sections={headings} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
|
||||
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
|
||||
<SectionTOC sections={headings} />
|
||||
|
||||
Reference in New Issue
Block a user