Move trust badges, shipping/payment methods, Werkzeuge cards, product spotlight into CMS

New Payload collections, all editable without a code deploy:
- TrustBadges: the horizontal Schneller-Versand/Versandkostenfrei/Mit-
  Liebe-verpackt row (TrustRow.tsx, now an async server component).
- CartTrustBadges: the Sichere-Zahlung/14-Tage-Rückgaberecht/Nachhaltig-
  verpackt sidebar bullets — shared by /cart (title only) and /checkout
  (title + description), which previously had two different hardcoded
  bullet lists for what's conceptually the same content.
- ShippingMethods: /checkout's Versandart radios. Each method has its own
  optional freeShippingThreshold — omitted means "never free" (Express),
  not "always free". /cart's FreeShippingBanner now targets the lowest
  threshold among active methods instead of a single global constant, and
  hides entirely if no active method has one.
- PaymentMethods: /checkout's Zahlungsart radios, icons as an array
  (Kreditkarte shows 3 logos, PayPal/Überweisung show 1).
- Products: new spotlight/spotlightHeadline/spotlightText/spotlightImage/
  compareAtPrice fields. ProductSpotlight.tsx (homepage) now shows
  whichever product has `spotlight` checked instead of being hardcoded to
  ToDo-Karten, with its own marketing copy separate from the plain
  catalog name/description. AddToCartButton takes an explicit productId
  prop now instead of a hardcoded "todo-karten" constant.
- WerkzeugeCards: the homepage's "Meine Werkzeuge" 3-card grid (Tools.tsx,
  now async). Icons use a uniform box instead of the previous per-card
  hand-tuned width/height/rotation, which only worked for 3 known,
  upside-down-authored SVGs — those were re-exported as pre-flipped PNGs.

lib/payload.ts gained getTrustBadges/getCartTrustBadges/getShippingMethods/
getPaymentMethods/getWerkzeugeCards/getSpotlightProduct, all with the same
graceful-empty-array-on-fetch-failure pattern as the existing functions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-19 23:15:46 +00:00
parent 754966c8ba
commit d472eb546f
10 changed files with 419 additions and 186 deletions
+62 -82
View File
@@ -6,11 +6,9 @@ import Image from "next/image";
import { useCart } from "../../lib/cart";
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";
const EXPRESS_SHIPPING_COST = 4.9;
import type { ShippingMethod, PaymentMethod, TrustBadge } from "../../lib/payload";
const steps = [
{ label: "Warenkorb", state: "done" as const },
@@ -59,11 +57,19 @@ function FormField({
);
}
export function CheckoutContent() {
export function CheckoutContent({
shippingMethods,
paymentMethods,
trustBadges,
}: {
shippingMethods: ShippingMethod[];
paymentMethods: PaymentMethod[];
trustBadges: TrustBadge[];
}) {
const cart = useCart();
const products = useProducts();
const [shippingMethod, setShippingMethod] = useState<"standard" | "express">("standard");
const [paymentMethod, setPaymentMethod] = useState<"card" | "paypal" | "bank">("card");
const [shippingMethodId, setShippingMethodId] = useState<number | null>(shippingMethods[0]?.id ?? null);
const [paymentMethodId, setPaymentMethodId] = useState<number | null>(paymentMethods[0]?.id ?? null);
const [versandOpen, setVersandOpen] = useState(false);
const productsLoading = products.length === 0 && cart.length > 0;
@@ -72,12 +78,12 @@ export function CheckoutContent() {
.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);
const freeShipping = subtotal >= FREE_SHIPPING_THRESHOLD;
const shipping = items.length === 0 || freeShipping
? 0
: shippingMethod === "express"
? EXPRESS_SHIPPING_COST
: SHIPPING_COST;
const selectedShipping = shippingMethods.find((m) => m.id === shippingMethodId) ?? null;
const freeShipping =
selectedShipping?.freeShippingThreshold !== null &&
selectedShipping?.freeShippingThreshold !== undefined &&
subtotal >= selectedShipping.freeShippingThreshold;
const shipping = items.length === 0 || freeShipping ? 0 : selectedShipping?.price ?? 0;
const total = subtotal + shipping;
if (!productsLoading && items.length === 0) {
@@ -209,29 +215,27 @@ export function CheckoutContent() {
>
2. Versandart
</p>
{(
[
{ id: "standard", label: "Standardversand (24 Werktage)", price: SHIPPING_COST },
{ id: "express", label: "Expressversand (12 Werktage)", price: EXPRESS_SHIPPING_COST },
] as const
).map((option) => (
<label key={option.id} className="flex items-center gap-3 w-full cursor-pointer">
<input
type="radio"
name="shipping"
checked={shippingMethod === option.id}
onChange={() => setShippingMethod(option.id)}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 flex flex-col gap-0.5">
<span className="text-body-sm text-text-primary">{option.label}</span>
<span className="text-label text-text-muted">innerhalb Deutschlands</span>
</span>
<span className="text-body-sm text-text-primary whitespace-nowrap">
{freeShipping ? "Kostenlos" : formatPrice(option.price)}
</span>
</label>
))}
{shippingMethods.map((method) => {
const methodFree = method.freeShippingThreshold !== null && subtotal >= method.freeShippingThreshold;
return (
<label key={method.id} className="flex items-center gap-3 w-full cursor-pointer">
<input
type="radio"
name="shipping"
checked={shippingMethodId === method.id}
onChange={() => setShippingMethodId(method.id)}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 flex flex-col gap-0.5">
<span className="text-body-sm text-text-primary">{method.title}</span>
<span className="text-label text-text-muted">{method.description}</span>
</span>
<span className="text-body-sm text-text-primary whitespace-nowrap">
{methodFree ? "Kostenlos" : formatPrice(method.price)}
</span>
</label>
);
})}
</Reveal>
{/* 3. Zahlungsart */}
@@ -243,45 +247,23 @@ export function CheckoutContent() {
3. Zahlungsart
</p>
<label className="flex items-center gap-3 w-full cursor-pointer">
<input
type="radio"
name="payment"
checked={paymentMethod === "card"}
onChange={() => setPaymentMethod("card")}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 text-body-sm text-text-primary">Kreditkarte</span>
<span className="flex items-center gap-2 shrink-0">
<img alt="Visa" src="/icon-payment-visa.png" className="h-[1.1875rem] w-auto" />
<img alt="Mastercard" src="/icon-payment-mastercard.png" className="h-[1.1875rem] w-auto" />
<img alt="American Express" src="/icon-payment-amex.png" className="h-[1.1875rem] w-auto" />
</span>
</label>
<label className="flex items-center gap-3 w-full cursor-pointer">
<input
type="radio"
name="payment"
checked={paymentMethod === "paypal"}
onChange={() => setPaymentMethod("paypal")}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 text-body-sm text-text-primary">PayPal</span>
<img alt="PayPal" src="/icon-payment-paypal.png" className="h-5 w-auto shrink-0" />
</label>
<label className="flex items-center gap-3 w-full cursor-pointer">
<input
type="radio"
name="payment"
checked={paymentMethod === "bank"}
onChange={() => setPaymentMethod("bank")}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 text-body-sm text-text-primary">Überweisung</span>
<img alt="" src="/icon-payment-bank.png" className="h-[1.3125rem] w-auto shrink-0" />
</label>
{paymentMethods.map((method) => (
<label key={method.id} className="flex items-center gap-3 w-full cursor-pointer">
<input
type="radio"
name="payment"
checked={paymentMethodId === method.id}
onChange={() => setPaymentMethodId(method.id)}
className="size-5 shrink-0 accent-brand"
/>
<span className="flex-1 text-body-sm text-text-primary">{method.title}</span>
<span className="flex items-center gap-2 shrink-0">
{method.icons.map((icon, i) => (
<img key={i} alt="" src={icon} className="h-5 w-auto" />
))}
</span>
</label>
))}
<Link
href="/bestellbestaetigung"
@@ -348,7 +330,7 @@ export function CheckoutContent() {
{shipping === 0 ? "Kostenlos" : formatPrice(shipping)}
</span>
</div>
<p className="text-label text-text-muted">innerhalb Deutschlands</p>
<p className="text-label text-text-muted">{selectedShipping?.description ?? "innerhalb Deutschlands"}</p>
</div>
<div className="h-px bg-border w-full" />
@@ -368,17 +350,15 @@ export function CheckoutContent() {
</div>
</div>
{/* title + description here — /cart's sidebar renders the same
CartTrustBadges docs with title only, see CartContent.tsx. */}
<div className="flex flex-col gap-4 items-start w-full">
{[
{ icon: "/icon-lock.svg", title: "Sichere Zahlung", desc: "Deine Daten sind bei uns sicher und geschützt." },
{ icon: "/icon-trust-return.png", title: "14 Tage Rückgaberecht", desc: "Nicht zufrieden? Sende deine Bestellung innerhalb von 14 Tagen zurück." },
{ icon: "/icon-trust-leaf.png", title: "Nachhaltig verpackt", desc: "Wir achten auf umweltfreundliche Materialien und plastikfreien Versand." },
].map((b) => (
<div key={b.title} className="flex gap-3 items-start w-full">
{trustBadges.map((b) => (
<div key={b.id} className="flex gap-3 items-start w-full">
<img alt="" src={b.icon} className="size-5 shrink-0 object-contain mt-0.5" />
<div className="flex-1 flex flex-col gap-0.5">
<p className="text-body-sm font-semibold text-text-primary">{b.title}</p>
<p className="text-label text-text-muted">{b.desc}</p>
<p className="text-label text-text-muted">{b.description}</p>
</div>
</div>
))}
+9 -2
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { CheckoutContent } from "./components/CheckoutContent";
import { TrustRow } from "../components/TrustRow";
import { Footer } from "../components/Footer";
import { getShippingMethods, getPaymentMethods, getCartTrustBadges } from "../lib/payload";
// robots: noindex — transactional page, same reasoning as /cart.
export const metadata: Metadata = {
@@ -13,11 +14,17 @@ export const metadata: Metadata = {
},
};
export default function CheckoutPage() {
export default async function CheckoutPage() {
const [shippingMethods, paymentMethods, trustBadges] = await Promise.all([
getShippingMethods(),
getPaymentMethods(),
getCartTrustBadges(),
]);
return (
<>
<main className="flex flex-col flex-1 bg-bg-base">
<CheckoutContent />
<CheckoutContent shippingMethods={shippingMethods} paymentMethods={paymentMethods} trustBadges={trustBadges} />
<TrustRow />
</main>
<Footer />