diff --git a/app/api/products/route.ts b/app/api/products/route.ts
new file mode 100644
index 0000000..cbd4a14
--- /dev/null
+++ b/app/api/products/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+import { getProducts } from "../../lib/payload";
+
+// Same-origin proxy for client components (CartContent, RelatedProducts)
+// that need the full catalog reactively — Payload's own API is public-read
+// and CORS wouldn't be a blocker, but going through the app's own origin
+// avoids depending on that, reuses Next.js's fetch cache from getProducts()
+// (no extra round trip to Payload beyond the first request within the 60s
+// revalidate window), and keeps the Payload URL itself as a server-only
+// implementation detail the client never talks to directly.
+export async function GET() {
+ const products = await getProducts();
+ return NextResponse.json(products);
+}
diff --git a/app/cart/components/CartContent.tsx b/app/cart/components/CartContent.tsx
index 7507abb..4a93e69 100644
--- a/app/cart/components/CartContent.tsx
+++ b/app/cart/components/CartContent.tsx
@@ -4,7 +4,8 @@ import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useCart, removeFromCart, setQuantity } from "../../lib/cart";
-import { PRODUCTS, formatPrice } from "../../lib/products";
+import { useProducts } from "../../lib/products";
+import { formatPrice } from "../../lib/format";
import { SHIPPING_COST, FREE_SHIPPING_THRESHOLD } from "../../lib/shipping";
import { Reveal } from "../../components/Reveal";
import { VersandModal } from "../../components/VersandModal";
@@ -13,8 +14,16 @@ import { FreeShippingBanner } from "./FreeShippingBanner";
export function CartContent() {
const [versandOpen, setVersandOpen] = useState(false);
const cart = useCart();
+ const products = useProducts();
+ // While the /api/products fetch is still pending, treat a non-empty
+ // cart as "loading" rather than "empty" — the old hardcoded PRODUCTS
+ // lookup was synchronous, so this distinction didn't exist before;
+ // without it, a returning shopper with items already in their cart
+ // would briefly see the empty-cart message flash before their real
+ // cart content gets a chance to render.
+ const productsLoading = products.length === 0 && cart.length > 0;
const items = cart
- .map((entry) => ({ entry, product: PRODUCTS[entry.id] }))
+ .map((entry) => ({ entry, product: products.find((p) => p.id === entry.id) }))
.filter((row): row is { entry: typeof cart[number]; product: NonNullable<(typeof row)["product"]> } => Boolean(row.product));
const subtotal = items.reduce((sum, { entry, product }) => sum + entry.qty * product.price, 0);
@@ -37,11 +46,15 @@ export function CartContent() {
Warenkorb
- {items.length > 0 ? "Schön, dass du da bist." : "Dein Warenkorb ist noch leer."}
+ {productsLoading
+ ? "Einen Moment…"
+ : items.length > 0
+ ? "Schön, dass du da bist."
+ : "Dein Warenkorb ist noch leer."}
Schau dir unsere Produkte an und finde, was zu dir passt.
diff --git a/app/cart/components/FreeShippingBanner.tsx b/app/cart/components/FreeShippingBanner.tsx
index 5cbd8c0..2e7d8e3 100644
--- a/app/cart/components/FreeShippingBanner.tsx
+++ b/app/cart/components/FreeShippingBanner.tsx
@@ -1,8 +1,9 @@
"use client";
import { useEffect, useState } from "react";
+import { AnimatePresence, motion } from "motion/react";
import { FREE_SHIPPING_THRESHOLD } from "../../lib/shipping";
-import { formatPrice } from "../../lib/products";
+import { formatPrice } from "../../lib/format";
const SUCCESS_VISIBLE_MS = 2500;
@@ -49,36 +50,45 @@ export function FreeShippingBanner({ subtotal }: { subtotal: number }) {
return () => clearTimeout(t);
}, [phase]);
- if (phase === "hidden") return null;
-
const remaining = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal);
const progressPct = Math.min(100, (subtotal / FREE_SHIPPING_THRESHOLD) * 100);
return (
-
-
- {phase === "success"
- ? "Kostenloser Versand freigeschaltet ✓"
- : `Noch ${formatPrice(remaining)} bis zum kostenlosen Versand!`}
-
-
-
+ {phase !== "hidden" && (
+
-
-
+ >
+
+ {phase === "success"
+ ? "Kostenloser Versand freigeschaltet ✓"
+ : `Noch ${formatPrice(remaining)} bis zum kostenlosen Versand!`}
+
+
+
+
+
+ )}
+
);
}
diff --git a/app/cart/components/RelatedProducts.tsx b/app/cart/components/RelatedProducts.tsx
index 2c8f4c2..de1d1d1 100644
--- a/app/cart/components/RelatedProducts.tsx
+++ b/app/cart/components/RelatedProducts.tsx
@@ -1,54 +1,61 @@
"use client";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import Image from "next/image";
-import { PRODUCTS, RELATED_PRODUCT_IDS, formatPrice } from "../../lib/products";
+import { useProducts } from "../../lib/products";
+import { formatPrice } from "../../lib/format";
import { Reveal } from "../../components/Reveal";
import { AddToCartInlineButton, FEEDBACK_MS } from "../../components/AddToCartInlineButton";
import { useCart } from "../../lib/cart";
-const ALL_PRODUCT_IDS = Object.keys(PRODUCTS);
const DISPLAY_COUNT = 3;
-function pickRandom(excludeIds: string[], count: number): string[] {
- const pool = ALL_PRODUCT_IDS.filter((id) => !excludeIds.includes(id));
+function pickRandom(allIds: string[], excludeIds: string[], count: number): string[] {
+ const pool = allIds.filter((id) => !excludeIds.includes(id));
const shuffled = [...pool].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
}
export function RelatedProducts() {
const cart = useCart();
+ const products = useProducts();
const hasItems = cart.length > 0;
const cartKey = cart
.map((i) => i.id)
.sort()
.join(",");
+ // useMemo, not a plain .map() — .map() would return a new array
+ // reference on every render regardless of whether `products` itself
+ // changed, which would make the effect below re-run (and re-pick) every
+ // single render if `productIds` were listed as its dependency.
+ const productIds = useMemo(() => products.map((p) => p.id), [products]);
- // Math.random() can't run during the render that has to match the SSR
- // pass (that would be a hydration mismatch — server and client would
- // pick different products) — so the first paint always shows the fixed
- // RELATED_PRODUCT_IDS fallback, and a client-only effect swaps in the
- // real, cart-aware random pick right after mount.
- const [displayIds, setDisplayIds] = useState(RELATED_PRODUCT_IDS);
+ // Starts empty — the catalog itself is now fetched (useProducts()), so
+ // there's nothing to pick a random set from until that resolves. The
+ // effect below picks as soon as products arrive, same idea as
+ // CartContent's productsLoading guard.
+ const [displayIds, setDisplayIds] = useState([]);
- // First run (mount/hydration): pick the initial random set. After that,
- // don't reshuffle everything on every cart change — one of these cards'
- // own AddToCartInlineButton adds to the cart without navigating away,
- // and a full reshuffle would yank the other two cards out from under
- // the user mid-browse. Instead, only swap out whichever displayed card
- // just became "already in the cart" (no longer relevant to recommend)
- // and top that one slot back up — the rest stay exactly where they were.
+ // First run once products have loaded: pick the initial random set.
+ // After that, don't reshuffle everything on every cart change — one of
+ // these cards' own AddToCartInlineButton adds to the cart without
+ // navigating away, and a full reshuffle would yank the other two cards
+ // out from under the user mid-browse. Instead, only swap out whichever
+ // displayed card just became "already in the cart" (no longer relevant
+ // to recommend) and top that one slot back up — the rest stay exactly
+ // where they were.
const pickedRef = useRef(false);
const swapTimeoutRef = useRef | undefined>(undefined);
useEffect(() => () => clearTimeout(swapTimeoutRef.current), []);
useEffect(() => {
+ if (productIds.length === 0) return;
const cartIds = cartKey ? cartKey.split(",") : [];
if (!pickedRef.current) {
pickedRef.current = true;
- setDisplayIds(pickRandom(cartIds, DISPLAY_COUNT));
+ setDisplayIds(pickRandom(productIds, cartIds, DISPLAY_COUNT));
return;
}
@@ -63,26 +70,30 @@ export function RelatedProducts() {
const missing = DISPLAY_COUNT - stillRelevant.length;
if (missing <= 0) return stillRelevant;
- let replacements = pickRandom([...cartIds, ...stillRelevant], missing);
+ let replacements = pickRandom(productIds, [...cartIds, ...stillRelevant], missing);
- // The catalog only has 5 products — once the cart holds 3+
- // distinct ones, "3 recommendations that aren't already in the
- // cart" becomes mathematically impossible (5 - 3 in cart leaves
- // only 2 to show). Falling back to re-suggesting something
- // already in the cart (a normal "grab another one" pattern) beats
- // silently shrinking the grid below DISPLAY_COUNT.
+ // The catalog only has a handful of products — once the cart
+ // holds enough distinct ones, "N recommendations that aren't
+ // already in the cart" can become impossible. Falling back to
+ // re-suggesting something already in the cart (a normal "grab
+ // another one" pattern) beats silently shrinking the grid below
+ // DISPLAY_COUNT.
if (replacements.length < missing) {
const stillMissing = missing - replacements.length;
- const fallback = pickRandom([...stillRelevant, ...replacements], stillMissing);
+ const fallback = pickRandom(productIds, [...stillRelevant, ...replacements], stillMissing);
replacements = [...replacements, ...fallback];
}
return [...stillRelevant, ...replacements];
});
}, FEEDBACK_MS);
- }, [cartKey]);
+ }, [cartKey, productIds]);
- if (displayIds.length === 0) return null;
+ const displayProducts = displayIds
+ .map((id) => products.find((p) => p.id === id))
+ .filter((p): p is NonNullable => Boolean(p));
+
+ if (displayProducts.length === 0) return null;
return (
@@ -114,35 +125,32 @@ export function RelatedProducts() {
scroll-reveal nicety on a list that mutates; a static grid
renders correctly with no animation risk. */}
);
diff --git a/app/components/AddToCartButton.tsx b/app/components/AddToCartButton.tsx
index 8bcb1f9..6e986d6 100644
--- a/app/components/AddToCartButton.tsx
+++ b/app/components/AddToCartButton.tsx
@@ -54,7 +54,22 @@ export function AddToCartButton({
onClick={handleClick}
className={`${base} ${stateClasses}`}
>
- {added ? "Hinzugefügt ✓" : label}
+ {/* CSS-grid text-stack, not just swapping the button's text node
+ directly — this button is inline-flex/content-sized (no w-full),
+ so "Hinzugefügt ✓" being shorter than most labels made the whole
+ button visibly shrink while showing the success state. Stacking
+ both possible texts in the same grid cell (both invisible ones
+ still contribute to sizing) reserves width for whichever is
+ wider, so the button's box never changes size either way. */}
+
+
+ {label}
+
+
+ Hinzugefügt ✓
+
+ {added ? "Hinzugefügt ✓" : label}
+
);
}
diff --git a/app/components/AddToCartInlineButton.tsx b/app/components/AddToCartInlineButton.tsx
index 3ffa63d..d1850fb 100644
--- a/app/components/AddToCartInlineButton.tsx
+++ b/app/components/AddToCartInlineButton.tsx
@@ -48,7 +48,7 @@ export function AddToCartInlineButton({
// border-border/border-success both being present would silently race on
// CSS source order instead of one cleanly winning.
const stateClasses = added
- ? "border-success bg-success-subtle scale-[1.02]"
+ ? "border-success bg-success-subtle"
: "border-border hover:border-brand";
return (
diff --git a/app/components/ProductSpotlight.tsx b/app/components/ProductSpotlight.tsx
index 4a8bc8f..de48d31 100644
--- a/app/components/ProductSpotlight.tsx
+++ b/app/components/ProductSpotlight.tsx
@@ -2,6 +2,8 @@ import Image from "next/image";
import Link from "next/link";
import { AddToCartButton } from "./AddToCartButton";
import { Reveal } from "./Reveal";
+import { getProductBySlug } from "../lib/payload";
+import { formatPrice } from "../lib/format";
/**
* Product teaser for ToDo-Karten, placed after the Werkzeuge section (not
@@ -13,15 +15,22 @@ import { Reveal } from "./Reveal";
* than dropped at the very top before the page has earned any trust.
* Not derived from a Figma frame (page-home never had this section) —
* a deliberate, code-only addition, styled to match the /todo-cards
- * pricing panel it's a teaser for.
+ * pricing panel it's a teaser for. Price/photo come from Payload (same
+ * "todo-karten" product the shop/cart use) rather than being duplicated
+ * here as a hardcoded literal, so they can never silently drift apart —
+ * the marketing headline/copy below stays hand-written, since it's
+ * deliberately punchier than the plain catalog description.
*/
-export function ProductSpotlight() {
+export async function ProductSpotlight() {
+ const product = await getProductBySlug("todo-karten");
+ if (!product) return null;
+
return (
-
12,90 €
+
{formatPrice(product.price)}
inkl. MwSt. zzgl. Versand
diff --git a/app/components/TrustRow.tsx b/app/components/TrustRow.tsx
index f00a68a..40503ac 100644
--- a/app/components/TrustRow.tsx
+++ b/app/components/TrustRow.tsx
@@ -1,5 +1,5 @@
import { FREE_SHIPPING_THRESHOLD, TOTAL_DAYS_DE } from "../lib/shipping";
-import { formatPrice } from "../lib/products";
+import { formatPrice } from "../lib/format";
const items = [
{
diff --git a/app/lib/format.ts b/app/lib/format.ts
new file mode 100644
index 0000000..23a7928
--- /dev/null
+++ b/app/lib/format.ts
@@ -0,0 +1,11 @@
+// Plain utility, no client-only behavior — kept out of lib/products.ts
+// (which is "use client" for its useProducts() hook) specifically so
+// Server Components can still call it directly. Any export from a
+// "use client" module becomes a client-only reference as far as Next.js's
+// RSC boundary is concerned, even a pure function with zero hooks — a
+// Server Component importing formatPrice from products.ts fails at
+// runtime with "Attempted to call formatPrice() from the server but
+// formatPrice is on the client."
+export function formatPrice(value: number): string {
+ return `${value.toFixed(2).replace(".", ",")} €`;
+}
diff --git a/app/lib/payload.ts b/app/lib/payload.ts
index 9806120..4239e0c 100644
--- a/app/lib/payload.ts
+++ b/app/lib/payload.ts
@@ -55,3 +55,60 @@ export async function getBlogPosts(limit = 3): Promise {
: null,
}));
}
+
+// Frontend-facing shape — `id` is Payload's `slug` field, not its numeric
+// row id. Cart items are stored in localStorage keyed by this string (see
+// lib/cart.ts), so slugs were chosen in the Products collection to match
+// the ids the old hardcoded catalog used ("todo-karten" etc.) — switching
+// to numeric ids here would silently orphan every existing shopper's cart.
+export type Product = {
+ id: string;
+ name: string;
+ description: string;
+ price: number;
+ image: string;
+ href: string | null;
+};
+
+type PayloadProduct = {
+ id: number;
+ name: string;
+ slug: string;
+ description: string | null;
+ price: number;
+ image: { url: string } | number | null;
+ detailHref: string | null;
+};
+
+export async function getProducts(): Promise {
+ const params = new URLSearchParams({
+ "where[tenant.slug][equals]": TENANT_SLUG,
+ sort: "sortOrder",
+ depth: "2",
+ limit: "100",
+ });
+
+ const res = await fetch(`${PAYLOAD_URL}/api/products?${params}`, {
+ next: { revalidate: 60 },
+ });
+ if (!res.ok) {
+ console.error(`getProducts: Payload returned ${res.status} ${res.statusText}`);
+ return [];
+ }
+
+ const data: { docs?: PayloadProduct[] } = await res.json();
+ const docs = Array.isArray(data.docs) ? data.docs : [];
+ return docs.map((product) => ({
+ id: product.slug,
+ name: product.name,
+ description: product.description ?? "",
+ price: product.price,
+ image: typeof product.image === "object" && product.image ? product.image.url : "",
+ href: product.detailHref || null,
+ }));
+}
+
+export async function getProductBySlug(slug: string): Promise {
+ const products = await getProducts();
+ return products.find((p) => p.id === slug) ?? null;
+}
diff --git a/app/lib/products.ts b/app/lib/products.ts
index 0dabb4e..d79066e 100644
--- a/app/lib/products.ts
+++ b/app/lib/products.ts
@@ -1,65 +1,49 @@
-// Small in-code product catalog — this project has no commerce backend,
-// so cart items (stored in localStorage as {id, qty} pairs, see cart.ts)
-// need somewhere to look up name/price/photo/description by id. Only
-// "todo-karten" has a real detail page (/todo-cards) right now; the
-// other four exist because they're shown in Figma's page-cart "Passt
-// perfekt dazu" row and are addable to the cart from there, same as the
-// real product — they just don't have their own detail pages built yet
-// (matching the project's established pattern of cross-linking to
-// not-yet-built routes rather than leaving buttons disconnected).
-export type Product = {
- id: string;
- name: string;
- description: string;
- price: number;
- image: string;
- href?: string;
-};
+"use client";
-export const PRODUCTS: Record = {
- "todo-karten": {
- id: "todo-karten",
- name: "ToDo-Karten – Set",
- description: "50 ToDo-Karten für mehr Fokus und Klarheit im Alltag.",
- price: 12.9,
- image: "/product-todo-karten.png",
- href: "/todo-cards",
- },
- "notizbuch-klarheit": {
- id: "notizbuch-klarheit",
- name: "Notizbuch – Klarheit",
- description: "Dein Begleiter für Gedanken, Notizen und neue Perspektiven.",
- price: 9.9,
- image: "/product-notizbuch-klarheit.png",
- },
- wochenplaner: {
- id: "wochenplaner",
- name: "Wochenplaner – Überblick",
- description: "Behalte deine Woche im Blick und setze klare Prioritäten.",
- price: 14.9,
- image: "/product-wochenplaner.png",
- },
- "notizbuch-fokus": {
- id: "notizbuch-fokus",
- name: "Notizbuch – Fokus",
- description: "Für mehr Konzentration und einen klaren Kopf im Alltag.",
- price: 9.9,
- image: "/product-notizbuch-fokus.png",
- },
- zielkarten: {
- id: "zielkarten",
- name: "Zielkarten – Set",
- description: "Definiere deine Ziele und behalte sie fest im Blick.",
- price: 11.9,
- image: "/product-zielkarten.png",
- },
-};
+import { useEffect, useState } from "react";
+import type { Product } from "./payload";
-export const RELATED_PRODUCT_IDS = ["wochenplaner", "notizbuch-fokus", "zielkarten"];
+export type { Product };
-// Shop overview grid order — mirrors page-shop-overview in Figma.
-export const SHOP_PRODUCT_IDS = ["todo-karten", "wochenplaner", "notizbuch-fokus", "zielkarten"];
+// Products now live in Payload's "products" collection (tenant
+// einfach-produktiv), not a hardcoded catalog — see lib/payload.ts's
+// getProducts() for the server-side fetch. Client components (CartContent,
+// RelatedProducts) can't call that directly the way a Server Component
+// can, so this hook fetches the same-origin /api/products proxy instead,
+// with a tiny module-level cache so /cart's two consumers (CartContent +
+// RelatedProducts) share one request instead of firing it twice.
+let cache: Product[] | null = null;
+let inflight: Promise | null = null;
-export function formatPrice(value: number): string {
- return `${value.toFixed(2).replace(".", ",")} €`;
+async function fetchProducts(): Promise {
+ if (cache) return cache;
+ if (!inflight) {
+ inflight = fetch("/api/products")
+ .then((res) => (res.ok ? res.json() : []))
+ .then((data: Product[]) => {
+ cache = data;
+ return data;
+ })
+ .catch(() => []);
+ }
+ return inflight;
+}
+
+// Starts empty (SSR-safe — matches useCart()'s pattern of a safe default
+// that fills in after a client-only effect, see lib/cart.ts) and updates
+// once the fetch resolves.
+export function useProducts(): Product[] {
+ const [products, setProducts] = useState(cache ?? []);
+
+ useEffect(() => {
+ let cancelled = false;
+ fetchProducts().then((data) => {
+ if (!cancelled) setProducts(data);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ return products;
}
diff --git a/app/shop/components/ProductGrid.tsx b/app/shop/components/ProductGrid.tsx
index bbcce05..1749bf4 100644
--- a/app/shop/components/ProductGrid.tsx
+++ b/app/shop/components/ProductGrid.tsx
@@ -1,61 +1,73 @@
import Link from "next/link";
import Image from "next/image";
-import { PRODUCTS, SHOP_PRODUCT_IDS, formatPrice } from "../../lib/products";
+import { getProducts } from "../../lib/payload";
+import { formatPrice } from "../../lib/format";
import { RevealGroup, RevealItem } from "../../components/Reveal";
import { AddToCartInlineButton } from "../../components/AddToCartInlineButton";
-export function ProductGrid() {
+// Server Component — fetches straight from Payload (getProducts(), ISR
+// cached 60s) rather than going through the client-side useProducts()
+// hook /cart's components need; this grid doesn't react to cart state, so
+// there's no reason to pay for a client fetch when a server one already
+// gives faster first paint and no loading flash.
+// "notizbuch-klarheit" is deliberately excluded from the shop grid — it
+// has no card in Figma's page-shop-overview (only 4 products do), even
+// though it's a real product in Payload. Still shown elsewhere as a
+// cross-sell (RelatedProducts on /cart).
+const SHOP_GRID_EXCLUDE_IDS = ["notizbuch-klarheit"];
+
+export async function ProductGrid() {
+ const products = (await getProducts()).filter((p) => !SHOP_GRID_EXCLUDE_IDS.includes(p.id));
+
return (
- {SHOP_PRODUCT_IDS.map((id) => {
- const product = PRODUCTS[id];
- return (
-
-
-
-
-
-
(
+
+
+
+
+
+
+ {product.name}
+
+
+ {formatPrice(product.price)}
+ inkl. MwSt.
+
+ {product.href && (
+
- {product.name}
-
-
{formatPrice(product.price)}
- {product.href && (
-
- Mehr erfahren
- →
-
- )}
+ Mehr erfahren
+ →
+
+ )}
- {/* flex-1 spacer — pins every card's button to the same Y
- regardless of the "Mehr erfahren" line only the
- todo-karten card has (see figma-to-nextjs skill's
- Tools.tsx equal-height lesson). */}
-
+ {/* flex-1 spacer — pins every card's button to the same Y
+ regardless of the "Mehr erfahren" line only products with
+ a detail page have (see figma-to-nextjs skill's Tools.tsx
+ equal-height lesson). */}
+
-
-
-
- );
- })}
+
+
+
+ ))}
-
-
Alle Preise inkl. MwSt., zzgl. Versandkosten.
);
}
diff --git a/app/todo-cards/components/Pricing.tsx b/app/todo-cards/components/Pricing.tsx
index f9fe32b..e2d5d97 100644
--- a/app/todo-cards/components/Pricing.tsx
+++ b/app/todo-cards/components/Pricing.tsx
@@ -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 (
{/* Three flex children (photo, info, price/CTA) competing for space
@@ -20,7 +29,7 @@ export function Pricing() {
-
12,90 €
+
{formatPrice(product.price)}
inkl. MwSt. zzgl. Versand
diff --git a/app/versand/components/VersandSections.tsx b/app/versand/components/VersandSections.tsx
index 699e7fe..f54df24 100644
--- a/app/versand/components/VersandSections.tsx
+++ b/app/versand/components/VersandSections.tsx
@@ -6,7 +6,7 @@ import {
TRANSIT_DAYS_DE,
TOTAL_DAYS_DE,
} from "../../lib/shipping";
-import { formatPrice } from "../../lib/products";
+import { formatPrice } from "../../lib/format";
// Single source of truth for both the full /versand page and the cart's
// quick-reference VersandModal — same section ids/titles/copy either way,
diff --git a/public/product-notizbuch-fokus.png b/public/product-notizbuch-fokus.png
deleted file mode 100644
index d882ef1..0000000
Binary files a/public/product-notizbuch-fokus.png and /dev/null differ
diff --git a/public/product-notizbuch-klarheit.png b/public/product-notizbuch-klarheit.png
deleted file mode 100644
index aa79e53..0000000
Binary files a/public/product-notizbuch-klarheit.png and /dev/null differ
diff --git a/public/product-todo-karten.png b/public/product-todo-karten.png
deleted file mode 100644
index dd538bc..0000000
Binary files a/public/product-todo-karten.png and /dev/null differ
diff --git a/public/product-wochenplaner.png b/public/product-wochenplaner.png
deleted file mode 100644
index 200cec6..0000000
Binary files a/public/product-wochenplaner.png and /dev/null differ
diff --git a/public/product-zielkarten.png b/public/product-zielkarten.png
deleted file mode 100644
index 0bd4332..0000000
Binary files a/public/product-zielkarten.png and /dev/null differ