Files
archy/docker/justworks/business/public/assets/order.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

15 lines
6.1 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');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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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));