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 (
);
if (!items.length) return (
);
const available = items.filter(p => !(p.stock===0||p.stock==='0'));
return (
{/* Header row */}
Wishlist ({items.length})
{available.length > 1 && (
🛍 Move all to bag ({available.length})
)}
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
?
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 ✕ */}
{ e.stopPropagation(); remove(p.id); }}
style={{ position:'absolute', top:8, right:8, width:26, height:26, borderRadius:999,
background:'rgba(255,255,255,.92)', border:'none', fontSize:12, cursor:'pointer',
display:'flex', alignItems:'center', justifyContent:'center',
boxShadow:'0 1px 4px rgba(0,0,0,.15)', color:'var(--ff-ink)' }}
title="Remove from wishlist">✕
{/* 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
) : (
moveToCart(p)}
style={{ gap:6 }}>
{isAdding ? '⏳ Adding…' : '🛍 Move to bag'}
)}
location.href = `/product/${p.id}`}>
View details →
);
})}
);
}
// ── 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}
{copied ? '✓ Copied!' : '📋 Copy'}
{/* 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 }) => (
{label}{required && ' *'}
setForm(f=>({...f,[k]:e.target.value}))}/>
);
return (
e.target===e.currentTarget&&onClose()}>
{error &&
{error}
}
State *
setForm(f=>({...f,state:e.target.value}))}>
Select state
{(window.INDIA_STATES||['Andhra Pradesh','Delhi','Gujarat','Karnataka','Kerala','Madhya Pradesh','Maharashtra','Punjab','Rajasthan','Tamil Nadu','Telangana','Uttar Pradesh','West Bengal']).map(s=>(
{s}
))}
Cancel
{saving ? 'Saving…' : 'Save address'}
);
}
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
goto({page:'home', openLogin:true})}>Log in
);
}
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.
{otpLoading ? 'Sending…' : 'Verify now →'}
)}
{/* 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/>
{otpLoading ? '…' : 'Verify ✓'}
setOtpStep(false)} style={{ color:'var(--ff-muted)' }}>Cancel
0||otpLoading} style={{ marginTop:10, fontSize:12, color: otpCd>0?'var(--ff-muted)':'var(--ff-accent)', background:'none', border:'none', cursor: otpCd>0?'default':'pointer' }}>
{otpCd>0 ? `Resend in ${otpCd}s` : 'Resend code'}
)}
{(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'}
{[
{ k:'orders', l:'Orders', i: },
{ k:'wishlist', l:'Wishlist', i:'♥' },
{ k:'referral', l:'Refer & Earn', i:'🎁' },
{ k:'addresses', l:'Addresses', i: },
{ k:'profile', l: isVerified ? 'Profile' : Profile ! , i: },
].map(t => (
setTab(t.k)} style={{
padding:'10px 12px', borderRadius:10, display:'flex', alignItems:'center', gap:10,
background: tab===t.k ? 'var(--ff-ink)' : 'transparent',
color: tab===t.k ? '#fff' : 'inherit', fontWeight: tab===t.k ? 600 : 500,
fontSize:14, textAlign:'left',
}}>{t.i} {t.l}
))}
Log out
{/* ORDERS TAB */}
{tab === 'orders' && (
Your orders
{ordLoading ? (
Loading orders…
) : orders.length === 0 ? (
📦
No orders yet
Your orders will appear here after checkout.
goto({page:'shop'})}>Start shopping
) : (
{orders.map(o => (
{o.created_at?.slice(0,10)}
#{o.id}
{fmtINR(o.total)}
setOpenOrder(openOrder===o.id ? null : o.id)}>
{openOrder===o.id ? 'Hide' : 'Track'}
View order
{/* Order items thumbnails */}
{(o.items||[]).map((it, i) => (
{it.image
?
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
setAddrModal(true)}>+ Add address
{addresses.length === 0 ? (
🏠
No saved addresses yet.
setAddrModal(true)}>+ Add your first address
) : (
{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' && (
{/* 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 && (
{otpLoading ? 'Sending…' : 'Send code'}
)}
{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 }}/>
{otpLoading?'…':'Verify'}
0||otpLoading}
style={{ fontSize:12, color: otpCd>0?'var(--ff-muted)':'var(--ff-accent)', background:'none', border:'none', cursor: otpCd>0?'default':'pointer', textAlign:'left' }}>
{otpCd>0?`Resend in ${otpCd}s`:'Resend code'}
)}
)}
);
}
// 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 });