← back to Dw Photo Capture

public/kid-simple/v14-ticket-printer.html

345 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>v14 — Ticket Printer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root{
    --bg:#F6F1E7; --ink:#241F1C; --paper:#FFFDF6; --muted:#8A7F70;
    --chassis:#23343B; --slot:#0F1A1E; --lamp:#7FD858;
    --new:#B5502D; --update:#3C6E71; --find:#B08D3E; --sample:#6B4E71;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;background:#ddd;font-family:'Fredoka',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(--bg);border-radius:36px;overflow:hidden;
    box-shadow:0 24px 60px rgba(0,0,0,.28);display:flex;flex-direction:column;position:relative;
  }
  #screen{flex:1;display:flex;flex-direction:column;position:relative;overflow-y:auto;}
  button{font-family:'Fredoka',sans-serif;cursor:pointer;border:none;}
  button:focus-visible, .linktext:focus-visible{outline:3px solid #2B6CB0;outline-offset:2px;}

  /* ---- home fab ---- */
  .homefab{
    position:absolute;top:14px;left:14px;z-index:30;min-width:56px;min-height:56px;
    border-radius:16px;background:rgba(255,255,255,.94);border:2px solid #e7e1d0;color:var(--ink);
    font-size:14px;font-weight:700;display:none;align-items:center;justify-content:center;gap:4px;
    padding:0 12px;box-shadow:0 4px 10px rgba(0,0,0,.18);
  }

  /* ---- printer chassis (home) ---- */
  .printer-top{background:var(--chassis);padding:24px 22px 20px;border-radius:0 0 30px 30px;position:relative;}
  .chassis-row{display:flex;align-items:center;justify-content:space-between;}
  .dw-mark{color:var(--paper);font-weight:700;font-size:13px;letter-spacing:2.5px;opacity:.8;}
  .lamp{width:12px;height:12px;border-radius:50%;background:var(--lamp);box-shadow:0 0 9px var(--lamp);}
  .chassis-title{color:var(--paper);font-weight:700;font-size:26px;text-align:center;margin:12px 0 18px;letter-spacing:.3px;}
  .slot{background:var(--slot);height:24px;border-radius:6px;margin:0 14px;position:relative;box-shadow:inset 0 3px 7px rgba(0,0,0,.6);}
  .paper-peek{position:absolute;left:50%;transform:translateX(-50%);top:-3px;width:64%;height:8px;background:var(--paper);border-radius:2px 2px 0 0;opacity:.4;}
  .readout{color:var(--paper);opacity:.72;text-align:center;font-size:13px;margin:14px 0 0;font-family:'Courier Prime',monospace;letter-spacing:.3px;}

  /* ---- keypad (home) ---- */
  .keypad{padding:22px;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:16px;flex:1;}
  .key{
    border-radius:24px;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:10px;font-weight:700;font-size:17px;text-align:center;
    box-shadow:0 6px 0 rgba(0,0,0,.2), 0 12px 20px rgba(0,0,0,.14);
  }
  .key:active{transform:translateY(5px);box-shadow:0 1px 0 rgba(0,0,0,.2);}
  .key .key-icon{font-size:40px;line-height:1;}
  .key-new{background:var(--new);}
  .key-update{background:var(--update);}
  .key-find{background:var(--find);color:#2B2109;}
  .key-sample{background:var(--sample);}
  .keypad-hint{grid-column:1/3;text-align:center;color:var(--muted);font-size:13px;font-family:'Courier Prime',monospace;align-self:end;}

  /* ---- generic padded question screen ---- */
  .pad{padding:32px 24px 28px;display:flex;flex-direction:column;flex:1;}
  .q{font-weight:600;font-size:25px;line-height:1.28;text-align:center;margin:4px 0 24px;}
  .stack{display:flex;flex-direction:column;gap:14px;}
  .rowbtn{
    width:100%;min-height:64px;border-radius:18px;font-size:18px;font-weight:700;
    background:#fff;border:2px solid #e9e1d0;color:var(--ink);padding:12px 16px;
  }
  .accent-new{background:var(--new)!important;color:#fff!important;border-color:var(--new)!important;}
  .accent-update{background:var(--update)!important;color:#fff!important;border-color:var(--update)!important;}
  .accent-find{background:var(--find)!important;color:#2B2109!important;border-color:var(--find)!important;}
  .accent-sample{background:var(--sample)!important;color:#fff!important;border-color:var(--sample)!important;}
  .linktext{
    background:none;border:none;color:var(--muted);font-size:16px;text-decoration:underline;
    text-align:center;min-height:56px;width:100%;font-family:'Fredoka',sans-serif;
  }
  .brandlist{display:flex;flex-direction:column;gap:12px;overflow-y:auto;}

  /* ---- swatch (mock item art) ---- */
  .swatch{width:100%;height:110px;border-radius:14px;margin-bottom:10px;}
  .sw1{background:repeating-linear-gradient(45deg,#B5502D 0 16px,#8f3f22 16px 32px);}
  .sw2{background:repeating-linear-gradient(-45deg,#3C6E71 0 16px,#2c5254 16px 32px);}
  .sw3{background:repeating-linear-gradient(90deg,#B08D3E 0 14px,#8f712c 14px 28px);}
  .itemname{font-size:19px;font-weight:700;margin:0;text-align:center;}
  .itemmeta{font-size:15px;color:#6d6459;margin:4px 0 0;text-align:center;font-family:'Courier Prime',monospace;}

  /* ---- camera ---- */
  .camwrap{flex:1;display:flex;flex-direction:column;}
  .camview{flex:1;background:#12181B;position:relative;display:flex;align-items:center;justify-content:center;color:#fff;min-height:520px;}
  .camview .tag{position:absolute;top:18px;left:18px;padding:7px 14px;border-radius:20px;font-weight:700;font-size:12.5px;letter-spacing:.4px;color:#fff;}
  .camview .hint{position:absolute;top:66px;left:20px;right:20px;text-align:center;font-weight:600;font-size:25px;color:var(--paper);}
  .camview .status{font-family:'Courier Prime',monospace;font-size:14px;color:#cfc6b5;}
  .shutter{
    position:absolute;bottom:32px;left:50%;transform:translateX(-50%);
    width:92px;height:92px;border-radius:50%;background:var(--paper);border:6px solid rgba(255,255,255,.15);
    font-size:34px;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 18px rgba(0,0,0,.45);
  }

  /* ---- ticket dock + ticket paper ---- */
  .ticket-dock{background:var(--chassis);padding:20px 22px 30px;position:relative;border-radius:0 0 30px 30px;}
  .ticket-dock .slot{margin:0 10px;}
  .ticket-dock .paper-peek.out{opacity:1;height:34px;top:-3px;transition:height .5s ease, opacity .5s ease;}
  .dockmsg{color:var(--paper);text-align:center;font-family:'Courier Prime',monospace;font-size:13.5px;margin:14px 0 0;opacity:.85;}
  .ticket{
    background:var(--paper);margin:-14px 22px 0;padding:24px 20px 20px;position:relative;
    box-shadow:0 12px 24px rgba(0,0,0,.16);
    clip-path:polygon(0% 0%,100% 0%,100% 90%,95% 100%,90% 90%,85% 100%,80% 90%,75% 100%,70% 90%,65% 100%,60% 90%,55% 100%,50% 90%,45% 100%,40% 90%,35% 100%,30% 90%,25% 100%,20% 90%,15% 100%,10% 90%,5% 100%,0% 90%);
  }
  .ticket-mono{font-family:'Courier Prime',monospace;font-size:13.5px;line-height:1.85;color:#2b2620;white-space:pre-wrap;text-align:center;}
  .ticket-head{font-weight:700;font-size:15px;letter-spacing:.5px;}
  .ticket-cta{padding:26px 24px 24px;display:flex;flex-direction:column;gap:14px;}
  .center{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;gap:10px;}
</style>
</head>
<body>
<div class="phone">
  <button class="homefab" id="homeFab" aria-label="Home" onclick="go('home')">⌂ Home</button>
  <div id="screen" role="main"></div>
</div>

<script>
const MOCK_ITEMS = [
  {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1'},
  {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2'},
  {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3'},
];
const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
const TAG = {new:{cls:'accent-new', label:'🆕 NEW ITEM'}, update:{cls:'accent-update', label:'🔄 UPDATE'},
             find:{cls:'accent-find', label:'🔍 FIND'}, sample:{cls:'accent-sample', label:'📦 SAMPLE IN'}};

const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0, printed:false };

function go(screen){
  state.screen = screen;
  if(screen.endsWith('-ticket')){
    state.printed = false;
    render();
    setTimeout(()=>{ if(state.screen===screen){ state.printed = true; render(); } }, 800);
    return;
  }
  render();
}
function startJob(job){
  state.job = job; state.item=null; state.brand=null; state.resultIndex=0;
  const map = {new:'new-cam', update:'update-cam', find:'find-cam', sample:'sample-cam'};
  go(map[job]);
}
function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
function stamp(){
  return new Date().toLocaleString(undefined,{month:'2-digit',day:'2-digit',hour:'numeric',minute:'2-digit'});
}

function cameraScreen(hint, onShootExpr){
  const t = TAG[state.job];
  return `<div class="camwrap"><div class="camview">
    <div class="tag ${t.cls}">${t.label}</div>
    <div class="hint">${hint}</div>
    <div class="status" id="camStatus">Tap the shutter</div>
    <button class="shutter" aria-label="Take photo" onclick="${onShootExpr}">📸</button>
  </div></div>`;
}
function shoot(cb){
  const st = document.getElementById('camStatus');
  const btn = document.querySelector('.shutter');
  if(btn) btn.disabled = true;
  if(st) st.textContent = 'Snapping…';
  setTimeout(()=>{ state.item = randomItem(); cb(); }, 700);
}

function ticketDock(msg){
  return `<div class="ticket-dock">
    <div class="chassis-row"><span class="dw-mark">DW</span><span class="lamp"></span></div>
    <div class="slot"><div class="paper-peek ${state.printed?'out':''}"></div></div>
    <p class="dockmsg">${state.printed ? 'Ticket printed! 🖨️' : msg}</p>
  </div>`;
}

function render(){
  const s = document.getElementById('screen');
  const fab = document.getElementById('homeFab');
  fab.style.display = state.screen==='home' ? 'none' : 'flex';
  let html = '';

  if(state.screen==='home'){
    html += `<div class="printer-top">
        <div class="chassis-row"><span class="dw-mark">DW</span><span class="lamp" aria-hidden="true"></span></div>
        <div class="chassis-title">TICKET PRINTER</div>
        <div class="slot"><div class="paper-peek"></div></div>
        <p class="readout">Ready — pick a job to print a ticket.</p>
      </div>
      <div class="keypad">
        <button class="key key-new" onclick="startJob('new')"><span class="key-icon">🆕</span><span>New Item</span></button>
        <button class="key key-update" onclick="startJob('update')"><span class="key-icon">🔄</span><span>Update</span></button>
        <button class="key key-find" onclick="startJob('find')"><span class="key-icon">🔍</span><span>Find</span></button>
        <button class="key key-sample" onclick="startJob('sample')"><span class="key-icon">📦</span><span>Sample In</span></button>
        <p class="keypad-hint">every job prints a ticket</p>
      </div>`;
  }

  /* ---------- NEW ITEM ---------- */
  else if(state.screen==='new-cam'){
    html += cameraScreen('Take a photo of it.', "shoot(()=>go('new-brand'))");
  }
  else if(state.screen==='new-brand'){
    html += `<div class="pad"><h1 class="q">Which brand is it?</h1><div class="brandlist">`;
    BRANDS.forEach(b=>{ html += `<button class="rowbtn" onclick="state.brand='${b}'; go('new-ticket')">${b}</button>`; });
    html += `</div></div>`;
  }
  else if(state.screen==='new-ticket'){
    html += ticketDock('Printing your ticket…');
    if(state.printed){
      html += `<div class="ticket">
          <div class="swatch ${state.item.sw}"></div>
          <p class="ticket-mono"><span class="ticket-head">★ NEW ITEM ADDED ★</span>
BRAND:  ${state.brand}
CODE:   ${state.item.code}
DATE:   ${stamp()}

  thanks — nice work!</p>
        </div>
        <div class="ticket-cta">
          <button class="rowbtn accent-new" onclick="startJob('new')">Print Another</button>
          <button class="linktext" onclick="go('home')">Home</button>
        </div>`;
    }
  }

  /* ---------- UPDATE ---------- */
  else if(state.screen==='update-cam'){
    html += cameraScreen('Scan the label or pattern.', "shoot(()=>go('update-match'))");
  }
  else if(state.screen==='update-match'){
    html += `<div class="pad"><h1 class="q">Is this it?</h1>
      <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 class="stack" style="margin-top:22px;">
        <button class="rowbtn accent-update" onclick="go('update-cam2')">Yes, That's It</button>
        <button class="rowbtn" onclick="go('update-cam')">No, Scan Again</button>
      </div></div>`;
  }
  else if(state.screen==='update-cam2'){
    html += cameraScreen('Take the new photo.', "shoot(()=>go('update-ticket'))");
  }
  else if(state.screen==='update-ticket'){
    html += ticketDock('Printing your ticket…');
    if(state.printed){
      html += `<div class="ticket">
          <div class="swatch ${state.item.sw}"></div>
          <p class="ticket-mono"><span class="ticket-head">★ ITEM UPDATED ★</span>
ITEM:   ${state.item.name}
CODE:   ${state.item.code}
DATE:   ${stamp()}

    all set!</p>
        </div>
        <div class="ticket-cta">
          <button class="rowbtn accent-update" onclick="go('home')">Back Home</button>
        </div>`;
    }
  }

  /* ---------- FIND ---------- */
  else if(state.screen==='find-cam'){
    html += cameraScreen('Scan it to find the item.', "shoot(()=>{ state.resultIndex=0; go('find-result'); })");
  }
  else if(state.screen==='find-result'){
    const it = MOCK_ITEMS[state.resultIndex];
    html += `<div class="pad"><h1 class="q">Is this the one?</h1>
      <div class="swatch ${it.sw}"></div>
      <p class="itemname">${it.name} — ${it.color}</p>
      <p class="itemmeta">code ${it.code}</p>
      <div class="stack" style="margin-top:22px;">
        <button class="rowbtn accent-find" onclick="pickResult()">Yes, This One</button>
        <button class="rowbtn" onclick="nextResult()">Next Result</button>
      </div></div>`;
  }
  else if(state.screen==='find-exhausted'){
    html += `<div class="pad center"><h1 class="q">That's everything. Try scanning again?</h1>
      <button class="rowbtn accent-find" onclick="go('find-cam')">Scan Again</button>
    </div>`;
  }
  else if(state.screen==='find-ticket'){
    html += ticketDock('Printing your ticket…');
    if(state.printed){
      html += `<div class="ticket">
          <div class="swatch ${state.item.sw}"></div>
          <p class="ticket-mono"><span class="ticket-head">★ HERE'S YOUR ITEM ★</span>
ITEM:   ${state.item.name}
CODE:   ${state.item.code}
DATE:   ${stamp()}

    found it!</p>
        </div>
        <div class="ticket-cta">
          <button class="rowbtn accent-update" onclick="go('update-cam2')">Update This</button>
          <button class="rowbtn" onclick="go('find-cam')">Search Again</button>
          <button class="linktext" onclick="go('home')">Home</button>
        </div>`;
    }
  }

  /* ---------- SAMPLE ---------- */
  else if(state.screen==='sample-cam'){
    html += cameraScreen('Scan the box label.', "shoot(()=>go('sample-match'))");
  }
  else if(state.screen==='sample-match'){
    html += `<div class="pad"><h1 class="q">Mark it received?</h1>
      <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 class="stack" style="margin-top:22px;">
        <button class="rowbtn accent-sample" onclick="go('sample-ticket')">Yes, Mark Received</button>
      </div></div>`;
  }
  else if(state.screen==='sample-ticket'){
    html += ticketDock('Printing your ticket…');
    if(state.printed){
      html += `<div class="ticket">
          <div class="swatch ${state.item.sw}"></div>
          <p class="ticket-mono"><span class="ticket-head">★ SAMPLE RECEIVED ★</span>
ITEM:   ${state.item.name}
CODE:   ${state.item.code}
DATE:   ${stamp()}

  sticker printed! ✅</p>
        </div>
        <div class="ticket-cta">
          <button class="rowbtn accent-sample" onclick="go('home')">Back Home</button>
        </div>`;
    }
  }

  s.innerHTML = html;
}
function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex]; go('find-ticket'); }
function nextResult(){
  if(state.resultIndex < MOCK_ITEMS.length-1){ state.resultIndex++; go('find-result'); }
  else { go('find-exhausted'); }
}

render();
</script>
</body>
</html>