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 exact price in a few tries." /> <style> :root{ --bg:#0b0c10; --panel:#10131a; --ink:#e9edf1; --muted:#a6b0bf; --accent:#ff7a59; --accent-2:#ffb84d; --good:#00c853; --bad:#ff5252; --warn:#ffb300; --card:#141926; --card-border:rgba(255,255,255,.08); --shadow:0 10px 30px rgba(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(255,122,89,.18), transparent 60%), radial-gradient(900px 700px at -20% 30%, rgba(255,184,77,.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{ display:flex; align-items:center; justify-content:space-between; gap:16px; padding:20px clamp(16px,4vw,32px); } .brand{ display:flex; align-items:center; gap:14px; text-decoration:none; color:inherit; } .logo{ inline-size:42px; block-size:42px; border-radius:10px; background: conic-gradient(from 230deg, var(--accent), var(--accent-2)); display:grid; place-items:center; color:white; font-weight:900; box-shadow: var(--shadow); } .brand h1{ font-size:20px; margin:0; letter-spacing:.3px } .tag{ color:var(--muted); font-size:13px } .pill{ padding:6px 10px; border-radius:999px; background:#ffecb3; color:#5d4100; font-size:12px; font-weight:600 } main{ width:min(980px, 92vw); margin: 6px auto 36px; } .wrap{ background: var(--panel); border:1px solid var(--card-border); border-radius: var(--r); box-shadow: var(--shadow); padding: clamp(18px,4vw,26px); display:grid; gap:18px } .grid{ display:grid; gap:18px } .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 } .title{ font-weight:700; line-height:1.3 } .meta{ color:var(--muted); font-size:13px; display:flex; gap:10px; flex-wrap:wrap } .controls{ display:flex; align-items:center; gap:12px; flex-wrap:wrap } .controls input[type="number"]{ width:70px; padding:8px 10px; border-radius:10px; border:1px solid var(--card-border); background:var(--card); color:var(--ink) } .controls input[type="text"]{ min-width:220px; 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(255,184,77,.08)) } footer{ margin-top:auto; padding:20px clamp(16px,4vw,32px); color:var(--muted); font-size:12px } .hidden{ display:none !important } .toast{ position:fixed; inset:auto 16px 16px auto; background:#1f2330; color:#fff; padding:10px 12px; border-radius:10px; opacity:.98 } .asof{ font-size:12px; color:var(--muted) } </style> </head> <body> <header> <a href="#" class="brand" aria-label="Best Deal On"> <div class="logo" aria-hidden="true">B</div> <div> <h1>Best Deal On</h1> <div class="tag">Hot/Cold Price — single product</div> </div> </a> <div class="pill" id="demoPill">DEMO DATA</div> </header> <main> <section class="wrap"> <div class="meta"> <span id="gameId" class="pill" style="background:rgba(255,255,255,.08); color:inherit">—</span> <span>Valid: <b id="validRange">—</b></span> </div> <div class="grid cols-2"> <article class="card"> <div class="imgwrap" id="imgWrap"> <!-- image or placeholder goes here --> </div> <div class="title" id="title">—</div> <div class="meta"> <span>Marketplace: <b id="market">—</b></span> <span id="availability">—</span> </div> <div class="controls"> <label for="tries">Tries:</label> <input id="tries" type="number" min="1" max="7" value="3" /> <button id="startBtn">Start</button> <a id="buyBtn" class="secondary" target="_blank" rel="noopener sponsored nofollow" href="#">Buy on Amazon</a> </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"> <div><b>Status:</b> <span id="status">Waiting to 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 Game: <span class="score" id="bestMiss">$0.00</span></div> <div>Lifetime Dollars Off: <span class="score" id="lifeMiss">$0.00</span></div> </div> </section> </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'), imgWrap: document.getElementById('imgWrap'), title: document.getElementById('title'), market: document.getElementById('market'), availability: document.getElementById('availability'), tries: document.getElementById('tries'), startBtn: document.getElementById('startBtn'), 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'), toast: document.getElementById('toast') }; // Local lifetime "dollars off" (minor units: cents) const LIFE_KEY = 'bdo_hotcold_lifetime_off_cents_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){ els.toast.textContent = msg; els.toast.classList.remove('hidden'); setTimeout(()=> els.toast.classList.add('hidden'), 1900); } function placeholderSVG(text){ const t = (text||'Item').slice(0,36).replace(/[&<]/g, s => s === '&' ? '&' : '<'); return ` <svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg" role="img"> <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="34" fill="rgba(255,255,255,.85)" font-family="ui-sans-serif,system-ui,Arial">${t}</text> </svg>`; } // Hot/Cold band labels by % error 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 maxTries = 3; let usedTries = 0; let bestMissCents = null; let lastDiff = null; // for hotter/colder function resetPlay(){ 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 start(){ const n = parseInt(els.tries.value, 10); if (!Number.isFinite(n) || n < 1 || n > 7){ showToast('Tries must be 1–7.'); return; } maxTries = n; resetPlay(); } function hotterColderText(diff){ if (lastDiff === null) return '—'; if (diff < lastDiff) return 'Hotter'; if (diff > lastDiff) return 'Colder'; return 'Same'; } function onGuess(){ if (!DATA) 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 = DATA.product.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); els.status.textContent = `${rel} — you are off by ${money(diff, DATA.marketplace.currency)} (${pct.toFixed(1)}%)`; els.hint.textContent = `${band}`; if (diff === 0){ reveal(true, diff); return; } lastDiff = diff; if (usedTries >= maxTries){ reveal(false, diff); } } function giveUp(){ if (!DATA) return; reveal(false, null); } function reveal(isWin, lastDiff){ const p = DATA.product; // Use best miss across attempts (what counts toward lifetime) const miss = (bestMissCents === null) ? Math.abs((toCents(els.guess.value)||0) - p.price_cents) : bestMissCents; // Update lifetime miss const cur = parseInt(localStorage.getItem(LIFE_KEY)||'0', 10) || 0; const next = cur + (miss || 0); localStorage.setItem(LIFE_KEY, String(next)); els.lifeMiss.textContent = money(next, DATA.marketplace.currency); els.bestMiss.textContent = money(miss, DATA.marketplace.currency); const revealHTML = ` <div><b>${isWin ? 'You got it! 🎉' : 'Round over'}</b></div> <div class="price-line"><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 style="font-size:12px; color:var(--muted); 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> `; els.revealBox.innerHTML = revealHTML; els.revealBox.classList.remove('hidden'); } // Boot fetch('./product.json', {cache:'no-store'}) .then(r => r.ok ? r.json() : Promise.reject(new Error('product.json not found'))) .then(data => { DATA = data; // Header/meta 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)}`; // Product block els.title.textContent = DATA.product.title || '—'; els.market.textContent = `${DATA.marketplace.code} (${DATA.marketplace.currency})`; els.availability.textContent = DATA.product.availability || ''; // Image if provided; otherwise placeholder if (DATA.product.image_url){ const img = document.createElement('img'); img.alt = DATA.product.title || 'Product image'; img.decoding = 'async'; img.loading = 'lazy'; img.src = DATA.product.image_url; els.imgWrap.innerHTML = ''; els.imgWrap.appendChild(img); } else { els.imgWrap.innerHTML = placeholderSVG(DATA.product.title); } // Affiliate link const aff = `https://${DATA.marketplace.domain}/dp/${encodeURIComponent(DATA.product.asin)}?tag=${encodeURIComponent(DATA.marketplace.tag)}`; els.buyBtn.href = aff; // Lifetime dollars off (local device only; number value) const life = parseInt(localStorage.getItem(LIFE_KEY)||'0', 10) || 0; els.lifeMiss.textContent = money(life, DATA.marketplace.currency); // Defaults document.getElementById('max').textContent = '0'; els.status.textContent = 'Set tries, then press Start.'; els.hint.textContent = '—'; }) .catch(err=>{ console.error(err); document.querySelector('main').innerHTML = ` <section class="wrap"> <h2>Best Deal On — Setup needed</h2> <p>Place a <code>product.json</code> file in this folder. Then reload.</p> <p style="color:var(--muted)">Tip: use the sample from our prototype response.</p> </section>`; }); // Events document.getElementById('startBtn').addEventListener('click', start); document.getElementById('submitBtn').addEventListener('click', onGuess); document.getElementById('giveUpBtn').addEventListener('click', giveUp); document.getElementById('guess').addEventListener('keydown', e => { if(e.key==='Enter'){ e.preventDefault(); onGuess(); }}); })(); </script> </body> </html>
Save changes
Create folder
writable 0777
Create
Cancel