← back to Shopify Order Bell
index.html
159 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Shopify Order Bell</title>
<style>
*{box-sizing:border-box}html,body{margin:0;height:100%;background:#0b0d10;color:#e6ecf2;font:14px/1.5 ui-monospace,Menlo,monospace}
header{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid #1c2430;background:#0e1116;position:sticky;top:0}
header .k{color:#7cf;font-weight:700;letter-spacing:2px;text-transform:uppercase;font-size:11px}
header h1{font-size:15px;margin:0;font-weight:600}
header .store{color:#8a93a0;font-size:11px;margin-left:auto}
.wrap{padding:16px;max-width:520px;margin:0 auto}
.card{background:#131820;border:1px solid #1c2430;border-radius:12px;padding:14px;margin-bottom:12px}
.card h2{margin:0 0 6px;font-size:13px;letter-spacing:1px;text-transform:uppercase;color:#7cf}
.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.btn{appearance:none;border:1px solid #243044;background:#1a2332;color:#e6ecf2;padding:8px 12px;border-radius:8px;cursor:pointer;font:600 12px/1 ui-monospace}
.btn:hover{border-color:#3a5578}
.btn.primary{background:#2a5bd7;border-color:#2a5bd7}
.btn.ghost{background:transparent}
.dot{width:8px;height:8px;border-radius:50%;background:#2ecc71;box-shadow:0 0 8px #2ecc71;display:inline-block;vertical-align:middle}
.dot.err{background:#ff4757;box-shadow:0 0 8px #ff4757}
.dot.idle{background:#8a93a0;box-shadow:none}
.mono{font:12px/1.4 ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-all;color:#b9c4d3}
.bell{font-size:42px;text-align:center;margin:6px 0 2px;filter:drop-shadow(0 0 12px rgba(124,204,255,0.5))}
.bell.ring{animation:ring 0.6s ease}
@keyframes ring{0%{transform:rotate(0)}15%{transform:rotate(14deg)}30%{transform:rotate(-12deg)}45%{transform:rotate(10deg)}60%{transform:rotate(-8deg)}75%{transform:rotate(4deg)}100%{transform:rotate(0)}}
.orders{max-height:240px;overflow:auto}
.orders .o{display:flex;justify-content:space-between;gap:8px;padding:6px 0;border-bottom:1px solid #1c2430;font-size:12px}
.orders .o:last-child{border:0}
.badge{font-size:10px;padding:2px 6px;border-radius:99px;background:#1a2332;border:1px solid #243044;color:#8a93a0}
small{color:#8a93a0}
</style>
</head>
<body>
<header>
<span class="k">🔔 Bell</span>
<h1>Shopify Order Bell</h1>
<span class="store" id="store">—</span>
</header>
<div class="wrap">
<div class="card" style="text-align:center">
<div class="bell" id="bell">🔔</div>
<div style="display:flex;gap:8px;justify-content:center;margin-top:8px">
<button class="btn primary" id="testBtn">🔔 Test Bell</button>
<button class="btn" id="pollBtn">↻ Poll now</button>
</div>
<div style="margin-top:8px"><span class="dot idle" id="dot"></span> <small id="status">Starting…</small></div>
<div class="mono" id="meta" style="margin-top:6px"></div>
</div>
<div class="card">
<h2>Last orders</h2>
<div class="orders" id="orders"><small>Waiting for first poll…</small></div>
</div>
<div class="card">
<h2>Settings</h2>
<div class="mono" id="cfg"></div>
<div class="row" style="margin-top:8px">
<button class="btn ghost" id="resetBtn" title="Forget last order ID and re-seed from Shopify (no retro bell)">Reset seed</button>
<label style="display:flex;gap:6px;align-items:center"><input type="range" id="vol" min="0" max="1" step="0.05" value="0.9" style="width:120px"><small>volume</small></label>
<label style="display:flex;gap:6px;align-items:center"><input type="number" id="repeats" min="1" max="6" value="3" style="width:48px"> <small>repeats</small></label>
</div>
<small>Polls <code id="pollEvery">15s</code> · Token: <span id="tok">—</span> · Plays bell + system notification + dock bounce on new order. First run seeds silently (no retro bell).</small>
</div>
</div>
<script>
const bellEl = document.getElementById('bell');
const statusEl = document.getElementById('status');
const dotEl = document.getElementById('dot');
const metaEl = document.getElementById('meta');
const ordersEl = document.getElementById('orders');
const cfgEl = document.getElementById('cfg');
const tokEl = document.getElementById('tok');
const pollEveryEl = document.getElementById('pollEvery');
let volEl = document.getElementById('vol');
let repeatsEl = document.getElementById('repeats');
let audioCtx;
function getCtx(){ if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); if(audioCtx.state==='suspended') audioCtx.resume(); return audioCtx; }
// synthesized bell: two decaying sines + high shimmer — no file needed, works under CSP
function playBell(volume=0.9, repeats=3){
const ctx = getCtx();
let t0 = ctx.currentTime + 0.02;
for(let r=0;r<repeats;r++){
const t = t0 + r*0.42;
// main partials
[{f: 740, g: 1.0, d: 1.6},{f: 1480, g: 0.45, d: 1.1},{f: 2220, g: 0.22, d: 0.8}].forEach(p=>{
const o = ctx.createOscillator(); o.type='sine'; o.frequency.value=p.f;
const g = ctx.createGain(); g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(volume*p.g*0.9, t+0.008);
g.gain.exponentialRampToValueAtTime(0.001, t+p.d);
o.connect(g).connect(ctx.destination); o.start(t); o.stop(t+p.d+0.05);
});
// click transient
const click = ctx.createOscillator(); click.frequency.value=3200;
const cg = ctx.createGain(); cg.gain.setValueAtTime(0, t); cg.gain.linearRampToValueAtTime(volume*0.18, t+0.001); cg.gain.exponentialRampToValueAtTime(0.001, t+0.06);
click.connect(cg).connect(ctx.destination); click.start(t); click.stop(t+0.07);
}
bellEl.classList.remove('ring'); void bellEl.offsetWidth; bellEl.classList.add('ring');
}
function fmtOrders(orders){
if(!orders || !orders.length) return '<small>No orders</small>';
return orders.slice(0,5).map(o=>{
const name = o.name || ('#'+o.id);
const price = o.total_price ? `${o.total_price} ${o.currency}` : '';
const when = o.created_at ? new Date(o.created_at).toLocaleString() : '';
const items = o.line_items ? o.line_items.length+' items' : '';
return `<div class="o"><span><b>${name}</b> <span class="badge">${price}</span> <small>${items}</small></span><small>${when}</small></div>`;
}).join('');
}
async function init(){
const cfg = await window.bellAPI.getConfig();
document.getElementById('store').textContent = cfg.store;
pollEveryEl.textContent = cfg.pollSeconds + 's';
tokEl.textContent = cfg.hasToken ? '✓ ready' : '✗ missing (set SHOPIFY_ORDERS_TOKEN)';
tokEl.style.color = cfg.hasToken ? '#2ecc71' : '#ff4757';
cfgEl.textContent = `store: ${cfg.store}\nlastOrderId: ${cfg.lastOrderId || '(none — will seed on first poll)'}\nseeded: ${cfg.seeded}\n`;
metaEl.textContent = `last check: —`;
}
init();
document.getElementById('testBtn').addEventListener('click', ()=>{ playBell(parseFloat(volEl.value), parseInt(repeatsEl.value,10)); window.bellAPI.testBell(); });
document.getElementById('pollBtn').addEventListener('click', async ()=>{
statusEl.textContent = 'Polling…'; dotEl.className='dot idle';
await window.bellAPI.pollNow();
});
document.getElementById('resetBtn').addEventListener('click', async ()=>{ await window.bellAPI.resetSeed(); statusEl.textContent='Re-seeded — watching for new orders'; });
window.bellAPI.onBell(({order,title,body})=>{
playBell(parseFloat(volEl.value), parseInt(repeatsEl.value,10));
statusEl.textContent = `${title} — ${body}`;
dotEl.className='dot';
// also flash meta
metaEl.textContent = `🔔 ${new Date().toLocaleString()} — ${body}`;
});
window.bellAPI.onStatus(({at,msg,lastId,orders,error})=>{
statusEl.textContent = msg;
dotEl.className = error ? 'dot err' : 'dot';
metaEl.textContent = `last check: ${new Date(at).toLocaleString()} · lastId: ${lastId || '—'}`;
if(orders) ordersEl.innerHTML = fmtOrders(orders);
});
// allow clicking the bell to test
bellEl.addEventListener('click', ()=> playBell(parseFloat(volEl.value), parseInt(repeatsEl.value,10)));
// prime audio on first interaction (autoplay policy)
document.addEventListener('click', ()=>{ try{ getCtx(); }catch{} }, {once:true});
</script>
</body>
</html>