Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions public/workers/demos/qr-ar-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>QR AR Demo — Admin & Client</title>
<style>
:root{
--bg:#071019; --card:#0b1620; --accent:#00d184; --muted:#86a0b2;
}
body{margin:0;font-family:Inter,Segoe UI,Roboto,Arial;background:var(--bg);color:#eaf6f1}
.wrap{display:flex;gap:12px;padding:14px;height:100vh;box-sizing:border-box}
.left{flex:1;min-width:420px;display:flex;flex-direction:column;gap:12px}
.right{width:480px;display:flex;flex-direction:column;gap:12px}
.card{background:var(--card);border-radius:10px;padding:12px;box-shadow:0 8px 22px rgba(0,0,0,0.6)}
h3{margin:4px 0 10px 0}
.row{display:flex;gap:8px;align-items:center}
input,select,textarea{background:transparent;border:1px solid rgba(255,255,255,0.04);padding:8px;border-radius:6px;color:#eef;min-width:0}
button{background:var(--accent);border:none;padding:8px 10px;border-radius:8px;cursor:pointer;font-weight:700}
button.secondary{background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--muted)}
.small{font-size:12px;color:var(--muted)}
#sampleQR{position:relative;width:160px;height:160px;background:#000;margin:12px auto;border:1px solid #0f2730}
#overlay{position:absolute;background:rgba(0,255,130,0.4);border:2px dashed #0f9;cursor:move;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:12px;color:#fff}
#clientView{position:relative;width:320px;height:320px;margin:auto;background:#111;border:1px solid #0f2730;overflow:hidden}
#clientOverlay{position:absolute;background:rgba(0,255,130,0.4);border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:12px;color:#fff;text-align:center;pointer-events:auto}
.ledCount{font-family:monospace;font-size:20px;margin:6px 0;font-weight:700}
a.link{color:var(--accent);text-decoration:none}
</style>
</head>
<body>
<div class="wrap">

<!-- Admin Panel -->
<div class="left">
<div class="card">
<h3>Admin — Generate QR & Overlay</h3>
<div class="row">
<label>Qty:</label>
<input id="qty" type="number" value="3" min="1" style="width:60px"/>
<button id="genBtn">Generate QR(s)</button>
</div>

<div id="sampleQR">
<div id="overlay">AR Overlay</div>
</div>
<div class="row">
<button id="saveTemplate">Save Position Template</button>
</div>
<div class="row">
<button id="exportBtn" class="secondary">Export QR Data</button>
</div>
<div id="qrList" style="margin-top:12px;max-height:150px;overflow:auto"></div>
</div>
</div>

<!-- Client Panel -->
<div class="right">
<div class="card">
<h3>Client AR View</h3>
<div id="clientView">
<div id="clientOverlay">
<div id="coCountdown" class="ledCount">—</div>
<div class="small" id="coNext">Next: —</div>
<div style="display:flex;gap:4px;margin-top:4px">
<button id="coActivate">Activate</button>
<button id="coContact">📧📲 Contact</button>
<button id="coProducts">🌐 Products</button>
</div>
</div>
</div>
</div>
</div>

</div>

<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"></script>
<script>
/* ===== Backend Simulation ===== */
const STORAGE_KEY = 'qr_ar_demo_rows_v1';
let overlayTemplate = {offsetX:10, offsetY:-10,width:100,height:50};
let countdownTimer=null;

function loadRows(){ return JSON.parse(localStorage.getItem(STORAGE_KEY)||'[]'); }
function saveRows(rows){ localStorage.setItem(STORAGE_KEY,JSON.stringify(rows)); refreshAdminList(); }
function refreshAdminList(){
const list = document.getElementById('qrList');
const rows = loadRows();
list.innerHTML='';
if(rows.length===0){ list.innerHTML='<div class="small">No QR codes generated</div>'; return; }
rows.forEach((r,i)=>list.innerHTML+=`<div class="small">${i+1}. ID:${r.id} Activated:${r.activated?'Yes':'No'} Next:${r.next_service?new Date(r.next_service).toLocaleString():'—'}</div>`);
}

/* ===== Admin — Drag Overlay ===== */
const overlay = document.getElementById('overlay');
let dragging=false, startX, startY, startLeft, startTop;
overlay.style.left = overlayTemplate.offsetX+'px';
overlay.style.top = overlayTemplate.offsetY+'px';
overlay.style.width = overlayTemplate.width+'px';
overlay.style.height = overlayTemplate.height+'px';

overlay.addEventListener('mousedown',e=>{
dragging=true; startX=e.clientX; startY=e.clientY;
startLeft=overlay.offsetLeft; startTop=overlay.offsetTop;
});
document.addEventListener('mousemove',e=>{
if(!dragging) return;
let dx=e.clientX-startX, dy=e.clientY-startY;
overlay.style.left=(startLeft+dx)+'px';
overlay.style.top=(startTop+dy)+'px';
});
document.addEventListener('mouseup',()=>dragging=false);

/* Save Template */
document.getElementById('saveTemplate').addEventListener('click',()=>{
overlayTemplate={offsetX:overlay.offsetLeft,offsetY:overlay.offsetTop,width:overlay.offsetWidth,height:overlay.offsetHeight};
alert('Template saved. Client overlay will use this position.');
applyClientOverlay();
});

/* Generate QR(s) */
function uid(n=8){ const chars='0123456789ABCDEFGHJKLMNPQRSTUVWXYZ'; let s=''; for(let i=0;i<n;i++) s+=chars[Math.floor(Math.random()*chars.length)]; return s;}
document.getElementById('genBtn').addEventListener('click',()=>{
const qty=Math.max(1,Number(document.getElementById('qty').value));
const rows=loadRows();
for(let i=0;i<qty;i++){
const id=uid(10);
rows.push({id,activated:false,contact_email:'',contact_phone:'',location:'',next_service:null,interval_hours:24});
}
saveRows(rows);
if(!currentQR && rows.length>0) currentQR=rows[0];
});

/* Export CSV/XLSX */
document.getElementById('exportBtn').addEventListener('click',()=>{
const rows=loadRows();
if(rows.length===0)return alert('No rows');
const out=rows.map(r=>({QR_ID:r.id,Activated:r.activated?1:0,Next_Service:r.next_service?new Date(r.next_service).toISOString():'',Email:r.contact_email,Phone:r.contact_phone,Location:r.location}));
const ws=XLSX.utils.json_to_sheet(out);
const wb=XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb,ws,'qrs');
const wbout=XLSX.write(wb,{bookType:'xlsx',type:'array'});
const blob=new Blob([wbout],{type:'application/octet-stream'});
const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='qr_data.xlsx'; a.click(); URL.revokeObjectURL(url);
});

/* ===== Client AR Overlay ===== */
const clientOverlay=document.getElementById('clientOverlay');
const coCountdown=document.getElementById('coCountdown');
const coNext=document.getElementById('coNext');

function applyClientOverlay(){
clientOverlay.style.left=overlayTemplate.offsetX+'px';
clientOverlay.style.top=overlayTemplate.offsetY+'px';
clientOverlay.style.width=overlayTemplate.width+'px';
clientOverlay.style.height=overlayTemplate.height+'px';
}
applyClientOverlay();

/* Client Activation */
let currentQR=null;
const rows=loadRows();
if(rows.length>0) currentQR=rows[0]; // for demo pick first

function updateClientView(){
if(!currentQR){
coCountdown.textContent='—';
coNext.textContent='Next: —';
return;
}
if(currentQR.next_service){
const now=Date.now();
const next=new Date(currentQR.next_service).getTime();
const diff=Math.max(0,next-now);
const hrs=Math.floor(diff/3600000);
const mins=Math.floor((diff%3600000)/60000);
const secs=Math.floor((diff%60000)/1000);
coCountdown.textContent=`${hrs.toString().padStart(2,'0')}:${mins.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;
coNext.textContent=`Next: ${new Date(currentQR.next_service).toLocaleString()}`;
}else{
coCountdown.textContent='—';
coNext.textContent='Next: —';
}
}

/* Activate Button */
document.getElementById('coActivate').addEventListener('click',()=>{
if(!currentQR)return alert('No QR selected');
const rows=loadRows();
const idx=rows.findIndex(r=>r.id===currentQR.id);
if(idx===-1)return alert('QR not found');
if(!rows[idx].activated){
rows[idx].activated=true;
rows[idx].next_service=Date.now()+(rows[idx].interval_hours*3600000);
saveRows(rows);
currentQR=rows[idx];
alert('QR Activated! Next service scheduled.');
if(countdownTimer) clearInterval(countdownTimer);
countdownTimer=setInterval(updateClientView,1000);
updateClientView();
}else{
alert('Already activated. Next service: '+new Date(rows[idx].next_service).toLocaleString());
}
});

/* Contact Button */
document.getElementById('coContact').addEventListener('click',()=>{
if(!currentQR)return alert('No QR selected');
const email=prompt('Enter email for contact:',currentQR.contact_email||'');
if(email!==null){
const phone=prompt('Enter phone for contact:',currentQR.contact_phone||'');
if(phone!==null){
const rows=loadRows();
const idx=rows.findIndex(r=>r.id===currentQR.id);
if(idx!==-1){
rows[idx].contact_email=email;
rows[idx].contact_phone=phone;
saveRows(rows);
currentQR=rows[idx];
alert('Contact info saved!');
}
}
}
});

/* Products Button */
document.getElementById('coProducts').addEventListener('click',()=>{
if(!currentQR)return alert('No QR selected');
const location=prompt('Enter location/product info:',currentQR.location||'');
if(location!==null){
const rows=loadRows();
const idx=rows.findIndex(r=>r.id===currentQR.id);
if(idx!==-1){
rows[idx].location=location;
saveRows(rows);
currentQR=rows[idx];
alert('Location/product info saved!');
}
}
});

/* Initialize */
refreshAdminList();
updateClientView();
if(currentQR && currentQR.next_service){
countdownTimer=setInterval(updateClientView,1000);
}
</script>
</body>
</html>