import { useEffect, useState } from 'react'; import { Layout, Typography, List, Button, Empty, Card, Radio, Form, Input, Checkbox, Modal, message, Tag, Spin, theme, Space } from 'antd'; import { ArrowLeftOutlined } from '@ant-design/icons'; import { useNavigate, Link } from 'react-router-dom'; import { CartItem, ShippingAddress, fetchCart, removeFromCart, fetchAddresses, createAddress, createCartPaypalOrder, captureCartPaypalOrder, demoCartPurchase } from './cartApi'; import { fetchConfig, SiteConfig } from '../api'; import { loadPaypalSdk } from '../paypal'; import { useCart } from './CartContext'; import { useCustomerAuth } from '../customer/CustomerAuthContext'; const { Header, Content } = Layout; const { Title, Text } = Typography; function timeRemaining(expiresAt: string): string { const diffMs = new Date(expiresAt).getTime() - Date.now(); if (diffMs <= 0) return 'expiring…'; const hours = Math.floor(diffMs / (1000 * 60 * 60)); const mins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); return `${hours}h ${mins}m left`; } export default function Cart() { const [items, setItems] = useState([]); const [addresses, setAddresses] = useState([]); const [selectedAddressId, setSelectedAddressId] = useState(null); const [addAddressOpen, setAddAddressOpen] = useState(false); const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [checkingOut, setCheckingOut] = useState(false); const [form] = Form.useForm(); const { refresh: refreshCartContext } = useCart(); const { customer, loading: authLoading } = useCustomerAuth(); const navigate = useNavigate(); const { token } = theme.useToken(); useEffect(() => { if (!authLoading && !customer) navigate('/login'); }, [authLoading, customer, navigate]); function loadAll() { setLoading(true); void Promise.all([fetchCart(), fetchAddresses(), fetchConfig()]).then(([cartData, addrs, cfg]) => { setItems(cartData.items); setAddresses(addrs); const def = addrs.find(a => a.is_default); setSelectedAddressId(def ? def.id : (addrs[0]?.id ?? null)); setConfig(cfg); }) // Anything here failing left the cart on a spinner with no explanation. .catch(() => message.error('Could not load your cart')) .finally(() => setLoading(false)); } useEffect(() => { if (customer) loadAll(); }, [customer]); // compute the total const total = items.reduce((sum, i) => sum + i.price_cents, 0); async function handleRemove(itemId: number) { await removeFromCart(itemId); message.success('Removed from cart'); refreshCartContext(); loadAll(); } async function handleAddAddress() { const values = await form.validateFields(); const result = await createAddress(values); if (result.uspsConfigured && !result.uspsCheck.deliverable) { Modal.warning({ title: 'Address could not be verified', content: result.uspsCheck.reason || 'USPS could not confirm this address is deliverable. It has been saved, but double-check it before checkout.' }); } message.success('Address saved'); setAddAddressOpen(false); form.resetFields(); loadAll(); } async function handleDemoCheckout() { if (!selectedAddressId) { message.error('Select a shipping address first'); return; } setCheckingOut(true); try { await demoCartPurchase(selectedAddressId); message.success('Order complete!'); refreshCartContext(); loadAll(); } catch (err) { message.error((err as Error).message); } finally { setCheckingOut(false); } } const [paypalReady, setPaypalReady] = useState(false); useEffect(() => { if (config?.paypalClientId) { loadPaypalSdk(config.paypalClientId, config.currency).then(() => setPaypalReady(true)).catch(() => {}); } }, [config]); useEffect(() => { if (!paypalReady || !selectedAddressId || items.length === 0) return; const container = document.getElementById('paypal-cart-buttons'); if (!container || !(window as any).paypal) return; container.innerHTML = ''; (window as any).paypal.Buttons({ createOrder: async () => { const { orderID } = await createCartPaypalOrder(selectedAddressId); return orderID; }, onApprove: async (data: { orderID: string }) => { try { await captureCartPaypalOrder(data.orderID); message.success('Order complete!'); refreshCartContext(); loadAll(); } catch (err) { message.error((err as Error).message); } }, onError: (err: unknown) => { console.error(err); message.error('Checkout error, please try again.'); } }).render('#paypal-cart-buttons'); // refreshCartContext is a useCallback with an empty dependency list, so // naming it here cannot re-run this effect and re-render the PayPal // buttons — it just makes the dependency honest. }, [paypalReady, selectedAddressId, items.length, refreshCartContext]); if (authLoading || loading) return ; return (
Your Cart
{items.length === 0 ? ( ) : ( <> ( handleRemove(item.item_id)}>Remove]}> } title={item.name} description={ {timeRemaining(item.expires_at)} } />
${(item.price_cents / 100).toFixed(2)}
)} /> Total: ${(total / 100).toFixed(2)} {addresses.length === 0 ? ( No saved addresses yet. ) : ( setSelectedAddressId(e.target.value)} style={{ display: 'flex', flexDirection: 'column', gap: 8 }} > {addresses.map(a => ( {a.full_name}, {a.address_line1}{a.address_line2 ? `, ${a.address_line2}` : ''}, {a.city}, {a.state} {a.postal_code}{' '} {a.usps_validated ? USPS Verified : Not Verified} ))} )} {!selectedAddressId && Select a shipping address to check out.} {config?.paypalClientId && selectedAddressId &&
} {config?.demoMode && selectedAddressId && ( )} )} setAddAddressOpen(false)} destroyOnClose >
Make this my default address
); }