Files
archy/docker/justworks/business/public/assets/book.js
T
yaya 94899e69d3 feat: integrate Just Works app and native business setup
Preserve hosted website UX inside Archipelago, add isolated Business and owner-authorized native wallet setup, and include deployment handoff. Based on main with native signer import already accepted.
2026-10-08 19:39:38 +01:00

14 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {frame} from './ds/frame.js';
import {npubFrom} from './ds/env.js';
const app=document.querySelector('#app'),npub=npubFrom();let merchant,mode='now',delay=0,guests=2,busy=false;
const esc=value=>String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
function timeValue(offsetMinutes){const date=new Date(Date.now()+offsetMinutes*60000);date.setMinutes(Math.ceil(date.getMinutes()/30)*30,0,0);return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}T${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}`}
function error(message){app.innerHTML=`<section class="center"><span class="orbit">⌁</span><p class="eyebrow">Table booking</p><h1>${esc(message)}</h1><small>Use book.justworks.cash/?npub=npub1…</small></section>`}
function render(data){merchant=data.merchant;document.title=`Book ${merchant.name} · Just Works`;frame(app,{label:'Bookings',surface:'book',html:`<section class="pay-shell"><div class="pay-copy"><p class="eyebrow">Reserve a table</p><h1>When should we expect you?</h1><p>Your table at ${esc(merchant.name)}, without the phone call.</p></div><form id="form" class="pay-card form-card"><div class="merchant"><span>⌁</span><div><strong>${esc(merchant.name)}</strong><small>Taking bookings</small></div></div><div class="chips booking-mode">${[['now','Soon'],['later','Later']].map(([value,label])=>`<button type="button" class="${mode===value?'active':''}" data-mode="${value}">${label}</button>`).join('')}</div><div id="when"></div><label class="field"><span>Guests</span><div class="stepper"><button type="button" id="less" aria-label="Fewer guests">−</button><strong id="guestCount">2</strong><button type="button" id="more" aria-label="More guests">+</button></div></label><label class="field"><span>Your name</span><input id="name" autocomplete="name" maxlength="80" placeholder="Who should we expect?" required></label><label class="field"><span>Note <small>(optional)</small></span><input id="notes" maxlength="240" placeholder="High chair, outside table…"></label><button id="submit" type="submit" class="primary">Request table <span>→</span></button><p class="privacy">No account · no app · sent directly to ${esc(merchant.name)}</p></form></section>`});document.querySelectorAll('[data-mode]').forEach(button=>button.onclick=()=>setMode(button.dataset.mode));document.querySelector('#less').onclick=()=>setGuests(guests-1);document.querySelector('#more').onclick=()=>setGuests(guests+1);document.querySelector('#form').onsubmit=submit;renderWhen()}
function setMode(next){mode=next;document.querySelectorAll('[data-mode]').forEach(button=>button.classList.toggle('active',button.dataset.mode===mode));renderWhen()}
function setGuests(next){guests=Math.max(1,Math.min(20,next));document.querySelector('#guestCount').textContent=guests}
function renderWhen(){const host=document.querySelector('#when');if(mode==='now'){host.innerHTML=`<label class="field"><span>Arriving in</span><div class="chips">${[[0,'Now'],[5,'+5 min'],[10,'+10 min'],[15,'+15 min']].map(([value,label])=>`<button type="button" data-delay="${value}" class="${delay===value?'active':''}">${label}</button>`).join('')}</div></label>`;host.querySelectorAll('[data-delay]').forEach(button=>button.onclick=()=>{delay=Number(button.dataset.delay);renderWhen()})}else host.innerHTML=`<label class="field"><span>Date and time</span><input id="date" type="datetime-local" min="${timeValue(30)}" value="${timeValue(24*60)}" required></label>`}
async function submit(event){event.preventDefault();if(busy)return;busy=true;const button=document.querySelector('#submit');button.disabled=true;button.firstChild.textContent='Sending… ';try{const scheduled=mode==='now'?new Date(Date.now()+delay*60000):new Date(document.querySelector('#date').value);const response=await fetch('/api/bookings',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({npub,name:document.querySelector('#name').value,notes:document.querySelector('#notes').value,guests,mode,scheduled_at:scheduled.toISOString()})});const result=await response.json();if(!response.ok)throw new Error(result.error||'Could not book');confirm(result)}catch(error){button.disabled=false;button.firstChild.textContent='Try again ';alert(error.message)}finally{busy=false}}
function confirm(result){const date=new Date(result.scheduled_at);frame(app,{label:'Bookings',surface:'book',html:`<section class="center"><span class="orbit">✓</span><p class="eyebrow">Request sent</p><h1>Table for ${result.guests} requested.</h1><p>${esc(merchant.name)} received your booking for <strong>${date.toLocaleDateString([],{weekday:'long',day:'numeric',month:'long'})} at ${date.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</strong>.</p><small>The merchant still needs to confirm it. Keep this screen open for your reference.</small></section>`})}
if(!npub)error('Add the merchant npub to book.');else fetch(`/api/order-merchant?npub=${encodeURIComponent(npub)}`,{cache:'no-store'}).then(async response=>{const data=await response.json();if(!response.ok)throw new Error(data.error);render(data)}).catch(reason=>error(reason.message||'Merchant unavailable'));