Add shop, cart, versand pages and Impulse & Tipps detail page
- New /shop overview, /cart (real cart state via useSyncExternalStore), /versand (shipping policy page with TOC) and /newsletter detail page - Cart: quantity/removal, order summary, related-products cross-sell with randomized picks, VersandModal quick-reference instead of navigating away, MwSt. disclosure next to unit prices - Add-to-cart UX: inline success feedback (green state) plus a fly-to-navbar-cart-icon animation (CartFlyProvider) with a delayed badge count-up; AddToCartButton no longer navigates straight to /cart - Shared lib/products.ts catalog and lib/shipping.ts constants (cost, free-shipping threshold, handling/transit days) so cart, trust badges and the versand page can never drift apart - Fix Navbar smooth-scroll easing (ease-out instead of ease-in-out, no more perceived start delay); compress newsletter modal photo 2.4MB -> 85KB to fix first-open jank
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Image from "next/image";
|
||||
import { AddToCartButton } from "./AddToCartButton";
|
||||
import { AddToCartButton } from "../../components/AddToCartButton";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { TOTAL_DAYS_DE } from "../../lib/shipping";
|
||||
|
||||
const bullets = [
|
||||
"50 ToDo-Karten",
|
||||
@@ -49,6 +50,9 @@ export function Pricing() {
|
||||
<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>
|
||||
<p className="text-label text-text-muted">
|
||||
Lieferzeit: {TOTAL_DAYS_DE.min}–{TOTAL_DAYS_DE.max} Werktage innerhalb Deutschlands
|
||||
</p>
|
||||
<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"
|
||||
|
||||
@@ -23,16 +23,23 @@ const testimonials = [
|
||||
},
|
||||
];
|
||||
|
||||
// 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).
|
||||
// Card layout/style matches app/challenge/page.tsx's and
|
||||
// app/newsletter/'s testimonial sections exactly (bg-muted filled card,
|
||||
// no border, decorative quote-mark, quote on top with flex-1 pushing
|
||||
// the avatar/name row to the bottom, hover-lift + avatar-scale) — all
|
||||
// three kept in sync deliberately as one shared visual pattern, overriding
|
||||
// each page's own (differing) Figma spec for this one section. Only the
|
||||
// photos differ per page (each has its own Figma-sourced avatars).
|
||||
// max-w-[1600px] on the inner wrapper matches Challenge's testimonial
|
||||
// container cap — without it this section's fluid px-[layout-padding-x]
|
||||
// alone has no upper bound, so cards kept growing wider than Challenge's
|
||||
// past ~1440px viewports (capped at 1280px there) and read as
|
||||
// inconsistently narrower on Challenge. 1600px is a deliberate compromise
|
||||
// between the two, not either page's original value.
|
||||
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)]">
|
||||
<div className="max-w-[1600px] mx-auto w-full flex flex-col gap-8 items-center">
|
||||
<Reveal
|
||||
className="font-semibold text-h-emphasis text-text-primary text-center"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
@@ -44,9 +51,15 @@ export function Testimonials() {
|
||||
{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"
|
||||
className="group relative 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>
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute top-4 right-6 font-bold text-[2.5rem] text-[#ccc] leading-none select-none"
|
||||
>
|
||||
”
|
||||
</span>
|
||||
<p className="flex-1 text-body-sm text-text-primary leading-[1.6] pr-8">{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" />
|
||||
@@ -59,6 +72,7 @@ export function Testimonials() {
|
||||
</RevealItem>
|
||||
))}
|
||||
</RevealGroup>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { AddToCartButton } from "./AddToCartButton";
|
||||
import { AddToCartButton } from "../../components/AddToCartButton";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
|
||||
const checklist = [
|
||||
@@ -15,11 +15,27 @@ export function TodoKartenHero() {
|
||||
{/* 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">
|
||||
Tablet widths under the site-wide md: convention. lg:items-center
|
||||
removed (default grid align-items is stretch) — that's what lets
|
||||
the breadcrumb below sit at a fixed Y position across every hero
|
||||
section on the site instead of shifting per page depending on how
|
||||
much content each one has above the fold. */}
|
||||
{/* pt-10 md:pt-12, no lg override — with lg:items-center gone, this
|
||||
padding is now the ONLY thing positioning the breadcrumb
|
||||
vertically at every tier (previously lg:pt-0 relied on centering
|
||||
to add its own implicit offset instead). Kept flat past md so it
|
||||
matches /newsletter's and /challenge's identical top offset. */}
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-12 gap-8 lg:gap-[var(--layout-grid-gap)] pt-10 md:pt-12">
|
||||
<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 */}
|
||||
{/* Breadcrumb — 3 levels (Startseite › Werkzeuge › ToDo-Karten),
|
||||
matching /newsletter's pattern; was missing "Startseite" as
|
||||
the first crumb. Deliberately NOT part of the centered block
|
||||
below — see the wrapper's comment. */}
|
||||
<p className="flex items-center gap-2 text-body-sm text-text-muted">
|
||||
<Link href="/" className="hover:text-brand transition-colors">
|
||||
Startseite
|
||||
</Link>
|
||||
<span>›</span>
|
||||
<Link href="/#werkzeuge" className="hover:text-brand transition-colors">
|
||||
Werkzeuge
|
||||
</Link>
|
||||
@@ -27,37 +43,46 @@ export function TodoKartenHero() {
|
||||
<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.
|
||||
{/* Everything else — centered in the remaining vertical space at
|
||||
lg+ (the column now stretches to match the image's height,
|
||||
since lg:items-center was removed above; flex-1 + justify-center
|
||||
here centers this block within that leftover space instead of
|
||||
the breadcrumb-inclusive whole column, which is what made the
|
||||
breadcrumb's Y position drift per page depending on total
|
||||
content height). */}
|
||||
<div className="flex flex-col gap-6 items-start w-full flex-1 lg:justify-center">
|
||||
{/* 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" />
|
||||
</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
|
||||
|
||||
Reference in New Issue
Block a user