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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user