From 81aee5f8c07903e3e8059bf0b228adc63773617a Mon Sep 17 00:00:00 2001 From: jyswee Date: Thu, 23 Jul 2026 22:09:22 +0000 Subject: [PATCH] Seed: ecommerce template (24 files) --- src/pages/Checkout.tsx | 346 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 src/pages/Checkout.tsx diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx new file mode 100644 index 0000000..d42b0fe --- /dev/null +++ b/src/pages/Checkout.tsx @@ -0,0 +1,346 @@ +import { useState } from "react"; +import { toast } from "sonner"; +import { Link, useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; +import { Check, CreditCard, MapPin, ClipboardList, ArrowLeft, ArrowRight, Lock } from "lucide-react"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import { useCart } from "@/lib/cart-context"; +import { useSaasMutation } from "@/hooks/use-tygarun"; + +const steps = [ + { num: 1, label: "Shipping", icon: MapPin }, + { num: 2, label: "Payment", icon: CreditCard }, + { num: 3, label: "Review", icon: ClipboardList }, +]; + +export default function CheckoutPage() { + const [step, setStep] = useState(1); + const [errors, setErrors] = useState>({}); + const { items, total, clearCart } = useCart(); + const navigate = useNavigate(); + const { mutate: createCheckout, loading: placingOrder } = useSaasMutation("billing", "/checkout"); + + const [shipping, setShipping] = useState({ + firstName: "", lastName: "", email: "", address: "", city: "", state: "", zip: "", + }); + const [payment, setPayment] = useState({ + cardNumber: "", expiry: "", cvv: "", cardholder: "", + }); + + const shippingCost = total >= 100 ? 0 : 12.99; + const tax = total * 0.08; + const grandTotal = total + shippingCost + tax; + + const validateShipping = () => { + const e: Record = {}; + if (!shipping.firstName.trim()) e.firstName = "First name is required"; + if (!shipping.lastName.trim()) e.lastName = "Last name is required"; + if (!shipping.email.includes("@")) e.email = "Valid email is required"; + if (!shipping.address.trim()) e.address = "Address is required"; + if (!shipping.city.trim()) e.city = "City is required"; + if (!shipping.zip.trim()) e.zip = "ZIP code is required"; + setErrors(e); + return Object.keys(e).length === 0; + }; + + const validatePayment = () => { + const e: Record = {}; + if (payment.cardNumber.replace(/\s/g, "").length < 16) e.cardNumber = "Enter a valid 16-digit card number"; + if (!payment.expiry.trim()) e.expiry = "Expiry date is required"; + if (payment.cvv.length < 3) e.cvv = "CVV must be at least 3 digits"; + setErrors(e); + return Object.keys(e).length === 0; + }; + + const handlePlaceOrder = async () => { + try { + const res = await createCheckout({ + amount: Math.round(grandTotal * 100), + email: shipping.email, + items: items.map((it) => ({ id: it.id, name: it.name, quantity: it.quantity, priceCents: Math.round(it.price * 100) })), + }); + if (res?.url && res.url.indexOf("#") !== 0) { window.location.href = res.url; return; } + toast.success("Order placed!"); + clearCart(); + navigate("/order-confirmation"); + } catch { + toast.error("Payment could not be started. Please try again."); + } + }; + + const updateShipping = (field: string, value: string) => { + setShipping((prev) => ({ ...prev, [field]: value })); + if (errors[field]) setErrors((prev) => { const n = { ...prev }; delete n[field]; return n; }); + }; + + const updatePayment = (field: string, value: string) => { + setPayment((prev) => ({ ...prev, [field]: value })); + if (errors[field]) setErrors((prev) => { const n = { ...prev }; delete n[field]; return n; }); + }; + + return ( +
+
+ +
+ {/* Step Indicator */} +
+ {steps.map((s, i) => ( +
+
= s.num ? "text-stone-900" : "text-stone-400"}`} onClick={() => s.num < step && setStep(s.num)}> +
s.num ? "bg-green-500/80 text-white" : step === s.num ? "bg-stone-900/80 text-white" : "bg-white/80 dark:bg-slate-900/80 text-stone-500" + }`}> + {step > s.num ? : s.num} +
+ {s.label} +
+ {i < steps.length - 1 && ( +
s.num ? "bg-green-500" : "bg-stone-200"}`} /> + )} +
+ ))} +
+ +
+ {/* Main Form */} +
+ {/* Step 1: Shipping */} + {step === 1 && ( + + +

+ Shipping Information +

+
{ e.preventDefault(); if (validateShipping()) setStep(2); }}> +
+
+ +
+ updateShipping("firstName", e.target.value)} placeholder="John" className={`\${errors.firstName ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.firstName && shipping.firstName.trim() && } +
+ {errors.firstName &&

{errors.firstName}

} +
+
+ +
+ updateShipping("lastName", e.target.value)} placeholder="Doe" className={`\${errors.lastName ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.lastName && shipping.lastName.trim() && } +
+ {errors.lastName &&

{errors.lastName}

} +
+
+ +
+ updateShipping("email", e.target.value)} placeholder="john@example.com" className={`\${errors.email ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.email && shipping.email?.includes("@") && } +
+ {errors.email &&

{errors.email}

} +
+
+ +
+ updateShipping("address", e.target.value)} placeholder="123 Main Street" className={`\${errors.address ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.address && shipping.address.trim() && } +
+ {errors.address &&

{errors.address}

} +
+
+ +
+ updateShipping("city", e.target.value)} placeholder="New York" className={`\${errors.city ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.city && shipping.city.trim() && } +
+ {errors.city &&

{errors.city}

} +
+
+
+ + updateShipping("state", e.target.value)} placeholder="NY" /> +
+
+ +
+ updateShipping("zip", e.target.value)} placeholder="10001" className={`\${errors.zip ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.zip && shipping.zip.trim() && } +
+ {errors.zip &&

{errors.zip}

} +
+
+
+
+ + +
+
+
+
+ )} + + {/* Step 2: Payment */} + {step === 2 && ( + + +

+ Payment Details +

+
{ e.preventDefault(); if (validatePayment()) setStep(3); }}> +
+
+ + updatePayment("cardholder", e.target.value)} placeholder="John Doe" /> +
+
+ +
+ updatePayment("cardNumber", e.target.value)} placeholder="4242 4242 4242 4242" className={`\${errors.cardNumber ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.cardNumber && payment.cardNumber.replace(/\s/g, "").length >= 16 && } +
+ {errors.cardNumber &&

{errors.cardNumber}

} +
+
+
+ +
+ updatePayment("expiry", e.target.value)} placeholder="MM/YY" className={`\${errors.expiry ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.expiry && payment.expiry.trim() && } +
+ {errors.expiry &&

{errors.expiry}

} +
+
+ +
+ updatePayment("cvv", e.target.value)} placeholder="123" className={`\${errors.cvv ? "border-red-500 focus-visible:ring-red-500" : ""}`} /> + {!errors.cvv && payment.cvv.length >= 3 && } +
+ {errors.cvv &&

{errors.cvv}

} +
+
+
+
+ Your payment information is encrypted and secure. +
+
+ + +
+
+
+
+ )} + + {/* Step 3: Review */} + {step === 3 && ( + + +

+ Review Your Order +

+ + {/* Items */} +
+ {items.map((item) => ( +
+
+
+

{item.name}

+

Qty: {item.quantity} | {item.size} | {item.color}

+
+ \${(item.price * item.quantity).toFixed(2)} +
+ ))} +
+ + + + {/* Shipping Address */} +
+

Shipping Address

+

+ {shipping.firstName} {shipping.lastName}
+ {shipping.address}
+ {shipping.city}, {shipping.state} {shipping.zip} +

+
+ + {/* Payment */} +
+

Payment Method

+
+ + Card ending in {payment.cardNumber.slice(-4) || "****"} +
+
+ +
+ + +
+ + + )} +
+ + {/* Right Sidebar:Order Summary */} +
+ + +

Order Summary

+
+ {items.map((item) => ( +
+ {item.name} x{item.quantity} + \${(item.price * item.quantity).toFixed(2)} +
+ ))} +
+ +
+
+ Subtotal + \${total.toFixed(2)} +
+
+ Shipping + + {shippingCost === 0 ? "Free" : `\$\${shippingCost.toFixed(2)}`} + +
+
+ Tax + \${tax.toFixed(2)} +
+
+ +
+ Total + \${grandTotal.toFixed(2)} +
+
+
+
+
+
+ +
+ ); +}