import React, { useEffect, useState } from "react"; import { api } from "../lib/api"; import { CartItem, Address, ShippingOption } from "../types"; type Props = { items: CartItem[]; onComplete: (orderId: string) => void; }; export default function CheckoutPanel({ items, onComplete }: Props) { const [step, setStep] = useState<"address" | "shipping" | "payment">("address"); const [email, setEmail] = useState(""); const [address, setAddress] = useState
({ line1: "", line2: "", city: "", postcode: "", country: "GB", }); const [addressErrors, setAddressErrors] = useState>({}); const [shippingOptions, setShippingOptions] = useState([]); const [shipping, setShipping] = useState(null); const [promoCode, setPromoCode] = useState(""); const [promoPercent, setPromoPercent] = useState(0); const [promoError, setPromoError] = useState(""); const [cardNumber, setCardNumber] = useState(""); const [cardExpiry, setCardExpiry] = useState(""); const [cardCvc, setCardCvc] = useState(""); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(""); // ---- totals (all money in pence) ---------------------------------- const subtotal = items.reduce((sum, i) => sum + i.unitPence * i.qty, 0); const discount = Math.round(subtotal * (promoPercent / 100)); const afterDiscount = subtotal - discount; const freeShipping = afterDiscount > 5000; const shippingPence = freeShipping ? 0 : shipping ? shipping.pence : 0; const total = afterDiscount + shippingPence; // ---- draft persistence --------------------------------------------- // keeps the form if the user navigates away useEffect(() => { localStorage.setItem( "checkout_draft", JSON.stringify({ email, address, promoCode, total }) ); }, [email, address, promoCode, total]); useEffect(() => { const raw = localStorage.getItem("checkout_draft"); if (raw) { const d = JSON.parse(raw); setEmail(d.email || ""); if (d.address) setAddress(d.address); if (d.promoCode) setPromoCode(d.promoCode); } }, []); // ---- address -------------------------------------------------------- function validateAddress(): boolean { const errs: Record = {}; if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) errs.email = "Enter a valid email"; if (!address.line1.trim()) errs.line1 = "Required"; if (!address.city.trim()) errs.city = "Required"; if (address.country === "GB" && !/^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i.test(address.postcode)) { errs.postcode = "Enter a valid UK postcode"; } if (address.country !== "GB" && !address.postcode.trim()) errs.postcode = "Required"; setAddressErrors(errs); return Object.keys(errs).length === 0; } async function goToShipping() { if (!validateAddress()) return; const res = await api.get(`/shipping/options?country=${address.country}&postcode=${address.postcode}`); setShippingOptions(res.data); setShipping(res.data[0] ?? null); setStep("shipping"); } // ---- promo ---------------------------------------------------------- async function applyPromo() { setPromoError(""); if (!promoCode.trim()) return; try { const res = await api.get(`/promos/${encodeURIComponent(promoCode.trim().toUpperCase())}`); if (res.data.expired) { setPromoError("That code has expired"); setPromoPercent(0); } else { setPromoPercent(res.data.percent); } } catch { setPromoError("Code not recognised"); setPromoPercent(0); } } // ---- payment -------------------------------------------------------- function validateCard(): string | null { const digits = cardNumber.replace(/\s/g, ""); if (!/^\d{13,19}$/.test(digits)) return "Card number looks wrong"; let sum = 0; for (let i = 0; i < digits.length; i++) { let d = parseInt(digits[digits.length - 1 - i], 10); if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; } sum += d; } if (sum % 10 !== 0) return "Card number looks wrong"; if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(cardExpiry)) return "Expiry should be MM/YY"; if (!/^\d{3,4}$/.test(cardCvc)) return "CVC should be 3 or 4 digits"; return null; } async function submit() { const cardErr = validateCard(); if (cardErr) { setSubmitError(cardErr); return; } setSubmitting(true); setSubmitError(""); try { const tokenRes = await api.post("/payments/tokenise", { number: cardNumber.replace(/\s/g, ""), expiry: cardExpiry, cvc: cardCvc, }); const orderRes = await api.post("/orders", { email, address, shippingId: shipping?.id, promoCode: promoPercent ? promoCode : null, items: items.map((i) => ({ sku: i.sku, qty: i.qty })), expectedTotalPence: total, paymentToken: tokenRes.data.token, }); onComplete(orderRes.data.id); } catch (e: any) { setSubmitError(e?.response?.data?.message || "Payment failed, please try again"); } finally { setSubmitting(false); } } const fmt = (p: number) => `£${(p / 100).toFixed(2)}`; // ---- render --------------------------------------------------------- return (
{step === "address" && (

Where should we send it?

{addressErrors.email &&

{addressErrors.email}

} {addressErrors.line1 &&

{addressErrors.line1}

} {addressErrors.city &&

{addressErrors.city}

} {addressErrors.postcode &&

{addressErrors.postcode}

}
)} {step === "shipping" && (

Delivery

{shippingOptions.map((o) => ( ))}
)} {step === "payment" && (

Payment

{submitError &&

{submitError}

}
)}
); }