← back to Dw Photo Capture
public/kid-simple/v4-level-up.html
445 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>v4 — Level Up</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Nunito:wght@500;700;800;900&display=swap" rel="stylesheet">
<style>
:root{
--plum:#4A2E4E; --gold:#E8B84B; --cream:#F5EFE6; --green:#59B37A;
--ink:#2B2B2B;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;background:#ddd;font-family:'Nunito',sans-serif;color:var(--ink);}
body{display:flex;align-items:flex-start;justify-content:center;min-height:100vh;padding:20px 0;}
.phone{
width:390px; min-height:780px; background:var(--cream);
border-radius:36px; overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.28);
display:flex; flex-direction:column; position:relative;
}
header{
background:var(--plum); color:#fff; padding:12px 14px; gap:8px;
display:flex; align-items:center; justify-content:space-between; min-height:56px;
}
header .mark{font-family:'Fraunces',serif; font-weight:700; font-size:18px; letter-spacing:.5px;}
.navbtn{
background:rgba(255,255,255,.14);border:none;color:#fff;font-size:16px;font-weight:800;cursor:pointer;
min-width:56px;min-height:56px;line-height:1;border-radius:14px; padding:0 14px;
font-family:'Nunito',sans-serif; display:inline-flex; align-items:center; justify-content:center; gap:4px;
}
.navbtn:hover{background:rgba(255,255,255,.24);}
.navbtn:active{transform:scale(.97);}
.screen{flex:1; display:flex; flex-direction:column; gap:16px; padding:22px 20px 28px; overflow-y:auto;}
h1.title{font-family:'Fraunces',serif; font-weight:700; font-size:26px; margin:0 0 2px; color:var(--plum);}
p.sub{margin:0; font-size:17px; color:#4a4a4a;}
.dots{display:flex; gap:8px; margin:4px 0 6px;}
.dots span{width:16px;height:16px;border-radius:50%;background:#e2d9c9;border:2px solid #d8cdb8;}
.dots span.filled{background:var(--gold); border-color:var(--gold);}
button{font-family:'Nunito',sans-serif; cursor:pointer;}
.btn{
min-height:56px; width:100%; border-radius:18px; font-size:18px; font-weight:800;
border:none; display:flex; align-items:center; justify-content:center; gap:8px; padding:0 16px;
}
.btn:active{transform:scale(.97);}
.btn-primary{background:var(--gold); color:#3a2a06;}
.btn-secondary{background:#fff; color:var(--plum); border:2px solid var(--plum);}
.btn-ghost{background:transparent; color:var(--plum); text-decoration:underline; min-height:44px;}
.btn:focus-visible, a:focus-visible, .brandcard:focus-visible, .resultcard:focus-visible,
.navbtn:focus-visible, .demotoggle:focus-visible, .codeinput:focus-visible, .searchbox:focus-visible,
.toastundo:focus-visible{
outline:3px solid #2B6CB0; outline-offset:2px;
}
.card{background:#fff; border-radius:20px; padding:18px; box-shadow:0 6px 18px rgba(0,0,0,.08);}
.levelcard{background:#fff; border-radius:20px; padding:18px; display:flex; flex-direction:column; gap:10px; box-shadow:0 6px 18px rgba(0,0,0,.08);}
.levelcard h2{margin:0; font-family:'Fraunces',serif; font-size:20px; color:var(--plum);}
.levelcard .startrow{display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:8px;}
.camview{
flex:1; min-height:260px; background:#1c1c1e; border-radius:20px; position:relative;
display:flex; align-items:center; justify-content:center; color:#eee; overflow:hidden;
}
.camview .hint{position:absolute; top:14px; left:0; right:0; text-align:center; font-size:15px; color:#f0e8d8;}
.camview .status{font-size:20px; font-weight:700;}
.shutter{
position:absolute; bottom:20px; left:50%; transform:translateX(-50%);
width:76px; height:76px; border-radius:50%; background:var(--gold); border:5px solid #fff;
font-size:30px; display:flex; align-items:center; justify-content:center;
}
.shutter:active{transform:translateX(-50%) scale(.97);}
.swatch{width:100%; height:180px; border-radius:16px;}
.sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 18px,#7c9c78 18px 36px), radial-gradient(circle at 30% 30%, #a9c7a4, transparent 60%);}
.sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#617a99 16px 32px);}
.sw3{background:repeating-linear-gradient(90deg,#D9A441 0 14px,#c98f2c 14px 28px), radial-gradient(circle at 70% 40%, #e9c06b, transparent 55%);}
.brandgrid{display:grid; grid-template-columns:1fr 1fr; gap:12px;}
.brandcard{
min-height:64px; border-radius:16px; background:#fff; border:2px solid #e3d9c6;
font-size:17px; font-weight:700; color:var(--plum); display:flex; align-items:center; justify-content:center; text-align:center; padding:8px;
}
.brandcard:active{transform:scale(.97);}
.brandcard.selected{border-color:var(--gold); background:#fff7e6;}
.sectionlabel{margin:0; font-size:14px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; color:#8a7a5a;}
.searchbox{
width:100%; min-height:56px; border-radius:16px; border:2px solid #e3d9c6; padding:0 16px;
font-size:17px; font-family:'Nunito',sans-serif; background:#fff; color:var(--ink);
}
.codeinput{
width:100%; min-height:56px; border-radius:16px; border:2px solid #e3d9c6; padding:0 16px;
font-size:17px; font-family:'Nunito',sans-serif; background:#fff; color:var(--ink);
}
.demotoggle{
background:none; border:2px dashed #cbbfa0; color:#4a4a4a; font-size:17px; font-weight:700;
border-radius:12px; padding:8px 12px; min-height:56px; align-self:flex-start;
}
.demotoggle:active{transform:scale(.97);}
.itemname{font-size:19px; font-weight:800; margin:0 0 2px;}
.itemmeta{font-size:17px; color:#5a5a5a; margin:0;}
.resultlist{display:flex; flex-direction:column; gap:10px;}
.resultcard{
display:flex; gap:12px; align-items:center; background:#fff; border-radius:16px; padding:10px;
border:2px solid #eee; min-height:72px; text-align:left; width:100%;
}
.resultcard:active{transform:scale(.97);}
.resultcard .thumb{width:52px; height:52px; border-radius:10px; flex:none;}
.confetti-banner{
background:var(--green); color:#fff; border-radius:18px; padding:18px; text-align:center;
font-family:'Fraunces',serif; font-size:22px; font-weight:700;
}
.row{display:flex; gap:12px;}
.row .btn{flex:1;}
.homegrid{display:flex; flex-direction:column; gap:16px;}
.sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0;}
.toast{
position:absolute; left:16px; right:16px; bottom:16px; background:#232323; color:#fff;
border-radius:14px; padding:14px 18px; display:flex; align-items:center; justify-content:space-between;
gap:12px; font-size:16px; font-weight:700; opacity:0; transform:translateY(12px); pointer-events:none;
transition:opacity .25s ease, transform .25s ease; z-index:30;
}
.toast.show{opacity:1; transform:translateY(0); pointer-events:auto;}
.toastundo{background:none; border:none; color:var(--gold); font-weight:800; font-size:16px; min-height:56px; padding:0 6px;}
@media (prefers-reduced-motion: reduce){
*{transition:none !important; animation:none !important;}
}
</style>
</head>
<body>
<div class="phone">
<header>
<button class="navbtn" aria-label="Back" onclick="goBack()" id="backBtn">‹ Back</button>
<span class="mark">DW · Photo</span>
<button class="navbtn" aria-label="Home" onclick="go('home')" id="homeBtnTop">⌂ Home</button>
</header>
<div class="screen" id="screen" role="main"></div>
<div class="toast" id="toast" role="status"></div>
<div class="sr-only" id="live" aria-live="polite"></div>
</div>
<script>
const JOBS = [
{id:'new', title:'Add New Item', emoji:'🆕', levels:3},
{id:'update', title:'Update an Item', emoji:'🔄', levels:3},
{id:'find', title:'Find an Item', emoji:'🔍', levels:2},
{id:'sample', title:'A Sample Came In', emoji:'📦', levels:2},
];
const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
const RECENT_BRANDS = BRANDS.slice(0,3);
const MOCK_ITEMS = [
{name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1', brand:'York Wallcoverings'},
{name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2', brand:'Cole & Son'},
{name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3', brand:'Thibaut'},
];
const DONE_TOAST = {'new-done':'Saved','update-done':'Updated','sample-done':'Marked Received'};
const state = {screen:'home', job:null, brand:null, item:null, demoNoMatch:false, checkNext:null, checkRetake:null, confirmScreen:null};
let historyStack = [];
let toastTimer = null;
function announce(text){ const el=document.getElementById('live'); if(el) el.textContent = text; }
function go(screen){
if(screen==='home'){ historyStack = []; }
else if(state.screen && state.screen!==screen){
if(String(screen).endsWith('-done')) state.confirmScreen = state.screen;
historyStack.push(state.screen);
}
state.screen = screen;
render();
}
function goReplace(screen){ state.screen = screen; render(); }
function goBack(){
if(historyStack.length){ state.screen = historyStack.pop(); render(); }
else go('home');
}
function startJob(id){ state.job = id; state.brand=null; state.item=null; go(id+'-1'); }
function jobTitle(){ return JOBS.find(j=>j.id===state.job).title; }
function jobLevels(){ return JOBS.find(j=>j.id===state.job).levels; }
function dots(filled){
const total = jobLevels();
let out = '<div class="dots">';
for(let i=1;i<=total;i++) out += `<span class="${i<=filled?'filled':''}"></span>`;
out += '</div>';
return out;
}
function levelFromScreen(scr){ const m = /-(\d+)$/.exec(scr||''); return m ? parseInt(m[1],10) : null; }
function levelHeader(n, name){
return `<h1 class="title">${jobTitle()}</h1>${dots(n)}<p class="sub"><strong>Level ${n} of ${jobLevels()}</strong> — ${name}</p>`;
}
function cameraScreen(hint, onShoot){
return `
<div class="camview">
<div class="hint">${hint}</div>
<div class="status" id="camStatus">Tap the shutter</div>
<button class="shutter" aria-label="Snap It" onclick="${onShoot}">📸</button>
</div>`;
}
function shoot(cb, loadingLabel){
const fromScreen = state.screen;
const st = document.getElementById('camStatus');
const btn = document.querySelector('.shutter');
if(btn) btn.disabled = true;
if(st) st.textContent = 'Snapping…';
announce('Snapping photo…');
setTimeout(()=>{
if(state.screen!==fromScreen) return; // user navigated away mid-shot
const item = MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)];
state.item = item;
if(loadingLabel){
if(st) st.textContent = loadingLabel;
announce(loadingLabel);
setTimeout(()=>{ if(state.screen===fromScreen) cb(); }, 500);
} else cb();
}, 700);
}
function shootThen(nextScreen, retakeScreen){
shoot(()=>{ state.checkNext=nextScreen; state.checkRetake=retakeScreen; go('photocheck'); });
}
function shootOrMiss(nextScreen, retakeScreen, missScreen, loadingLabel){
shoot(()=>{
if(state.demoNoMatch){ go(missScreen); return; }
state.checkNext=nextScreen; state.checkRetake=retakeScreen; go('photocheck');
}, loadingLabel);
}
function finishCheck(){
const next = state.checkNext;
go(next);
if(next==='update-done') state.confirmScreen = 'update-3';
if(DONE_TOAST[next]) showToast(DONE_TOAST[next]);
}
function toggleDemo(){ state.demoNoMatch = !state.demoNoMatch; render(); }
function demoToggleHtml(){
return `<button class="demotoggle" aria-pressed="${state.demoNoMatch}" onclick="toggleDemo()">Demo: show "no match" ${state.demoNoMatch?'✅ On':'⬜ Off'}</button>`;
}
function tryFind(){
go('find-loading');
announce('Looking for matches…');
setTimeout(()=>{ if(state.screen==='find-loading') goReplace(state.demoNoMatch ? 'find-nomatch' : 'find-2'); }, 600);
}
function filterBrands(q){
q = q.trim().toLowerCase();
const recentWrap = document.getElementById('brandRecent');
if(recentWrap) recentWrap.style.display = q ? 'none' : '';
const full = document.getElementById('brandFullGrid');
if(!full) return;
full.querySelectorAll('.brandcard').forEach(btn=>{
btn.style.display = btn.textContent.toLowerCase().includes(q) ? '' : 'none';
});
}
function showToast(msg){
const t = document.getElementById('toast');
t.innerHTML = `${msg} <button class="toastundo" onclick="undoLast()">Undo</button>`;
t.classList.add('show');
announce(msg + '. Undo available for 5 seconds.');
clearTimeout(toastTimer);
toastTimer = setTimeout(()=>{ t.classList.remove('show'); }, 5000);
}
function undoLast(){
const t = document.getElementById('toast');
t.classList.remove('show');
clearTimeout(toastTimer);
if(state.confirmScreen) go(state.confirmScreen);
}
function saveDraft(){ go('new-done'); showToast('Saved'); }
function markReceived(){
const fromScreen = state.screen;
const btn = document.getElementById('markReceivedBtn');
if(btn){ btn.disabled = true; btn.textContent = 'Printing sticker…'; }
announce('Printing sticker…');
setTimeout(()=>{
if(state.screen!==fromScreen) return; // user navigated away mid-print
go('sample-done'); showToast('Marked Received');
}, 700);
}
function brandGridHtml(nextScreen){
let html = `<input type="text" class="searchbox" id="brandSearch" placeholder="Search brands…" aria-label="Search brands" oninput="filterBrands(this.value)">`;
html += `<div id="brandRecent"><p class="sectionlabel">Recent</p><div class="brandgrid">`;
RECENT_BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
html += `</div></div>`;
html += `<p class="sectionlabel">All Brands</p><div class="brandgrid" id="brandFullGrid">`;
BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
html += `</div>`;
return html;
}
function render(){
const s = document.getElementById('screen');
let html = '';
const onHome = state.screen==='home';
document.getElementById('homeBtnTop').style.display = onHome ? 'none' : 'inline-flex';
document.getElementById('backBtn').style.display = onHome ? 'none' : 'inline-flex';
if(state.screen==='home'){
html += `<h1 class="title">Pick a Job</h1><p class="sub">Every job is a quick level path.</p>
<div class="homegrid">`;
JOBS.forEach(j=>{
html += `<div class="levelcard">
<h2>${j.emoji} ${j.title}</h2>
<p class="sub">Level 1 of ${j.levels}</p>
<div class="startrow">
${dots(0)}
<button class="btn btn-primary" style="width:auto;min-width:110px;" onclick="startJob('${j.id}')">Start</button>
</div>
</div>`;
});
html += `</div>`;
}
else if(state.screen==='photocheck'){
const lvl = levelFromScreen(state.checkRetake);
html += lvl ? levelHeader(lvl, 'Photo Check') : `<h1 class="title">Photo Check</h1>`;
html += `<div class="card"><div class="swatch ${state.item.sw}"></div></div>`;
html += `<p class="sub">Does this look right?</p>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="go(state.checkRetake)">Retake</button><button class="btn btn-primary" onclick="finishCheck()">Looks good</button></div>`;
}
else if(state.screen==='new-1'){
html += levelHeader(1,'Snap It');
html += cameraScreen('Take a photo of the sample', "shootThen('new-2','new-1')");
}
else if(state.screen==='new-2'){
html += levelHeader(2,'Pick the Brand');
html += brandGridHtml('new-3');
}
else if(state.screen==='new-3'){
html += levelHeader(3,'Save It');
html += `<div class="card">
<div class="swatch ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta">Brand: ${state.brand} · code ${state.item.code}</p>
</div>`;
html += `<button class="btn btn-primary" onclick="saveDraft()">Save as Draft ⭐</button>`;
}
else if(state.screen==='new-done'){
html += `<div class="confetti-banner">🎉 +10 points! Nice work</div>`;
html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">Brand: ${state.brand} · Code: ${state.item.code}</p><p class="itemmeta">Saved as a new item draft.</p></div>`;
html += `<div class="row"><button class="btn btn-primary" onclick="startJob('new')">Do Another</button><button class="btn btn-secondary" onclick="go('home')">Home</button></div>`;
}
else if(state.screen==='update-1'){
html += levelHeader(1,'Find It');
html += `<p class="sub">Scan the label, or search by typing.</p>`;
html += cameraScreen('Show me the label or pattern', "shootOrMiss('update-2','update-1','update-nomatch','Looking for matches…')");
html += demoToggleHtml();
}
else if(state.screen==='update-nomatch'){
html += `<h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="go('update-1')">Try again</button><button class="btn btn-primary" onclick="go('update-typecode')">Type the code instead</button></div>`;
}
else if(state.screen==='update-typecode'){
html += `<h1 class="title">Type the Code</h1>`;
html += `<input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">`;
html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('update-2')">Find</button>`;
}
else if(state.screen==='update-2'){
html += levelHeader(2,'Confirm It');
html += `<div class="card">
<div class="swatch ${state.item.sw}"></div>
<p class="itemname">${state.item.name} — ${state.item.color}</p>
<p class="itemmeta">code ${state.item.code}</p>
</div>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="go('update-1')">No, search again</button><button class="btn btn-primary" onclick="go('update-3')">Yes, that's it</button></div>`;
}
else if(state.screen==='update-3'){
html += levelHeader(3,'Snap the New Photo');
html += cameraScreen('Take the new photo', "shootThen('update-done','update-3')");
}
else if(state.screen==='update-done'){
html += `<div class="confetti-banner">✅ Updated!</div>`;
html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p><p class="itemmeta">Photo updated.</p></div>`;
html += `<div class="row"><button class="btn btn-primary" onclick="startJob('update')">Do Another</button><button class="btn btn-secondary" onclick="go('home')">Home</button></div>`;
}
else if(state.screen==='find-1'){
html += levelHeader(1,'Search');
html += `<div class="row" style="flex-direction:column;">
<button class="btn btn-secondary" onclick="tryFind()">📷 Scan</button>
<button class="btn btn-secondary" onclick="tryFind()">🎤 Voice</button>
<button class="btn btn-secondary" onclick="tryFind()">⌨️ Type</button>
</div>`;
html += demoToggleHtml();
}
else if(state.screen==='find-loading'){
html += `<div class="card" style="text-align:center;"><p class="sub">Looking for matches…</p></div>`;
}
else if(state.screen==='find-nomatch'){
html += `<h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="go('find-1')">Try again</button><button class="btn btn-primary" onclick="go('find-typecode')">Type the code instead</button></div>`;
}
else if(state.screen==='find-typecode'){
html += `<h1 class="title">Type the Code</h1>`;
html += `<input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">`;
html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('find-done')">Find</button>`;
}
else if(state.screen==='find-2'){
html += levelHeader(2,'Pick It');
html += `<div class="resultlist">`;
MOCK_ITEMS.forEach((it,i)=>{
html += `<button class="resultcard" onclick="state.item=MOCK_ITEMS[${i}]; go('find-done')">
<div class="thumb ${it.sw}"></div>
<div><p class="itemname" style="font-size:17px;">${it.name} — ${it.color}</p><p class="itemmeta">code ${it.code}</p></div>
</button>`;
});
html += `</div>`;
}
else if(state.screen==='find-done'){
html += `<h1 class="title">Found It</h1>`;
html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p></div>`;
html += `<div class="row"><button class="btn btn-primary" onclick="startJob('find')">Do Another</button><button class="btn btn-secondary" onclick="go('home')">Home</button></div>`;
}
else if(state.screen==='sample-1'){
html += levelHeader(1,'Scan the Box');
html += cameraScreen('Scan the label on the box', "shootOrMiss('sample-2','sample-1','sample-nomatch','Looking for matches…')");
html += demoToggleHtml();
}
else if(state.screen==='sample-nomatch'){
html += `<h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p>`;
html += `<div class="row"><button class="btn btn-secondary" onclick="go('sample-1')">Try again</button><button class="btn btn-primary" onclick="go('sample-typecode')">Type the code instead</button></div>`;
}
else if(state.screen==='sample-typecode'){
html += `<h1 class="title">Type the Code</h1>`;
html += `<input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">`;
html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('sample-2')">Find</button>`;
}
else if(state.screen==='sample-2'){
html += levelHeader(2,'Confirm & Print');
html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">code ${state.item.code}</p></div>`;
html += `<button class="btn btn-primary" id="markReceivedBtn" onclick="markReceived()">Mark It Received</button>`;
}
else if(state.screen==='sample-done'){
html += `<div class="confetti-banner">🖨️ Sticker printed! +10 points</div>`;
html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p><p class="itemmeta">Marked received.</p></div>`;
html += `<div class="row"><button class="btn btn-primary" onclick="startJob('sample')">Do Another</button><button class="btn btn-secondary" onclick="go('home')">Home</button></div>`;
}
s.innerHTML = html;
}
render();
</script>
</body>
</html>