// pages-checkout.jsx — CartDrawer, CheckoutPage, OrderConfirmPage — wired to real API + Razorpay const { useState: useStateC, useEffect: useEffectC } = React; // ─── CART DRAWER ───────────────────────────────────────────────────────────── function CartDrawer({ open, onClose, cart, updateQty, removeItem, goto }) { const subtotal = cart.reduce((s, i) => s + i.price * i.qty, 0); if (!open) return null; return (

Your bag ({cart.length})

{cart.length === 0 ? (
🛍️

Empty bag

Go find something fussy.

) : ( <>
{cart.map(item => (
{item.image ? {item.name}e.target.style.display='none'}/> : }
{item.name}
{fmtINR(item.price)}
{item.qty}
{fmtINR(item.price*item.qty)}
))}
Subtotal{fmtINR(subtotal)}
)}
); } // ─── India States + Major Cities ───────────────────────────────────────────── // ─── PincodeInput — auto-fills city + state, fully controlled ─────────────────── function PincodeInput({ value, onChange }) { const [loading, setLoading] = useStateC(false); const [msg, setMsg] = useStateC(''); const prevPin = React.useRef(''); // Lookup whenever value reaches 6 digits AND is different from last lookup useEffectC(() => { if (value && value.length === 6 && value !== prevPin.current) { prevPin.current = value; setLoading(true); setMsg(''); fetch('https://api.postalpincode.in/pincode/' + value) .then(r => r.json()) .then(data => { const post = data?.[0]; if (post?.Status === 'Success' && post?.PostOffice?.length) { const po = post.PostOffice[0]; const city = po.Block && po.Block !== 'NA' ? po.Block : po.District; const state = po.State; onChange(value, city, state); setMsg('✓ ' + po.District + ', ' + state); } else { setMsg('Pincode not found — fill city/state manually'); } }) .catch(() => setMsg('')) .finally(() => setLoading(false)); } if (!value || value.length < 6) { setMsg(''); prevPin.current = ''; } }, [value]); return (
{ const pin = e.target.value.replace(/\D/g,'').slice(0,6); onChange(pin, null, null); // null = don't clear city/state setMsg(''); }} style={{ paddingRight: loading ? 40 : undefined }} /> {loading &&
}
{msg &&
{msg}
}
); } // ─── CitySelect — searchable city dropdown ─────────────────────────────────── function CitySelect({ state, value, onChange }) { const [query, setQuery] = useStateC(''); const [showList, setShowList] = useStateC(false); const [other, setOther] = useStateC(false); const allCities = (window.INDIA_CITIES && state) ? (window.INDIA_CITIES[state] || []) : []; const filtered = query.trim() ? allCities.filter(c => c.toLowerCase().includes(query.toLowerCase())) : allCities; // When state changes, reset useEffectC(() => { setQuery(''); setOther(false); }, [state]); // If currently entered value matches a city, show it const displayValue = value && value !== '__other__' ? value : (other ? query : ''); if (!state) return ( ); return (
{ setQuery(e.target.value); onChange(e.target.value); setShowList(true); setOther(true); }} onFocus={() => { setShowList(true); setQuery(''); }} onBlur={() => setTimeout(() => setShowList(false), 180)} placeholder={`Search ${allCities.length} cities in ${state}…`} /> {showList && filtered.length > 0 && (
{filtered.slice(0, 40).map(city => (
{ onChange(city); setQuery(city); setShowList(false); setOther(false); }} style={{ padding:'9px 14px', fontSize:14, cursor:'pointer', borderBottom:'1px solid var(--ff-line)', background: value===city ? 'var(--ff-lime)' : 'transparent', }} onMouseOver={e=>e.currentTarget.style.background='var(--ff-bg-2)'} onMouseOut={e=>e.currentTarget.style.background=value===city?'var(--ff-lime)':'transparent'}> {city}
))} {filtered.length === 0 && (
No match — your city will be saved as typed
)}
)}
); } // ─── CHECKOUT PAGE ──────────────────────────────────────────────────────────── function CheckoutPage({ cart, user, goto, placeOrder, setToast }) { useEffectA(() => { if (window.FF_TRACK) window.FF_TRACK('checkout_start'); }, []); const [settings, setSettings] = useStateA({ free_shipping_min:999, shipping_fee:49, cod_available:true, cod_fee:49 }); useEffectA(() => { if (typeof FF_API === 'undefined') return; FF_API.content?.settings().then(s=>{ if(s?.free_shipping_min) setSettings(s); }); }, []); const [step, setStep] = useStateC(1); const [addresses, setAddresses] = useStateC([]); const [selectedAddr,setSelectedAddr]=useStateC(null); const [newAddr, setNewAddr] = useStateC({ name:'', phone:'', line1:'', line2:'', city:'', state:'', pincode:'' }); const [showNewAddr, setShowNewAddr]= useStateC(false); const [promo, setPromo] = useStateC(''); const [promoData, setPromoData] = useStateC(null); const [promoErr, setPromoErr] = useStateC(''); const [processing, setProcessing] = useStateC(false); const [payMethod, setPayMethod] = useStateC('razorpay'); // razorpay | cod const subtotal = cart.reduce((s, i) => s + i.price * i.qty, 0); const discount = promoData?.discount || 0; const shipping = subtotal >= settings.free_shipping_min ? 0 : settings.shipping_fee; const total = subtotal - discount + shipping; // Load saved addresses for logged-in users useEffectC(() => { if (user) { FF_API.orders.getAddresses().then(a => { setAddresses(a); const def = a.find(x => x.is_default == 1) || a[0]; if (def) setSelectedAddr(def.id); }); } }, [user]); // Pre-fill new address from user profile useEffectC(() => { if (user) setNewAddr(a => ({ ...a, name: user.name || '', phone: user.phone || '' })); }, [user]); if (!cart.length) return (

Your bag is empty

); const applyPromo = async () => { setPromoErr(''); const { ok, data } = await FF_API.cart.applyPromo(promo, subtotal); if (ok) { setPromoData(data); setToast('Promo applied! ✦'); } else { setPromoErr(data.error || 'Invalid code'); setPromoData(null); } }; const saveAddressAndProceed = async () => { // Entering new address — either showNewAddr is true OR user has no saved addresses const enteringNew = showNewAddr || addresses.length === 0; if (enteringNew) { // Validate required fields const required = ['name','phone','line1','city','state','pincode']; for (const f of required) { if (!newAddr[f]?.trim()) { setToast(`Please fill in ${f}`); return; } } if (!/^\d{6}$/.test(newAddr.pincode)) { setToast('Pincode must be 6 digits'); return; } const { ok, data } = await FF_API.orders.addAddress({ ...newAddr, isDefault: addresses.length === 0 }); if (!ok) { setToast(data.error || 'Address save failed'); return; } setAddresses(a => [...a, data]); setSelectedAddr(data.id); setShowNewAddr(false); } else { // Using saved address if (!selectedAddr) { setToast('Please select an address'); return; } } setStep(2); }; const handleCOD = async () => { if (!user) { goto({ page:'home', openLogin:true }); return; } const addrId = selectedAddr; if (!addrId) { setToast('Please select an address'); return; } setProcessing(true); try { const items = cart.map(i => ({ productId: i.product_id, qty: i.qty })); console.log('[COD] items:', items, 'addrId:', addrId); const { ok, data } = await FF_API.orders.createCOD(items, addrId, promoData?.code || null); console.log('[COD] response ok:', ok, 'data:', data); if (!ok) { const errMsg = data?.error || JSON.stringify(data) || 'COD order failed'; setToast(errMsg); setProcessing(false); return; } if (!data?.orderId) { setToast('Order placed but no ID returned. Check phpMyAdmin orders table.'); setProcessing(false); return; } placeOrder(data.orderId); // clear cart location.href = FF_URLS.order(data.orderId); // redirect to order page } catch (err) { console.error('[COD] exception:', err); setToast('Order failed: ' + err.message); setProcessing(false); } }; const handlePayment = async () => { if (payMethod === 'cod') { handleCOD(); return; } if (!user) { goto({ page:'home', openLogin:true }); return; } const addrId = selectedAddr; if (!addrId) { setToast('Please select an address'); return; } setProcessing(true); try { // Step 1: create Razorpay order on our server const items = cart.map(i => ({ productId: i.product_id, qty: i.qty })); const { ok, data } = await FF_API.orders.createPayment(items, addrId, promoData?.code || null); if (!ok) { setToast(data.error || 'Payment init failed'); setProcessing(false); return; } // Step 2: open real Razorpay checkout const rzpOptions = { key: data.keyId, amount: data.amount, currency: 'INR', order_id: data.razorpayOrderId, name: 'Fussy Finds', description: 'Order payment', prefill: data.prefill, theme: { color: '#E4439C' }, handler: async (response) => { // Don't setProcessing here — show a redirect message instead setToast('✓ Payment successful! Taking you to your order…'); try { const { ok: cOk, data: cData } = await FF_API.orders.confirm({ razorpayOrderId: response.razorpay_order_id, razorpayPaymentId: response.razorpay_payment_id, razorpaySignature: response.razorpay_signature, }, data.orderSummary); if (cOk) { placeOrder(cData.orderId); location.href = FF_URLS.order(cData.orderId); } else { setToast('Payment received but confirmation failed. Your payment ID: ' + response.razorpay_payment_id + ' — contact support.'); setProcessing(false); } } catch(e) { setToast('Confirmation error. Your payment ID: ' + response.razorpay_payment_id); setProcessing(false); } }, modal: { ondismiss: () => { setProcessing(false); setToast('Payment cancelled — your cart is safe.'); } }, }; // Load Razorpay SDK if not loaded if (!window.Razorpay) { await new Promise((res, rej) => { const s = document.createElement('script'); s.src = 'https://checkout.razorpay.com/v1/checkout.js'; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); } const rzp = new window.Razorpay(rzpOptions); rzp.open(); // Reset button after modal opens so it doesn't stay stuck setTimeout(() => setProcessing(false), 1500); } catch (err) { console.error(err); setToast('Payment error. Please try again.'); setProcessing(false); } }; const currentAddr = addresses.find(a => a.id === selectedAddr); return (

Checkout

{/* Steps indicator */}
{['Delivery','Payment','Confirm'].map((s,i)=>(
i ? 'var(--ff-accent)' : step===i+1 ? 'var(--ff-ink)' : 'var(--ff-bg-2)', color:'#fff', display:'grid', placeItems:'center', fontSize:12, fontWeight:700 }}>{step>i ? '✓' : i+1}
{s}
{i<2 &&
i+1 ? 'var(--ff-accent)' : 'var(--ff-bg-2)', maxWidth:40 }}/>} ))}
{/* STEP 1: Address */} {step === 1 && (

Delivery address

{!user && ( )} {/* Saved addresses */} {addresses.length > 0 && !showNewAddr && (
{addresses.map(a => ( ))}
)} {/* New address form */} {(showNewAddr || addresses.length === 0) && (
setNewAddr(a=>({...a,name:e.target.value}))} placeholder="Priya Sharma"/>
setNewAddr(a=>({...a,phone:e.target.value}))} placeholder="9876543210"/>
setNewAddr(a=>({...a,line1:e.target.value}))} placeholder="Flat, building, street"/>
setNewAddr(a=>({...a,line2:e.target.value}))} placeholder="Area, landmark"/>
{ // Use functional update to get fresh state setNewAddr(a => { const updated = {...a, city}; // Reverse pincode lookup with current state if (city && a.state) { fetch('https://api.postalpincode.in/postoffice/' + encodeURIComponent(city)) .then(r => r.json()) .then(data => { const offices = data?.[0]?.PostOffice || []; const match = offices.find(o => o.State === a.state) || offices[0]; if (match?.Pincode) setNewAddr(prev => ({...prev, city, pincode: match.Pincode})); }).catch(()=>{}); } return updated; }); }} />
setNewAddr(a => ({ ...a, pincode: pin, // null means "don't change", '' means "clear", string means "set" ...(autoCity !== null ? { city: autoCity||a.city } : {}), ...(autoState !== null ? { state: autoState||a.state } : {}), }))} />
{showNewAddr && }
)}
)} {/* STEP 2: Payment */} {step === 2 && (
{currentAddr && (
Delivering to
{currentAddr.name} · {currentAddr.line1}, {currentAddr.city}
)}

Payment method

{/* Method selector */}
{/* Razorpay option */}
)}
{/* Order summary sidebar */}

Order summary

{cart.map(i => (
{i.image ? ( {i.name}e.target.style.display='none'}/> ) : ( )}
{i.name}
Qty: {i.qty}
{fmtINR(i.price*i.qty)}
))}
{/* Promo code */}
setPromo(e.target.value.toUpperCase())} style={{ flex:1 }}/>
{promoErr &&
{promoErr}
} {promoData &&
✓ {promoData.code} — saving {fmtINR(discount)}
}
Subtotal{fmtINR(subtotal)}
{discount > 0 &&
Discount−{fmtINR(discount)}
}
Shipping{shipping === 0 ? Free : fmtINR(shipping)}
Total{fmtINR(total)}
); } // ─── ORDER CONFIRMATION PAGE ────────────────────────────────────────────────── function OrderConfirmPage({ orderId, goto }) { const [order, setOrder] = useStateC(null); useEffectC(() => { if (!orderId) return; FF_API.orders.get(orderId) .then(setOrder) .catch(console.error); }, [orderId]); return (

Order placed!

Your order #{orderId} is confirmed. We'll ship it in 48 hours.

{order && (

Tracking

{(order.tracking || []).map((t, i) => (
{t.done==1 && }
{t.label}
{t.event_date && t.event_date !== '—' &&
{t.event_date}
}
))}
)}
); } Object.assign(window, { CartDrawer, CheckoutPage, OrderConfirmPage });