diff --git a/app/components/ProductBlocks.tsx b/app/components/ProductBlocks.tsx new file mode 100644 index 0000000..015fa5e --- /dev/null +++ b/app/components/ProductBlocks.tsx @@ -0,0 +1,204 @@ +import Link from "next/link"; +import Image from "next/image"; +import type { Product, ProductBlock, PageBlock } from "../lib/payload"; +import { getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../lib/payload"; +import { formatPrice, discountPercent } from "../lib/format"; +import { effectiveTaxRate } from "../lib/cartTotals"; +import { AddToCartButton } from "./AddToCartButton"; +import { ProductName } from "./ProductName"; +import { RichText } from "./RichText"; +import { renderPageBlockSync } from "./PageBlocks"; + +// Renders a Payload Products document's `layout` blocks field — same +// "page sections, not inline Lexical nodes" shape as PageBlocks.tsx, which +// this delegates to directly for every block type the two fields share +// (richTextSection/stepRow/quote/image/icon/pillList/checklistImage/table/ +// ctaCard are the exact same Block configs, just registered a second time +// on Products.layout — see that field's own comment in Products.ts). Only +// `productHero` and `productPricingPanel` are product-specific, since they +// need the live product/shipping/tax context that a generic content page +// doesn't have. +export async function ProductBlocks({ product }: { product: Product }) { + const [shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([ + getShippingSettings(), + getDefaultTaxRatePercent(), + getKleinunternehmer(), + ]); + const ctx: ProductBlockContext = { product, shipping, taxRate: effectiveTaxRate(product, defaultTaxRate), kleinunternehmer }; + + return ( + <> + {product.layout.map((block) => ( +
+ {renderProductBlock(block, ctx)} +
+ ))} + + ); +} + +type ProductBlockContext = { + product: Product; + shipping: Awaited>; + taxRate: number; + kleinunternehmer: boolean; +}; + +function renderProductBlock(block: ProductBlock, ctx: ProductBlockContext): React.ReactNode { + switch (block.blockType) { + case "productHero": + return ; + case "productPricingPanel": + return ; + // Every other block type is identical to Pages.layout's own — same + // Block config, reused a second time (see this file's own comment). + default: + return renderPageBlockSync(block as PageBlock); + } +} + +function ProductHero({ product, shipping, taxRate, kleinunternehmer, body }: ProductBlockContext & { body: unknown }) { + const discount = discountPercent(product.price, product.compareAtPrice); + 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 ( +
+
+

+ + Startseite + + + + Werkzeuge + + + + + +

+ +
+

+ +

+ {product.subline && ( +

+ {product.subline} +

+ )} +
+ + {body ? : null} + +
+
+ {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 +

+ )} + {anyLowStock &&

Nur noch wenige verfügbar

} +
+ + +
+ +
+ {product.name} +
+
+ ); +} + +function ProductPricingPanel({ product, shipping, taxRate, kleinunternehmer }: ProductBlockContext) { + const discount = discountPercent(product.price, product.compareAtPrice); + 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 ( +
+
+ {product.name} + {fullyOutOfStock ? ( + + Ausverkauft + + ) : ( + discount !== null && ( + + -{discount}% + + ) + )} +
+ +
+

+ +

+ {product.subline &&

{product.subline}

} +
+ +
+
+
+ {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 +

+ )} +
+ {anyLowStock &&

Nur noch wenige verfügbar

} + +
+
+ ); +} diff --git a/app/einfach-anfangen/components/Focus.tsx b/app/einfach-anfangen/components/Focus.tsx deleted file mode 100644 index 92fd1e6..0000000 --- a/app/einfach-anfangen/components/Focus.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Image from "next/image"; -import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal"; - -const bullets = [ - "10 ToDo-Karten", - "Platz für bis zu 7 Aufgaben", - "7,5 × 12,5 cm – passt in jede Tasche", - "Freie Rückseite für Notizen", - "Kleine Trink-Erinnerung auf jeder Karte", - "Hochwertiges Papier", - "Produziert in Deutschland", -]; - -// Spec bullet list — same "hand-written, not modeled in Products" reasoning -// as todo-cards/Pricing.tsx's own bullets. Two-column at sm+ since 7 items -// in one column ran noticeably longer than every other section on the page. -export function Focus() { - return ( -
- -

- 10 Karten für deinen Alltag -

-

- Eine Karte für heute. Für das, was gerade ansteht. -

-
- - - {bullets.map((b) => ( - - - {b} - - ))} - -
- ); -} diff --git a/app/einfach-anfangen/components/Hero.tsx b/app/einfach-anfangen/components/Hero.tsx deleted file mode 100644 index 4817b77..0000000 --- a/app/einfach-anfangen/components/Hero.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import Link from "next/link"; -import Image from "next/image"; -import { AddToCartButton } from "../../components/AddToCartButton"; -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"; - -// Same "compact early teaser + delivery-time repeated next to every buy -// button" reasoning as TodoKartenHero.tsx/der-eine's Hero.tsx. Body copy is -// hardcoded here (not read from product.description) — same convention as -// those two, see der-eine/page.tsx's own comment on why the CMS field and -// this page's tuned prose are allowed to say different things. -export async function Hero() { - const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([ - getProductBySlug("todo-starter"), - getShippingSettings(), - getDefaultTaxRatePercent(), - getKleinunternehmer(), - ]); - const discount = product ? discountPercent(product.price, product.compareAtPrice) : null; - const taxRate = product ? effectiveTaxRate(product, defaultTaxRate) : null; - const fullyOutOfStock = product - ? !product.active || (product.variants.length > 0 ? product.variants.every((v) => v.outOfStock) : product.outOfStock) - : false; - const anyLowStock = product - ? product.active && (product.variants.length > 0 ? product.variants.some((v) => v.lowStock) : product.lowStock) - : false; - - return ( -
-
- -

- - Startseite - - - - Werkzeuge - - - {product ? : "Einfach anfangen."} -

- -
-
-

- {product ? : "Einfach anfangen."} -

- {product?.subline && ( -

- {product.subline} -

- )} -
- -
-

- Nicht alles. Nicht irgendwann. Nur die paar Dinge, die heute wichtig sind. -

-

- Einfach anfangen. sind ToDo-Karten für deinen Alltag – mit Platz für bis zu sieben Aufgaben, einem kleinen Feld für „Das war heute schön" und einer freien Rückseite für Notizen. -

-

- Dazu eine kleine Trink-Erinnerung. Weil man das zwischen all den anderen Dingen gern mal vergisst. -

-
- -
- {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 -

- )} - {anyLowStock &&

Nur noch wenige verfügbar

} -
- - {product && ( - - )} -
-
- -
- {product && {product.name}} -
-
-
- ); -} diff --git a/app/einfach-anfangen/components/HowItWorks.tsx b/app/einfach-anfangen/components/HowItWorks.tsx deleted file mode 100644 index e2200da..0000000 --- a/app/einfach-anfangen/components/HowItWorks.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Fragment } from "react"; -import Image from "next/image"; -import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal"; -import { StepArrow } from "../../components/StepArrow"; - -// Same icon-step-*.png set and layout as todo-cards/components/HowItWorks.tsx -// — write / choose-and-start / check off maps onto the same three glyphs. -const steps = [ - { - icon: "/icon-step-1.png", - width: 165, - height: 177, - title: "1. Aufschreiben", - desc: "Was steht heute an?", - }, - { - icon: "/icon-step-2.png", - width: 180, - height: 168, - title: "2. Anfangen", - desc: "Eine Sache auswählen und loslegen.", - }, - { - icon: "/icon-step-3.png", - width: 180, - height: 177, - title: "3. Abhaken", - desc: "Was erledigt ist, darf weg. Der Rest kommt später.", - }, -]; - -export function HowItWorks() { - return ( -
- -

- Eine Karte. Ein Tag. -

-
- - - {steps.map((step, i) => ( - - - -

{step.title}

-

{step.desc}

-
- {i < steps.length - 1 && ( -
- -
- )} -
- ))} -
-
- ); -} diff --git a/app/einfach-anfangen/components/Pricing.tsx b/app/einfach-anfangen/components/Pricing.tsx deleted file mode 100644 index bc1261e..0000000 --- a/app/einfach-anfangen/components/Pricing.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import Image from "next/image"; -import { AddToCartButton } from "../../components/AddToCartButton"; -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"; - -// Same closing-CTA-panel pattern as todo-cards/components/Pricing.tsx. -export async function Pricing() { - const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([ - getProductBySlug("todo-starter"), - getShippingSettings(), - getDefaultTaxRatePercent(), - getKleinunternehmer(), - ]); - if (!product) return null; - const discount = discountPercent(product.price, product.compareAtPrice); - const taxRate = effectiveTaxRate(product, defaultTaxRate); - 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 ( -
- -
- {product.name} - {fullyOutOfStock ? ( - - Ausverkauft - - ) : ( - discount !== null && ( - - -{discount}% - - ) - )} -
- -
-

- -

- {product.subline &&

{product.subline}

} -
- -
-
-
- {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 -

- )} -
- {anyLowStock &&

Nur noch wenige verfügbar

} - -
-
-
- ); -} diff --git a/app/einfach-anfangen/page.tsx b/app/einfach-anfangen/page.tsx index f14dad7..6f83ee3 100644 --- a/app/einfach-anfangen/page.tsx +++ b/app/einfach-anfangen/page.tsx @@ -1,8 +1,6 @@ import type { Metadata } from "next"; -import { Hero } from "./components/Hero"; -import { HowItWorks } from "./components/HowItWorks"; -import { Focus } from "./components/Focus"; -import { Pricing } from "./components/Pricing"; +import { notFound } from "next/navigation"; +import { ProductBlocks } from "../components/ProductBlocks"; import { Footer } from "../components/Footer"; import { getProductBySlug, getCompanySettings } from "../lib/payload"; import { buildProductSchema } from "../lib/structuredData"; @@ -24,25 +22,25 @@ export async function generateMetadata(): Promise { }; } +// First PDP driven entirely by Products.layout (see that field's own +// comment in Products.ts) instead of its own bespoke Hero/HowItWorks/ +// Focus/Pricing components — those were deleted once this product's +// content was authored as blocks in the admin (see the PDP page-builder +// rollout). der-eine/todo-cards/tasse-die-pause are untouched and keep +// their own hand-coded components; nothing about them depends on this. export default async function EinfachAnfangenPage() { const [product, seller] = await Promise.all([ getProductBySlug("todo-starter"), getCompanySettings(), ]); - const productSchema = product - ? buildProductSchema(product, "https://einfach-produktiv.mk360.de/einfach-anfangen", seller) - : null; + if (!product) notFound(); + const productSchema = buildProductSchema(product, "https://einfach-produktiv.mk360.de/einfach-anfangen", seller); return ( <> - {productSchema && ( -