Add bespoke product detail pages for Der Alltagsstift and the mug

/der-alltagsstift mirrors the todo-cards pattern (Hero/HowItWorks/
Focus/Pricing) — renamed from "Der Eine" for brand-tonality reasons,
copy grounded in visually-verified product facts only.

/tasse-die-pause is a deliberately minimal Hero+Pricing scaffold —
naming/design concept for that product is still undecided, so the
headline reads product.name live from Payload instead of a hardcoded
tagline.
This commit is contained in:
Marco
2026-08-24 22:52:41 +00:00
parent c065a3223f
commit b6e76ccd22
8 changed files with 690 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import Image from "next/image";
import { Reveal } from "../../components/Reveal";
import { getProductBySlug } from "../../lib/payload";
// Only visually-verifiable claims (Metallkorpus/Chromringe from the
// product photos) — no engraving/color claim, both still unconfirmed
// (see plan notes: Farbvariante/Gravur-Merkmal open as of 2026-08-24).
const bullets = [
"Korpus aus Metall, kein Plastik",
"Liegt gut in der Hand",
"Passt in jede Tasche",
];
// Same fix/reasoning as Hero.tsx's own IconCheck.
function IconCheck() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" className="size-5 shrink-0 mt-1">
<path d="M4 10.5l4.5 4.5L16 5.5" stroke="#f6a701" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
// Unlike todo-cards/components/Focus.tsx, this product has no dedicated
// lifestyle photo yet — only 3 supplier catalog macro shots exist, all
// 338×338px (no larger source, confirmed against the Payload media API).
// Rather than object-cover-stretching a small square into a wide rectangle
// (blurs badly, and is exactly what Pricing.tsx's photo slot already does —
// see plan notes on the "einheitlich" feedback), this uses a contained,
// padded square tile so the source resolution reads as a deliberate macro
// crop instead of a blown-up thumbnail. Uses gallery[1] (the chrome-rings
// close-up) — deliberately not gallery[0], which shows an assortment
// including a black pen and would imply a color variant that isn't
// confirmed yet (see Focus's bullets comment).
export async function Focus() {
const product = await getProductBySlug("stift-kugelschreiber");
if (!product) return null;
const photo = product.gallery[1] ?? product.image;
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)]">
{/* Fixed max-width, not a viewport-percentage width — same reasoning
as Hero.tsx's gallery cap: the source is only 338×338px, and a
percentage width grows past that on wide screens, exaggerating
the upscale blur. */}
<Reveal className="group relative w-full max-w-[20rem] lg:shrink-0 aspect-square rounded-md overflow-hidden bg-bg-muted p-10">
<Image
src={photo}
alt={product.name}
fill
sizes="320px"
className="object-contain 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)" }}
>
Ein Stift, der bleibt
</p>
<p className="text-body text-text-body">
Die meisten Kugelschreiber landen irgendwann in einer Schublade und werden nicht wieder rausgeholt. Der Alltagsstift soll man behalten.
</p>
<ul className="flex flex-col gap-3 items-start w-full">
{bullets.map((b) => (
<li key={b} className="flex gap-[0.625rem] items-start w-full">
<IconCheck />
<span className="flex-1 font-semibold text-body text-text-primary">{b}</span>
</li>
))}
</ul>
</Reveal>
</section>
);
}
+139
View File
@@ -0,0 +1,139 @@
import Link from "next/link";
import { AddToCartButton } from "../../components/AddToCartButton";
import { ProductGallery } from "../../components/ProductGallery";
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 checklist = [
"Funktioniert ohne Strom oder Internet",
"Aus Metall, kein Einwegstift",
"Passt in jede Tasche",
];
// Same fix/reasoning as TodoKartenHero.tsx's IconCheck — icon-check.svg's
// fill can't be recolored from outside the SVG when loaded via <img
// src>/next/image, so this stays an inline stroke path per page.
function IconCheck() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" className="size-5 shrink-0 mt-1">
<path d="M4 10.5l4.5 4.5L16 5.5" stroke="#f6a701" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
// Same "compact early teaser + delivery-time repeated next to every buy
// button" reasoning as TodoKartenHero.tsx.
export async function Hero() {
const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
getProductBySlug("stift-kugelschreiber"),
getShippingSettings(),
getDefaultTaxRatePercent(),
getKleinunternehmer(),
]);
const discount = product ? discountPercent(product.price, product.compareAtPrice) : null;
const taxRate = product ? effectiveTaxRate(product, defaultTaxRate) : null;
const anyLowStock = product
? product.active && (product.variants.length > 0 ? product.variants.some((v) => v.lowStock) : product.lowStock)
: false;
return (
<section className="bg-bg-base w-full overflow-hidden">
<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">
<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>
<span></span>
<span className="text-text-primary">Der Alltagsstift</span>
</p>
<div className="flex flex-col gap-6 items-start w-full flex-1 lg:justify-center">
<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)" }}
>
Der Alltagsstift<span className="text-brand">.</span>
</p>
<p
className="font-semibold text-h3 text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
Ein Stift, der einfach da ist, wenn du ihn brauchst.
</p>
</div>
<p className="text-body text-text-body">
Der Kugelschreiber für die eine Sache, die heute zählt. Schlicht, hochwertig und immer griffbereit.
</p>
<ul className="flex flex-col gap-3 items-start w-full">
{checklist.map((item) => (
<li key={item} className="flex gap-[0.625rem] items-start w-full">
<IconCheck />
<span className="flex-1 text-body text-text-primary">{item}</span>
</li>
))}
</ul>
<div className="flex flex-col gap-1 items-start">
{product && (
<div className="flex gap-2 items-baseline">
{discount !== null && (
<p className="text-body text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</p>
)}
<p className="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
<p className="text-label text-text-muted">
{kleinunternehmer
? product.noShippingCost
? "Keine Versandkosten"
: "zzgl. Versand"
: `inkl. ${taxRate}% MwSt. ${product.noShippingCost ? " keine Versandkosten" : "zzgl. Versand"}`}
</p>
</div>
)}
{!product?.noShippingCost && (
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
)}
{anyLowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</p>}
</div>
{product && (
<AddToCartButton
label="Alltagsstift bestellen"
numericId={product.numericId}
outOfStock={!product.active || product.outOfStock}
maxQty={product.maxQty}
variants={product.active ? product.variants : []}
/>
)}
</div>
</Reveal>
{/* max-w cap, not the full lg:col-span-7 width — the source photos
are only 338×338px (checked directly against the Payload media
API, no larger size exists), so letting ProductGallery stretch
to the full column blows them up hard. Capping the display
width keeps the upscale factor small enough that it doesn't
look broken; it can't fix the underlying resolution, only hide
it. Real fix is new source photos, not a layout tweak. */}
{product && product.gallery.length > 0 && (
<Reveal className="order-2 lg:order-none lg:col-span-7 flex lg:items-center" delay={0.15}>
<div className="w-full max-w-[28rem] mx-auto lg:mx-0">
<ProductGallery image={product.image} gallery={product.gallery} alt={product.name} />
</div>
</Reveal>
)}
</div>
</section>
);
}
@@ -0,0 +1,72 @@
import { Fragment } from "react";
import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
import { StepArrow } from "../../components/StepArrow";
// Same 3 generic step icons as todo-cards/components/HowItWorks.tsx — their
// artwork (pen tip, checklist, arrow) reads generically enough to carry a
// different 3-step flow without looking mismatched.
const steps = [
{
icon: "/icon-step-1.png",
width: 165,
height: 177,
title: "1. Griffbereit halten",
desc: "Der Alltagsstift liegt dort, wo du ihn brauchst Tasche, Schreibtisch, Notizbuch.",
},
{
icon: "/icon-step-2.png",
width: 180,
height: 168,
title: "2. Sofort notieren",
desc: "Du musst nicht erst dein Handy entsperren oder eine App öffnen.",
},
{
icon: "/icon-step-3.png",
width: 180,
height: 177,
title: "3. Weitermachen",
desc: "Notiert ist notiert, du machst einfach da weiter, wo du warst.",
},
];
export function HowItWorks() {
return (
<section className="w-full bg-bg-base flex flex-col gap-12 items-center py-12 sm: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 ist er im Alltag
</p>
<p className="text-body text-text-muted">
Du nimmst ihn, schreibst, steckst ihn wieder ein. Mehr braucht es nicht.
</p>
</Reveal>
<RevealGroup className="flex flex-col sm:flex-row gap-8 items-center sm: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 sm:max-w-none">
<Image
src={step.icon}
alt=""
width={step.width}
height={step.height}
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 sm:mt-[1.5rem]">
<StepArrow className="w-8 h-8 rotate-90 sm:w-10 sm:h-4 sm:rotate-0" />
</div>
)}
</Fragment>
))}
</RevealGroup>
</section>
);
}
+116
View File
@@ -0,0 +1,116 @@
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";
// Only visually-verifiable claims — same reasoning as Focus.tsx's bullets
// (no engraving/color claim, both unconfirmed as of 2026-08-24).
const bullets = [
"Kugelschreiber aus Metall",
"Chromfarbene Details",
"Handlich, für jede Tasche",
];
// Price/photo come from Payload (same "stift-kugelschreiber" product the
// shop/cart use), not a hardcoded literal — same reasoning as
// todo-cards/components/Pricing.tsx.
export async function Pricing() {
const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
getProductBySlug("stift-kugelschreiber"),
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 (
<section className="w-full bg-bg-base px-[var(--layout-padding-x)] py-8">
<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">
{/* object-contain on a bg-bg-base tile, not object-cover — the
source is a 338×338px supplier macro shot (no larger original
exists), so stretching it across a 410×227 landscape crop like
before blows it up and blurs it. A contained square reads as a
deliberate close-up instead. Uses product.image (id 85, the
confident macro tip shot) — distinct from Focus.tsx's photo
(gallery[1], the chrome-rings shot) so the two sections don't
repeat the same frame. */}
<div className="group relative w-full lg:w-[15.625rem] lg:shrink-0 aspect-square rounded-sm overflow-hidden bg-bg-base">
<Image
src={product.image}
alt="Der Alltagsstift"
fill
sizes="(min-width: 1024px) 250px, 100vw"
className="object-contain p-4 transition-transform duration-500 group-hover:scale-105"
/>
{fullyOutOfStock ? (
<span className="absolute top-3 left-3 rounded-full bg-text-muted px-2.5 py-1 text-label font-bold text-bg-base">
Ausverkauft
</span>
) : (
discount !== null && (
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
-{discount}%
</span>
)
)}
</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)" }}
>
Der Alltagsstift
</p>
<ul className="flex flex-col gap-[0.375rem] items-start">
{bullets.map((b) => (
<li key={b} className="flex gap-2 items-center">
<Image alt="" src="/icon-bullet-dot.svg" width={4} height={4} 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 flex-col gap-1 items-start">
<div className="flex gap-2 items-baseline">
{discount !== null && (
<p className="text-body text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</p>
)}
<p className="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
</div>
<p className="text-label text-text-muted">
{kleinunternehmer
? product.noShippingCost
? "Keine Versandkosten"
: "zzgl. Versand"
: `inkl. ${taxRate}% MwSt. ${product.noShippingCost ? " keine Versandkosten" : "zzgl. Versand"}`}
</p>
{!product.noShippingCost && (
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
)}
</div>
{anyLowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</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"
numericId={product.numericId}
outOfStock={!product.active || product.outOfStock}
maxQty={product.maxQty}
variants={product.active ? product.variants : []}
/>
</div>
</Reveal>
</section>
);
}
+54
View File
@@ -0,0 +1,54 @@
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 { Footer } from "../components/Footer";
import { getProductBySlug, getCompanySettings } from "../lib/payload";
import { buildProductSchema } from "../lib/structuredData";
const title = "Der Alltagsstift Ein Stift, der einfach da ist, wenn du ihn brauchst.";
const description =
"Der Kugelschreiber für die eine Sache, die heute zählt. Schlicht, hochwertig und immer griffbereit.";
export const metadata: Metadata = {
title,
description,
alternates: {
canonical: "/der-alltagsstift",
},
openGraph: {
title,
description,
url: "/der-alltagsstift",
},
twitter: {
title,
description,
},
};
export default async function DerAlltagsstiftPage() {
const [product, seller] = await Promise.all([
getProductBySlug("stift-kugelschreiber"),
getCompanySettings(),
]);
const productSchema = product
? buildProductSchema(product, "https://einfach-produktiv.mk360.de/der-alltagsstift", seller)
: null;
return (
<>
{productSchema && (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productSchema) }} />
)}
<main className="flex flex-col flex-1">
<Hero />
<HowItWorks />
<Focus />
<Pricing />
</main>
<Footer />
</>
);
}
+101
View File
@@ -0,0 +1,101 @@
import Link from "next/link";
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";
// Provisional scaffold — naming/design concept for this product is still
// undecided (parked 2026-08-24, see memory: project_tasse_die_pause_product).
// Headline reads product.name directly from Payload instead of a hardcoded
// string like every other bespoke page here (TodoKartenHero.tsx, der-
// alltagsstift/Hero.tsx) — so renaming in the CMS updates this page with no
// code change. No tagline/checklist/benefit copy yet; those depend on
// whatever concept the name ends up carrying, which isn't decided.
export async function Hero() {
const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
getProductBySlug("tasse-die-pause"),
getShippingSettings(),
getDefaultTaxRatePercent(),
getKleinunternehmer(),
]);
const discount = product ? discountPercent(product.price, product.compareAtPrice) : null;
const taxRate = product ? effectiveTaxRate(product, defaultTaxRate) : null;
const anyLowStock = product
? product.active && (product.variants.length > 0 ? product.variants.some((v) => v.lowStock) : product.lowStock)
: false;
return (
<section className="bg-bg-base w-full overflow-hidden">
<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">
<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>
<span></span>
<span className="text-text-primary">{product?.name ?? "Tasse"}</span>
</p>
<div className="flex flex-col gap-6 items-start w-full flex-1 lg:justify-center">
<p
className="font-semibold text-h-page text-text-primary"
style={{ fontFamily: "var(--font-playfair)" }}
>
{product?.name ?? "Tasse"}
</p>
{product?.description && <p className="text-body text-text-body">{product.description}</p>}
<div className="flex flex-col gap-1 items-start">
{product && (
<div className="flex gap-2 items-baseline">
{discount !== null && (
<p className="text-body text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</p>
)}
<p className="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
<p className="text-label text-text-muted">
{kleinunternehmer
? product.noShippingCost
? "Keine Versandkosten"
: "zzgl. Versand"
: `inkl. ${taxRate}% MwSt. ${product.noShippingCost ? " keine Versandkosten" : "zzgl. Versand"}`}
</p>
</div>
)}
{!product?.noShippingCost && (
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
)}
{anyLowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</p>}
</div>
{product && (
<AddToCartButton
label="In den Warenkorb"
numericId={product.numericId}
outOfStock={!product.active || product.outOfStock}
maxQty={product.maxQty}
variants={product.active ? product.variants : []}
/>
)}
</div>
</Reveal>
{product && (
<Reveal className="order-2 lg:order-none lg:col-span-7 flex lg:items-center" delay={0.15}>
<div className="relative w-full max-w-[28rem] mx-auto lg:mx-0 aspect-[4/3.1] rounded-md overflow-hidden border border-border bg-bg-base">
<Image src={product.image} alt={product.name} fill sizes="(min-width: 1024px) 45vw, 100vw" className="object-cover" />
</div>
</Reveal>
)}
</div>
</section>
);
}
@@ -0,0 +1,88 @@
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";
// Provisional scaffold — see Hero.tsx's own comment. No spec bullets yet;
// material/capacity claims beyond what's already in product.description
// aren't confirmed (the product photo is an AI-generated design mockup,
// not a photo of the manufactured object).
export async function Pricing() {
const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
getProductBySlug("tasse-die-pause"),
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 (
<section className="w-full bg-bg-base px-[var(--layout-padding-x)] py-8">
<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="relative w-full lg:w-[25.625rem] lg:shrink-0 aspect-[410/227] rounded-sm overflow-hidden">
<Image src={product.image} alt={product.name} fill sizes="(min-width: 1024px) 410px, 100vw" className="object-cover" />
{fullyOutOfStock ? (
<span className="absolute top-3 left-3 rounded-full bg-text-muted px-2.5 py-1 text-label font-bold text-bg-base">
Ausverkauft
</span>
) : (
discount !== null && (
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
-{discount}%
</span>
)
)}
</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)" }}
>
{product.name}
</p>
{product.description && <p className="text-body-sm text-text-primary">{product.description}</p>}
</div>
<div className="flex flex-col gap-3 items-start w-full lg:w-[18.75rem] lg:shrink-0">
<div className="flex flex-col gap-1 items-start">
<div className="flex gap-2 items-baseline">
{discount !== null && (
<p className="text-body text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</p>
)}
<p className="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
</div>
<p className="text-label text-text-muted">
{kleinunternehmer
? product.noShippingCost
? "Keine Versandkosten"
: "zzgl. Versand"
: `inkl. ${taxRate}% MwSt. ${product.noShippingCost ? " keine Versandkosten" : "zzgl. Versand"}`}
</p>
{!product.noShippingCost && (
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
)}
</div>
{anyLowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</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"
numericId={product.numericId}
outOfStock={!product.active || product.outOfStock}
maxQty={product.maxQty}
variants={product.active ? product.variants : []}
/>
</div>
</Reveal>
</section>
);
}
+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from "next";
import { Hero } from "./components/Hero";
import { Pricing } from "./components/Pricing";
import { Footer } from "../components/Footer";
import { getProductBySlug, getCompanySettings } from "../lib/payload";
import { buildProductSchema } from "../lib/structuredData";
// Provisional scaffold page — naming/design concept still undecided (parked
// 2026-08-24). Metadata reads from the live product instead of a hardcoded
// tagline, unlike every other bespoke page here — see Hero.tsx's comment.
export async function generateMetadata(): Promise<Metadata> {
const product = await getProductBySlug("tasse-die-pause");
const title = product ? `${product.name} einfach produktiv` : "Tasse einfach produktiv";
const description = product?.description ?? undefined;
return {
title,
description,
alternates: { canonical: "/tasse-die-pause" },
openGraph: { title, description, url: "/tasse-die-pause" },
twitter: { title, description },
};
}
export default async function TasseDiePausePage() {
const [product, seller] = await Promise.all([
getProductBySlug("tasse-die-pause"),
getCompanySettings(),
]);
const productSchema = product
? buildProductSchema(product, "https://einfach-produktiv.mk360.de/tasse-die-pause", seller)
: null;
return (
<>
{productSchema && (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productSchema) }} />
)}
<main className="flex flex-col flex-1">
<Hero />
<Pricing />
</main>
<Footer />
</>
);
}