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.
15 lines
6.1 KiB
JavaScript
15 lines
6.1 KiB
JavaScript
import {frame} from './ds/frame.js';
|
||
import {npubFrom} from './ds/env.js';
|
||
const app=document.querySelector('#app');const params=new URLSearchParams(location.search);const npub=npubFrom();const prefilledTable=params.get('table')||'';let merchant,quantities={},statusTimer;
|
||
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
function error(message){app.innerHTML=`<section class="center"><span class="orbit">⌁</span><p class="eyebrow">Just Works · Order</p><h1>${esc(message)}</h1><small>Use order.justworks.cash/?npub=npub1…</small></section>`}
|
||
function productCard(item){return `<article class="menu-item" data-id="${esc(item.id)}"><div><strong>${esc(item.name)}</strong>${item.description?`<small>${esc(item.description)}</small>`:''}${item.price?`<small>${esc(item.price)}</small>`:''}</div><div class="step"><button data-minus="${esc(item.id)}" aria-label="Remove ${esc(item.name)}">−</button><b data-qty="${esc(item.id)}">0</b><button data-plus="${esc(item.id)}" aria-label="Add ${esc(item.name)}">+</button></div></article>`}
|
||
function render(data){merchant=data;document.title=`Order ${data.merchant.name} · Just Works`;frame(app,{label:'Order',surface:'order',html:`<section class="pay-shell"><div class="pay-copy"><p class="eyebrow">Order to table</p><h1>What can we bring you?</h1><p>Order from ${esc(data.merchant.name)} without waiting for the waiter.</p></div><form id="orderForm" class="pay-card form-card"><div class="merchant"><span>↗</span><div><strong>${esc(data.merchant.name)}</strong><small>Taking orders</small></div></div>${data.products.length?`<div class="order-menu"><p class="eyebrow">Merchant menu</p>${data.products.map(productCard).join('')}</div>`:''}<label class="field"><span>Table number</span><input id="table" type="number" inputmode="numeric" min="1" max="999" value="${esc(prefilledTable)}" placeholder="e.g. 12" required></label><label class="field"><span>Order or request</span><textarea id="request" rows="5" placeholder="${data.products.length?'Two coffees, one without sugar…':'This merchant has not published a structured menu yet. Tell the staff exactly what you want… two coffees, one without sugar'}" ${data.products.length?'':'required'}></textarea></label><label class="field"><span>Your name <small>(optional)</small></span><input id="guest" maxlength="80" autocomplete="name" placeholder="So staff knows who asked"></label><button id="submit" type="submit" class="primary">Send to ${esc(data.merchant.name)} <span>→</span></button><p class="privacy">No account · no app · sent directly to this merchant</p></form></section>`});bind(data.products)}
|
||
function bind(products){for(const item of products){quantities[item.id]=0;document.querySelector(`[data-plus="${CSS.escape(item.id)}"]`).onclick=()=>setQty(item.id,quantities[item.id]+1);document.querySelector(`[data-minus="${CSS.escape(item.id)}"]`).onclick=()=>setQty(item.id,quantities[item.id]-1)}document.querySelector('#orderForm').onsubmit=submit}
|
||
function setQty(id,value){quantities[id]=Math.max(0,Math.min(20,value));document.querySelector(`[data-qty="${CSS.escape(id)}"]`).textContent=quantities[id]}
|
||
const statusCopy={'new':{eyebrow:'Sent to the staff',title:'Your order is on its way.',body:'Waiting for the team to confirm it.',step:0},accepted:{eyebrow:'Confirmed by the staff',title:'They are preparing it.',body:'Your order has been accepted.',step:1},delivered:{eyebrow:'Coming to your table',title:'It is ready for you.',body:'The team is bringing it now.',step:2},closed:{eyebrow:'Order complete',title:'Just Works.',body:'Enjoy—and order again whenever you like.',step:3}};
|
||
function renderStatus(order){const state=statusCopy[order.status]||statusCopy['new'];const card=document.querySelector('#orderForm,.order-status-card');if(!card)return;card.outerHTML=`<section class="pay-card form-card order-status-card" aria-live="polite"><div class="merchant"><span>✓</span><div><strong>${esc(merchant.merchant.name)}</strong><small>Table ${order.table||order.table_number}</small></div></div><div class="live-pill"><i></i> Live order status</div><p class="eyebrow">${esc(state.eyebrow)}</p><h2>${esc(state.title)}</h2><p class="status-copy">${esc(state.body)}</p><small class="order-reference">Table ${order.table||order.table_number} · ${esc(order.id.toUpperCase())}</small><div class="journey">${['Sent','Staff confirmed','Coming to table'].map((label,index)=>`<div class="journey-step ${index<=state.step?'done':''} ${index===state.step?'current':''}"><i>${index<state.step?'✓':index+1}</i><span>${label}</span></div>`).join('')}</div><a class="link-button" href="${location.href}">Add another order <span>+</span></a></section>`}
|
||
async function pollStatus(order){try{const response=await fetch(`/api/merchant-order-status?npub=${encodeURIComponent(npub)}&id=${encodeURIComponent(order.id)}`,{cache:'no-store'});if(response.ok){const latest=await response.json();if(latest.status!==order.status){order.status=latest.status;order.table=latest.table_number;renderStatus(order)}}}catch{}if(order.status!=='closed')statusTimer=setTimeout(()=>pollStatus(order),1500)}
|
||
async function submit(event){event.preventDefault();const button=document.querySelector('#submit');button.disabled=true;button.innerHTML='Sending to staff…';const items=merchant.products.filter(item=>quantities[item.id]>0).map(item=>({id:item.id,name:item.name,qty:quantities[item.id]}));const response=await fetch('/api/merchant-orders',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({npub,table:Number(document.querySelector('#table').value),request:document.querySelector('#request').value,guest:document.querySelector('#guest').value,items})});const data=await response.json();if(!response.ok){button.disabled=false;button.innerHTML=`${esc(data.error||'Try again')} <span>↻</span>`;return}renderStatus(data);pollStatus(data)}
|
||
if(!npub)error('Add the merchant npub to order.');else fetch(`/api/order-merchant?npub=${encodeURIComponent(npub)}`,{cache:'no-store'}).then(async r=>{const data=await r.json();if(!r.ok)throw new Error(data.error||'Merchant unavailable');return data}).then(render).catch(e=>error(e.message));
|