diff --git a/app/agb/components/VertragspartnerBlock.tsx b/app/agb/components/VertragspartnerBlock.tsx new file mode 100644 index 0000000..f3a0241 --- /dev/null +++ b/app/agb/components/VertragspartnerBlock.tsx @@ -0,0 +1,59 @@ +import { headingId } from "../../components/RichText"; +import type { CompanySettings } from "../../lib/payload"; +import type { TOCSection } from "../../components/SectionTOC"; + +// Renders "2. Vertragspartner" straight from company-settings, same +// single-source-of-truth pattern as Impressum's AnbieterAngaben.tsx and +// Datenschutz's VerantwortlicherBlock.tsx — this used to be hand-typed +// name/address/email baked into the AGB richText (seed-agb.ts on the +// Payload side), and had already drifted from the real company-settings +// values once (the richText's placeholder "Björn Wendt"/"Musterstraße +// 12" never got updated when the real address was set). Sits mid-document +// (section 1 "Geltungsbereich" comes before it), which is why AGB's +// content is split into `content` (section 1) + `contentPart2` (sections +// 3 onward) rather than just prepending this block like Datenschutz did — +// see LegalPages.ts's `contentPart2` field comment. +export function vertragspartnerHeadings(): TOCSection[] { + return [{ id: headingId("2. Vertragspartner"), title: "2. Vertragspartner" }]; +} + +function Heading({ children }: { children: string }) { + return ( +

+ {children} + +

+ ); +} + +function P({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function VertragspartnerBlock({ seller }: { seller: CompanySettings }) { + return ( +
+ 2. Vertragspartner +

Der Kaufvertrag kommt zustande mit:

+
+

+ einfach produktiv. – {seller.sellerName} +

+

+ + {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} + +

+

+ + E-Mail: {seller.sellerEmail} + +

+
+
+ ); +} diff --git a/app/agb/page.tsx b/app/agb/page.tsx index 294d8b4..6d40515 100644 --- a/app/agb/page.tsx +++ b/app/agb/page.tsx @@ -8,8 +8,9 @@ import { TrustRow } from "../components/TrustRow"; import { RichText, extractHeadings } from "../components/RichText"; import { LiveRichText } from "../components/LiveRichText"; import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC"; -import { getLegalPage } from "../lib/payload"; +import { getLegalPage, getCompanySettings } from "../lib/payload"; import { formatMonthYear } from "../lib/format"; +import { VertragspartnerBlock, vertragspartnerHeadings } from "./components/VertragspartnerBlock"; export const metadata: Metadata = { title: "AGB", @@ -19,8 +20,16 @@ export const metadata: Metadata = { export default async function AgbPage() { const { isEnabled: isPreview } = await draftMode(); - const page = await getLegalPage("agb", { draft: isPreview }); - const headings = page ? extractHeadings(page.content) : []; + const [page, seller] = await Promise.all([getLegalPage("agb", { draft: isPreview }), getCompanySettings()]); + // Section 1 ("Geltungsbereich") comes first from `content`, THEN + // "2. Vertragspartner" (dynamic, sits between two CMS-driven halves — + // see LegalPages.ts's `contentPart2` comment), then the rest from + // `contentPart2`. + const headings = [ + ...(page ? extractHeadings(page.content) : []), + ...vertragspartnerHeadings(), + ...(page?.contentPart2 ? extractHeadings(page.contentPart2) : []), + ]; return ( <> @@ -69,9 +78,18 @@ export default async function AgbPage() { -
+
{page ? ( - isPreview ? : + <> + {isPreview ? : } + {/* Name/Adresse/E-Mail kommen direkt aus company-settings, + nicht aus der CMS-Richtext — single-sourced, gleiche + Begründung wie Impressum/Datenschutz. */} + {seller && } + {page.contentPart2 ? ( + isPreview ? : + ) : null} + ) : (

Inhalte werden gerade aktualisiert.

)} diff --git a/app/cart/components/CartContent.tsx b/app/cart/components/CartContent.tsx index 84ecaee..f182d81 100644 --- a/app/cart/components/CartContent.tsx +++ b/app/cart/components/CartContent.tsx @@ -524,7 +524,13 @@ export function CartContent({ )} - setVersandOpen(false)} shipping={shippingSettings} /> + setVersandOpen(false)} + shipping={shippingSettings} + shippingCost={shippingCost} + freeShippingThreshold={freeShippingThreshold} + /> ); } diff --git a/app/checkout/components/CheckoutContent.tsx b/app/checkout/components/CheckoutContent.tsx index f0be638..e0f911d 100644 --- a/app/checkout/components/CheckoutContent.tsx +++ b/app/checkout/components/CheckoutContent.tsx @@ -397,6 +397,11 @@ export function CheckoutContent({ selectedShipping?.freeShippingThreshold !== undefined && subtotal >= selectedShipping.freeShippingThreshold; const shipping = items.length === 0 || !cartHasShippableItem(items) || freeShipping ? 0 : selectedShipping?.price ?? 0; + // For VersandModal — same "lowest freeShippingThreshold among active + // methods" rule as CartContent.tsx/getTrustBadges(), independent of + // whichever method is currently selected in the radio list above. + const shippingMethodThresholds = shippingMethods.map((m) => m.freeShippingThreshold).filter((t): t is number => t !== null); + const lowestFreeShippingThreshold = shippingMethodThresholds.length > 0 ? Math.min(...shippingMethodThresholds) : null; const { totalSavings, discountAmount, total } = computeCartTotals(items, shipping, discount); const taxBreakdown = computeTaxBreakdown( items.map(({ entry, product }) => ({ @@ -1551,7 +1556,13 @@ export function CheckoutContent({ - setVersandOpen(false)} shipping={shippingSettings} /> + setVersandOpen(false)} + shipping={shippingSettings} + shippingCost={shippingMethods[0]?.price ?? 0} + freeShippingThreshold={lowestFreeShippingThreshold} + /> ); } diff --git a/app/components/VersandModal.tsx b/app/components/VersandModal.tsx index 8f5618e..711889f 100644 --- a/app/components/VersandModal.tsx +++ b/app/components/VersandModal.tsx @@ -17,10 +17,14 @@ export function VersandModal({ open, onClose, shipping, + shippingCost, + freeShippingThreshold, }: { open: boolean; onClose: () => void; shipping: ShippingSettings; + shippingCost: number; + freeShippingThreshold: number | null; }) { const dialogRef = useRef(null); const closeButtonRef = useRef(null); @@ -109,7 +113,7 @@ export function VersandModal({
- +
diff --git a/app/lib/payload.ts b/app/lib/payload.ts index 28acf5f..aab61c5 100644 --- a/app/lib/payload.ts +++ b/app/lib/payload.ts @@ -855,6 +855,12 @@ export type LegalPage = { type: LegalPageType; title: string; content: unknown; + // Only populated for pages that need a dynamically-rendered section + // MID-document (currently just AGB's "Vertragspartner", sourced from + // company-settings — see VertragspartnerBlock.tsx). null everywhere + // else, including pages like Datenschutz whose one dynamic section sits + // at the very start and so can just be prepended instead of split. + contentPart2: unknown | null; attachment: { url: string; title: string } | null; updatedAt: string; }; @@ -863,6 +869,7 @@ type PayloadLegalPage = { type: LegalPageType; title: string; content: unknown; + contentPart2?: unknown; attachment: { url: string; title: string } | number | null; updatedAt: string; }; @@ -888,6 +895,7 @@ export async function getLegalPage(type: LegalPageType, options?: { draft?: bool type: doc.type, title: doc.title, content: doc.content, + contentPart2: doc.contentPart2 ?? null, attachment: typeof doc.attachment === "object" && doc.attachment ? { url: doc.attachment.url, title: doc.attachment.title } diff --git a/app/lib/shipping.ts b/app/lib/shipping.ts deleted file mode 100644 index 0b169b8..0000000 --- a/app/lib/shipping.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 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; diff --git a/app/versand/components/VersandSections.tsx b/app/versand/components/VersandSections.tsx index ee062ba..a3a6751 100644 --- a/app/versand/components/VersandSections.tsx +++ b/app/versand/components/VersandSections.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import { SHIPPING_COST, FREE_SHIPPING_THRESHOLD } from "../../lib/shipping"; import { formatPrice } from "../../lib/format"; import type { ShippingSettings } from "../../lib/payload"; @@ -49,9 +48,21 @@ function Section({ export function VersandSections({ withAnchors = false, shipping, + shippingCost, + freeShippingThreshold, }: { withAnchors?: boolean; shipping: ShippingSettings; + /** Price of the default (first active, i.e. Standard) ShippingMethod — + * same value/estimate CartContent.tsx already computes for its own + * order-summary line, threaded down here instead of this component + * having its own hardcoded lib/shipping.ts constant, which is exactly + * what let this text drift from the real Payload ShippingMethods price + * (see lib/shipping.ts's own comment on this). */ + shippingCost: number; + /** Lowest freeShippingThreshold among active ShippingMethods, or null if + * none has one — same rule CartContent.tsx/getTrustBadges() already use. */ + freeShippingThreshold: number | null; }) { return (
@@ -72,9 +83,9 @@ export function VersandSections({

- Die Versandkosten innerhalb Deutschlands betragen pauschal {formatPrice(SHIPPING_COST)}{" "} - pro Bestellung. Ab einem Bestellwert von {formatPrice(FREE_SHIPPING_THRESHOLD)} versenden - wir kostenlos. + Die Versandkosten innerhalb Deutschlands betragen pauschal {formatPrice(shippingCost)}{" "} + pro Bestellung. + {freeShippingThreshold !== null && ` Ab einem Bestellwert von ${formatPrice(freeShippingThreshold)} versenden wir kostenlos.`}

Alle angegebenen Preise verstehen sich inklusive der gesetzlichen Mehrwertsteuer.

diff --git a/app/versand/page.tsx b/app/versand/page.tsx index aefc95b..db5693f 100644 --- a/app/versand/page.tsx +++ b/app/versand/page.tsx @@ -4,7 +4,7 @@ import { Reveal } from "../components/Reveal"; import { Footer } from "../components/Footer"; import { VersandSections } from "./components/VersandSections"; import { VersandTOC, MobileVersandTOC } from "./components/VersandTOC"; -import { getShippingSettings } from "../lib/payload"; +import { getShippingSettings, getShippingMethods } from "../lib/payload"; export const metadata: Metadata = { title: "Versand", @@ -14,7 +14,12 @@ export const metadata: Metadata = { }; export default async function VersandPage() { - const shipping = await getShippingSettings(); + const [shipping, shippingMethods] = await Promise.all([getShippingSettings(), getShippingMethods()]); + // Same "first active method / lowest threshold among active methods" + // convention as CartContent.tsx/getTrustBadges() — see VersandSections.tsx. + const shippingCost = shippingMethods[0]?.price ?? 0; + const shippingMethodThresholds = shippingMethods.map((m) => m.freeShippingThreshold).filter((t): t is number => t !== null); + const freeShippingThreshold = shippingMethodThresholds.length > 0 ? Math.min(...shippingMethodThresholds) : null; return ( <> @@ -47,7 +52,7 @@ export default async function VersandPage() {

- +