Siteβ―Builder
Editing:
index.html
writable 0666
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Best Deal On β Hot/Cold Price (Beta)</title> <meta name="description" content="Best Deal On β Hot/Cold Price: guess the price in a few tries. Daily categories and a collapsible settings panel." /> <style> :root{ --bg:#0b0c10; --panel:#10131a; --ink:#e9edf1; --muted:#a6b0bf; --accent:#7c5cff; --accent-2:#00d4ff; --good:#00c853; --bad:#ff5252; --warn:#ffb300; --card:#141926; --card-border:rgba(255,255,255,.08); --shadow:0 10px 30px rgba(0,0,0,0.35); --r:14px; } @media (prefers-color-scheme: light){ :root{ --bg:#f6f7fb; --panel:#ffffff; --ink:#0c0d11; --muted:#5a6472; --card:#ffffff; --card-border:rgba(0,0,0,.08); --shadow:0 12px 30px rgba(0,0,0,.08); } } *{ box-sizing:border-box } body{ margin:0; background: radial-gradient(1200px 900px at 110% -20%, rgba(124,92,255,.18), transparent 60%), radial-gradient(900px 700px at -20% 30%, rgba(0,212,255,.12), transparent 60%), var(--bg); color:var(--ink); font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial; min-height:100svh; display:flex; flex-direction:column; } header{ padding:16px clamp(14px,4vw,32px); display:flex; align-items:center; justify-content:space-between; gap:12px } .brand{ display:flex; align-items:center; gap:10px } .logo{ inline-size:38px; block-size:38px; border-radius:10px; background: conic-gradient(from 220deg, var(--accent), var(--accent-2)); display:grid; place-items:center; color:white; font-weight:900; box-shadow: var(--shadow); } .title{ margin:0; font-size:18px; letter-spacing:.3px } .pill{ padding:6px 10px; border-radius:999px; background:#ffecb3; color:#5d4100; font-size:12px; font-weight:700 } .meta{ color:var(--muted); font-size:13px; display:flex; gap:10px; flex-wrap:wrap } main{ width:min(1000px, 92vw); margin: 10px auto 40px; display:grid; gap:16px } .catbar{ display:flex; gap:8px; flex-wrap:wrap; align-items:center; background: var(--panel); border:1px solid var(--card-border); border-radius: 12px; padding:10px; box-shadow: var(--shadow); } .chip{ appearance:none; border:1px solid var(--card-border); background:var(--card); color:var(--ink); padding:8px 12px; border-radius:999px; cursor:pointer; font-weight:700; font-size:13px; } .chip.active{ background: linear-gradient(135deg, var(--accent), var(--accent-2)); color:white; border-color:transparent } .wrap{ background: var(--panel); border:1px solid var(--card-border); border-radius: var(--r); box-shadow: var(--shadow); padding: clamp(16px,4vw,24px); display:grid; gap:16px } .grid{ display:grid; gap:16px } .cols-2{ grid-template-columns: 1.1fr .9fr } @media (max-width: 880px){ .cols-2{ grid-template-columns: 1fr } } .card{ background: var(--card); border:1px solid var(--card-border); border-radius: var(--r); box-shadow: var(--shadow); padding:14px; display:grid; gap:12px } .imgwrap{ border-radius: 12px; background: linear-gradient(145deg, rgba(255,255,255,.08), rgba(0,0,0,.06)); display:grid; place-items:center; min-block-size:220px; overflow:hidden; position:relative; } .imgwrap img{ max-width:100%; max-height:320px; object-fit:contain } .imgwrap svg{ width:88%; height:auto; opacity:.85 } .title2{ font-weight:800; line-height:1.3 } .controls{ display:flex; align-items:center; gap:12px; flex-wrap:wrap } input[type="text"], input[type="number"]{ padding:12px 14px; border-radius:12px; border:1px solid var(--card-border); background:var(--card); color:var(--ink); font-size:16px } button{ appearance:none; border:none; padding:12px 16px; border-radius:12px; cursor:pointer; font-weight:800; background: linear-gradient(135deg, var(--accent), var(--accent-2)); color:white; box-shadow: var(--shadow) } button.secondary{ background: transparent; color: var(--ink); border:1px solid var(--card-border) } .feedback{ display:grid; gap:8px; font-size:14px } .thermo{ height:10px; border-radius:999px; background: linear-gradient(90deg, #1e88e5, #ff7043, #ff1744); border:1px solid var(--card-border) } .scorebar{ display:flex; gap:12px; flex-wrap:wrap; align-items:center; font-size:14px; background: var(--panel); border:1px solid var(--card-border); border-radius: var(--r); padding:12px 14px } .score{ font-weight:900 } .reveal{ border:1px solid var(--card-border); border-radius: var(--r); padding:14px; display:grid; gap:10px; background: linear-gradient(160deg, rgba(0,200,83,.08), rgba(124,92,255,.08)) } .asof{ font-size:12px; color:var(--muted) } .muted{ color:var(--muted) } footer{ margin-top:auto; padding:20px clamp(16px,4vw,32px); color:var(--muted); font-size:12px } /* Collapsible settings */ details.settings{ background: var(--panel); border:1px solid var(--card-border); border-radius: 12px; box-shadow: var(--shadow); } details.settings > summary{ list-style:none; cursor:pointer; padding:12px 14px; font-weight:800; display:flex; align-items:center; justify-content:space-between; } details.settings > summary::after{ content:'βΎ'; opacity:.8 } details.settings[open] > summary::after{ content:'β΄' } .settings-body{ padding: 0 14px 14px; display:grid; gap:12px } .row{ display:flex; gap:12px; align-items:center; flex-wrap:wrap } .toast{ position:fixed; inset:auto 16px 16px auto; background:#1f2330; color:#fff; padding:10px 12px; border-radius:10px; opacity:.98 } .hidden{ display:none !important } </style> </head> <body> <header> <div class="brand"> <div class="logo" aria-hidden="true">B</div> <div> <p class="title" style="margin:0">Best Deal On</p> <div class="muted" style="font-size:12px">Hot/Cold Price β guess the price in a few tries</div> </div> </div> <div class="pill" id="demoPill">DEMO DATA</div> </header> <main> <div class="meta" style="padding:0 clamp(14px,4vw,32px)"> <span id="gameId" class="pill" style="background:rgba(255,255,255,.08); color:inherit">β</span> <span>Valid: <b id="validRange">β</b></span> </div> <!-- Category chips --> <nav class="catbar" id="catBar" aria-label="Categories" style="margin:0 clamp(14px,4vw,32px)"></nav> <!-- Game area --> <section class="wrap" style="margin:0 clamp(14px,4vw,32px)"> <div class="grid cols-2"> <article class="card"> <div class="imgwrap" id="imgWrap"><!-- image or placeholder here --></div> <div class="title2" id="prodTitle">β</div> <div class="meta"> <span>Marketplace: <b id="market">β</b></span> <span>Item <b id="itemIndex">0</b>/<b id="itemTotal">0</b> in <b id="catName">β</b></span> </div> <div class="controls"> <button id="startBtn">Start</button> <a id="buyBtn" class="secondary" target="_blank" rel="noopener sponsored nofollow" href="#">Buy on Amazon</a> <button id="skipBtn" class="secondary">Skip</button> </div> <div class="muted" style="font-size:12px" id="availability">β</div> </article> <article class="card" id="playCard"> <div class="controls"> <label for="guess">Your guess:</label> <input id="guess" type="text" inputmode="decimal" placeholder="$0.00" autocomplete="off" /> <button id="submitBtn">Guess</button> <button id="giveUpBtn" class="secondary">Give up</button> </div> <div class="feedback" aria-live="polite"> <div><b>Status:</b> <span id="status">Select a category and press Start.</span></div> <div><b>Hint:</b> <span id="hint">β</span></div> <div class="thermo" aria-hidden="true"></div> <div class="meta"> Tries used: <b id="used">0</b>/<b id="max">0</b> </div> </div> <div class="reveal hidden" id="revealBox" aria-live="polite"></div> </article> </div> <div class="scorebar"> <div>Best Miss (this item): <span class="score" id="bestMiss">$0.00</span></div> <div>Lifetime Dollars Off: <span class="score" id="lifeMiss">$0.00</span></div> </div> </section> <!-- Collapsible settings at bottom --> <details class="settings" id="settingsPanel" style="margin:0 clamp(14px,4vw,32px)"> <summary>Settings</summary> <div class="settings-body"> <div class="row"> <label for="tries">Tries per item:</label> <input id="tries" type="number" min="1" max="7" value="3" /> </div> <div class="row"> <label for="hintMode">Hints:</label> <select id="hintMode"> <option value="none">No hints</option> <option value="relative">Hot/Cold only</option> <option value="rel_dollars" selected>Hot/Cold + dollars off</option> <option value="rel_dollars_pct">Hot/Cold + dollars & percent</option> </select> </div> <div class="row"> <input id="useTestImg" type="checkbox" checked /> <label for="useTestImg">Use testing image for products (/amazon/hero.png)</label> </div> <div class="row"> <button id="applyBtn">Apply</button> <button id="resetStatsBtn" class="secondary">Reset lifetime dollars off</button> </div> </div> </details> </main> <footer> <div style="margin-bottom:6px"> <b>Associates disclosure:</b> As an Amazon Associate I earn from qualifying purchases. </div> <div style="opacity:.85"> Certain content that appears on this site comes from Amazon. This content is provided βas isβ and is subject to change or removal at any time. </div> </footer> <div id="toast" class="toast hidden" role="status" aria-live="polite"></div> <script> (function(){ const els = { demoPill: document.getElementById('demoPill'), gameId: document.getElementById('gameId'), validRange: document.getElementById('validRange'), catBar: document.getElementById('catBar'), imgWrap: document.getElementById('imgWrap'), prodTitle: document.getElementById('prodTitle'), market: document.getElementById('market'), itemIndex: document.getElementById('itemIndex'), itemTotal: document.getElementById('itemTotal'), catName: document.getElementById('catName'), availability: document.getElementById('availability'), startBtn: document.getElementById('startBtn'), skipBtn: document.getElementById('skipBtn'), buyBtn: document.getElementById('buyBtn'), guess: document.getElementById('guess'), submitBtn: document.getElementById('submitBtn'), giveUpBtn: document.getElementById('giveUpBtn'), status: document.getElementById('status'), hint: document.getElementById('hint'), used: document.getElementById('used'), max: document.getElementById('max'), revealBox: document.getElementById('revealBox'), bestMiss: document.getElementById('bestMiss'), lifeMiss: document.getElementById('lifeMiss'), tries: document.getElementById('tries'), hintMode: document.getElementById('hintMode'), useTestImg: document.getElementById('useTestImg'), applyBtn: document.getElementById('applyBtn'), resetStatsBtn: document.getElementById('resetStatsBtn') }; // Local storage keys (numbers/flags only; never Program Content) const LIFE_KEY = 'bdo_hotcold_lifetime_off_cents_v3'; const TRIES_KEY = 'bdo_hotcold_tries_v3'; const HINTMODE_KEY = 'bdo_hotcold_hintmode_v1'; const TESTIMG_KEY = 'bdo_hotcold_use_testimg_v1'; function toCents(input){ if (typeof input === 'number') return Math.round(input * 100); const s = (input || '').toString().replace(/[^0-9.]/g,''); if (!s) return 0; const v = Number.parseFloat(s); if (!Number.isFinite(v)) return 0; return Math.round(v * 100); } function money(cents, currency){ try{ return new Intl.NumberFormat(undefined, { style:'currency', currency }).format((cents||0)/100); } catch(e){ return '$' + ((cents||0)/100).toFixed(2); } } function fmtAsOf(iso){ if(!iso) return 'β'; try{ const d = new Date(iso); const date = new Intl.DateTimeFormat(undefined, {year:'numeric',month:'short',day:'2-digit'}).format(d); const time = new Intl.DateTimeFormat(undefined, {hour:'2-digit',minute:'2-digit'}).format(d); return `${date} ${time}`; }catch(e){ return iso; } } function showToast(msg){ const t = document.querySelector('.toast'); t.textContent = msg; t.classList.remove('hidden'); setTimeout(()=> t.classList.add('hidden'), 1900); } function placeholderSVG(text){ const t = (text||'Item').slice(0,42).replace(/[&<]/g, s => s === '&' ? '&' : '<'); return ` <svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Product image placeholder"> <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="rgba(255,255,255,.12)"/><stop offset="100%" stop-color="rgba(0,0,0,.06)"/> </linearGradient></defs> <rect width="600" height="400" rx="18" fill="url(#g)"/> <g fill="none" stroke="rgba(255,255,255,.35)" stroke-width="12"> <rect x="90" y="70" width="420" height="260" rx="22"/><path d="M150 320h300"/> </g> <text x="50%" y="52%" dominant-baseline="middle" text-anchor="middle" font-size="30" fill="rgba(255,255,255,.85)" font-family="ui-sans-serif,system-ui,Arial">${t}</text> </svg>`; } function bandName(pct){ if (pct <= 1) return 'Boiling π₯'; if (pct <= 3) return 'Hot'; if (pct <= 7) return 'Warm'; if (pct <= 15) return 'Cool'; return 'Cold'; } // State let DATA = null; let currentCat = null; let idx = 0; let maxTries = 3; let usedTries = 0; let bestMissCents = null; let lastDiff = null; let hintMode = 'rel_dollars'; // default let useTestImage = true; function hydrateSettings(){ const savedTries = parseInt(localStorage.getItem(TRIES_KEY) || '3', 10); if (Number.isFinite(savedTries)) { maxTries = Math.min(7, Math.max(1, savedTries)); els.tries.value = String(maxTries); } const savedHM = localStorage.getItem(HINTMODE_KEY); if (savedHM && ['none','relative','rel_dollars','rel_dollars_pct'].includes(savedHM)){ hintMode = savedHM; } els.hintMode.value = hintMode; const savedTest = localStorage.getItem(TESTIMG_KEY); useTestImage = (savedTest === null) ? true : savedTest === '1'; els.useTestImg.checked = useTestImage; const life = parseInt(localStorage.getItem(LIFE_KEY)||'0', 10) || 0; els.lifeMiss.textContent = DATA ? money(life, DATA.marketplace.currency) : '$0.00'; } function buildCatBar(){ els.catBar.innerHTML = ''; DATA.categories.forEach((c, i)=>{ const btn = document.createElement('button'); btn.className = 'chip' + (i===0 ? ' active' : ''); btn.textContent = c.name; btn.addEventListener('click', ()=> selectCategory(c, btn)); els.catBar.appendChild(btn); }); selectCategory(DATA.categories[0], els.catBar.querySelector('.chip')); } function selectCategory(cat, btn){ currentCat = cat; document.querySelectorAll('.chip').forEach(el => el.classList.remove('active')); if (btn) btn.classList.add('active'); idx = 0; renderItem(); } function currentItem(){ return currentCat ? currentCat.items[idx] : null; } function setImageForItem(p){ els.imgWrap.innerHTML = ''; const testURL = DATA.testing_image_url; if (useTestImage && testURL){ const img = document.createElement('img'); img.alt = p.title || 'Product image'; img.decoding = 'async'; img.loading = 'lazy'; img.src = testURL; els.imgWrap.appendChild(img); return; } if (p.image_url){ const img = document.createElement('img'); img.alt = p.title || 'Product image'; img.decoding = 'async'; img.loading = 'lazy'; img.src = p.image_url; els.imgWrap.appendChild(img); } else { els.imgWrap.innerHTML = placeholderSVG(p.title); } } function renderItem(){ const p = currentItem(); if (!p){ finishCategory(); return; } els.catName.textContent = currentCat.name; els.itemIndex.textContent = String(idx+1); els.itemTotal.textContent = String(currentCat.items.length); els.prodTitle.textContent = p.title || 'β'; els.market.textContent = `${DATA.marketplace.code} (${DATA.marketplace.currency})`; els.buyBtn.href = `https://${DATA.marketplace.domain}/dp/${encodeURIComponent(p.asin)}?tag=${encodeURIComponent(DATA.marketplace.tag)}`; els.availability.textContent = p.availability || ''; setImageForItem(p); // Reset round state usedTries = 0; bestMissCents = null; lastDiff = null; els.status.textContent = 'Press Start to begin.'; els.hint.textContent = 'β'; els.used.textContent = '0'; els.max.textContent = String(maxTries); els.revealBox.classList.add('hidden'); els.guess.value = ''; } function finishCategory(){ document.querySelector('#playCard').innerHTML = ` <div class="reveal" aria-live="polite"> <div><b>Category complete π</b></div> <div class="muted">Pick another category above.</div> </div>`; } function start(){ resetForPlay(); } function resetForPlay(){ usedTries = 0; bestMissCents = null; lastDiff = null; els.status.textContent = 'Game ready β make your first guess.'; els.hint.textContent = 'β'; els.used.textContent = '0'; els.max.textContent = String(maxTries); els.revealBox.classList.add('hidden'); els.guess.value = ''; els.guess.focus(); } function hotterColderText(diff){ if (lastDiff === null) return 'β'; if (diff < lastDiff) return 'Hotter'; if (diff > lastDiff) return 'Colder'; return 'Same'; } function onGuess(){ const p = currentItem(); if (!p) return; if (usedTries >= maxTries){ showToast('No tries left'); return; } const guessCents = toCents(els.guess.value); if (guessCents <= 0){ showToast('Enter a valid price'); els.guess.focus(); return; } usedTries++; els.used.textContent = String(usedTries); const actual = p.price_cents; const diff = Math.abs(guessCents - actual); bestMissCents = (bestMissCents === null) ? diff : Math.min(bestMissCents, diff); const pct = actual > 0 ? (diff / actual) * 100 : 100; const rel = hotterColderText(diff); const band = bandName(pct); // Build status/hint per hint mode let statusMsg = ''; if (hintMode === 'none'){ statusMsg = `Attempt ${usedTries}/${maxTries} recorded.`; els.hint.textContent = 'β'; } else if (hintMode === 'relative'){ statusMsg = `${rel}`; els.hint.textContent = `${band}`; } else if (hintMode === 'rel_dollars'){ statusMsg = `${rel} β you are off by ${money(diff, DATA.marketplace.currency)}`; els.hint.textContent = `${band}`; } else { // rel_dollars_pct statusMsg = `${rel} β you are off by ${money(diff, DATA.marketplace.currency)} (${pct.toFixed(1)}%)`; els.hint.textContent = `${band}`; } els.status.textContent = statusMsg; if (diff === 0){ reveal(true); return; } lastDiff = diff; if (usedTries >= maxTries){ reveal(false); } } function giveUp(){ reveal(false); } function reveal(isWin){ const p = currentItem(); const miss = (bestMissCents === null) ? 0 : bestMissCents; // Update lifetime miss (numeric only) const cur = parseInt(localStorage.getItem(LIFE_KEY)||'0', 10) || 0; const next = cur + miss; localStorage.setItem(LIFE_KEY, String(next)); els.lifeMiss.textContent = money(next, DATA.marketplace.currency); els.bestMiss.textContent = money(miss, DATA.marketplace.currency); els.revealBox.innerHTML = ` <div><b>${isWin ? 'You got it! π' : 'Round over'}</b></div> <div><b>Actual price:</b> ${money(p.price_cents, DATA.marketplace.currency)} <span class="asof">as of ${fmtAsOf(p.price_as_of)}</span></div> <div><b>Your best miss:</b> ${money(miss, DATA.marketplace.currency)}</div> <div class="muted" style="font-size:12px; margin-top:8px;"> Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply to the purchase of this product. </div> <div class="controls" style="margin-top:8px"> <button id="nextItemBtn">Next item</button> </div> `; els.revealBox.classList.remove('hidden'); document.getElementById('nextItemBtn').addEventListener('click', ()=>{ idx++; if (idx >= currentCat.items.length){ finishCategory(); } else { renderItem(); } }); } function applySettings(){ const n = parseInt(els.tries.value, 10); if (!Number.isFinite(n) || n < 1 || n > 7){ showToast('Tries must be 1β7'); return; } maxTries = n; hintMode = els.hintMode.value; useTestImage = !!els.useTestImg.checked; localStorage.setItem(TRIES_KEY, String(maxTries)); localStorage.setItem(HINTMODE_KEY, hintMode); localStorage.setItem(TESTIMG_KEY, useTestImage ? '1' : '0'); els.max.textContent = String(maxTries); // Re-render current image if toggled const p = currentItem(); if (p) setImageForItem(p); showToast('Settings applied'); } function resetLifetime(){ localStorage.removeItem(LIFE_KEY); els.lifeMiss.textContent = DATA ? money(0, DATA.marketplace.currency) : '$0.00'; showToast('Lifetime dollars off reset'); } // Boot fetch('./catalog.json', {cache:'no-store'}) .then(r => r.ok ? r.json() : Promise.reject(new Error('catalog.json not found'))) .then(data => { DATA = data; els.demoPill.classList.toggle('hidden', !DATA.is_demo); els.gameId.textContent = `Game ${DATA.game_id}`; els.validRange.textContent = `${fmtAsOf(DATA.starts_at)} β ${fmtAsOf(DATA.ends_at)}`; els.market.textContent = `${DATA.marketplace.code} (${DATA.marketplace.currency})`; hydrateSettings(); buildCatBar(); els.availability.textContent = ''; document.getElementById('startBtn').focus(); }) .catch(err=>{ console.error(err); document.querySelector('main').innerHTML = ` <section class="wrap"> <h2>Best Deal On β Setup needed</h2> <p>Place a <code>catalog.json</code> file in this folder. Then reload.</p> </section>`; }); // Events els.startBtn.addEventListener('click', start); els.submitBtn.addEventListener('click', onGuess); els.giveUpBtn.addEventListener('click', giveUp); els.skipBtn.addEventListener('click', ()=>{ idx++; (idx >= currentCat.items.length) ? finishCategory() : renderItem(); }); els.guess.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); onGuess(); }}); els.applyBtn.addEventListener('click', applySettings); els.resetStatsBtn.addEventListener('click', resetLifetime); })(); </script> </body> </html>
Save changes
Create folder
writable 0777
Create
Cancel