Closet

Loading…

await loadSavedOutfits(); renderSavedOutfits(); } catch (e) { toast('Save failed: ' + e.message); } } function showOutfitsForPiece(pieceId, pieceLabel) { const matching = (savedOutfits || []).filter(o => (o.pieces||[]).includes(pieceId)); $('sheet').innerHTML = `

Saved outfits with ${escapeHtml(pieceLabel)}

${matching.length === 0 ? '

No saved outfits use this piece.

' : matching.map(o => `
${escapeHtml(o.name||'Untitled')}
${escapeHtml(o.occasion || 'β€”')} Β· ${(o.pieces||[]).length} pieces
`).join('') } `; openSheet(); } async function deleteSavedOutfit(id, label) { if (!confirm('Delete "' + label + '"?')) return; try { await airtable('DELETE', OUTFITS_TABLE + '/' + id); savedOutfits = savedOutfits.filter(o => o.id !== id); toast('Deleted'); renderSavedOutfits(); } catch (e) { toast('Delete failed: ' + e.message); } } // ============================================================ // Password gate + init // ============================================================ async function decryptKeysWithPassword(password) { const r = await fetch('/keys.enc'); if (!r.ok) throw new Error('keys.enc not found'); const buf = new Uint8Array(await r.arrayBuffer()); const salt = buf.slice(0, 16); const iv = buf.slice(16, 28); const ct = buf.slice(28); const enc = new TextEncoder(); const passKey = await crypto.subtle.importKey('raw', enc.encode(password), {name:'PBKDF2'}, false, ['deriveKey']); const key = await crypto.subtle.deriveKey( {name:'PBKDF2', salt, iterations: 100000, hash: 'SHA-256'}, passKey, {name:'AES-GCM', length: 256}, false, ['decrypt'] ); const dec = await crypto.subtle.decrypt({name:'AES-GCM', iv}, key, ct); const json = JSON.parse(new TextDecoder().decode(dec)); if (json.airtable) localStorage.setItem(STORE.airtable, json.airtable); if (json.anthropic) localStorage.setItem(STORE.anthropic, json.anthropic); if (json.baseId) localStorage.setItem(STORE.baseId, json.baseId); } function showPasswordGate(errMsg) { $('content').innerHTML = `

Closet

Enter password to unlock

${errMsg || ''}

`; setTimeout(() => $('gate-pw')?.focus(), 100); } async function doUnlock() { const pw = $('gate-pw').value; if (!pw) return; $('gate-msg').textContent = 'Decrypting…'; $('gate-msg').style.color = 'var(--muted)'; try { await decryptKeysWithPassword(pw); init(); } catch (e) { $('gate-msg').textContent = 'Wrong password'; $('gate-msg').style.color = '#c00'; } } async function init() { if (!validateConfig()) { showPasswordGate(); return; } try { await Promise.all([loadItems(), loadSavedOutfits(), loadEditorialLooks().catch(() => {})]); } catch (e) { showPasswordGate('Could not load: ' + e.message); return; } showTab('browse'); } document.addEventListener('DOMContentLoaded', init); try { await Promise.all([loadItems(), loadSavedOutfits(), loadEditorialLooks().catch(() => {})]); } catch (e) { showPasswordGate('Could not load: ' + e.message); return; } showTab('browse'); } document.addEventListener('DOMContentLoaded', init); 'browse'); } document.addEventListener('DOMContentLoaded', init);