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 — Cart Tally (Beta)</title> <meta name="description" content="Best Deal On — a daily shopping game. Guess the total and find your next deal." /> <style> :root{ --bg: #0b0c10; --panel: #111319; --ink: #e9edf1; --muted: #a6b0bf; --accent: #7c5cff; /* can be overridden by JSON */ --accent-2: #00d4ff; --good: #00c853; --bad: #ff5252; --warn: #ffb300; --card: #151926; --card-border: rgba(255,255,255,0.08); --shadow: 0 10px 30px rgba(0,0,0,0.35); --radius: 14px; } @media (prefers-color-scheme: light){ :root{ --bg:#f6f7fb; --panel:#ffffff; --ink:#0c0d11; --muted:#5a6472; --card:#ffffff; --card-border:rgba(0,0,0,0.08); --shadow: 0 12px 30px rgba(0,0,0,0.08); } } *{ box-sizing:border-box } body{ margin:0; background: radial-gradient(1200px 800px at 90% -10%, rgba(124,92,255,.25), transparent 60%), radial-gradient(900px 700px at -20% 30%, rgba(0,212,255,.15), transparent 60%), var(--bg); color:var(--ink); font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Apple Color Emoji","Segoe UI Emoji"; 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 200deg, 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 } .tagline{ color:var(--muted); font-size:13px } .demo-pill{ padding:6px 10px; border-radius:999px; background:#ffecb3; color:#5d4100; font-size:12px; font-weight:600 } main{ width:min(1100px, 92vw); margin: 8px auto 40px; } .game-hero{ background: linear-gradient(160deg, rgba(124,92,255,.12), rgba(0,212,255,.10)), var(--panel); border:1px solid var(--card-border); border-radius: var(--radius); box-shadow: var(--shadow); padding: clamp(18px, 4vw, 28px); display:grid; gap:14px; } .grid{ display:grid; gap:18px } .grid.cols-3{ grid-template-columns: repeat(3, minmax(0,1fr)) } @media (max-width: 880px){ .grid.cols-3{ grid-template-columns: 1fr } } .meta{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; color:var(--muted); font-size:13px } .meta b{ color:var(--ink) } .round-wrap{ margin-top:18px; display:grid; gap:18px } .cards{ display:grid; gap:16px } .cards.cols-3{ grid-template-columns: repeat(3, minmax(0,1fr)) } @media (max-width: 900px){ .cards.cols-3{ grid-template-columns: 1fr } } .card{ background: var(--card); border:1px solid var(--card-border); border-radius: var(--radius); box-shadow: var(--shadow); padding:14px; display:grid; gap:12px; } .imgwrap{ border-radius: 10px; background: linear-gradient(145deg, rgba(255,255,255,.08), rgba(0,0,0,.06)); display:grid; place-items:center; min-block-size:160px; overflow:hidden; position:relative; } .imgwrap svg{ width:90%; height:auto; opacity:.85 } .title{ font-weight:600; line-height:1.35 } .actions{ display:flex; align-items:center; gap:10px; flex-wrap:wrap } .actions a{ text-decoration:none; font-size:13px; color:var(--accent) } .guess-panel{ display:flex; gap:12px; flex-wrap:wrap; align-items:center; padding: 14px; background:var(--panel); border:1px dashed var(--card-border); border-radius: var(--radius) } input[type="text"]{ appearance:none; border:none; outline:none; padding:12px 14px; border-radius:10px; background:var(--card); color:var(--ink); min-width: 220px; font-size:16px; border:1px solid var(--card-border); } button{ appearance:none; border:none; padding:12px 16px; border-radius:12px; cursor:pointer; font-weight:700; 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) } .scorebar{ display:flex; align-items:center; gap:12px; flex-wrap:wrap; font-size:14px; background: var(--panel); border:1px solid var(--card-border); border-radius: var(--radius); padding:12px 14px; } .score{ font-weight:800 } .reveal{ border:1px solid var(--card-border); border-radius: var(--radius); padding:14px; display:grid; gap:10px; background: linear-gradient(160deg, rgba(0,200,83,.08), rgba(124,92,255,.06)); } .price-line{ font-feature-settings: "tnum"; font-variant-numeric: tabular-nums; } .pill{ padding:5px 8px; border-radius:999px; font-size:12px; background:rgba(255,255,255,.08); border:1px solid var(--card-border); color:var(--muted) } 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 } </style> </head> <body> <header> <a class="brand" href="#" aria-label="Best Deal On"> <div class="logo" aria-hidden="true">B</div> <div> <h1>Best Deal On</h1> <div class="tagline">Play & find your next deal</div> </div> </a> <div class="demo-pill" id="demoPill">DEMO DATA — concept only</div> </header> <main> <section class="game-hero" id="hero"> <div class="meta"> <span class="pill" id="gameId"></span> <span>Category of the day: <b id="catOfDay">—</b></span> <span>Rounds: <b id="roundCount">0</b></span> <span>Valid: <b id="validRange">—</b></span> </div> <h2 style="margin:2px 0 6px">Cart Tally</h2> <p style="margin:0;color:var(--muted)">We show you three items. <b>Guess the total</b> price. Closer = more points. Exact hit earns a bonus.</p> <div class="round-wrap"> <div id="cards" class="cards cols-3" aria-live="polite"></div> <div class="guess-panel" id="guessPanel"> <label for="guess">Your guess (total):</label> <input type="text" id="guess" inputmode="decimal" autocomplete="off" placeholder="$0.00" aria-label="Enter your total guess" /> <button id="lockBtn">Lock Guess</button> <button id="nextBtn" class="secondary hidden">Next Round</button> <span class="pill" id="roundMeta"></span> </div> <div class="reveal hidden" id="revealBox" aria-live="polite"></div> <div class="scorebar" id="scoreBar"> <div>Round <span id="roundIndex">1</span>/<span id="roundTotal">—</span></div> <div>Score this round: <span class="score" id="scoreThis">0</span></div> <div>Game total: <span class="score" id="scoreTotal">0</span></div> <div>Best (this device): <span class="score" id="bestScore">0</span></div> </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 id="disclaimer" 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 = { gameId: document.getElementById('gameId'), catOfDay: document.getElementById('catOfDay'), roundCount: document.getElementById('roundCount'), validRange: document.getElementById('validRange'), cards: document.getElementById('cards'), guess: document.getElementById('guess'), lockBtn: document.getElementById('lockBtn'), nextBtn: document.getElementById('nextBtn'), roundMeta: document.getElementById('roundMeta'), revealBox: document.getElementById('revealBox'), roundIndex: document.getElementById('roundIndex'), roundTotal: document.getElementById('roundTotal'), scoreThis: document.getElementById('scoreThis'), scoreTotal: document.getElementById('scoreTotal'), bestScore: document.getElementById('bestScore'), demoPill: document.getElementById('demoPill'), toast: document.getElementById('toast') }; // Simple money helpers (minor units = cents) function toCents(x){ if (typeof x === 'number') return Math.round(x * 100); const s = (x || '').toString().replace(/[^0-9.]/g,''); return Math.round(parseFloat(s || '0') * 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'), 2200); } // Score: 1000 max; exact +200 bonus; subtract 10 pts per 1% error (cap at 0) function scoreRound(guessCents, actualCents){ if(actualCents <= 0) return 0; const err = Math.abs(guessCents - actualCents); const pct = (err / actualCents) * 100; // % const base = Math.max(0, Math.round(1000 - pct * 10)); const bonus = (err === 0) ? 200 : 0; return base + bonus; } function svgPlaceholder(title){ const t = (title || 'Item').slice(0,36).replace(/&/g,'&').replace(/</g,'<'); return ` <svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg" role="img" aria-label=""> <defs> <linearGradient id="g" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="rgba(255,255,255,.15)"/> <stop offset="100%" stop-color="rgba(0,0,0,.05)"/> </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>`; } // State let GAME = null; let rIndex = 0; let totalScore = 0; const BEST_KEY = 'bdo_best_score_v1'; function setTheme(accent){ if(!accent) return; document.documentElement.style.setProperty('--accent', accent); } function renderRound(){ const round = GAME.rounds[rIndex]; els.roundIndex.textContent = (rIndex+1).toString(); els.roundTotal.textContent = GAME.rounds.length.toString(); els.roundMeta.textContent = `Mode: ${round.mode}`; els.cards.innerHTML = ''; round.items.forEach((it, i)=>{ const card = document.createElement('article'); card.className = 'card'; card.innerHTML = ` <div class="imgwrap">${svgPlaceholder(it.title)}</div> <div class="title">${it.title}</div> <div class="actions"> <a href="https://${GAME.marketplace.domain}/dp/${it.asin}?tag=${encodeURIComponent(GAME.marketplace.tag)}" target="_blank" rel="noopener sponsored nofollow"> Buy on Amazon </a> </div> `; els.cards.appendChild(card); }); // Reset UI pieces els.revealBox.classList.add('hidden'); els.nextBtn.classList.add('hidden'); els.lockBtn.disabled = false; els.guess.value = ''; els.guess.focus(); els.scoreThis.textContent = '0'; } function reveal(){ const round = GAME.rounds[rIndex]; const currency = GAME.marketplace.currency; const actual = round.items.reduce((sum, it)=> sum + (it.price_cents||0), 0); const guessCents = toCents(els.guess.value); const pts = scoreRound(guessCents, actual); totalScore += pts; els.scoreThis.textContent = pts.toString(); els.scoreTotal.textContent = totalScore.toString(); const asOf = (()=>{ // show the newest as-of among items const arr = round.items.map(it=> it.price_as_of).filter(Boolean); if(arr.length===0) return null; return arr.sort((a,b)=> new Date(b)-new Date(a))[0]; })(); // Build reveal HTML const list = round.items.map(it=>{ const line = `${money(it.price_cents, currency)} <span class="pill">as of ${fmtAsOf(it.price_as_of)}</span>`; return `<li class="price-line"><b>${it.title}</b> — ${line}</li>`; }).join(''); els.revealBox.innerHTML = ` <div><b>Your guess:</b> ${money(guessCents, currency)}</div> <div><b>Actual total:</b> ${money(actual, currency)}</div> <ul style="margin:10px 0 0 18px; padding:0">${list}</ul> <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.classList.remove('hidden'); els.lockBtn.disabled = true; els.nextBtn.classList.remove('hidden'); // Update best score (number only; no Program Content) const best = parseInt(localStorage.getItem(BEST_KEY) || '0', 10); if(isFinite(best) && totalScore > best){ localStorage.setItem(BEST_KEY, String(totalScore)); els.bestScore.textContent = String(totalScore); showToast('New personal best!'); } } function nextRound(){ if(rIndex < GAME.rounds.length - 1){ rIndex++; renderRound(); } else { els.cards.innerHTML = `<div class="card" style="text-align:center; padding:28px"> <h3 style="margin:8px 0 2px">Game complete 🎉</h3> <p style="margin:0;color:var(--muted)">Your total score: <b>${totalScore}</b></p> <div style="margin-top:14px"><button class="secondary" id="replayBtn">Play again</button></div> </div>`; document.getElementById('replayBtn').addEventListener('click', ()=>{ rIndex = 0; totalScore = 0; els.scoreTotal.textContent = '0'; renderRound(); }); els.guessPanel.classList.add('hidden'); els.revealBox.classList.add('hidden'); } } // Wire events els.lockBtn.addEventListener('click', ()=>{ if(!els.guess.value.trim()){ showToast('Enter a guess first'); return; } reveal(); }); els.guess.addEventListener('keydown', (e)=>{ if(e.key==='Enter'){ e.preventDefault(); els.lockBtn.click(); }}); els.nextBtn.addEventListener('click', nextRound); // Boot fetch('./game.json', {cache:'no-store'}) .then(r=> r.ok ? r.json() : Promise.reject(new Error('game.json not found'))) .then(data=>{ GAME = data; // Theme & header if(GAME.theme && GAME.theme.accent) setTheme(GAME.theme.accent); els.gameId.textContent = `Game ${GAME.game_id}`; els.catOfDay.textContent = GAME.category_of_day || '—'; els.roundCount.textContent = (GAME.rounds||[]).length; els.validRange.textContent = `${fmtAsOf(GAME.starts_at)} → ${fmtAsOf(GAME.ends_at)}`; els.demoPill.classList.toggle('hidden', !GAME.is_demo); els.roundTotal.textContent = (GAME.rounds||[]).length; // Validate expiry const now = Date.now(); if (new Date(GAME.ends_at).getTime() < now){ showToast('This daily game has expired.'); } // Best score const best = parseInt(localStorage.getItem(BEST_KEY) || '0', 10); els.bestScore.textContent = isFinite(best) ? String(best) : '0'; // Render first round renderRound(); }) .catch(err=>{ console.error(err); document.querySelector('main').innerHTML = ` <section class="game-hero"> <h2>Best Deal On — Setup needed</h2> <p>Place a <code>game.json</code> file in this folder. Then reload.</p> <p style="color:var(--muted)">Tip: use the sample from our prototype response.</p> </section>`; }); })(); </script> </body> </html>
Save changes
Create folder
writable 0777
Create
Cancel