// DiveDex — Dive Boutique screens + components // ── Pearl glyph ──────────────────────────────────────────────────── function PearlIcon({ size = 14 }) { return ( ); } function PearlBalanceChip({ amount, big = false }) { return ( {amount.toLocaleString()} Pearls ); } function ItemRarityBadge({ rarity }) { return ( {rarity} ); } function EquippedBadge() { return ( EQUIPPED ); } function LockedRequirementPill({ text }) { return ( {text} ); } // ── Item thumbnail SVGs ──────────────────────────────────────────── function ItemArt({ kind, rarity = 'epic', size = 90 }) { const palette = ({ common:['#B7CCDE','#7A93AB'], uncommon:['#63FFD2','#1FB6A0'], rare:['#82ECFF','#31D7FF'], epic:['#C8B6FF','#8B7BFF'], legendary:['#FBE6A6','#D7B46A'], mythic:['#82ECFF','#FF7A6B'], })[rarity] || ['#82ECFF','#31D7FF']; const a = palette[0], b = palette[1]; return ( ); } function Glyph({ kind, grad, a, b }) { const g = `url(#${grad})`; switch (kind) { case 'suit-baroque': return {[30,40,50,60,70].map(y=>)} ; case 'suit-champagne': return ; case 'suit-neon': return ; case 'suit-stealth': return ; case 'bcd-quilt': return {[40,52,64].map(y=>)} {[32,46,60,74].map(x=>)} ; case 'bcd-gold': return ; case 'bcd-tactical': return ; case 'bcd-couture': return ; case 'fins-pearl': case 'fins-coral': return ; case 'mask-tokyo': case 'mask-gold': return ; case 'acc-charm': return ; case 'acc-camera': return ; case 'acc-snack': return 7-11 ; case 'acc-blanket': return {[24,32,40,48,56,64,72].map(x=>)} ; case 'title-shark': return WHISPERER ; case 'title-snack': return SNACK CHAMPION ; case 'title-zen': return AIR ZEN ; case 'title-crown': case 'crown': return ; case 'frame-hammer': return ; case 'frame-manta': return ; case 'frame-tokyo': return ; case 'frame-champ': return ; case 'frame-zen': return ; case 'trophy-shark': return ; case 'crest': return R S ; case 'sash': return ; case 'halo': return ; default: return ; } } // ── Card ─────────────────────────────────────────────────────────── function ShopItemCard({ item, onClick, compact = false }) { const r = item.rarity; const isEq = item.state === 'equipped'; const isLocked = item.state === 'locked'; const isAch = item.state === 'achievement'; const owned = item.state === 'owned' || isEq; return (
{isLocked && (
)} {/* When equipped, the EQUIPPED label takes the top — the card's colored frame already communicates rarity, so we hide the rarity badge to avoid an overlap on narrow phone widths. */} {isEq ?
:
}
{item.name}
{isLocked ? : isAch ? ACHIEVEMENT : owned ? {isEq?'IN LOCKER':'OWNED'} : {item.price} }
); } // ── Screens ──────────────────────────────────────────────────────── function BoutiqueScreen({ nav }) { const { state } = useDiveDex(); const [cat, setCat] = React.useState('all'); // Decorate each catalog entry with its effective per-user state so // ShopItemCard reads owned/buy/equipped from the live store. const decorated = BOUTIQUE_ITEMS.map(it => ({ ...it, state: selectBoutiqueItemState(state, it) })); const items = cat === 'all' ? decorated : decorated.filter(i => i.cat === cat); const pearls = state.currencies.pearls; return (
nav('back')}>} right={} />
BALANCE
HOW PEARLS WORK
Earn by logging dives & milestones. Spend on cosmetics only.
{[{id:'all',label:'All'}, ...BOUTIQUE_CATEGORIES].map(c => { const on = cat === c.id; return ( ); })}
{items.map(it => nav('item', it.id)}/>)}
HOW THE SHOP WORKS
XP · progression
Pearls · cosmetics
Achievements · prestige unlocks
Equip · profile flair only
); } function BoutiqueItemScreen({ nav, id }) { const { state, actions } = useDiveDex(); const catalogItem = BOUTIQUE_ITEMS.find(i => i.id === id) || BOUTIQUE_ITEMS[0]; const effective = selectBoutiqueItemState(state, catalogItem); const item = { ...catalogItem, state: effective }; const r = item.rarity; const equippedByList = selectItemEquippedBy(state, item.id); // ['rick','siet'] subset const owned = state.inventory.indexOf(item.id) >= 0; const [banner, setBanner] = React.useState(null); const flash = (kind, msg) => { setBanner({ kind, msg }); setTimeout(() => setBanner(b => (b && b.msg === msg ? null : b)), 2800); }; const onBuy = () => { const result = actions.buyShopItem(item.id); flash(result.ok ? 'ok' : 'err', result.ok ? 'Purchased.' : (result.reason || 'Could not buy.')); }; const onEquip = (userId) => { const result = actions.equipShopItem(item.id, userId); flash(result.ok ? 'ok' : 'err', result.ok ? `Equipped on ${userId === 'rick' ? 'Rick' : 'Siet'}.` : (result.reason || 'Could not equip.')); }; const onUnequip = (userId) => { actions.unequipShopItem(item.id, userId); flash('ok', `Unequipped from ${userId === 'rick' ? 'Rick' : 'Siet'}.`); }; return (
nav('back')}>} right={} />
{item.state==='equipped' && }
{item.name}
{item.cat}
"{item.flavor}"
Cosmetic only No stats {item.state==='achievement' && Achievement} {(item.state==='locked' || item.state==='achievement') && item.req && } {item.state==='not-enough' && Not enough Pearls}
PREVIEW · STORY CARD
{/* Primary action area */} {item.state === 'buy' && (
)} {item.state === 'not-enough' && (
)} {(item.state === 'locked' || item.state === 'achievement') && (
)} {owned && (
Equip on
{['rick','siet'].map(uid => { const u = state.users.find(x => x.id === uid); const isOn = equippedByList.indexOf(uid) >= 0; return ( ); })}
)} {banner && (
{banner.msg}
)}
); } function GearPreviewCard({ name, color, title, suit, frame, trophy, onChange }) { return (
{name}
{title?.name || '—'}
{[suit, frame, trophy].map((it, i) => it ? (
{it.name}
{['SUIT','FRAME','TROPHY'][i]}
) :
+ add
)}
); } function LockerScreen({ nav }) { const { state } = useDiveDex(); const byId = (id) => BOUTIQUE_ITEMS.find(i => i.id === id); const slotFor = (userId, slot) => { const itemId = (state.equippedItems[userId] || {})[slot]; return itemId ? byId(itemId) : null; }; const rick = state.users.find(u => u.id === 'rick'); const siet = state.users.find(u => u.id === 'siet'); return (
nav('back')}>} right={} />
nav('boutique')}/> nav('boutique')}/>
Prestige shelf
{['pelagic-crown','sharknado','whisperer-sash','zen-halo'].map(id => { const it = byId(id); if (!it) return null; const decorated = { ...it, state: selectBoutiqueItemState(state, it) }; return nav('item', id)}/>; })}
); } function PrestigeTrophyCard({ item, mini = false, onClick }) { const r = item.rarity; const locked = item.state === 'locked'; return (
{locked &&
}
{!mini &&
{item.name}
} {mini &&
{r.toUpperCase()}
}
); } function PrestigeScreen({ nav }) { const { state } = useDiveDex(); const list = BOUTIQUE_ITEMS .filter(i => i.cat === 'trophies' || i.cat === 'specials' || (i.cat === 'frames' && i.state === 'achievement')) .map(i => ({ ...i, state: selectBoutiqueItemState(state, i) })); return (
nav('back')}>}/>
TROPHY SHELF
Things you can't buy.
Unlocked through legendary encounters, expedition milestones and Air Zen streaks. No Pearls accepted.
{list.map(it => nav('item', it.id)}/>)}
); } function StoryFramePreview({ item }) { // Show a mini fake story card with the frame applied const r = item.rarity; const isFrame = item.cat === 'frames'; return (
{isFrame ? r.toUpperCase()+' FRAME' : 'STORY CARD'}
Mikomoto · 26 m
Scalloped Hammerhead
"Siet spotted it first. Obviously."
); } // ── QA panel for design canvas ───────────────────────────────────── function BoutiqueQAPanel() { const cell = { display:'flex', flexDirection:'column', alignItems:'center', gap: 6, padding: 6, borderRadius: 12, background:'rgba(130,236,255,0.04)', border:'1px solid rgba(130,236,255,0.10)' }; const label = { fontFamily:'JetBrains Mono', fontSize: 8, color:'rgba(255,255,255,0.55)', letterSpacing:'.06em', textAlign:'center', lineHeight: 1.2 }; return (
QA · DIVE BOUTIQUE
20 cosmetic items + states
Thumbnails grouped by category. Card states: buy · owned · equipped · locked · achievement · not-enough-pearls.
{BOUTIQUE_CATEGORIES.map(c => { const list = BOUTIQUE_ITEMS.filter(i => i.cat === c.id); if (!list.length) return null; return (
{c.label.toUpperCase()}
{list.map(it => (
{it.name}
{it.rarity} · {it.state}
))}
); })}
CARD STATES
{['buy','owned','equipped','locked','achievement'].map((st, i) => { const sample = { ...BOUTIQUE_ITEMS.find(x => x.state === st) || BOUTIQUE_ITEMS[0], state: st }; return {}}/>; })}
); } Object.assign(window, { PearlIcon, PearlBalanceChip, ItemRarityBadge, EquippedBadge, LockedRequirementPill, ItemArt, ShopItemCard, GearPreviewCard, PrestigeTrophyCard, StoryFramePreview, BoutiqueScreen, BoutiqueItemScreen, LockerScreen, PrestigeScreen, BoutiqueQAPanel, });