Feed checkout's country selects from Payload instead of a hardcoded list

Both the billing and shipping-override country selects, plus PLZ
maxLength/pattern validation, now read from the new shipping-countries
collection (getShippingCountries()) rather than a hardcoded
Deutschland/Österreich(/Schweiz) array. Lets an admin add or reorder
destination countries without a frontend deploy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-23 20:21:13 +00:00
parent 50db2fcb4b
commit 47d03dd61b
3 changed files with 78 additions and 26 deletions
+34 -24
View File
@@ -19,19 +19,20 @@ import { dispatchAuthChanged } from "../../lib/auth";
import { readCheckoutDraft, writeCheckoutDraft, clearCheckoutDraft } from "../../lib/checkoutDraft";
import { normalizeVatId, isValidVatId } from "../../lib/vatId";
import { computeExemptTotals, destinationCountry, isExemptionEligibleCountry } from "../../lib/vatExemption";
import type { ShippingMethod, PaymentMethod, TrustBadge, ShippingSettings } from "../../lib/payload";
import type { ShippingMethod, ShippingCountry, PaymentMethod, TrustBadge, ShippingSettings } from "../../lib/payload";
import type { CustomerProfile } from "../../lib/customerAuth";
// Native HTML5 pattern validation (instant, no round-trip) mirroring the
// same rules Orders.ts/Customers.ts enforce server-side — a plausibility
// check, not the source of truth (the backend re-validates regardless of
// what a customer's browser did or didn't catch). Only Germany/Austria/
// Switzerland are offered in the country <select>, so a fixed 3-way
// lookup is enough — unlike the backend's own zip check, which stays
// free-text-country-tolerant since Payload's admin has no such select.
const PLZ_DIGITS: Record<string, number> = { Deutschland: 5, Österreich: 4, Schweiz: 4 };
function plzPattern(country: string): string {
const digits = PLZ_DIGITS[country] ?? 4;
// what a customer's browser did or didn't catch). `plzDigitsMap` comes
// from Payload's shipping-countries collection (see lib/payload.ts's
// getShippingCountries()) — which countries are offered, and how many PLZ
// digits each expects, is admin-configurable now, not a hardcoded array
// here. `?? 4` only matters if a country somehow isn't in the map at all
// (shouldn't happen — the <select> options are built from the same list).
function plzPattern(country: string, plzDigitsMap: Record<string, number>): string {
const digits = plzDigitsMap[country] ?? 4;
return `\\d{${digits}}`;
}
@@ -48,9 +49,9 @@ function validateEmailFormat(value: string): string {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? "" : "Bitte eine gültige E-Mail-Adresse angeben.";
}
function validateZip(value: string, country: string): string {
function validateZip(value: string, country: string, plzDigitsMap: Record<string, number>): string {
if (!value.trim()) return "PLZ ist erforderlich.";
const digits = PLZ_DIGITS[country] ?? 4;
const digits = plzDigitsMap[country] ?? 4;
return new RegExp(`^\\d{${digits}}$`).test(value) ? "" : `PLZ muss aus ${digits} Ziffern bestehen.`;
}
@@ -95,6 +96,7 @@ function FormField({
export function CheckoutContent({
shippingMethods,
shippingCountries,
paymentMethods,
trustBadges,
shippingSettings,
@@ -103,6 +105,10 @@ export function CheckoutContent({
savedProfile,
}: {
shippingMethods: ShippingMethod[];
/** Which countries the "Land" <select>s offer, and each one's PLZ digit
* count — admin-configurable (Payload's shipping-countries collection),
* not a hardcoded array here anymore. */
shippingCountries: ShippingCountry[];
paymentMethods: PaymentMethod[];
trustBadges: TrustBadge[];
/** Delivery-time disclosure (Payload's Shipping Settings) — named
@@ -124,6 +130,10 @@ export function CheckoutContent({
const cart = useCart();
const products = useProducts();
const discount = useDiscount();
// { "Deutschland": 5, "Österreich": 4, ... } — built once from the
// fetched list rather than re-deriving it inline at every plzPattern()/
// validateZip() call site.
const plzDigitsMap: Record<string, number> = Object.fromEntries(shippingCountries.map((c) => [c.name, c.plzDigits]));
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);
@@ -828,14 +838,14 @@ export function CheckoutContent({
type="text"
value={zip}
onChange={(e) => setZip(e.target.value)}
onBlur={(e) => setFieldError("zip", validateZip(e.target.value, country), e.target)}
onBlur={(e) => setFieldError("zip", validateZip(e.target.value, country, plzDigitsMap), e.target)}
error={fieldErrors.zip}
placeholder="10115"
autoComplete="postal-code"
inputMode="numeric"
pattern={plzPattern(country)}
maxLength={PLZ_DIGITS[country] ?? 4}
title={`PLZ muss aus ${PLZ_DIGITS[country] ?? 4} Ziffern bestehen.`}
pattern={plzPattern(country, plzDigitsMap)}
maxLength={plzDigitsMap[country] ?? 4}
title={`PLZ muss aus ${plzDigitsMap[country] ?? 4} Ziffern bestehen.`}
required
/>
<FormField
@@ -860,9 +870,9 @@ export function CheckoutContent({
required
className="w-full border border-border rounded-sm px-4 py-3 text-body-sm text-text-primary outline-none focus:border-brand transition-colors bg-bg-base"
>
<option>Deutschland</option>
<option>Österreich</option>
<option>Schweiz</option>
{shippingCountries.map((c) => (
<option key={c.name}>{c.name}</option>
))}
</select>
</label>
@@ -987,14 +997,14 @@ export function CheckoutContent({
type="text"
value={shippingZip}
onChange={(e) => setShippingZip(e.target.value)}
onBlur={(e) => setFieldError("shippingZip", validateZip(e.target.value, shippingCountry), e.target)}
onBlur={(e) => setFieldError("shippingZip", validateZip(e.target.value, shippingCountry, plzDigitsMap), e.target)}
error={fieldErrors.shippingZip}
placeholder="10115"
autoComplete="off"
inputMode="numeric"
pattern={plzPattern(shippingCountry)}
maxLength={PLZ_DIGITS[shippingCountry] ?? 4}
title={`PLZ muss aus ${PLZ_DIGITS[shippingCountry] ?? 4} Ziffern bestehen.`}
pattern={plzPattern(shippingCountry, plzDigitsMap)}
maxLength={plzDigitsMap[shippingCountry] ?? 4}
title={`PLZ muss aus ${plzDigitsMap[shippingCountry] ?? 4} Ziffern bestehen.`}
required
/>
<FormField
@@ -1017,9 +1027,9 @@ export function CheckoutContent({
required
className="w-full border border-border rounded-sm px-4 py-3 text-body-sm text-text-primary outline-none focus:border-brand transition-colors bg-bg-base"
>
<option>Deutschland</option>
<option>Österreich</option>
<option>Schweiz</option>
{shippingCountries.map((c) => (
<option key={c.name}>{c.name}</option>
))}
</select>
</label>
</div>