diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index d098d4b..b41df89 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -63,7 +63,7 @@ function getNavLinks(singleActiveProduct: boolean) { // /der-eine and /tasse-die-pause (both bespoke product detail pages, same // "own route, not nested" shape as /todo-cards) added here for the same // reason (2026-08-25). -const WERKZEUGE_ROUTES = ["/todo-cards", "/7-tage-klarheits-check", "/newsletter", "/der-eine", "/tasse-die-pause", "/lebensuhr", "/3x3-system"]; +const WERKZEUGE_ROUTES = ["/7-tage-klarheits-check", "/newsletter", "/der-eine", "/tasse-die-pause", "/lebensuhr", "/3x3-system"]; function isNavLinkActive(href: string, pathname: string, activeSection: string): boolean { if (href === "#werkzeuge") { diff --git a/app/components/ProductBlocks.tsx b/app/components/ProductBlocks.tsx index 72841fe..39699c7 100644 --- a/app/components/ProductBlocks.tsx +++ b/app/components/ProductBlocks.tsx @@ -149,10 +149,27 @@ function renderProductBlock(block: ProductBlock, ctx: ProductBlockContext): Reac // selector here (SVG presentation attributes lose to any stylesheet // rule) scopes the recolor to PDPs only, without touching the shared // icon set or PageBlocks.tsx's own rendering. + // Parallel fact cards, not a connected sequence — matches der-eine's + // own hand-coded Focus.tsx ("Was kann er?") exactly: icon on top, + // centered, no StepArrow connectors between them (those imply an + // order these facts don't have). Reuses stepRow's existing icon/ + // title/description shape rather than a new block type; only the + // rendering diverges from Pages.layout's own connected-arrow stepRow + // (renderPageBlockSync), scoped to ProductBlocks.tsx. Icons black + // (#231f20), same override reasoning as the quote block below. case "stepRow": return ( -
- {renderPageBlockSync(block as PageBlock)} +
+ {block.items.map((item) => { + const Icon = STEP_ICONS[item.icon]; + return ( +
+ {Icon && {Icon()}} +

{item.title}

+

{item.description}

+
+ ); + })}
); // A PDP-specific full-bleed treatment for this one block type only — @@ -278,6 +295,7 @@ function ProductHero({ product, shipping, taxRate, kleinunternehmer, body }: Pro /next/image). -function IconCheck() { - return ( - - - - ); -} - -export function Focus() { - return ( -
- - Notizbuch mit ToDo-Karten in natürlichem Licht - - -

- Für mehr Fokus im Alltag -

-

- ToDo-Karten helfen dir, den Überblick zu behalten, Entscheidungen zu vereinfachen und deinen Tag bewusst zu gestalten. -

-
    - {bullets.map((b) => ( -
  • - - {b} -
  • - ))} -
-
-
- ); -} diff --git a/app/todo-cards/components/HowItWorks.tsx b/app/todo-cards/components/HowItWorks.tsx deleted file mode 100644 index 9b45b8e..0000000 --- a/app/todo-cards/components/HowItWorks.tsx +++ /dev/null @@ -1,96 +0,0 @@ -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 -// the correct aspect ratio instead of stretching/squashing to a guessed one. -const steps = [ - { - icon: "/icon-step-1.png", - width: 165, - height: 177, - title: "1. Aufschreiben", - desc: "Notiere deine Aufgaben und alles, was dir im Kopf herumgeht.", - }, - { - icon: "/icon-step-2.png", - width: 180, - height: 168, - title: "2. Priorisieren", - desc: "Wähle deine 3 wichtigsten Aufgaben für heute aus und schreibe sie auf.", - }, - { - icon: "/icon-step-3.png", - width: 180, - height: 177, - title: "3. Umsetzen", - desc: "Konzentriere dich auf das Wesentliche und hake ab, was wirklich zählt.", - }, -]; - -export function HowItWorks() { - return ( -
- -

- So einfach funktioniert es -

-

- Ein paar Minuten am Morgen – mehr Klarheit den ganzen Tag. -

-
- - {/* Arrows always visible (see Divider lesson — hiding them below a - breakpoint isn't the fix), just rotated 90° to point down while - stacked below sm:. 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. */} - - {steps.map((step, i) => ( - - - {/* Explicit per-icon width/height (real pixel dimensions, not - a guessed/uniform size) — matters since one of the three - source PNGs isn't square (icon-step-2 is 180x168); passing - the real intrinsic size is what avoids next/image's - "width or height modified, but not the other" warning that - a wrong/guessed size would trigger against the `h-16 - w-auto` sizing below. 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. */} - -

{step.title}

-

{step.desc}

-
- {i < steps.length - 1 && ( - // sm:mt-[1.625rem] (26px) centers the arrow on the h-16 - // (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. Bigger below - // sm: (w-8 h-8, was w-6 h-6) per explicit feedback. -
- -
- )} -
- ))} -
-
- ); -} diff --git a/app/todo-cards/components/Pricing.tsx b/app/todo-cards/components/Pricing.tsx deleted file mode 100644 index e586109..0000000 --- a/app/todo-cards/components/Pricing.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import Image from "next/image"; -import { AddToCartButton } from "../../components/AddToCartButton"; -import { Reveal } from "../../components/Reveal"; -import { getProductBySlug, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload"; -import { formatPrice, discountPercent } from "../../lib/format"; -import { effectiveTaxRate } from "../../lib/cartTotals"; - -const bullets = [ - "50 ToDo-Karten", - "Hochwertiges Papier (120 g/m²)", - "Format: 105 x 148 mm (A6)", - "Inklusive Mini-Anleitung", - "Nachhaltig produziert in Deutschland", -]; - -// Price/photo come from Payload (same "todo-karten" product the shop/cart -// use), not a hardcoded literal — see ProductSpotlight.tsx for the same -// reasoning; the bullet list stays hand-written since it's spec detail, -// not something the Products collection models. -export async function Pricing() { - const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([ - getProductBySlug("todo-karten"), - getShippingSettings(), - getDefaultTaxRatePercent(), - getKleinunternehmer(), - ]); - if (!product) return null; - const discount = discountPercent(product.price, product.compareAtPrice); - const taxRate = effectiveTaxRate(product, defaultTaxRate); - // Same "any vs. every" split as ProductGrid.tsx/ProductSpotlight.tsx. - // A deactivated product (product.active === false) isn't caught by - // either — the shop grid/spotlight filter those out before they'd ever - // reach this page, but this page resolves a product regardless of - // active status (existing links to it should still 200, see - // mapPayloadProduct's own comment in lib/payload.ts) — so it needs its - // own explicit check here to read as "Ausverkauft" rather than fully - // buyable (fixed 2026-07-24, feedback: deactivated should look - // sold-out on its own detail page). - const fullyOutOfStock = - !product.active || (product.variants.length > 0 ? product.variants.every((v) => v.outOfStock) : product.outOfStock); - const anyLowStock = product.active && (product.variants.length > 0 ? product.variants.some((v) => v.lowStock) : product.lowStock); - - return ( -
- {/* 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. */} - -
- ToDo-Karten Set - {fullyOutOfStock ? ( - - Ausverkauft - - ) : ( - discount !== null && ( - - -{discount}% - - ) - )} -
- -
-

- ToDo-Karten – Set -

-
    - {bullets.map((b) => ( -
  • - - {b} -
  • - ))} -
-
- -
- {/* MwSt./Versand disclosure on its own line, not crammed into - the price row itself — "inkl. X% MwSt. zzgl. Versand" is - long enough (once the rate is spelled out) that sharing a - row with the price in this panel's narrow fixed-width - column wrapped it mid-sentence onto a second line. */} -
-
- {discount !== null && ( -

{formatPrice(product.compareAtPrice!)}

- )} -

{formatPrice(product.price)}

-
-

- {kleinunternehmer - ? product.noShippingCost - ? "Keine Versandkosten" - : "zzgl. Versand" - : `inkl. ${taxRate}% MwSt. ${product.noShippingCost ? "– keine Versandkosten" : "zzgl. Versand"}`} -

- {!product.noShippingCost && ( -

- Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands -

- )} -
- {/* Single product, no grid siblings to stay equal-height with — - plain conditional line, same reasoning as ProductSpotlight.tsx. */} - {anyLowStock &&

Nur noch wenige verfügbar

} - {/* No "Sichere Zahlung" trust note here (unlike Cart/Checkout) — - this is an add-to-cart step, not the actual payment step, so - a payment-security reassurance is premature here and just - duplicates the one shown later at checkout. */} - {/* variants={[]} when deactivated — AddToCartButton's own - outOfStock prop is ignored whenever variants is non-empty (it - defers to each variant's own outOfStock flag instead, see its - currentlyOutOfStock calc), so a deactivated product with - in-stock variants would otherwise still render as buyable. */} - -
-
-
- ); -} diff --git a/app/todo-cards/components/TodoKartenHero.tsx b/app/todo-cards/components/TodoKartenHero.tsx deleted file mode 100644 index 2e6cbde..0000000 --- a/app/todo-cards/components/TodoKartenHero.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import Link from "next/link"; -import Image from "next/image"; -import { AddToCartButton } from "../../components/AddToCartButton"; -import { ProductGallery } from "../../components/ProductGallery"; -import { Reveal } from "../../components/Reveal"; -import { ProductName } from "../../components/ProductName"; -import { getProductBySlug, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload"; -import { formatPrice, discountPercent } from "../../lib/format"; -import { effectiveTaxRate } from "../../lib/cartTotals"; - -const checklist = [ - "Klarer Fokus auf das, was wirklich zählt", - "Flexibel einsetzbar – jeden Tag", - "Minimalistisch, analog, effektiv", -]; - -// Inline, brand-orange stroke — icon-check.svg's fill is hardcoded to -// #222221 via an internal CSS var that only resolves inside the SVG's own -// document, so it can't be recolored from the host page when loaded via -// /next/image. Same simple checkmark path as Challenge's own -// Check() component, for the same orange-checkmark look (fixed 2026-07-24). -function IconCheck() { - return ( - - - - ); -} - -// Same "todo-karten" product Pricing.tsx reads further down the page — -// this is just a compact early teaser so the hero's CTA isn't asking for -// a click without saying what it costs. Delivery time is repeated here too -// (not just in Pricing.tsx) since this is also a purchase CTA — Art. 246a -// §1 Abs.1 Nr.8 EGBGB's delivery-date disclosure needs to sit next to every -// buy button, not just one of them. -export async function TodoKartenHero() { - const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([ - getProductBySlug("todo-karten"), - getShippingSettings(), - getDefaultTaxRatePercent(), - getKleinunternehmer(), - ]); - const discount = product ? discountPercent(product.price, product.compareAtPrice) : null; - const taxRate = product ? effectiveTaxRate(product, defaultTaxRate) : null; - // Same "any vs. every" split as ProductGrid.tsx/Pricing.tsx. - const anyLowStock = product - ? product.active && (product.variants.length > 0 ? product.variants.some((v) => v.lowStock) : product.lowStock) - : false; - - return ( -
- {/* 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 even under the site-wide sm: (640px) structural - consolidation. 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. */} -
- - {/* 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. */} -

- - Startseite - - - - Werkzeuge - - - {product ? : "ToDo-Karten"} -

- - {/* 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). */} -
- {/* Heading — 48px Desktop doesn't match any existing type-scale - step, uses the new --text-h-page token (see globals.css). */} -
-

- ToDo-Karten. -

- {product?.subline && ( -

- {product.subline} -

- )} -
- -

- Ein einfaches Werkzeug, das dir hilft, deinen Kopf frei zu bekommen und das Wesentliche zu sehen. -

- -
    - {checklist.map((item) => ( -
  • - - {item} -
  • - ))} -
- -
- {product && ( -
- {discount !== null && ( -

{formatPrice(product.compareAtPrice!)}

- )} -

{formatPrice(product.price)}

-

- {kleinunternehmer - ? product.noShippingCost - ? "Keine Versandkosten" - : "zzgl. Versand" - : `inkl. ${taxRate}% MwSt. ${product.noShippingCost ? "– keine Versandkosten" : "zzgl. Versand"}`} -

-
- )} - {!product?.noShippingCost && ( -

- Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands -

- )} - {/* Single product, no grid siblings to stay equal-height with — - plain conditional line, same reasoning as ProductSpotlight.tsx/Pricing.tsx. */} - {anyLowStock &&

Nur noch wenige verfügbar

} -
- - {product && ( - // variants={[]} when deactivated — see Pricing.tsx's own - // comment on why AddToCartButton's outOfStock prop alone - // isn't enough once variants is non-empty. - - )} -
-
- - {/* 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. - Falls back to the curated static hero photo (a lifestyle shot, - "in Anwendung, mit Notizbuch und Kaffee" — deliberately not - just the plain catalog image) whenever this product has no - `gallery` entries — the common case, and zero visual change - from before ProductGallery existed. Only switches to the - gallery once someone actually adds extra photos in the admin. */} - {product && product.gallery.length > 0 ? ( - - - - ) : ( - - ToDo-Karten in Anwendung, mit Notizbuch und Kaffee - - )} -
-
- ); -} diff --git a/app/todo-cards/page.tsx b/app/todo-cards/page.tsx deleted file mode 100644 index 1cfb6a6..0000000 --- a/app/todo-cards/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { Metadata } from "next"; -import { draftMode } from "next/headers"; -import { TodoKartenHero } from "./components/TodoKartenHero"; -import { HowItWorks } from "./components/HowItWorks"; -import { Focus } from "./components/Focus"; -import { Pricing } from "./components/Pricing"; -import { Footer } from "../components/Footer"; -import { TestimonialsGrid } from "../components/TestimonialsGrid"; -import { LiveTestimonialsGrid } from "../components/LiveTestimonialsGrid"; -import { getTestimonials, getProductBySlug, getCompanySettings } from "../lib/payload"; -import { buildProductSchema } from "../lib/structuredData"; - -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 async function TodoCardsPage() { - const { isEnabled: isPreview } = await draftMode(); - const [testimonials, product, seller] = await Promise.all([ - getTestimonials("todo-cards", { draft: isPreview }), - getProductBySlug("todo-karten"), - getCompanySettings(), - ]); - const productSchema = product ? buildProductSchema(product, "https://einfach-produktiv.mk360.de/todo-cards", seller) : null; - - return ( - <> - {productSchema && ( -