function WishlistTab({ user }) { const [items, setItems] = useStateA([]); const [loading, setLoading] = useStateA(true); const [adding, setAdding] = useStateA(new Set()); // tracks which items are being added const load = () => { if (!user || typeof FF_API === 'undefined') return; setLoading(true); FF_API.wishlist.get() .then(d => setItems(Array.isArray(d) ? d : [])) .catch(()=>setItems([])) .finally(()=>setLoading(false)); }; useEffectA(load, [user]); const remove = async (productId) => { setItems(prev => prev.filter(p => p.id !== productId)); await FF_API.wishlist.toggle(productId).catch(()=>{}); }; const moveToCart = async (p) => { if (adding.has(p.id)) return; setAdding(prev => new Set([...prev, p.id])); try { await FF_API.cart.add(p.id, 1); window.FF_TOAST && window.FF_TOAST(p.name + ' moved to bag ✦'); await remove(p.id); } catch { window.FF_TOAST && window.FF_TOAST('Could not add to bag'); } finally { setAdding(prev => { const n = new Set(prev); n.delete(p.id); return n; }); } }; const moveAllToCart = async () => { const available = items.filter(p => !(p.stock === 0 || p.stock === '0')); if (!available.length) return; window.FF_TOAST && window.FF_TOAST('Adding all to bag…'); for (const p of available) await moveToCart(p); }; if (loading) return (
Loading your wishlist…
); if (!items.length) return (
Nothing saved yet
Tap ♡ on any product to save it here.
Browse products →
); const available = items.filter(p => !(p.stock===0||p.stock==='0')); return (
{/* Header row */}

Wishlist ({items.length})

{available.length > 1 && ( )} Keep browsing →
{/* Cards grid */}
{items.map(p => { const imgs = Array.isArray(p.images) ? p.images : []; const img = imgs[0] || null; const outOfStock = p.stock === 0 || p.stock === '0'; const isAdding = adding.has(p.id); const discount = p.compare ? Math.round((1 - p.price/p.compare)*100) : 0; return (
{/* Image */}
location.href = `/product/${p.id}`}>
{img ? {p.name}e.currentTarget.style.transform='scale(1.04)'} onMouseOut={e=>e.currentTarget.style.transform='scale(1)'}/> :
{p.name?.slice(0,10)}
}
{/* Badges */} {outOfStock && (
Out of stock
)} {discount >= 10 && !outOfStock && (
{discount}% off
)} {/* Remove ✕ */}
{/* Info */}
location.href = `/product/${p.id}`}>{p.name}
{p.tagline &&
{p.tagline}
} {/* Price row */}
{fmtINR(p.price)} {p.compare > p.price && ( {fmtINR(p.compare)} )}
{/* Action buttons */}
{outOfStock ? (
Notify me when back ·{' '} location.href = `/product/${p.id}`}>View
) : ( )}
); })}
); } // ── ReferralTab ─────────────────────────────────────────────────────────────── function ReferralTab({ user }) { const [data, setData] = useStateA(null); const [copied, setCopied] = useStateA(false); const [loading, setLoading] = useStateA(true); useEffectA(() => { if (!user) return; fetch('/api/newsletter.php?action=referral-code', { headers: { 'Authorization': 'Bearer ' + (localStorage.getItem('ff_token') || '') } }) .then(r => r.json()) .then(d => { if (d.ok) setData(d); }) .catch(() => {}) .finally(() => setLoading(false)); }, [user]); const copy = () => { if (!data?.code) return; const link = window.location.origin + '/?ref=' + data.code; navigator.clipboard.writeText(link).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2500); }); }; if (loading) return (
Loading…
); return (

Refer & Earn

Share your link. When a friend signs up, they get 10% off — and you get ₹100 off your next order.

{/* How it works */}
{[ { icon:'🔗', title:'Share your link', desc:'Send it to friends, post on WhatsApp, Instagram' }, { icon:'✅', title:'They sign up', desc:'Friend subscribes using your referral link' }, { icon:'🎁', title:'You both win', desc:'They get 10% off · You get ₹100 off' }, ].map(s => (
{s.icon}
{s.title}
{s.desc}
))}
{/* Your link */} {data?.code && (
Your referral link
{window.location.origin}/?ref={data.code}
{/* Share buttons */}
{[ { label:'WhatsApp', color:'#25D366', emoji:'💬', url: `https://wa.me/?text=${encodeURIComponent('Hey! Check out Fussy Finds — amazing products. Use my link for 10% off: ' + window.location.origin + '/?ref=' + data.code)}` }, { label:'Instagram', color:'#E4439C', emoji:'📸', url: `https://www.instagram.com/` }, { label:'Copy code', color:'var(--ff-ink)', emoji:'🔤', action: () => { navigator.clipboard.writeText(data.code); setCopied(true); setTimeout(()=>setCopied(false),2500); } }, ].map(s => ( {e.preventDefault();s.action();} : undefined} style={{ display:'flex', alignItems:'center', gap:6, padding:'8px 14px', borderRadius:8, background:s.color, color:'#fff', textDecoration:'none', fontSize:12, fontWeight:600, cursor:'pointer' }}> {s.emoji} {s.label} ))}
{/* Stats */}
{data.uses || 0}
Referrals
₹{(data.earnings || 0).toLocaleString('en-IN')}
Earned
)}
); } // ── AddressModal — add a new saved address ──────────────────────────────────── function AddressModal({ onClose, onSaved }) { const [form, setForm] = React.useState({ name:'', phone:'', line1:'', line2:'', city:'', state:'', pincode:'' }); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(''); const save = async () => { if (!form.name||!form.phone||!form.line1||!form.city||!form.state||!form.pincode) { setError('Please fill all required fields'); return; } if (!/^\d{6}$/.test(form.pincode)) { setError('Enter a valid 6-digit pincode'); return; } setSaving(true); setError(''); try { const token = localStorage.getItem('ff_token'); const r = await fetch('/api/auth.php?action=add-address', { method:'POST', headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer '+token }, body: JSON.stringify(form), }); const d = await r.json(); if (d.id || d.ok) { onSaved({ ...form, id: d.id }); } else setError(d.error || 'Could not save address'); } catch(e) { setError('Network error'); } finally { setSaving(false); } }; const F = ({ label, k, placeholder, required, half }) => (
setForm(f=>({...f,[k]:e.target.value}))}/>
); return (
e.target===e.currentTarget&&onClose()}>
Add address
{error &&
{error}
}
); } function AccountPage({ user, onLogout, goto, loadOrders, initialTab, refreshUser }) { const [tab, setTab] = useStateA(initialTab || 'orders'); const [openOrder, setOpenOrder] = useStateA(null); const [orders, setOrders] = useStateA([]); const [addresses, setAddresses] = useStateA([]); const [addrModal, setAddrModal] = useStateA(false); const [ordLoading, setOrdLoading]= useStateA(false); // Profile edit state const [profName, setProfName] = useStateA(''); const [profPhone, setProfPhone] = useStateA(''); const [profSaved, setProfSaved] = useStateA(false); const [pwCurr, setPwCurr] = useStateA(''); const [pwNew, setPwNew] = useStateA(''); const [pwMsg, setPwMsg] = useStateA(''); // Email OTP verification const [otpStep, setOtpStep] = useStateA(false); const [otpCode, setOtpCode] = useStateA(''); const [otpMsg, setOtpMsg] = useStateA(''); const [otpLoading, setOtpLoading]= useStateA(false); const [otpCd, setOtpCd] = useStateA(0); const [isVerified, setIsVerified]= useStateA(user?.email_verified == 1); useEffectA(() => { setIsVerified(user?.email_verified == 1); }, [user]); useEffectA(() => { if (otpCd <= 0) return; const t = setTimeout(() => setOtpCd(c => c-1), 1000); return () => clearTimeout(t); }, [otpCd]); const sendVerifyOtp = async () => { setOtpLoading(true); setOtpMsg(''); const { ok, data } = await FF_API.auth.sendOtp(); setOtpLoading(false); if (ok) { setOtpStep(true); setOtpCd(30); setOtpMsg('Code sent to ' + (data.email || user.email)); } else setOtpMsg(data.error || 'Failed to send'); }; const submitOtp = async () => { if (otpCode.length < 6) return; setOtpLoading(true); setOtpMsg(''); const { ok, data } = await FF_API.auth.verifyOtp(otpCode); setOtpLoading(false); if (ok) { setIsVerified(true); setOtpStep(false); setOtpMsg(''); setOtpCode(''); // Refresh user object so email_verified persists across reloads if (refreshUser) await refreshUser(); } else { setOtpMsg(data.error || 'Wrong code'); } }; useEffectA(() => { if (!user) return; setProfName(user.name || ''); setProfPhone(user.phone || ''); }, [user]); // Load orders when tab opens useEffectA(() => { if (typeof FF_API === 'undefined') return; if (tab === 'orders' && user) { setOrdLoading(true); FF_API.orders.list() .then(d => setOrders(Array.isArray(d) ? d : [])) .catch(console.error) .finally(() => setOrdLoading(false)); } if (tab === 'addresses' && user) { FF_API.orders.getAddresses() .then(d => setAddresses(Array.isArray(d) ? d : [])) .catch(console.error); } }, [tab, user]); if (!user) { return (

Log in to see your account

); } const saveProfile = async () => { const { ok } = await FF_API.auth.updateProfile({ name: profName, phone: profPhone }); if (ok) setProfSaved(true); setTimeout(() => setProfSaved(false), 2000); }; const changePassword = async () => { setPwMsg(''); const { ok, data } = await FF_API.auth.changePassword(pwCurr, pwNew); setPwMsg(ok ? '✓ Password changed' : (data.error || 'Failed')); if (ok) { setPwCurr(''); setPwNew(''); } }; return (
{/* Unverified email warning banner */} {!isVerified && (
⚠️
Your email is not verified
Verify to secure your account and receive order updates.
)} {/* Inline OTP verification (from banner) */} {otpStep && !isVerified && (
📧 Enter verification code
{otpMsg}
setOtpCode(e.target.value.replace(/\D/g,'').slice(0,6))} placeholder="6-digit code" style={{ fontFamily:'monospace', fontSize:22, letterSpacing:'.2em', textAlign:'center', fontWeight:700, maxWidth:200 }} autoFocus/>
)}
{(user.name||'F')[0].toUpperCase()}

Hey {user.name?.split(' ')[0] || 'fussy'} ✦

{user.email} {isVerified ? ✓ Verified : setTab('profile')} style={{ fontSize:11, fontWeight:700, padding:'2px 8px', borderRadius:4, background:'#FFE4E4', color:'#8B0000', cursor:'pointer' }}>⚠ Unverified · Click to verify } · Member since {user.created_at?.slice(0,7) || 'recently'}
{/* ORDERS TAB */} {tab === 'orders' && (

Your orders

{ordLoading ? (
Loading orders…
) : orders.length === 0 ? (
📦

No orders yet

Your orders will appear here after checkout.

) : (
{orders.map(o => (
{o.created_at?.slice(0,10)}
#{o.id}
{fmtINR(o.total)}
View order
{/* Order items thumbnails */}
{(o.items||[]).map((it, i) => (
{it.image ? {it.name}e.target.style.display='none'}/> : }
))}
{/* Tracking accordion */} {openOrder === o.id && ( )}
))}
)}
)} {/* ADDRESSES TAB */} {tab === 'wishlist' && ( )} {tab === 'referral' && ( )} {tab === 'addresses' && (
{addrModal && setAddrModal(false)} onSaved={(a)=>{ setAddresses(p=>[...p,a]); setAddrModal(false); window.FF_TOAST&&window.FF_TOAST('Address saved ✓'); }}/>}

Saved addresses

{addresses.length === 0 ? (
🏠

No saved addresses yet.

) : (
{addresses.map(a => (
{a.label} {a.is_default==1 && Default}
{a.name}
{a.line1}{a.line2 ? ', '+a.line2 : ''}
{a.city}, {a.state} — {a.pincode}
+91 {a.phone}
))}
)}
)} {/* PROFILE TAB */} {tab === 'profile' && (

Profile details

setProfName(e.target.value)}/>
setProfPhone(e.target.value.replace(/\D/g,'').slice(0,10))}/>
{/* Email verification card */}
{isVerified ? '✅ Email verified' : '⚠️ Email not verified'}
{isVerified ? `${user.email} is verified.` : 'Verify your email to secure your account and receive order updates.' }
{!isVerified && ( )}
{otpStep && !isVerified && (
{otpMsg}
setOtpCode(e.target.value.replace(/\D/g,'').slice(0,6))} placeholder="6-digit code" style={{ fontFamily:'monospace', fontSize:20, letterSpacing:'.2em', textAlign:'center', fontWeight:700 }}/>
)}

Change password

setPwCurr(e.target.value)}/>
setPwNew(e.target.value)} placeholder="Min 8 chars, upper+lower+number"/>
{pwMsg &&
{pwMsg}
}
)}
); } // Loads order tracking lazily — shows live Shiprocket data if AWB assigned function OrderTracking({ orderId }) { const [order, setOrder] = useStateA(null); useEffectA(() => { // orders.php?action=get automatically fetches live Shiprocket data // when awb_code is present — no manual sync needed FF_API.orders.get(orderId) .then(o => setOrder(o)) .catch(console.error); }, [orderId]); if (!order) return (
Loading…
); // ── Static milestone steps (always shown, top = placed, bottom = delivered) const STEPS = [ { key:'placed', label:'Order placed', icon:'📋' }, { key:'packed', label:'Packed', icon:'📦' }, { key:'shipped', label:'Shipped', icon:'🚚' }, { key:'delivery', label:'Out for delivery', icon:'🛵' }, { key:'delivered',label:'Delivered', icon:'✅' }, ]; // Map order status → which step is current const statusMap = { 'Processing': 'placed', 'Packed': 'packed', 'Shipped': 'shipped', 'Out for delivery': 'delivery', 'Delivered': 'delivered', }; const currentKey = statusMap[order.status] || 'placed'; const currentIdx = STEPS.findIndex(s => s.key === currentKey); // Live Shiprocket activities (auto-fetched by orders.php when AWB present) const activities = order.sr_tracking?.shipment_track_activities || []; const hasLive = activities.length > 0; return (
{/* AWB badge */} {order.awb_code && (

{hasLive ? 'Live Tracking' : 'Order Updates'}

{order.courier_name && {order.courier_name}} {order.awb_code}
)} {/* ── Static milestone tracker (always shown) ── */} {!order.awb_code && (

Order Updates

)}
{STEPS.map((step, i) => { const done = i < currentIdx; const current = i === currentIdx; const future = i > currentIdx; return (
{done ? '✓' : current ? '●' : ''}
{step.label}
{current && order.status === 'Processing' && (
{order.created_at ? new Date(order.created_at).toLocaleDateString('en-IN',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'}) : 'Just now'}
)} {current && order.awb_code && order.status === 'Shipped' && (
AWB: {order.awb_code}
)}
); })}
{/* ── Live Shiprocket activity feed (shown when AWB assigned) ── */} {hasLive && (
Courier updates
{order.sr_tracking?.shipment_status && (
{order.sr_tracking.shipment_status==='Delivered'?'✅': order.sr_tracking.shipment_status==='Out For Delivery'?'🛵':'🚚'} {order.sr_tracking.shipment_status} {order.sr_tracking.etd && ( · ETA {new Date(order.sr_tracking.etd).toLocaleDateString('en-IN',{day:'numeric',month:'short'})} )}
)}
{activities.slice(0,8).map((a,i) => (
{a['sr-status'] || a.activity}
{a.date}{a.location ? ' · ' + a.location : ''}
))}
)} {/* Dispatched but no activities yet */} {order.awb_code && !hasLive && (
🚚 Dispatched via {order.courier_name||'courier'} — tracking updates coming soon.
)}
); } function OrderStatusChip({ status }) { const map = { 'Delivered': { bg:'var(--ff-mint)' }, 'Out for delivery': { bg:'var(--ff-sun)' }, 'Processing': { bg:'var(--ff-lav)' }, 'Packed': { bg:'var(--ff-lav)' }, 'Shipped': { bg:'var(--ff-sky)' }, 'Refunded': { bg:'#E5E5E5' }, 'Cancelled': { bg:'#FFDDDD' }, }; const m = map[status] || { bg:'#fff' }; return {status}; } // ─── ADMIN DASHBOARD ────────────────────────────────────────────────────────── // ─── ADMIN DASHBOARD — Full control panel ──────────────────────────────────── Object.assign(window, { AccountPage, WishlistTab, OrderTracking, OrderStatusChip });