// Florio Analytics — Shop tab (ported from js/pages/shop.js). // Cosmetic-shop admin config, rebuilt in the new design kit. NOT range-based. // // Behaviour preserved exactly from the old vanilla page: // • Rarity prices — set a coin price for each rarity tier (saved to // appConfig/shop.rarityPrices) // • Item rarities — assign a rarity to each catalog item (saved to // appConfig/shop.itemRarities); price is derived from rarity // • Weekly shop — pick 6 items for "this week" + "next week", each item must // have a rarity (and that rarity must have a price). Saved to // appConfig/shop.weeks[mondayStr] = [{ itemId, slot, rarity, price }] // • Item picker modal (dedupes items already used in that week) // // Reads/writes the SAME Firestore doc the old page used (appConfig/shop) via // window.FlorioDB: { db, doc, getDoc, setDoc }. (function () { const NS = window.HyperChartsDesignSystem_e51a95; const I = window.HyperIcons; const { useState, useEffect, useRef } = React; // ─── Item Catalog (verbatim from shop.js) ────────────────────────────────── const ITEMS = [ { id: "BirdNestHat", label: "Bird Nest", slot: "hatwearEnum", img: "birdNestHat.png" }, { id: "LilyPadHat", label: "Lily Pad", slot: "hatwearEnum", img: "lilyPadHat.png" }, { id: "TulipHat", label: "Tulip", slot: "hatwearEnum", img: "RoseBulbHat.png" }, { id: "CloudHat", label: "Cloud", slot: "hatwearEnum", img: "Cloud.png" }, { id: "TopHat", label: "Top Hat", slot: "hatwearEnum", img: "TopHat.png" }, { id: "TrafficConeHat", label: "Traffic Cone", slot: "hatwearEnum", img: "TrafficConeHat.png" }, { id: "Beanie", label: "Beanie", slot: "hatwearEnum", img: "Beanie.png" }, { id: "StrawHat", label: "Straw Hat", slot: "hatwearEnum", img: "StrawHat.png" }, { id: "GraduationHat", label: "Graduation", slot: "hatwearEnum", img: "GraduationCap.png" }, { id: "SkiGoggles", label: "Ski Goggles", slot: "eyewearEnum", img: "SkiGoggles.png" }, { id: "LedGlasses", label: "LED Glasses", slot: "eyewearEnum", img: "NeonGlasses.png" }, { id: "SunGlasses", label: "Sunglasses", slot: "eyewearEnum", img: "sunglasses.png" }, { id: "SnorkelGlasses", label: "Snorkel", slot: "eyewearEnum", img: "snorkelGlasses.png" }, { id: "AviationGlasses", label: "Aviation", slot: "eyewearEnum", img: "aviationGlasses.png" }, { id: "ComicDisquiseGlasses", label: "Comic Disguise", slot: "eyewearEnum", img: "ComicDisguiseGlasses.png" }, { id: "PixelGlasses", label: "Pixel", slot: "eyewearEnum", img: "PixelGlasses.png" }, { id: "monacle", label: "Monocle", slot: "eyewearEnum", img: "Monacle.png" }, { id: "VrHeadset", label: "VR Headset", slot: "eyewearEnum", img: "VRHeadset.png" }, { id: "GoldenBracelet", label: "Golden", slot: "jewelleryEnum", img: "GoldenBracelet.png" }, { id: "LedBracelet", label: "LED", slot: "jewelleryEnum", img: "Led-bracelet.png" }, { id: "SnakeBracelet", label: "Snake", slot: "jewelleryEnum", img: "SnakeBracelet.png" }, { id: "VineBracelet", label: "Vine", slot: "jewelleryEnum", img: "VineBracelet.png" }, { id: "BeadBracelet", label: "Bead", slot: "jewelleryEnum", img: "BeadBracelet.png" }, { id: "WaterBracelet", label: "Water", slot: "jewelleryEnum", img: "WaterBracelet.png" }, { id: "NeonPants", label: "Neon", slot: "lowerBodyEnum", img: "neonPants.png" }, { id: "LeopardSwimmingPants", label: "Leopard", slot: "lowerBodyEnum", img: "LeopardSwimmingPants.png" }, { id: "Diaper", label: "Diaper", slot: "lowerBodyEnum", img: "Diaper.png" }, { id: "OrangeSwimmingPants", label: "Orange Swimming", slot: "lowerBodyEnum", img: "orangeSwimPants.png" }, ]; // The old page lives at js/pages/ → uses "../assets/...". This tab lives in // app/ (one level deeper than index.html's root sibling), so prepend one more // "../" so the path still resolves to the web-root assets dir. const IMG_BASE = "../../assets/images/store/"; const ITEM_MAP = {}; for (const item of ITEMS) ITEM_MAP[item.id] = item; const SLOT_LABELS = { hatwearEnum: "Hats", eyewearEnum: "Eyewear", jewelleryEnum: "Jewellery", lowerBodyEnum: "Pants", }; const SLOT_KEYS = Object.keys(SLOT_LABELS); const RARITIES = [ { id: "common", label: "Common", color: "#8E8E93" }, { id: "uncommon", label: "Uncommon", color: "#34C759" }, { id: "rare", label: "Rare", color: "#007AFF" }, { id: "epic", label: "Epic", color: "#AF52DE" }, { id: "legendary", label: "Legendary", color: "#FF9500" }, ]; const RARITY_MAP = {}; for (const r of RARITIES) RARITY_MAP[r.id] = r; // ─── Date helpers (verbatim logic from shop.js) ──────────────────────────── function getMonday(date) { const d = new Date(date); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); d.setDate(diff); return d; } function fmtDate(d) { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function fmtWeekRange(mondayStr) { const mon = new Date(mondayStr + "T00:00:00"); const sun = new Date(mon); sun.setDate(sun.getDate() + 6); const m = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; return `${m[mon.getMonth()]} ${mon.getDate()} – ${m[sun.getMonth()]} ${sun.getDate()}`; } const currentWeek = fmtDate(getMonday(new Date())); const _nextMon = getMonday(new Date()); _nextMon.setDate(_nextMon.getDate() + 7); const nextWeek = fmtDate(_nextMon); const FB = () => window.FlorioDB; // ─── Small presentational helpers ────────────────────────────────────────── function InlineStatus({ status }) { if (!status || !status.msg) return null; const ok = status.type === "ok"; return ( {status.msg} ); } function SaveButton({ onClick, children, disabled }) { return ( ); } function ItemImg({ img, label, size = 52 }) { const [err, setErr] = useState(false); if (err) { return (
{I.grid ? I.grid({ size: Math.round(size * 0.4) }) : null}
); } return ( {label} setErr(true)} style={{ width: size, height: size, objectFit: "contain" }} /> ); } function RarityTag({ rarity, children }) { const ro = RARITY_MAP[rarity]; if (!ro) return null; return ( {children != null ? children : ro.label} ); } // ─── Main component ───────────────────────────────────────────────────────── function Shop() { // shopData mirrors appConfig/shop. null === loading. const [shopData, setShopData] = useState(null); const [loadError, setLoadError] = useState(false); // local editable state const [prices, setPrices] = useState({}); // { rarityId: number } const [itemRarities, setItemRarities] = useState({}); // { itemId: rarityId } const [weekItems, setWeekItems] = useState({ current: [], next: [] }); // arrays of itemId|null (len 6) // per-section status messages const [statusPrices, setStatusPrices] = useState(null); const [statusRarities, setStatusRarities] = useState(null); const [statusWeek, setStatusWeek] = useState({ current: null, next: null }); // item picker modal const [picker, setPicker] = useState(null); // { prefix, index } | null const statusTimers = useRef({}); function flashStatus(setter, key, msg, type) { setter(key ? (prev) => ({ ...prev, [key]: { msg, type } }) : { msg, type }); const tk = (key || "") + "_" + (setter.name || Math.random()); if (statusTimers.current[tk]) clearTimeout(statusTimers.current[tk]); statusTimers.current[tk] = setTimeout(() => { setter(key ? (prev) => ({ ...prev, [key]: null }) : null); }, 4000); } useEffect(() => { let alive = true; (async () => { await window.FlorioReady; const { db, doc, getDoc } = FB(); let data = {}; try { const snap = await getDoc(doc(db, "appConfig", "shop")); data = snap.exists() ? (snap.data() || {}) : {}; } catch (e) { console.error("[Shop] load failed:", e); if (!alive) return; setLoadError(true); data = {}; } if (!alive) return; // hydrate local editable state const rp = data.rarityPrices || {}; const np = {}; for (const r of RARITIES) if (rp[r.id]) np[r.id] = rp[r.id]; setPrices(np); setItemRarities({ ...(data.itemRarities || {}) }); const hydrateWeek = (mondayStr) => { const arr = new Array(6).fill(null); const list = data.weeks && data.weeks[mondayStr]; if (Array.isArray(list)) { for (let i = 0; i < Math.min(list.length, 6); i++) { const wi = list[i]; if (wi && wi.itemId && ITEM_MAP[wi.itemId]) arr[i] = wi.itemId; } } return arr; }; setWeekItems({ current: hydrateWeek(currentWeek), next: hydrateWeek(nextWeek) }); setShopData(data); })(); return () => { alive = false; }; }, []); // ── Save: rarity prices ── async function saveRarityPrices() { const out = {}; for (const r of RARITIES) { const val = parseInt(prices[r.id], 10); if (!val || val <= 0) { flashStatus(setStatusPrices, null, `Set a price for ${r.label}`, "err"); return; } out[r.id] = val; } try { const { db, doc, setDoc } = FB(); await setDoc(doc(db, "appConfig", "shop"), { rarityPrices: out }, { merge: true }); setShopData((p) => ({ ...(p || {}), rarityPrices: out })); flashStatus(setStatusPrices, null, "Saved", "ok"); } catch (e) { flashStatus(setStatusPrices, null, "Failed: " + e.message, "err"); } } // ── Save: item rarities ── async function saveItemRarities() { const out = {}; for (const item of ITEMS) { if (itemRarities[item.id]) out[item.id] = itemRarities[item.id]; } try { const { db, doc, setDoc } = FB(); await setDoc(doc(db, "appConfig", "shop"), { itemRarities: out }, { merge: true }); setShopData((p) => ({ ...(p || {}), itemRarities: out })); flashStatus(setStatusRarities, null, `Saved ${Object.keys(out).length} items`, "ok"); } catch (e) { flashStatus(setStatusRarities, null, "Failed: " + e.message, "err"); } } // ── Save: a week column ── async function saveWeek(prefix, mondayStr) { const slots = weekItems[prefix] || []; const items = []; for (let i = 0; i < 6; i++) { const itemId = slots[i]; if (!itemId) continue; const itemInfo = ITEM_MAP[itemId]; const rarity = itemRarities[itemId]; if (!rarity) { flashStatus(setStatusWeek, prefix, `"${(itemInfo && itemInfo.label) || itemId}" has no rarity set`, "err"); return; } const price = prices[rarity]; if (!price) { flashStatus(setStatusWeek, prefix, `No price set for ${rarity} rarity`, "err"); return; } items.push({ itemId, slot: itemInfo.slot, rarity, price }); } if (items.length === 0) { flashStatus(setStatusWeek, prefix, "Add at least one item", "err"); return; } try { const { db, doc, setDoc } = FB(); const weeks = { ...((shopData && shopData.weeks) || {}) }; weeks[mondayStr] = items; await setDoc(doc(db, "appConfig", "shop"), { currentWeek, nextWeek, weeks }, { merge: true }); setShopData((p) => ({ ...(p || {}), weeks, currentWeek, nextWeek })); flashStatus(setStatusWeek, prefix, `Saved ${items.length} items`, "ok"); } catch (e) { flashStatus(setStatusWeek, prefix, "Failed: " + e.message, "err"); } } // ── Week slot mutations ── function fillSlot(prefix, index, itemId) { setWeekItems((p) => { const next = { ...p, [prefix]: [...p[prefix]] }; next[prefix][index] = itemId; return next; }); } function clearSlot(prefix, index) { setWeekItems((p) => { const next = { ...p, [prefix]: [...p[prefix]] }; next[prefix][index] = null; return next; }); } // ── Loading state ── if (!shopData) { return (
); } const P = window.PA; return (
{loadError && (
Couldn't load the saved shop config — starting from a blank config. Saving will overwrite.
)} {/* ── Section 1: Rarity prices ── */}
{RARITIES.map((r) => (
{r.label}
{I.coin ? I.coin({ size: 16 }) : "🪙"} setPrices((p) => ({ ...p, [r.id]: e.target.value }))} style={{ width: 70, fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 700, textAlign: "right", color: "var(--text-primary)", border: "1px solid var(--border-default)", borderRadius: 8, padding: "6px 8px", background: "var(--surface-card)", }} />
))}
Save Prices
{/* ── Section 2: Item rarities ── */} {SLOT_KEYS.map((slot) => { const items = ITEMS.filter((i) => i.slot === slot); return (
{SLOT_LABELS[slot]}
{items.map((item) => { const sel = itemRarities[item.id]; const selObj = sel ? RARITY_MAP[sel] : null; return (
{item.label}
{RARITIES.map((r) => { const active = sel === r.id; return (
{selObj ? selObj.label : ""}
); })}
); })}
Save Rarities
{/* ── Section 3: Weekly shop ── */}
{[ { prefix: "current", monday: currentWeek, title: "This Week" }, { prefix: "next", monday: nextWeek, title: "Next Week" }, ].map((col) => (
{col.title}
{fmtWeekRange(col.monday)}
saveWeek(col.prefix, col.monday)}>Save
{statusWeek[col.prefix] && statusWeek[col.prefix].msg && (
)}
{Array.from({ length: 6 }).map((_, i) => { const itemId = weekItems[col.prefix][i]; const item = itemId ? ITEM_MAP[itemId] : null; const rarity = item ? itemRarities[itemId] : null; if (!item) { return ( ); } return (
{item.label} {rarity && }
); })}
))}
{/* ── Item picker modal ── */} {picker && (
{ if (e.target === e.currentTarget) setPicker(null); }} style={{ position: "fixed", inset: 0, zIndex: 1000, background: "rgba(0,0,0,.4)", display: "grid", placeItems: "center", padding: 24, }}>
Select Item
{ fillSlot(picker.prefix, picker.index, itemId); setPicker(null); }} />
)}
); } // ─── Picker body (grouped by slot, dedupes already-used items) ────────────── function PickerBody({ used, itemRarities, prices, onPick }) { return (
{SLOT_KEYS.map((slot) => { const items = ITEMS.filter((i) => i.slot === slot); return (
{SLOT_LABELS[slot]}
{items.map((item) => { const isUsed = used.has(item.id); const rarity = itemRarities[item.id]; const ro = rarity ? RARITY_MAP[rarity] : null; const price = rarity ? (prices[rarity] != null ? prices[rarity] : "?") : "?"; return ( ); })}
); })}
); } (window.FlorioTabs = window.FlorioTabs || {}).shop = Shop; })();