// ============================================================================ // StyleKro — WARDROBE module (split from app.jsx on 2026-06-26). // WardrobePage + MyLooksPage (+ CatIcon, categorizeWardrobeItem, WARDROBE_*). // Depends on symbols from common.jsx (loaded first). // ============================================================================ // ===== My Looks page ===== function MyLooksPage() { const { apiCall, t } = useAuth(); const confirmAction = useConfirmDialog(); const [looks, setLooks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [zoom, setZoom] = useState(null); const [sheet360LookId, setSheet360LookId] = useState(null); const fetchLookSheet360 = useCallback( () => fetchSheet360(`/merges/${sheet360LookId}/sheet360`), [sheet360LookId], ); const [shareOpen, setShareOpen] = useState(null); // look.id with open share menu (now: 'bulk' for top-bar share) const [copied, setCopied] = useState(null); const [selected, setSelected] = useState(() => new Set()); // selected look ids const [compareOpen, setCompareOpen] = useState(false); const [search, setSearch] = useState(''); const [filter, setFilter] = useState('all'); // all | shop | chat const [filterShop, setFilterShop] = useState(''); // shop name filter const [sort, setSort] = useState('new'); // new | old const [view, setView] = useState('grid'); // grid | list const [filtersOpen, setFiltersOpen] = useState(false); useEffect(() => { document.body.classList.toggle('filters-open', filtersOpen); return () => document.body.classList.remove('filters-open'); }, [filtersOpen]); const load = useCallback(async () => { setLoading(true); try { const data = await apiCall('/merges'); setLooks(data || []); } catch (e) { setError(e.message); } finally { setLoading(false); } }, [apiCall]); useEffect(() => { load(); }, [load]); async function remove(id) { if (!(await confirmAction(t('looks.deleteOne'), { confirmLabel: t('common.delete'), tone: 'danger' }))) return; try { await apiCall(`/merges/${id}`, { method: 'DELETE' }); setLooks((cur) => cur.filter((l) => l.id !== id)); setSelected((cur) => { if (!cur.has(id)) return cur; const next = new Set(cur); next.delete(id); return next; }); if (zoom?.id === id) setZoom(null); } catch (e) { setError(e.message); } } function toggleSelect(id) { setSelected((cur) => { const next = new Set(cur); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function clearSelection() { setSelected(new Set()); } async function removeSelected() { const ids = Array.from(selected); if (ids.length === 0) return; if (!(await confirmAction(t('looks.deleteMany').replace('{count}', ids.length).replace('{plural}', ids.length === 1 ? '' : 's'), { confirmLabel: t('common.delete'), tone: 'danger' }))) return; const results = await Promise.allSettled( ids.map((id) => apiCall(`/merges/${id}`, { method: 'DELETE' })) ); const failed = new Set(); results.forEach((r, i) => { if (r.status === 'rejected') failed.add(ids[i]); }); setLooks((cur) => cur.filter((l) => !ids.includes(l.id) || failed.has(l.id))); setSelected(failed); if (zoom && ids.includes(zoom.id) && !failed.has(zoom.id)) setZoom(null); if (failed.size > 0) setError(t('looks.couldNotDelete').replace('{count}', failed.size).replace('{plural}', failed.size === 1 ? '' : 's')); } function fmtDate(s) { if (!s) return ''; const d = new Date(s.replace(' ', 'T') + (s.endsWith('Z') ? '' : 'Z')); if (isNaN(d.getTime())) return s; return d.toLocaleString(); } async function copyLink(url, id) { try { await navigator.clipboard.writeText(url); setCopied(id); setTimeout(() => setCopied((c) => (c === id ? null : c)), 1800); } catch { window.prompt('Copy this link:', url); } } async function nativeShare(l, url) { const text = `My latest look, styled with AI on StyleKro${l.title ? `: ${l.title}` : ''}`; if (navigator.share) { try { await navigator.share({ title: 'My StyleKro look', text, url }); return true; } catch { /* user cancelled */ } } return false; } return (
{(() => { const total = looks.length; const shopCount = looks.filter((l) => l.source === 'shop').length; const chatCount = total - shopCount; const q = search.trim().toLowerCase(); let filtered = looks.filter((l) => { if (filter === 'shop' && l.source !== 'shop') return false; if (filter === 'chat' && l.source === 'shop') return false; if (filterShop && (l.shop_name || '') !== filterShop) return false; if (!q) return true; return ( (l.title || '').toLowerCase().includes(q) || (l.shop_name || '').toLowerCase().includes(q) ); }); filtered = [...filtered].sort((a, b) => { const ta = new Date((a.created_at || '').replace(' ', 'T')).getTime() || 0; const tb = new Date((b.created_at || '').replace(' ', 'T')).getTime() || 0; return sort === 'new' ? tb - ta : ta - tb; }); // Counts for the left-rail filters const shopNameCounts = makeCountMap( looks.filter((l) => l.source === 'shop' && l.shop_name), 'shop_name' ); const anyFilter = filter !== 'all' || !!filterShop || !!q; return ( <> {error &&

{error}

} {loading ? (

{t('boot.loading')}

) : total === 0 ? (
👗

{t('looks.empty')}

{t('looks.emptyHint')}

{t('looks.browseShops')} {t('looks.chatStylist')}
) : (
{filtersOpen &&
setFiltersOpen(false)} />}
setSearch(e.target.value)} />
{selected.size > 0 && (() => { const selectedLooks = filtered.filter((l) => selected.has(l.id)); const first = selectedLooks[0]; const firstUrl = first ? imgUrl(first.image_url) : ''; const titles = selectedLooks .map((l) => l.title) .filter(Boolean) .slice(0, 3) .join(', '); const shareText = encodeURIComponent( `My latest looks, styled with AI on StyleKro${titles ? `: ${titles}` : ''}` ); const u = encodeURIComponent(firstUrl); const isOpen = shareOpen === 'bulk'; return (
{selected.size} {t('looks.selected')}
{selected.size >= 2 && ( )} {isOpen && first && (
setShareOpen(null)}> 𝕏 / Twitter Facebook WhatsApp LinkedIn Pinterest Telegram
)}
); })()} {filtered.length === 0 ? (

{t('looks.noMatching')}

) : view === 'grid' ? (
{filtered.map((l) => { const shareUrl = imgUrl(l.image_url); const isSelected = selected.has(l.id); const inSelectMode = selected.size > 0; return (
{ if (inSelectMode) toggleSelect(l.id); else setZoom(l); }} > {l.title {l.source === 'shop' ? '🛍 Shop' : '💬 Chat'}
{inSelectMode ? (isSelected ? '' : 'Tap to select') : 'Click to view'}

{l.title || 'Try-on'}

{l.source === 'shop' && l.shop_id ? ( {l.shop_name || 'shop'} ) : l.source === 'chat' ? ( {t('looks.stylistChat')} ) : null}
); })}
) : (
{filtered.map((l) => { const shareUrl = imgUrl(l.image_url); const isSelected = selected.has(l.id); const inSelectMode = selected.size > 0; return (
{ if (inSelectMode) toggleSelect(l.id); else setZoom(l); }} />
{l.title || 'Try-on'} {l.source === 'shop' ? 'Shop' : 'Chat'}
{l.source === 'shop' && l.shop_id ? (
{l.shop_name || 'shop'}
) : null}
); })}
)}
)} ); })()} {zoom && (
setZoom(null)}>
e.stopPropagation()}>
{t('looks.savedLook')}

{zoom.title || 'Try-on'}

)} setSheet360LookId(null)} /> {compareOpen && (() => { const items = looks.filter((l) => selected.has(l.id)).slice(0, 4); if (items.length < 2) return null; return (
setCompareOpen(false)}>
e.stopPropagation()}>
{t('compare.sideBySide')}

{t('compare.compareLooks')}

{items.map((l) => (
{l.title
{l.title || 'Try-on'} {fmtDate(l.created_at)}
))}
{selected.size > 4 && (

{t('compare.showingPrefix')} {selected.size} {t('compare.showingSuffix')}

)}
); })()}
); } // ===== AI Wardrobe Page ===== const CatIcon = ({ name }) => { const common = { width: 26, height: 26, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }; switch (name) { case 'all': return (); case 'Topwear': return (); case 'Bottomwear': return (); case 'Dresses': return (); case 'Outerwear': return (); case 'Footwear': return (); case 'Bags': return (); case 'Accessories': return (); case 'Others': return (); default: return null; } }; const WARDROBE_CATEGORIES = [ { key: 'Topwear', label: 'Tops', match: ['topwear', 't-shirt', 'shirt', 'tops', 'kurti', 'blouse', 'hoodie', 'sweater', 'jacket', 'kurta'] }, { key: 'Bottomwear', label: 'Bottoms', match: ['bottomwear', 'jeans', 'trousers', 'shorts', 'leggings', 'skirts', 'pants', 'palazzo', 'chinos'] }, { key: 'Dresses', label: 'Dresses', match: ['dresses', 'gowns', 'sarees', 'salwar', 'lehenga', 'ethnic'] }, { key: 'Outerwear', label: 'Outerwear', match: ['outerwear', 'jacket', 'coat', 'blazer'] }, { key: 'Footwear', label: 'Shoes', match: ['footwear', 'sneakers', 'shoes', 'heels', 'sandals', 'boots', 'flats'] }, { key: 'Bags', label: 'Bags', match: ['handbag', 'bag', 'backpack', 'wallet'] }, { key: 'Accessories', label: 'Accessories',match: ['accessories', 'watch', 'belt', 'sunglasses', 'jewellery', 'scarf', 'hat', 'cap', 'tie', 'socks'] }, { key: 'Others', label: 'Others', match: [] }, ]; const WARDROBE_RECOMMENDATIONS = [ { key: 'brunch', title: 'Brunch Date', sub: 'Light, airy, photo-ready', emoji: '☕' }, { key: 'work', title: 'Work Meeting', sub: 'Sharp & confident', emoji: '💼' }, { key: 'evening', title: 'Evening Out', sub: 'Polished after dark', emoji: '🌙' }, { key: 'weekend', title: 'Weekend Vibes', sub: 'Effortless casual', emoji: '✨' }, { key: 'party', title: 'Party Night', sub: 'Bold statement', emoji: '🎉' }, { key: 'travel', title: 'Travel Day', sub: 'Comfy yet styled', emoji: '🧳' }, ]; function categorizeWardrobeItem(item) { const hay = `${item.group || ''} ${item.subcategory || ''} ${item.title || ''}`.toLowerCase(); for (const cat of WARDROBE_CATEGORIES) { if (cat.match.length && cat.match.some((m) => hay.includes(m))) return cat.key; } return 'Others'; } function WardrobePage() { const { apiCall, token, t } = useAuth(); const confirmAction = useConfirmDialog(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState({ current: 0, total: 0 }); const [error, setError] = useState(''); const [activeCat, setActiveCat] = useState('all'); const [selected, setSelected] = useState([]); // array of item ids const [generating, setGenerating] = useState(false); const [outfit, setOutfit] = useState(null); // { image_url, title, match } const [occasion, setOccasion] = useState(''); const [lightbox, setLightbox] = useState(null); // url string when zoomed const fileRef = useRef(null); const load = useCallback(async () => { try { setLoading(true); const data = await apiCall('/wardrobe'); setItems(Array.isArray(data) ? data : []); } catch (e) { setError(errMsg(e)); } finally { setLoading(false); } }, [apiCall]); useEffect(() => { load(); }, [load]); async function onUpload(e) { const files = Array.from(e.target.files || []); if (!files.length) return; setUploading(true); setUploadProgress({ current: 0, total: files.length }); setError(''); try { let done = 0; for (const f of files) { setUploadProgress({ current: done + 1, total: files.length }); const fd = new FormData(); fd.append('photo', f); const created = await apiCall('/wardrobe', { method: 'POST', body: fd }); setItems((prev) => [created, ...prev]); done += 1; } } catch (err) { setError(errMsg(err)); } finally { setUploading(false); setUploadProgress({ current: 0, total: 0 }); if (fileRef.current) fileRef.current.value = ''; } } function toggleSelect(id) { setSelected((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : (prev.length >= 6 ? prev : [...prev, id])); } async function removeItem(id) { if (!(await confirmAction(t('wardrobe.confirmRemove'), { confirmLabel: t('common.delete'), tone: 'danger' }))) return; try { await apiCall(`/wardrobe/${id}`, { method: 'DELETE' }); setItems((prev) => prev.filter((x) => x.id !== id)); setSelected((prev) => prev.filter((x) => x !== id)); } catch (err) { setError(errMsg(err)); } } async function generateOutfit(occLabel) { if (!selected.length) { setError(t('wardrobe.pickOne')); return; } setError(''); setGenerating(true); setOutfit(null); const titleLabel = occLabel || occasion || 'Clean & Casual'; const match = Math.min(99, 78 + selected.length * 3); const pickedItems = items.filter((it) => selected.includes(it.id)); try { const body = JSON.stringify({ item_ids: selected, occasion: occLabel || occasion || null }); let gotFinal = false; await streamSSE('/wardrobe/outfit/stream', { body, token, headers: { 'Content-Type': 'application/json' }, onEvent: (evt, data) => { if (evt === 'tryon_fast') { setOutfit({ image_url: imgUrl(data.image_url), title: titleLabel, match, items: pickedItems, pending: true, }); } else if (evt === 'tryon_final') { gotFinal = true; setOutfit({ image_url: imgUrl(data.image_url), title: titleLabel, match, items: pickedItems, pending: false, }); } else if (evt === 'tryon_error') { setError(data.detail || 'Try-on failed'); } }, }); if (!gotFinal) setGenerating(false); } catch (err) { setError(errMsg(err)); } finally { setGenerating(false); } } const itemsByCat = {}; for (const it of items) { const k = categorizeWardrobeItem(it); (itemsByCat[k] = itemsByCat[k] || []).push(it); } const filteredItems = activeCat === 'all' ? items : (itemsByCat[activeCat] || []); const selectedItems = items.filter((it) => selected.includes(it.id)); return (
{error &&
{error}
} {/* === Upload card === */}
{/* === Categories === */}

{t('wardrobe.category')}

{items.length} {items.length === 1 ? t('wardrobe.items') : t('wardrobe.itemsPlural')}
{(() => { // rank top 3 non-empty cats for badges const ranked = [...WARDROBE_CATEGORIES] .map((c) => ({ key: c.key, n: (itemsByCat[c.key] || []).length })) .filter((c) => c.n > 0) .sort((a, b) => b.n - a.n); const badgeFor = (key) => { const idx = ranked.findIndex((r) => r.key === key); if (idx === 0) return 'Most Worn'; if (idx === 1) return 'Trending'; if (idx === 2) return 'New'; return null; }; return WARDROBE_CATEGORIES.map((cat) => { const list = itemsByCat[cat.key] || []; const badge = badgeFor(cat.key); return ( ); }); })()}
{/* === Item grid === */} {loading ? (
{t('wardrobe.loading')}
) : filteredItems.length === 0 ? (

{activeCat === 'all' ? t('wardrobe.empty') : `No ${activeCat} yet`}

{activeCat === 'all' ? t('wardrobe.emptyHint') : 'Upload to start building this category.'}

) : (
{filteredItems.map((it) => { const isSel = selected.includes(it.id); return (
{(() => { const primary = it.title || it.subcategory || 'Item'; const secondary = it.subcategory || it.group || ''; const showSecondary = secondary && secondary.toLowerCase() !== primary.toLowerCase(); return ( <> {primary} {showSecondary && {secondary}} ); })()}
); })}
)} {/* === Floating Try On bar === */} {selected.length > 0 && (
{selected.length} generateOutfit('')} />
)} {/* === AI Recommendation card === */} {(outfit || generating) && (
{generating && !outfit &&
} {outfit && ( Generated outfit setLightbox(outfit.image_url)} title="Click to zoom" /> )} {outfit?.pending && (
)}
)} {lightbox && (
setLightbox(null)}>
)}
); }