Add ToDo-Karten product page, fluid design tokens, and Navbar/Hero fixes
New /todo-cards page (Hero, How-it-works, Focus, Testimonials, Pricing) built with the fluid clamp() token system (app/lib/fluid.ts) and scroll-reveal animations (app/components/Reveal.tsx), matching styling consistency with /challenge's testimonial section. Navbar: page-aware active state and anchor-link navigation from any route (not just "/"), fixed logo click to actually navigate instead of silently rewriting the URL, fluid nav text size, custom hash-scroll handling for cross-page anchor links. Hero: responsive breakpoint fix for the text/image split at Tablet widths, entrance animation for the brand's orange dot, removed a red dot artifact from hero.png. Also includes prior uncommitted work on About/Blog/Newsletter/Footer and the cart lib (app/lib/cart.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { addToCart } from "../../lib/cart";
|
||||
|
||||
/**
|
||||
* Shared by the Hero's "ToDo-Karten bestellen" and the pricing panel's "In
|
||||
* den Warenkorb" — both are real add-to-cart actions per app/lib/cart.ts,
|
||||
* not just decorative links, and both go to /cart per the click-through
|
||||
* convention already established for page-todo-karten's two CTAs.
|
||||
*/
|
||||
export function AddToCartButton({
|
||||
label,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href="/cart"
|
||||
onClick={() => addToCart("todo-karten")}
|
||||
className={
|
||||
className ??
|
||||
"inline-flex items-center justify-center px-6 py-[0.8125rem] rounded-sm bg-brand hover:bg-brand-hover active:scale-[0.97] transition-all font-bold text-body text-text-primary whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base"
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
|
||||
const bullets = [
|
||||
"Set mit 50 hochwertigen ToDo-Karten",
|
||||
"Viel Platz für Gedanken, Aufgaben und Notizen",
|
||||
"Passt in jede Tasche – überall einsetzbar",
|
||||
"Nachhaltig produziert in Deutschland",
|
||||
"Inklusive Mini-Anleitung mit Tipps für den Start",
|
||||
];
|
||||
|
||||
export function Focus() {
|
||||
return (
|
||||
<section className="w-full bg-bg-base flex flex-col lg:flex-row gap-10 lg:gap-16 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
|
||||
<Reveal className="group relative w-full lg:w-[42%] lg:shrink-0 aspect-[580/328] rounded-md overflow-hidden">
|
||||
<Image
|
||||
src="/focus-todo-karten.png"
|
||||
alt="Notizbuch mit ToDo-Karten in natürlichem Licht"
|
||||
fill
|
||||
sizes="(min-width: 1024px) 42vw, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal className="flex flex-col gap-6 items-start flex-1 min-w-0 w-full" delay={0.1}>
|
||||
<p
|
||||
className="font-semibold text-h-emphasis text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Für mehr Fokus im Alltag
|
||||
</p>
|
||||
<p className="text-body text-text-body">
|
||||
ToDo-Karten helfen dir, den Überblick zu behalten, Entscheidungen zu vereinfachen und deinen Tag bewusst zu gestalten.
|
||||
</p>
|
||||
<ul className="flex flex-col gap-3 items-start w-full">
|
||||
{bullets.map((b) => (
|
||||
<li key={b} className="flex gap-[0.625rem] items-center w-full">
|
||||
<img alt="" src="/icon-check.svg" className="size-5 shrink-0" />
|
||||
<span className="flex-1 font-semibold text-body text-text-primary">{b}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Reveal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Fragment } from "react";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: "/icon-step-1.png",
|
||||
title: "1. Aufschreiben",
|
||||
desc: "Notiere deine Aufgaben und alles, was dir im Kopf herumgeht.",
|
||||
},
|
||||
{
|
||||
icon: "/icon-step-2.png",
|
||||
title: "2. Priorisieren",
|
||||
desc: "Wähle deine 3 wichtigsten Aufgaben für heute aus und schreibe sie auf.",
|
||||
},
|
||||
{
|
||||
icon: "/icon-step-3.png",
|
||||
title: "3. Umsetzen",
|
||||
desc: "Konzentriere dich auf das Wesentliche und hake ab, was wirklich zählt.",
|
||||
},
|
||||
];
|
||||
|
||||
export function HowItWorks() {
|
||||
return (
|
||||
<section className="w-full bg-bg-base flex flex-col gap-12 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
|
||||
<Reveal className="flex flex-col gap-2 items-center text-center">
|
||||
<p
|
||||
className="font-semibold text-h-emphasis text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
So einfach funktioniert es
|
||||
</p>
|
||||
<p className="text-body text-text-muted">
|
||||
Ein paar Minuten am Morgen – mehr Klarheit den ganzen Tag.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
{/* Arrows always visible (see Divider lesson — hiding them below a
|
||||
breakpoint isn't the fix), just rotated 90° to point down while
|
||||
stacked below md:. lg:px-[16.25rem] mirrors Figma's Desktop-only
|
||||
px-[260px] inset — that value doesn't get its own fluid token
|
||||
since it's a pure aesthetic narrowing, not something that breaks
|
||||
if it's just absent below lg:. RevealGroup/RevealItem stagger the
|
||||
3 steps in on scroll, same pattern as Tools.tsx's card grid —
|
||||
arrows aren't part of the stagger (purely decorative connectors,
|
||||
not content), they just sit in the DOM between items. */}
|
||||
<RevealGroup className="flex flex-col md:flex-row gap-8 items-center md:items-start w-full lg:px-[16.25rem]">
|
||||
{steps.map((step, i) => (
|
||||
<Fragment key={step.title}>
|
||||
<RevealItem className="group flex flex-col gap-4 items-center text-center flex-1 max-w-xs md:max-w-none">
|
||||
{/* Plain <img>, not next/image — one of the three source PNGs
|
||||
isn't square (icon-step-2 is 68.6x64), and next/image's
|
||||
required width/height props fought with Tailwind's
|
||||
`img { height: auto }` preflight reset (console warning:
|
||||
"width or height modified, but not the other"). A fixed
|
||||
h-16 + w-auto lets each icon keep its own real aspect
|
||||
ratio without that conflict — matches how every other
|
||||
small decorative icon in this codebase (Tools.tsx etc.)
|
||||
is already a plain <img>, not next/image. group-hover
|
||||
scale is a small "step acknowledges you" touch — the
|
||||
whole text column is the hover target, not just the icon,
|
||||
so it's easy to trigger without precise pointing. */}
|
||||
<img
|
||||
src={step.icon}
|
||||
alt=""
|
||||
className="h-16 w-auto object-contain transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
<p className="font-semibold text-body text-text-primary">{step.title}</p>
|
||||
<p className="text-body-sm text-text-primary text-center">{step.desc}</p>
|
||||
</RevealItem>
|
||||
{i < steps.length - 1 && (
|
||||
<div className="flex items-center justify-center shrink-0">
|
||||
<img
|
||||
alt=""
|
||||
src="/icon-arrow-connector.svg"
|
||||
className="w-6 h-6 rotate-90 md:w-10 md:h-3 md:rotate-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</RevealGroup>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import Image from "next/image";
|
||||
import { AddToCartButton } from "./AddToCartButton";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
|
||||
const bullets = [
|
||||
"50 ToDo-Karten",
|
||||
"Hochwertiges Papier (120 g/m²)",
|
||||
"Format: 105 x 148 mm (A6)",
|
||||
"Inklusive Mini-Anleitung",
|
||||
"Nachhaltig produziert in Deutschland",
|
||||
];
|
||||
|
||||
export function Pricing() {
|
||||
return (
|
||||
<section className="w-full bg-bg-base px-[var(--layout-padding-x)] py-8">
|
||||
{/* Three flex children (photo, info, price/CTA) competing for space
|
||||
— same "big image next to text" shape as the Hero/Focus, so the
|
||||
same lg:-only structural exception applies here too. */}
|
||||
<Reveal className="bg-bg-muted rounded-md flex flex-col lg:flex-row gap-8 lg:gap-12 items-center p-6 lg:pl-8 lg:pr-10 lg:py-6">
|
||||
<div className="group relative w-full lg:w-[25.625rem] lg:shrink-0 aspect-[410/227] rounded-sm overflow-hidden">
|
||||
<Image
|
||||
src="/product-todo-karten.png"
|
||||
alt="ToDo-Karten Set"
|
||||
fill
|
||||
sizes="(min-width: 1024px) 410px, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 items-start flex-1 min-w-0 w-full">
|
||||
<p
|
||||
className="font-semibold text-h-small text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
ToDo-Karten – Set
|
||||
</p>
|
||||
<ul className="flex flex-col gap-[0.375rem] items-start">
|
||||
{bullets.map((b) => (
|
||||
<li key={b} className="flex gap-2 items-center">
|
||||
<img alt="" src="/icon-bullet-dot.svg" className="size-1 shrink-0" />
|
||||
<span className="text-body-sm text-text-primary">{b}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 items-start w-full lg:w-[18.75rem] lg:shrink-0">
|
||||
<div className="flex gap-2 items-center">
|
||||
<p className="font-bold text-h3 text-text-primary">12,90 €</p>
|
||||
<p className="text-label text-text-muted">inkl. MwSt. zzgl. Versand</p>
|
||||
</div>
|
||||
<AddToCartButton
|
||||
label="In den Warenkorb"
|
||||
className="w-full inline-flex items-center justify-center px-6 py-[0.8125rem] rounded-sm bg-brand hover:bg-brand-hover active:scale-[0.97] transition-all font-bold text-body text-text-primary text-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-muted"
|
||||
/>
|
||||
<div className="flex gap-[0.375rem] items-center">
|
||||
<img alt="" src="/icon-lock.svg" className="size-3.5" />
|
||||
<span className="text-body-sm text-text-primary">Sichere Zahlung</span>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import Image from "next/image";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
quote:
|
||||
"„Die ToDo-Karten sind mein täglicher Begleiter. Endlich mehr Fokus auf das, was wirklich wichtig ist.“",
|
||||
name: "Sarah M.",
|
||||
role: "Marketing Managerin",
|
||||
avatar: "/avatar-todo-1.png",
|
||||
},
|
||||
{
|
||||
quote: "„Ein simples Konzept, das mein Arbeiten komplett verändert hat. Klare Empfehlung!“",
|
||||
name: "Thomas K.",
|
||||
role: "Selbstständiger Berater",
|
||||
avatar: "/avatar-todo-2.png",
|
||||
},
|
||||
{
|
||||
quote: "„Weniger Chaos im Kopf, mehr Struktur im Alltag. Die Karten helfen mir jeden Tag.“",
|
||||
name: "Miriam L.",
|
||||
role: "Projektleiterin",
|
||||
avatar: "/avatar-todo-3.png",
|
||||
},
|
||||
];
|
||||
|
||||
// Card layout/style matches app/challenge/page.tsx's testimonial section
|
||||
// exactly (bg-muted filled card, no border, no decorative quote-mark,
|
||||
// quote on top with flex-1 pushing the avatar/name row to the bottom) —
|
||||
// including its hover-lift + avatar-scale micro-interactions, kept in
|
||||
// sync deliberately since both sections are meant to look and behave
|
||||
// identically. Only the photos differ (this page's own Figma-sourced
|
||||
// avatars, not Challenge's shared avatar-1/2/3.jpg).
|
||||
export function Testimonials() {
|
||||
return (
|
||||
<section className="w-full bg-bg-base flex flex-col gap-8 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
|
||||
<Reveal
|
||||
className="font-semibold text-h-emphasis text-text-primary text-center"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Was andere sagen
|
||||
</Reveal>
|
||||
|
||||
<RevealGroup className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-[var(--layout-grid-gap)] w-full">
|
||||
{testimonials.map((t) => (
|
||||
<RevealItem
|
||||
key={t.name}
|
||||
className="group md:col-span-4 bg-bg-muted rounded-xl p-6 flex flex-col gap-4 transition-transform duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<p className="flex-1 text-body-sm text-text-primary leading-[1.6]">{t.quote}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative size-10 shrink-0 rounded-full overflow-hidden transition-transform duration-300 group-hover:scale-110">
|
||||
<Image src={t.avatar} alt={t.name} fill sizes="40px" className="object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-body-sm text-text-primary">{t.name}</p>
|
||||
<p className="text-body-sm text-text-muted">{t.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</RevealItem>
|
||||
))}
|
||||
</RevealGroup>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { AddToCartButton } from "./AddToCartButton";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
|
||||
const checklist = [
|
||||
"Klarer Fokus auf das, was wirklich zählt",
|
||||
"Flexibel einsetzbar – jeden Tag",
|
||||
"Minimalistisch, analog, effektiv",
|
||||
];
|
||||
|
||||
export function TodoKartenHero() {
|
||||
return (
|
||||
<section className="bg-bg-base w-full overflow-hidden">
|
||||
{/* Same lg:-only structural exception as the Home Hero (see
|
||||
figma-to-nextjs skill Gotcha #5) — a wide fixed-ratio photo next
|
||||
to a text column is exactly the shape that gets too cramped at
|
||||
Tablet widths under the site-wide md: convention. */}
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-12 lg:items-center gap-8 lg:gap-[var(--layout-grid-gap)] pt-10 md:pt-12 lg:pt-0">
|
||||
<Reveal className="order-1 lg:order-none lg:col-span-5 flex flex-col gap-6 items-start pl-[var(--layout-padding-x)] pr-10 lg:pr-0">
|
||||
{/* Breadcrumb */}
|
||||
<p className="flex items-center gap-2 text-body-sm text-text-muted">
|
||||
<Link href="/#werkzeuge" className="hover:text-brand transition-colors">
|
||||
Werkzeuge
|
||||
</Link>
|
||||
<span>›</span>
|
||||
<span className="text-text-primary">ToDo-Karten</span>
|
||||
</p>
|
||||
|
||||
{/* Heading — 48px Desktop doesn't match any existing type-scale
|
||||
step, uses the new --text-h-page token (see globals.css). */}
|
||||
<div className="flex flex-col gap-2 items-start w-full">
|
||||
<p
|
||||
className="font-semibold text-h-page text-text-primary"
|
||||
style={{ fontFamily: "var(--font-playfair)" }}
|
||||
>
|
||||
ToDo-Karten
|
||||
</p>
|
||||
<p
|
||||
className="font-semibold text-h3 text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Kleine Karten. Große Wirkung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-body text-text-body">
|
||||
Ein einfaches Werkzeug, das dir hilft, deinen Kopf frei zu bekommen und das Wesentliche zu sehen.
|
||||
</p>
|
||||
|
||||
<ul className="flex flex-col gap-3 items-start w-full">
|
||||
{checklist.map((item) => (
|
||||
<li key={item} className="flex gap-[0.625rem] items-center w-full">
|
||||
<img alt="" src="/icon-check.svg" className="size-5 shrink-0" />
|
||||
<span className="flex-1 text-body text-text-primary">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<AddToCartButton label="ToDo-Karten bestellen" />
|
||||
</Reveal>
|
||||
|
||||
{/* group + subtle scale-on-hover — a common, restrained "photo
|
||||
feels alive" micro-interaction; overflow-hidden on the wrapper
|
||||
(not the image) keeps the zoom from spilling past the rounded
|
||||
corners. delay={0.15} matches the Home Hero's text→image
|
||||
stagger — both fire ~immediately since Hero is already in the
|
||||
initial viewport, so this doubles as the page's entrance. */}
|
||||
<Reveal
|
||||
className="order-2 lg:order-none lg:col-span-7 group relative w-full aspect-[3/2] rounded-md overflow-hidden"
|
||||
delay={0.15}
|
||||
>
|
||||
<Image
|
||||
src="/hero-todo-karten.png"
|
||||
alt="ToDo-Karten in Anwendung, mit Notizbuch und Kaffee"
|
||||
fill
|
||||
priority
|
||||
sizes="(min-width: 1024px) 58vw, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Metadata } from "next";
|
||||
import { TodoKartenHero } from "./components/TodoKartenHero";
|
||||
import { HowItWorks } from "./components/HowItWorks";
|
||||
import { Focus } from "./components/Focus";
|
||||
import { Testimonials } from "./components/Testimonials";
|
||||
import { Pricing } from "./components/Pricing";
|
||||
import { Footer } from "../components/Footer";
|
||||
|
||||
const title = "ToDo-Karten – Kleine Karten. Große Wirkung.";
|
||||
const description =
|
||||
"Ein einfaches Werkzeug, das dir hilft, deinen Kopf frei zu bekommen und das Wesentliche zu sehen. 50 hochwertige ToDo-Karten für mehr Fokus im Alltag.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: "/todo-cards",
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: "/todo-cards",
|
||||
images: ["/hero-todo-karten.png"],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images: ["/hero-todo-karten.png"],
|
||||
},
|
||||
};
|
||||
|
||||
export default function TodoCardsPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col flex-1">
|
||||
<TodoKartenHero />
|
||||
<HowItWorks />
|
||||
<Focus />
|
||||
<Testimonials />
|
||||
<Pricing />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user