Move product catalog to Payload CMS

Products now come from Payload's new "products" collection instead of a
hardcoded catalog, same pattern already used for blog posts:

- lib/payload.ts: getProducts()/getProductBySlug() (server-side fetch,
  60s ISR)
- New /api/products route so client components (CartContent,
  RelatedProducts) can reach the same data without a server-only import
- lib/products.ts: useProducts() hook replacing the old PRODUCTS record
- ProductGrid (/shop) fetches server-side directly; now shows all
  catalog products except notizbuch-klarheit (matches Figma's 4-card
  page-shop-overview — still cross-sold via RelatedProducts)
- ProductSpotlight and /todo-cards' Pricing now pull price/photo from
  the same CMS product instead of a separately hardcoded "12,90 €", so
  the two can't silently drift apart
- formatPrice moved to a new lib/format.ts (plain, no "use client") —
  Server Components can't call functions exported from a "use client"
  module directly, which lib/products.ts now is because of the hook

Also fixes two unrelated bugs surfaced along the way: the add-to-cart
button visibly resizing when its "Hinzugefügt ✓" success state showed
(fixed with a CSS-grid text stack sized to the wider of the two
strings), and removes the now-unused local product images from public/.
This commit is contained in:
Marco
2026-07-19 17:00:07 +00:00
parent c595e89305
commit 8c397c5dcb
19 changed files with 346 additions and 204 deletions
+12 -3
View File
@@ -2,6 +2,8 @@ import Image from "next/image";
import { AddToCartButton } from "../../components/AddToCartButton";
import { Reveal } from "../../components/Reveal";
import { TOTAL_DAYS_DE } from "../../lib/shipping";
import { getProductBySlug } from "../../lib/payload";
import { formatPrice } from "../../lib/format";
const bullets = [
"50 ToDo-Karten",
@@ -11,7 +13,14 @@ const bullets = [
"Nachhaltig produziert in Deutschland",
];
export function Pricing() {
// 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 = await getProductBySlug("todo-karten");
if (!product) return null;
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
@@ -20,7 +29,7 @@ export function Pricing() {
<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"
src={product.image}
alt="ToDo-Karten Set"
fill
sizes="(min-width: 1024px) 410px, 100vw"
@@ -47,7 +56,7 @@ export function Pricing() {
<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="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
<p className="text-label text-text-muted">inkl. MwSt. zzgl. Versand</p>
</div>
<p className="text-label text-text-muted">