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:
@@ -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 (2–4 Werktage)", price: SHIPPING_COST },
|
||||
{ id: "express", label: "Expressversand (1–2 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>
|
||||
))}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user