Compare commits

...

3 Commits

Author SHA1 Message Date
Marco 225a8567a9 feat(shipping): move delivery-time settings to Payload, polish product/cart CTAs
The delivery-time range (handling + transit days) was a hardcoded
HANDLING_DAYS/TRANSIT_DAYS_DE pair in lib/shipping.ts — changing it
needed a code deploy. Now sourced from Payload's new Shipping Settings
collection via getShippingSettings(), threaded down as a prop to the
few Client Components (Cart/Checkout/VersandModal) that can't fetch it
themselves, with the old code constants removed.

Also: the delivery-time note is now shown on every purchase CTA (shop
grid, home spotlight, ToDo-Karten hero + pricing panel), not just one
of them — required next to each buy button per Art. 246a §1 Abs.1
Nr.8 EGBGB, not just somewhere reachable via a link. Checkout's
sidebar was missing the "ab 39€ kostenlos" note Cart already had;
that's fixed too, and both now show the delivery-time range on its own
line instead of crammed onto the shipping-cost line.

Related smaller fixes bundled in since they touch the same files:
price/delivery-time spacing tightened into its own group, the
redundant "Sichere Zahlung" note under Cart's checkout button (already
shown via the trustBadges list right below) replaced with "Sichere
SSL-Verschlüsselung" to match Checkout, and ToDo-Karten's pricing panel
no longer shows a premature payment-security note at the add-to-cart
step.
2026-07-21 12:44:35 +00:00
Marco a6edd7ff61 fix(cart): make the "Hinzugefügt" success state subtler
Was a solid bright-green fill with white text; now the same pale
success-subtle fill + success-colored text/border AddToCartInlineButton
already uses elsewhere (RelatedProducts, shop cards) — consistent, less
loud.
2026-07-21 12:44:16 +00:00
Marco 9c9f0b02c0 perf(images): convert remaining <img> tags to next/image project-wide
Clears every remaining @next/next/no-img-element warning — automatic
responsive srcset, lazy-loading, and format optimization instead of
always loading the original file at full size. Fixed-size icons got
explicit width/height; dynamic-aspect photos got fill inside a
relative wrapper.
2026-07-21 12:44:03 +00:00
32 changed files with 355 additions and 140 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { TrustRow } from "../components/TrustRow";
@@ -42,7 +43,7 @@ export default async function AgbPage() {
{/* Static, not part of the CMS content — same reasoning as
the Impressum/Datenschutz pages' own callout cards. */}
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
<p
className="font-semibold text-body text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
@@ -270,9 +270,12 @@ export function BestellbestaetigungContent() {
gap shows the image itself, not whatever's behind it — the
parent's overflow-hidden clips the small overflow back
down to a clean box either way. */}
<img
<Image
alt=""
src="/bestellbestaetigung-testimonial-photo.jpg"
width={366}
height={126}
sizes="(min-width: 768px) 45vw, 100vw"
className="absolute -inset-1 w-[calc(100%+0.5rem)] h-[calc(100%+0.5rem)] object-cover"
/>
<div className="hidden md:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
+1 -1
View File
@@ -110,7 +110,7 @@ export default async function BlogDetailPage({
href="/todo-cards"
className="group flex items-center gap-6 border border-border rounded-md px-9 py-7 hover:border-brand transition-colors"
>
<img alt="" src="/icon-todo-passend-dazu.png" className="w-16 h-[4.6875rem] shrink-0 object-contain" />
<Image alt="" src="/icon-todo-passend-dazu.png" width={64} height={75} className="w-16 h-[4.6875rem] shrink-0 object-contain" />
<div className="flex-1 min-w-0 flex flex-col gap-2.5">
<p className="font-bold text-[0.8125rem] text-brand">Passend dazu:</p>
<div className="flex items-end justify-between gap-4 w-full">
+21 -7
View File
@@ -9,12 +9,13 @@ import { formatPrice, discountPercent } from "../../lib/format";
import { Reveal } from "../../components/Reveal";
import { VersandModal } from "../../components/VersandModal";
import { FreeShippingBanner } from "./FreeShippingBanner";
import type { TrustBadge } from "../../lib/payload";
import type { TrustBadge, ShippingSettings } from "../../lib/payload";
export function CartContent({
trustBadges,
shippingCost,
freeShippingThreshold,
shippingSettings,
}: {
trustBadges: TrustBadge[];
/** Price of the default (first active, i.e. Standard) ShippingMethod — an
@@ -24,6 +25,12 @@ export function CartContent({
/** Lowest freeShippingThreshold among active ShippingMethods, or null if
* none has one (in which case FreeShippingBanner just doesn't render). */
freeShippingThreshold: number | null;
/** Delivery-time disclosure (Payload's Shipping Settings), fetched by the
* page and threaded down here — this is a Client Component, so it can't
* fetch it itself. Also passed straight through to VersandModal. Named
* "shippingSettings", not "shipping" — that name is already the local
* computed shipping-cost value below. */
shippingSettings: ShippingSettings;
}) {
const [versandOpen, setVersandOpen] = useState(false);
const cart = useCart();
@@ -222,6 +229,9 @@ export function CartContent({
? `ab ${formatPrice(freeShippingThreshold)} innerhalb Deutschlands`
: "innerhalb Deutschlands"}
</p>
<p className="text-label text-text-muted">
Lieferzeit {shippingSettings.totalDays.min}{shippingSettings.totalDays.max} Werktage
</p>
</div>
<div className="h-px bg-border w-full" />
@@ -247,9 +257,13 @@ export function CartContent({
Zur Kasse gehen
</Link>
<div className="flex gap-[0.625rem] items-center justify-center w-full">
<img alt="" src="/icon-lock.svg" className="w-4 h-[1.125rem]" />
<span className="text-body-sm text-text-muted">Sichere Zahlung</span>
{/* "Sichere SSL-Verschlüsselung", not "Sichere Zahlung" (what
used to be here) — matches /checkout's identical note
under its own buy button; the old wording duplicated the
"Sichere Zahlung" trustBadges entry right below. */}
<div className="flex gap-2 items-center justify-center w-full">
<Image alt="" src="/icon-lock.svg" width={14} height={16} className="w-3.5 h-4" />
<span className="text-body-sm text-text-muted">Sichere SSL-Verschlüsselung</span>
</div>
</div>
@@ -258,7 +272,7 @@ export function CartContent({
<div className="flex flex-col gap-4 items-start w-full">
{trustBadges.map((b) => (
<div key={b.id} className="flex gap-3 items-center w-full">
<img alt="" src={b.icon} className="size-[1.375rem] shrink-0 object-contain" />
<Image alt="" src={b.icon} width={22} height={22} className="size-[1.375rem] shrink-0 object-contain" />
<span className="flex-1 text-body-sm text-text-primary">{b.title}</span>
</div>
))}
@@ -272,7 +286,7 @@ export function CartContent({
<Reveal className="flex flex-col items-start pb-10 px-[var(--layout-padding-x)] w-full">
<div className="bg-bg-muted flex gap-5 items-start p-6 rounded-md w-full lg:max-w-[51.875rem]">
<div className="flex flex-col gap-3 items-center justify-center shrink-0">
<img alt="" src="/icon-envelope-hint.png" className="h-[2.8125rem] w-16 object-contain" />
<Image alt="" src="/icon-envelope-hint.png" width={64} height={45} className="h-[2.8125rem] w-16 object-contain" />
<div className="h-[0.1875rem] w-6 bg-brand" />
</div>
<div className="flex flex-col gap-2 items-start flex-1 min-w-0 text-text-primary">
@@ -294,7 +308,7 @@ export function CartContent({
</Reveal>
)}
<VersandModal open={versandOpen} onClose={() => setVersandOpen(false)} />
<VersandModal open={versandOpen} onClose={() => setVersandOpen(false)} shipping={shippingSettings} />
</>
);
}
+7 -2
View File
@@ -3,7 +3,7 @@ import { CartContent } from "./components/CartContent";
import { RelatedProducts } from "./components/RelatedProducts";
import { TrustRow } from "../components/TrustRow";
import { Footer } from "../components/Footer";
import { getCartTrustBadges, getShippingMethods } from "../lib/payload";
import { getCartTrustBadges, getShippingMethods, getShippingSettings } from "../lib/payload";
// robots: noindex — transactional page (mirrors a specific shopper's cart
// contents), per the figma-to-nextjs skill's Step 5 guidance: indexing
@@ -18,7 +18,11 @@ export const metadata: Metadata = {
};
export default async function CartPage() {
const [trustBadges, shippingMethods] = await Promise.all([getCartTrustBadges(), getShippingMethods()]);
const [trustBadges, shippingMethods, shipping] = await Promise.all([
getCartTrustBadges(),
getShippingMethods(),
getShippingSettings(),
]);
// The cart doesn't ask which shipping method the shopper wants yet
// (that's /checkout) — it just estimates using the first active method
@@ -38,6 +42,7 @@ export default async function CartPage() {
trustBadges={trustBadges}
shippingCost={defaultShipping?.price ?? 0}
freeShippingThreshold={freeShippingThreshold}
shippingSettings={shipping}
/>
<RelatedProducts />
<TrustRow />
+15 -3
View File
@@ -10,7 +10,7 @@ import { Reveal } from "../../components/Reveal";
import { VersandModal } from "../../components/VersandModal";
import { CheckoutSteps } from "../../components/CheckoutSteps";
import { ORDER_KEY, generateOrderNumber, type OrderSnapshot } from "../../lib/order";
import type { ShippingMethod, PaymentMethod, TrustBadge } from "../../lib/payload";
import type { ShippingMethod, PaymentMethod, TrustBadge, ShippingSettings } from "../../lib/payload";
function FormField({
label,
@@ -32,10 +32,15 @@ export function CheckoutContent({
shippingMethods,
paymentMethods,
trustBadges,
shippingSettings,
}: {
shippingMethods: ShippingMethod[];
paymentMethods: PaymentMethod[];
trustBadges: TrustBadge[];
/** Delivery-time disclosure (Payload's Shipping Settings) — named
* "shippingSettings", not "shipping", since that name is already the
* local computed shipping-cost value below. */
shippingSettings: ShippingSettings;
}) {
const cart = useCart();
const products = useProducts();
@@ -386,7 +391,14 @@ export function CheckoutContent({
{shipping === 0 ? "Kostenlos" : formatPrice(shipping)}
</span>
</div>
<p className="text-label text-text-muted">{selectedShipping?.description ?? "innerhalb Deutschlands"}</p>
<p className="text-label text-text-muted">
{shipping === 0 && selectedShipping?.freeShippingThreshold != null
? `ab ${formatPrice(selectedShipping.freeShippingThreshold)} innerhalb Deutschlands`
: (selectedShipping?.description ?? "innerhalb Deutschlands")}
</p>
<p className="text-label text-text-muted">
Lieferzeit {shippingSettings.totalDays.min}{shippingSettings.totalDays.max} Werktage
</p>
</div>
<div className="h-px bg-border w-full" />
@@ -448,7 +460,7 @@ export function CheckoutContent({
</Reveal>
</div>
<VersandModal open={versandOpen} onClose={() => setVersandOpen(false)} />
<VersandModal open={versandOpen} onClose={() => setVersandOpen(false)} shipping={shippingSettings} />
</>
);
}
+9 -3
View File
@@ -2,7 +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";
import { getShippingMethods, getPaymentMethods, getCartTrustBadges, getShippingSettings } from "../lib/payload";
// robots: noindex — transactional page, same reasoning as /cart.
export const metadata: Metadata = {
@@ -15,16 +15,22 @@ export const metadata: Metadata = {
};
export default async function CheckoutPage() {
const [shippingMethods, paymentMethods, trustBadges] = await Promise.all([
const [shippingMethods, paymentMethods, trustBadges, shippingSettings] = await Promise.all([
getShippingMethods(),
getPaymentMethods(),
getCartTrustBadges(),
getShippingSettings(),
]);
return (
<>
<main className="flex flex-col flex-1 bg-bg-base">
<CheckoutContent shippingMethods={shippingMethods} paymentMethods={paymentMethods} trustBadges={trustBadges} />
<CheckoutContent
shippingMethods={shippingMethods}
paymentMethods={paymentMethods}
trustBadges={trustBadges}
shippingSettings={shippingSettings}
/>
<TrustRow />
</main>
<Footer />
+5 -2
View File
@@ -1,3 +1,4 @@
import Image from "next/image";
import { Reveal } from "./Reveal";
export function About() {
@@ -75,10 +76,12 @@ export function About() {
style={{ minHeight: "14rem" }}
delay={0.15}
>
<img
<Image
alt="Björn"
src="/about-author.jpg"
className="absolute inset-0 w-full h-full object-cover object-center pointer-events-none"
fill
sizes="(min-width: 768px) 58vw, 100vw"
className="object-cover object-center pointer-events-none"
/>
{/* Left gradient: wide enough to cover the text-column overlap — md+ only */}
<div className="hidden md:block absolute inset-y-0 left-0 w-72 bg-gradient-to-r from-bg-dark to-transparent pointer-events-none" />
+9 -1
View File
@@ -49,7 +49,15 @@ export function AddToCartButton({
// into `base`, since appending on top can't rely on CSS source order
// the way branching a whole className (AddToCartInlineButton's
// approach) can when the base itself varies per caller.
const stateClasses = added ? "bg-success! hover:bg-success! text-white!" : "";
// Pale success-subtle fill + success text + a success-colored border, not
// a solid success-green fill with white text — same restrained pairing
// AddToCartInlineButton already uses (border-success + bg-success-subtle),
// a solid bright-green button read as too loud here. `border` (width) is
// added here too since `base` has none by default, unlike
// AddToCartInlineButton's own base which already carries a plain border.
const stateClasses = added
? "border border-success! bg-success-subtle! hover:bg-success-subtle! text-success!"
: "";
return (
<button
+2 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { addToCart } from "../lib/cart";
import { useCartFly } from "./CartFly";
@@ -61,7 +62,7 @@ export function AddToCartInlineButton({
>
{added ? "Hinzugefügt ✓" : label}
</span>
<img alt="" src="/icon-cart-outline.png" className="h-[1.875rem] w-8 object-contain" />
<Image alt="" src="/icon-cart-outline.png" width={32} height={30} className="h-[1.875rem] w-8 object-contain" />
</button>
);
}
+3 -2
View File
@@ -1,3 +1,4 @@
import Image from "next/image";
import { Reveal } from "./Reveal";
function Word({ children }: { children: string }) {
@@ -16,7 +17,7 @@ function Arrow() {
<div className="flex items-center justify-center shrink-0">
<div className="-scale-y-100 rotate-180">
<div className="relative" style={{ height: "var(--divider-arrow-h)", width: "var(--divider-arrow-w)" }}>
<img alt="" src="/icon-separator.svg" className="absolute inset-0 w-full h-full" />
<Image alt="" src="/icon-separator.svg" fill sizes="48px" />
</div>
</div>
</div>
@@ -60,7 +61,7 @@ export function Divider() {
className="relative"
style={{ height: "var(--divider-sparkle-inner-h)", width: "var(--divider-sparkle-inner-w)" }}
>
<img alt="" src="/icon-separator-right.svg" className="absolute inset-0 w-full h-full" />
<Image alt="" src="/icon-separator-right.svg" fill sizes="48px" />
</div>
</div>
</div>
+1 -5
View File
@@ -61,11 +61,7 @@ export function Hero() {
Starte mit der 7-Tage-Challenge
</span>
<div className="relative h-[1.1875rem] w-[1.5625rem] shrink-0">
<img
alt=""
src="/icon-check.svg"
className="absolute inset-0 w-full h-full"
/>
<Image alt="" src="/icon-check.svg" fill sizes="26px" />
</div>
</Link>
+13 -8
View File
@@ -2,7 +2,7 @@ import Image from "next/image";
import Link from "next/link";
import { AddToCartButton } from "./AddToCartButton";
import { Reveal } from "./Reveal";
import { getSpotlightProduct } from "../lib/payload";
import { getSpotlightProduct, getShippingSettings } from "../lib/payload";
import { formatPrice, discountPercent } from "../lib/format";
/**
@@ -22,7 +22,7 @@ import { formatPrice, discountPercent } from "../lib/format";
* see Products.ts), not duplicated here as hardcoded literals.
*/
export async function ProductSpotlight() {
const product = await getSpotlightProduct();
const [product, shipping] = await Promise.all([getSpotlightProduct(), getShippingSettings()]);
if (!product) return null;
const image = product.spotlightImage || product.image;
@@ -57,12 +57,17 @@ export async function ProductSpotlight() {
<p className="text-body text-text-body">
{product.spotlightText || product.description}
</p>
<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">inkl. MwSt. zzgl. Versand</p>
<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>
<p className="text-label text-text-muted">inkl. MwSt. zzgl. Versand</p>
</div>
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
{/* No className override — the section's bg is bg-bg-base now
+6 -3
View File
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import Image from "next/image";
import type { TOCSection } from "./SectionTOC";
// Minimal Lexical JSON → JSX renderer for Payload's richText fields.
@@ -146,8 +147,8 @@ function renderNode(node: LexicalNode, key: string): ReactNode {
return (
<div key={key} className="relative flex items-start gap-6 w-full my-6">
<div className="flex items-center gap-2 shrink-0">
<span className="flex h-7 w-6 items-center justify-center shrink-0">
<img alt="" src="/icon-sparkle-merke-dir.png" className="w-full h-full object-contain" />
<span className="relative flex h-7 w-6 items-center justify-center shrink-0">
<Image alt="" src="/icon-sparkle-merke-dir.png" fill sizes="24px" className="object-contain" />
</span>
<span
className="font-bold text-text-primary text-[1.625rem] whitespace-nowrap"
@@ -158,9 +159,11 @@ function renderNode(node: LexicalNode, key: string): ReactNode {
</div>
{/* Hand-drawn underline image, not a plain bar — exported
straight from the Figma node (label-underline). */}
<img
<Image
alt=""
src="/icon-merke-dir-underline.png"
width={136}
height={23}
className="absolute left-8 top-[2.1875rem] w-[8.5rem] h-[1.4375rem] object-cover pointer-events-none"
/>
<div className="w-px self-stretch bg-brand shrink-0" />
+3 -2
View File
@@ -1,4 +1,5 @@
import Link from "next/link";
import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "./Reveal";
import { getWerkzeugeCards } from "../lib/payload";
@@ -38,8 +39,8 @@ export async function Tools() {
className="md:col-span-4 flex gap-8 items-start rounded-md transition-transform duration-300 hover:-translate-y-1"
>
{/* Icon — uniform box, pre-flipped/rotated source asset */}
<div className="flex items-center justify-center shrink-0 size-14">
<img alt="" src={tool.icon} className="max-w-full max-h-full object-contain" />
<div className="relative flex items-center justify-center shrink-0 size-14">
<Image alt="" src={tool.icon} fill sizes="56px" className="object-contain" />
</div>
{/* Card content — self-stretch + h-full + justify-between so
+4 -1
View File
@@ -1,3 +1,4 @@
import Image from "next/image";
import { getTrustBadges } from "../lib/payload";
// Content now lives in Payload (TrustBadges collection) instead of being
@@ -15,7 +16,9 @@ export async function TrustRow() {
<div key={item.id} className="flex items-center gap-6 md:gap-12">
{i > 0 && <div className="hidden md:block h-10 w-px bg-border" />}
<div className="flex gap-4 items-center">
<img alt="" src={item.icon} className="size-8 shrink-0 object-contain" />
<div className="relative size-8 shrink-0">
<Image alt="" src={item.icon} fill sizes="32px" className="object-contain" />
</div>
<div className="flex flex-col gap-0.5 items-start">
<p className="font-semibold text-body text-text-primary whitespace-nowrap">{item.title}</p>
<p className="text-body-sm text-text-muted whitespace-nowrap">{item.description}</p>
+13 -2
View File
@@ -3,14 +3,25 @@
import { useEffect, useRef } from "react";
import { AnimatePresence, motion } from "motion/react";
import { VersandSections } from "../versand/components/VersandSections";
import type { ShippingSettings } from "../lib/payload";
/**
* Quick-reference version of /versand, opened from the cart's order
* summary "Versand" info link — a full page navigation would pull you out
* of checkout, which is exactly what the link is there to avoid. Reuses
* VersandSections so the two never carry different numbers/copy.
* `shipping` is threaded down from CartContent/CheckoutContent's own page
* (a Server Component), not fetched here — this is a Client Component.
*/
export function VersandModal({ open, onClose }: { open: boolean; onClose: () => void }) {
export function VersandModal({
open,
onClose,
shipping,
}: {
open: boolean;
onClose: () => void;
shipping: ShippingSettings;
}) {
const dialogRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
@@ -98,7 +109,7 @@ export function VersandModal({ open, onClose }: { open: boolean; onClose: () =>
</div>
<div className="px-8 py-6 pb-8">
<VersandSections />
<VersandSections shipping={shipping} />
</div>
</motion.div>
</motion.div>
+2 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { RichText, extractHeadings } from "../components/RichText";
@@ -43,7 +44,7 @@ export default async function DatenschutzPage() {
callout doesn't belong in the generic LegalPages richText
field shared across all 4 legal page types. */}
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
<p
className="font-semibold text-body text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
+2 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { RichText, extractHeadings } from "../components/RichText";
@@ -47,7 +48,7 @@ export default async function ImpressumPage() {
the actual numbers/copy that need single-sourcing from
data. */}
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
<p
className="font-semibold text-body text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
+89 -1
View File
@@ -1,3 +1,5 @@
import { formatPrice } from "./format";
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de";
const TENANT_SLUG = "einfach-produktiv";
@@ -251,10 +253,44 @@ async function fetchTrustBadgeList(collection: "trust-badges" | "cart-trust-badg
}));
}
// Substitutes literal "{{lieferzeit}}" / "{{kostenfreiab}}" tokens (e.g. in
// a badge's "In {{lieferzeit}} bei dir." or "Ab {{kostenfreiab}}
// Bestellwert innerhalb DE." copy) with the live delivery-time range /
// free-shipping threshold — lets an editor reference these numbers from
// free text without duplicating and hand-maintaining them, which is
// exactly how the "Schneller Versand" and "Versandkostenfrei" badges used
// to drift from the real numbers whenever those changed elsewhere but not
// here too. The threshold comes from the lowest freeShippingThreshold
// among active ShippingMethods (same rule /cart's own banner uses), not
// lib/shipping.ts's separate FREE_SHIPPING_THRESHOLD constant — that
// constant is a third, independent copy of the same fact and not
// necessarily what actually governs checkout.
function resolveShippingTokens(text: string, shipping: ShippingSettings, freeShippingThreshold: number | null): string {
let result = text.replaceAll("{{lieferzeit}}", `${shipping.totalDays.min}${shipping.totalDays.max} Werktagen`);
if (freeShippingThreshold !== null) {
result = result.replaceAll("{{kostenfreiab}}", formatPrice(freeShippingThreshold));
}
return result;
}
// Powers TrustRow.tsx — the horizontal "Schneller Versand /
// Versandkostenfrei / Mit Liebe verpackt" row.
export async function getTrustBadges(): Promise<TrustBadge[]> {
return fetchTrustBadgeList("trust-badges");
const [badges, shipping, shippingMethods] = await Promise.all([
fetchTrustBadgeList("trust-badges"),
getShippingSettings(),
getShippingMethods(),
]);
const thresholds = shippingMethods
.map((m) => m.freeShippingThreshold)
.filter((t): t is number => t !== null);
const freeShippingThreshold = thresholds.length > 0 ? Math.min(...thresholds) : null;
return badges.map((b) => ({
...b,
title: resolveShippingTokens(b.title, shipping, freeShippingThreshold),
description: resolveShippingTokens(b.description, shipping, freeShippingThreshold),
}));
}
// Powers the "Sichere Zahlung / 14 Tage Rückgaberecht / Nachhaltig
@@ -301,6 +337,58 @@ export async function getShippingMethods(): Promise<ShippingMethod[]> {
}));
}
export type ShippingSettings = {
handlingDays: { min: number; max: number };
transitDays: { min: number; max: number };
/** Derived here, not stored in Payload — a third independently-editable
* copy of the same fact is exactly the drift this collection replaces. */
totalDays: { min: number; max: number };
};
type PayloadShippingSettings = {
handlingDaysMin: number;
handlingDaysMax: number;
transitDaysMin: number;
transitDaysMax: number;
};
// Falls back to the site's long-standing real-world numbers (12 handling,
// 24 transit) if Payload has no row yet or the fetch fails — same values
// the old hardcoded HANDLING_DAYS/TRANSIT_DAYS_DE constants used, so
// nothing regresses before this collection gets seeded/edited.
const SHIPPING_SETTINGS_FALLBACK: ShippingSettings = {
handlingDays: { min: 1, max: 2 },
transitDays: { min: 2, max: 4 },
totalDays: { min: 3, max: 6 },
};
export async function getShippingSettings(): Promise<ShippingSettings> {
const params = new URLSearchParams({
"where[tenant.slug][equals]": TENANT_SLUG,
limit: "1",
});
const res = await fetch(`${PAYLOAD_URL}/api/shipping-settings?${params}`, {
next: { revalidate: 60 },
});
if (!res.ok) {
console.error(`getShippingSettings: Payload returned ${res.status} ${res.statusText}`);
return SHIPPING_SETTINGS_FALLBACK;
}
const data: { docs?: PayloadShippingSettings[] } = await res.json();
const doc = Array.isArray(data.docs) ? data.docs[0] : undefined;
if (!doc) return SHIPPING_SETTINGS_FALLBACK;
const handlingDays = { min: doc.handlingDaysMin, max: doc.handlingDaysMax };
const transitDays = { min: doc.transitDaysMin, max: doc.transitDaysMax };
return {
handlingDays,
transitDays,
totalDays: { min: handlingDays.min + transitDays.min, max: handlingDays.max + transitDays.max },
};
}
export type PaymentMethod = { id: number; title: string; icons: string[] };
type PayloadPaymentMethod = {
+12 -15
View File
@@ -1,17 +1,14 @@
// Single source of truth for shipping numbers/timeframes — consumed by
// both the cart's order summary (CartContent.tsx) and the /versand policy
// page, so the two can never drift apart.
// Single source of truth for shipping numbers — consumed by both the
// cart's order summary (CartContent.tsx) and the /versand policy page, so
// the two can never drift apart.
//
// The delivery-timeframe numbers (handling/transit days, Art. 246a § 1
// Abs. 1 Nr. 8 EGBGB) used to live here too, but now come from Payload's
// Shipping Settings collection instead (see lib/payload.ts's
// getShippingSettings()) — that duplication is exactly why the
// ShippingMethods CMS title once carried a different, drifted day-range
// than this file did. Cost/threshold below have the same drift risk
// against Payload's ShippingMethods price/freeShippingThreshold fields,
// not yet addressed here.
export const SHIPPING_COST = 2.9;
export const FREE_SHIPPING_THRESHOLD = 39;
// Kept as an explicit range (not "so schnell wie möglich") per Art. 246a
// § 1 Abs. 1 Nr. 8 EGBGB — German law requires disclosing a concrete
// delivery timeframe before contract conclusion, and split into
// Bearbeitungszeit/Versanddauer so it's clear whether processing time is
// included in the stated number, not just a single ambiguous figure.
export const HANDLING_DAYS = { min: 1, max: 2 };
export const TRANSIT_DAYS_DE = { min: 2, max: 4 };
export const TOTAL_DAYS_DE = {
min: HANDLING_DAYS.min + TRANSIT_DAYS_DE.min,
max: HANDLING_DAYS.max + TRANSIT_DAYS_DE.max,
};
+18 -2
View File
@@ -1,24 +1,36 @@
import { Fragment } from "react";
import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
// Each icon's own real pixel dimensions (not uniformly square) — needed so
// the `h-16 w-auto` sizing below infers the correct aspect ratio instead of
// stretching/squashing to a guessed one.
const steps = [
{
icon: "/icon-weekly-step-1.png",
width: 177,
height: 177,
title: "1. Anmelden",
desc: "Trage dich mit deiner E-Mail-Adresse ein und bestätige kurz.",
},
{
icon: "/icon-weekly-step-2.png",
width: 225,
height: 177,
title: "2. E-Mail erhalten",
desc: "Jeden Mittwoch bekommst du deinen neuen Impuls direkt in dein Postfach.",
},
{
icon: "/icon-weekly-step-3.png",
width: 198,
height: 198,
title: "3. Lesen & umsetzen",
desc: "Lies in wenigen Minuten und setze die Tipps direkt in deinem Alltag um.",
},
{
icon: "/icon-weekly-step-4.png",
width: 180,
height: 177,
title: "4. Dranbleiben",
desc: "Kleine Schritte, jede Woche für mehr Klarheit, Fokus und Leichtigkeit.",
},
@@ -48,9 +60,11 @@ export function HowItWorks() {
{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 md:max-w-none">
<img
<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>
@@ -62,9 +76,11 @@ export function HowItWorks() {
// technique as Challenge's step connector and todo-cards'
// identical HowItWorks, not just the same icon asset.
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
<img
<Image
alt=""
src="/icon-arrow-connector.svg"
width={24}
height={24}
className="w-6 h-6 rotate-90 md:w-10 md:h-3 md:rotate-0"
/>
</div>
+1 -1
View File
@@ -31,7 +31,7 @@ export function WeeklyBenefits() {
<ul className="flex flex-col gap-4 items-start w-full">
{benefits.map((b) => (
<li key={b.title} className="flex gap-[0.625rem] items-start w-full">
<img alt="" src="/icon-check.svg" className="size-5 shrink-0 mt-0.5" />
<Image alt="" src="/icon-check.svg" width={20} height={20} className="size-5 shrink-0 mt-0.5" />
<div className="flex flex-col gap-0.5 items-start flex-1 min-w-0">
<p className="font-semibold text-body text-text-primary">{b.title}</p>
<p className="text-body-sm text-text-muted">{b.desc}</p>
+9 -2
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "./components/Reveal";
import { Footer } from "./components/Footer";
import { TrustRow } from "./components/TrustRow";
@@ -116,9 +117,12 @@ export default function NotFound() {
actual photographed/rendered note rather than recreating its
handwriting in CSS. */}
<Reveal delay={0.15} className="w-full lg:flex-1 flex items-center justify-center">
<img
<Image
alt="Sticky Note: 404. Nicht hier. Aber dein Ziel ist sicher nicht weit."
src="/404-sticky-note.png"
width={600}
height={689}
sizes="(min-width: 1024px) 22rem, 100vw"
className="w-full max-w-[22rem] rotate-2 hover:rotate-0 transition-transform duration-300"
/>
</Reveal>
@@ -178,9 +182,12 @@ export default function NotFound() {
the flat mockup image instead of in Figma. */}
<Reveal className="relative w-full flex items-stretch h-[14rem] md:h-[16rem] bg-bg-muted overflow-hidden">
<div className="relative w-full md:w-[45%] shrink-0">
<img
<Image
alt=""
src="/404-testimonial-photo.jpg"
width={810}
height={291}
sizes="(min-width: 768px) 45vw, 100vw"
className="absolute -inset-1 w-[calc(100%+0.5rem)] h-[calc(100%+0.5rem)] object-cover"
/>
<div className="hidden md:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
+15 -9
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
import Image from "next/image";
import { getProducts } from "../../lib/payload";
import { getProducts, getShippingSettings } from "../../lib/payload";
import { formatPrice, discountPercent } from "../../lib/format";
import { RevealGroup, RevealItem } from "../../components/Reveal";
import { AddToCartInlineButton } from "../../components/AddToCartInlineButton";
@@ -17,7 +17,8 @@ import { AddToCartInlineButton } from "../../components/AddToCartInlineButton";
const SHOP_GRID_EXCLUDE_IDS = ["notizbuch-klarheit"];
export async function ProductGrid() {
const products = (await getProducts()).filter((p) => !SHOP_GRID_EXCLUDE_IDS.includes(p.id));
const [allProducts, shipping] = await Promise.all([getProducts(), getShippingSettings()]);
const products = allProducts.filter((p) => !SHOP_GRID_EXCLUDE_IDS.includes(p.id));
return (
<section className="w-full bg-bg-base flex flex-col pb-16 md:pb-20 px-[var(--layout-padding-x)]">
@@ -50,13 +51,18 @@ export async function ProductGrid() {
>
{product.name}
</p>
<p className="flex items-baseline gap-1.5">
{discount !== null && (
<span className="text-label text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</span>
)}
<span className="font-bold text-h4 text-text-primary">{formatPrice(product.price)}</span>
<span className="text-label text-text-muted">inkl. MwSt.</span>
</p>
<div className="flex flex-col gap-1 items-start">
<p className="flex items-baseline gap-1.5">
{discount !== null && (
<span className="text-label text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</span>
)}
<span className="font-bold text-h4 text-text-primary">{formatPrice(product.price)}</span>
<span className="text-label text-text-muted">inkl. MwSt.</span>
</p>
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
</div>
{product.href && (
<Link
href={product.href}
+1 -1
View File
@@ -34,7 +34,7 @@ export function Focus() {
<ul className="flex flex-col gap-3 items-start w-full">
{bullets.map((b) => (
<li key={b} className="flex gap-[0.625rem] items-center w-full">
<img alt="" src="/icon-check.svg" className="size-5 shrink-0" />
<Image alt="" src="/icon-check.svg" width={20} height={20} className="size-5 shrink-0" />
<span className="flex-1 font-semibold text-body text-text-primary">{b}</span>
</li>
))}
+26 -14
View File
@@ -1,19 +1,29 @@
import { Fragment } from "react";
import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
// Each icon's own real pixel dimensions (not uniformly square, e.g.
// icon-step-2 is 180x168) — needed so the `h-16 w-auto` sizing below infers
// the correct aspect ratio instead of stretching/squashing to a guessed one.
const steps = [
{
icon: "/icon-step-1.png",
width: 165,
height: 177,
title: "1. Aufschreiben",
desc: "Notiere deine Aufgaben und alles, was dir im Kopf herumgeht.",
},
{
icon: "/icon-step-2.png",
width: 180,
height: 168,
title: "2. Priorisieren",
desc: "Wähle deine 3 wichtigsten Aufgaben für heute aus und schreibe sie auf.",
},
{
icon: "/icon-step-3.png",
width: 180,
height: 177,
title: "3. Umsetzen",
desc: "Konzentriere dich auf das Wesentliche und hake ab, was wirklich zählt.",
},
@@ -47,21 +57,21 @@ export function HowItWorks() {
{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 md:max-w-none">
{/* Plain <img>, not next/image — one of the three source PNGs
isn't square (icon-step-2 is 68.6x64), and next/image's
required width/height props fought with Tailwind's
`img { height: auto }` preflight reset (console warning:
"width or height modified, but not the other"). A fixed
h-16 + w-auto lets each icon keep its own real aspect
ratio without that conflict — matches how every other
small decorative icon in this codebase (Tools.tsx etc.)
is already a plain <img>, not next/image. group-hover
scale is a small "step acknowledges you" touch — the
whole text column is the hover target, not just the icon,
so it's easy to trigger without precise pointing. */}
<img
{/* Explicit per-icon width/height (real pixel dimensions, not
a guessed/uniform size) — matters since one of the three
source PNGs isn't square (icon-step-2 is 180x168); passing
the real intrinsic size is what avoids next/image's
"width or height modified, but not the other" warning that
a wrong/guessed size would trigger against the `h-16
w-auto` sizing below. group-hover scale is a small "step
acknowledges you" touch — the whole text column is the
hover target, not just the icon, so it's easy to trigger
without precise pointing. */}
<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>
@@ -73,9 +83,11 @@ export function HowItWorks() {
// — same margin-based centering technique as Challenge's
// step connector, not just the same icon asset.
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
<img
<Image
alt=""
src="/icon-arrow-connector.svg"
width={24}
height={24}
className="w-6 h-6 rotate-90 md:w-10 md:h-3 md:rotate-0"
/>
</div>
+18 -17
View File
@@ -1,8 +1,7 @@
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 { getProductBySlug, getShippingSettings } from "../../lib/payload";
import { formatPrice, discountPercent } from "../../lib/format";
const bullets = [
@@ -18,7 +17,7 @@ const bullets = [
// 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");
const [product, shipping] = await Promise.all([getProductBySlug("todo-karten"), getShippingSettings()]);
if (!product) return null;
const discount = discountPercent(product.price, product.compareAtPrice);
@@ -53,7 +52,7 @@ export async function Pricing() {
<ul className="flex flex-col gap-[0.375rem] items-start">
{bullets.map((b) => (
<li key={b} className="flex gap-2 items-center">
<img alt="" src="/icon-bullet-dot.svg" className="size-1 shrink-0" />
<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>
))}
@@ -61,24 +60,26 @@ export async function Pricing() {
</div>
<div className="flex flex-col gap-3 items-start w-full lg:w-[18.75rem] lg:shrink-0">
<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">inkl. MwSt. zzgl. Versand</p>
<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>
<p className="text-label text-text-muted">inkl. MwSt. zzgl. Versand</p>
</div>
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
</div>
<p className="text-label text-text-muted">
Lieferzeit: {TOTAL_DAYS_DE.min}{TOTAL_DAYS_DE.max} Werktage innerhalb Deutschlands
</p>
{/* No "Sichere Zahlung" trust note here (unlike Cart/Checkout) —
this is an add-to-cart step, not the actual payment step, so
a payment-security reassurance is premature here and just
duplicates the one shown later at checkout. */}
<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"
/>
<div className="flex gap-[0.375rem] items-center">
<img alt="" src="/icon-lock.svg" className="size-3.5" />
<span className="text-body-sm text-text-primary">Sichere Zahlung</span>
</div>
</div>
</Reveal>
</section>
+21 -14
View File
@@ -2,7 +2,7 @@ import Link from "next/link";
import Image from "next/image";
import { AddToCartButton } from "../../components/AddToCartButton";
import { Reveal } from "../../components/Reveal";
import { getProductBySlug } from "../../lib/payload";
import { getProductBySlug, getShippingSettings } from "../../lib/payload";
import { formatPrice, discountPercent } from "../../lib/format";
const checklist = [
@@ -13,10 +13,12 @@ const checklist = [
// Same "todo-karten" product Pricing.tsx reads further down the page —
// this is just a compact early teaser so the hero's CTA isn't asking for
// a click without saying what it costs; the full price/VAT/shipping-time
// detail still lives only in Pricing.tsx, not duplicated here.
// a click without saying what it costs. Delivery time is repeated here too
// (not just in Pricing.tsx) since this is also a purchase CTA — Art. 246a
// §1 Abs.1 Nr.8 EGBGB's delivery-date disclosure needs to sit next to every
// buy button, not just one of them.
export async function TodoKartenHero() {
const product = await getProductBySlug("todo-karten");
const [product, shipping] = await Promise.all([getProductBySlug("todo-karten"), getShippingSettings()]);
const discount = product ? discountPercent(product.price, product.compareAtPrice) : null;
return (
@@ -84,21 +86,26 @@ export async function TodoKartenHero() {
<ul className="flex flex-col gap-3 items-start w-full">
{checklist.map((item) => (
<li key={item} className="flex gap-[0.625rem] items-center w-full">
<img alt="" src="/icon-check.svg" className="size-5 shrink-0" />
<Image alt="" src="/icon-check.svg" width={20} height={20} className="size-5 shrink-0" />
<span className="flex-1 text-body text-text-primary">{item}</span>
</li>
))}
</ul>
{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">inkl. MwSt.</p>
</div>
)}
<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">inkl. MwSt.</p>
</div>
)}
<p className="text-label text-text-muted">
Lieferzeit: {shipping.totalDays.min}{shipping.totalDays.max} Werktage innerhalb Deutschlands
</p>
</div>
<AddToCartButton label="ToDo-Karten bestellen" />
</div>
+16 -14
View File
@@ -1,12 +1,7 @@
import Link from "next/link";
import {
SHIPPING_COST,
FREE_SHIPPING_THRESHOLD,
HANDLING_DAYS,
TRANSIT_DAYS_DE,
TOTAL_DAYS_DE,
} from "../../lib/shipping";
import { SHIPPING_COST, FREE_SHIPPING_THRESHOLD } from "../../lib/shipping";
import { formatPrice } from "../../lib/format";
import type { ShippingSettings } from "../../lib/payload";
// Single source of truth for both the full /versand page and the cart's
// quick-reference VersandModal — same section ids/titles/copy either way,
@@ -51,20 +46,27 @@ function Section({
);
}
export function VersandSections({ withAnchors = false }: { withAnchors?: boolean }) {
export function VersandSections({
withAnchors = false,
shipping,
}: {
withAnchors?: boolean;
shipping: ShippingSettings;
}) {
return (
<div className="flex flex-col gap-10 items-start w-full">
<Section id="lieferzeiten" title="Lieferzeiten" withAnchor={withAnchors}>
<p>
Nach Zahlungseingang bereiten wir deine Bestellung in der Regel innerhalb von{" "}
{HANDLING_DAYS.min}{HANDLING_DAYS.max} Werktagen zum Versand vor. Die Versanddauer
innerhalb Deutschlands beträgt anschließend zusätzlich {TRANSIT_DAYS_DE.min}
{TRANSIT_DAYS_DE.max} Werktage.
{shipping.handlingDays.min}{shipping.handlingDays.max} Werktagen zum Versand vor. Die
Versanddauer innerhalb Deutschlands beträgt anschließend zusätzlich{" "}
{shipping.transitDays.min}{shipping.transitDays.max} Werktage.
</p>
<p>
Damit ist deine Bestellung in der Regel nach {TOTAL_DAYS_DE.min}{TOTAL_DAYS_DE.max}{" "}
Werktagen bei dir, sofern auf der jeweiligen Produktseite nichts anderes angegeben ist.
Als Werktage gelten Montag bis Freitag, ausgenommen gesetzliche Feiertage.
Damit ist deine Bestellung in der Regel nach {shipping.totalDays.min}
{shipping.totalDays.max} Werktagen bei dir, sofern auf der jeweiligen Produktseite nichts
anderes angegeben ist. Als Werktage gelten Montag bis Freitag, ausgenommen gesetzliche
Feiertage.
</p>
</Section>
+5 -2
View File
@@ -4,6 +4,7 @@ import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { VersandSections } from "./components/VersandSections";
import { VersandTOC } from "./components/VersandTOC";
import { getShippingSettings } from "../lib/payload";
export const metadata: Metadata = {
title: "Versand",
@@ -12,7 +13,9 @@ export const metadata: Metadata = {
alternates: { canonical: "/versand" },
};
export default function VersandPage() {
export default async function VersandPage() {
const shipping = await getShippingSettings();
return (
<>
<main className="flex flex-col flex-1 bg-bg-base">
@@ -38,7 +41,7 @@ export default function VersandPage() {
<VersandTOC />
</div>
<div className="w-full lg:flex-1 max-w-[45rem]">
<VersandSections withAnchors />
<VersandSections withAnchors shipping={shipping} />
</div>
</div>
</main>
+2 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { TrustRow } from "../components/TrustRow";
@@ -42,7 +43,7 @@ export default async function WiderrufPage() {
{/* Static, not part of the CMS content — same reasoning as
the Impressum/Datenschutz/AGB pages' own callout cards. */}
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
<p
className="font-semibold text-body text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}