← back to Dw Photo Capture

public/kid-simple/v1-magic-camera.html

551 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>v1 — Magic Camera</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
  :root{
    --charcoal:#1C1C1E;
    --gold:#C9A24B;
    --cream:#FDFBF7;
    --green:#3FA66B;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',sans-serif;}
  .stage{
    width:390px;height:780px;margin:24px auto;position:relative;
    background:var(--charcoal);border-radius:36px;overflow:hidden;
    box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;
  }
  .screen{
    position:absolute;inset:0;display:none;flex-direction:column;
    background:var(--charcoal);color:var(--cream);
  }
  .screen.active{display:flex;}
  .light{background:var(--cream);color:var(--charcoal);}
  h1,h2,h3{font-family:'Fraunces',serif;margin:0;}
  .dw-mark{
    position:absolute;top:18px;left:18px;font-family:'Fraunces',serif;
    font-weight:700;font-size:15px;letter-spacing:1px;color:var(--cream);opacity:.85;z-index:5;
  }
  .hint{
    position:absolute;top:56px;left:0;right:0;text-align:center;
    font-size:17px;color:var(--cream);opacity:.9;z-index:5;
  }
  .demo-toggle{
    position:absolute;top:86px;left:0;right:0;text-align:center;z-index:5;
    font-size:14px;color:var(--cream);opacity:.7;display:flex;align-items:center;justify-content:center;gap:8px;
  }
  .demo-toggle input{width:18px;height:18px;}
  .viewfinder{
    position:absolute;inset:0;
    background:
      repeating-linear-gradient(135deg, #232326 0 40px, #1c1c1e 40px 80px);
  }
  .shutter-wrap{
    position:absolute;bottom:48px;left:0;right:0;display:flex;justify-content:center;z-index:5;
  }
  .shutter{
    width:96px;height:96px;border-radius:50%;background:var(--gold);
    border:6px solid var(--cream);cursor:pointer;font-size:40px;
    display:flex;align-items:center;justify-content:center;
    box-shadow:0 8px 24px rgba(0,0,0,.4);
  }
  .shutter:active, .btn:active, .brand-tile:active, .nav-btn:active, .chip:active{transform:scale(.97);}
  .shutter:focus-visible, button:focus-visible, .card-btn:focus-visible, .brand-tile:focus-visible, input:focus-visible{
    outline:3px solid var(--gold);outline-offset:2px;
  }
  .btn:disabled{opacity:.4;cursor:not-allowed;}
  .demo-caption{font-size:14px;opacity:.6;margin-top:-8px;}
  .looking-wrap{align-items:center;justify-content:center;gap:18px;}
  .looking-wrap .spinner{
    width:64px;height:64px;border-radius:50%;
    border:6px solid rgba(255,255,255,.15);border-top-color:var(--gold);
    animation:spin 1s linear infinite;
  }
  @keyframes spin{to{transform:rotate(360deg);}}
  .looking-wrap p{font-size:19px;}
  /* topbar: always-there way out */
  .topbar{
    display:flex;align-items:center;justify-content:space-between;gap:10px;
    padding:14px 16px;flex-shrink:0;z-index:6;
  }
  .nav-btn{
    min-height:56px;min-width:56px;border-radius:14px;border:2px solid rgba(253,251,247,.4);
    background:transparent;color:var(--cream);font-family:'Inter',sans-serif;font-size:17px;
    font-weight:600;cursor:pointer;padding:0 16px;display:flex;align-items:center;gap:6px;
  }
  .light .nav-btn{border-color:#ddd4c2;color:var(--charcoal);}
  .step-indicator{padding:0 24px;font-size:17px;opacity:.7;flex-shrink:0;}
  .content{padding:8px 24px 24px;display:flex;flex-direction:column;gap:16px;flex:1;overflow-y:auto;}
  .actions{margin-top:auto;display:flex;flex-direction:column-reverse;gap:12px;padding-top:16px;}
  .swatch{
    width:100%;height:200px;border-radius:18px;margin-top:4px;
    background:
      repeating-linear-gradient(45deg,#8FAE8B 0 22px,#B9CBB2 22px 44px),
      linear-gradient(135deg,#8FAE8B,#6f8d6c);
    background-blend-mode:multiply;
  }
  .swatch.blue{background:
      repeating-linear-gradient(45deg,#7C93B0 0 22px,#B3C3D6 22px 44px),
      linear-gradient(135deg,#7C93B0,#5f7692);
    background-blend-mode:multiply;}
  .thumb{width:96px;height:96px;border-radius:14px;flex-shrink:0;
    background:repeating-linear-gradient(45deg,#8FAE8B 0 16px,#B9CBB2 16px 32px),linear-gradient(135deg,#8FAE8B,#6f8d6c);
    background-blend-mode:multiply;}
  .item-title{font-size:22px;font-weight:700;}
  .item-sub{font-size:17px;opacity:.75;}
  .btn{
    min-height:56px;border-radius:16px;border:none;font-family:'Inter',sans-serif;
    font-size:18px;font-weight:600;cursor:pointer;padding:0 20px;width:100%;
  }
  .btn-primary{background:var(--gold);color:#1C1C1E;}
  .btn-secondary{background:transparent;color:var(--cream);border:2px solid rgba(253,251,247,.4);}
  .btn-light-primary{background:var(--gold);color:#1C1C1E;}
  .btn-light-secondary{background:#ece7dd;color:var(--charcoal);border:2px solid #ddd4c2;}
  .btn-success{background:var(--green);color:#fff;}
  .btn-link{background:none;border:none;color:var(--cream);text-decoration:underline;font-size:17px;min-height:44px;cursor:pointer;font-family:'Inter',sans-serif;}
  .light .btn-link{color:var(--charcoal);}
  .search-bar{background:#2a2a2d;border:2px solid #3a3a3d;border-radius:14px;padding:0 14px;display:flex;align-items:center;}
  .search-bar input{background:none;border:none;color:var(--cream);font-size:17px;padding:14px 8px;width:100%;font-family:'Inter',sans-serif;}
  .search-bar input::placeholder{color:rgba(253,251,247,.5);}
  .section-label{font-size:14px;opacity:.6;text-transform:uppercase;letter-spacing:.5px;margin-top:4px;}
  .brand-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
  .brand-tile{
    background:#2a2a2d;border:2px solid #3a3a3d;border-radius:14px;
    min-height:72px;display:flex;align-items:center;justify-content:center;
    font-size:17px;font-weight:600;color:var(--cream);cursor:pointer;text-align:center;padding:8px;
  }
  .brand-tile:hover{border-color:var(--gold);}
  .brand-tile.selected{border-color:var(--gold);background:#3a331f;box-shadow:inset 0 0 0 2px var(--gold);}
  .brand-tile.hidden{display:none;}
  .center-flex{align-items:center;justify-content:center;text-align:center;}
  .big-emoji{font-size:64px;}
  .pick-list{display:flex;flex-direction:column;gap:14px;margin-top:8px;}
  .home-hdr{text-align:center;font-size:17px;margin-top:4px;}
  .done-row{display:flex;gap:14px;align-items:center;}
  .done-facts{font-size:16px;line-height:1.5;}
  .done-facts strong{font-size:18px;}
  .toast{
    position:absolute;left:16px;right:16px;bottom:24px;background:#2a2a2d;color:var(--cream);
    border-radius:14px;padding:14px 18px;display:none;align-items:center;justify-content:space-between;
    gap:12px;box-shadow:0 8px 24px rgba(0,0,0,.5);z-index:20;font-size:17px;border:2px solid var(--gold);
  }
  .toast.show{display:flex;}
  .toast button{background:none;border:none;color:var(--gold);font-weight:700;font-size:17px;cursor:pointer;min-height:56px;min-width:56px;padding:0 6px;}
  @media (prefers-reduced-motion: reduce){
    .looking-wrap .spinner{animation:none;border-top-color:var(--gold);}
    *{transition:none !important;}
  }
</style>
</head>
<body>
<div class="stage" id="stage">

  <!-- HOME -->
  <div class="screen active" id="home">
    <div class="dw-mark">DW</div>
    <div class="hint">Point at anything and tap 📸</div>
    <label class="demo-toggle"><input type="checkbox" id="demoToggle" onchange="demoNoMatch=this.checked"> Demo: show "no match"</label>
    <div class="viewfinder"></div>
    <div class="shutter-wrap">
      <button class="shutter" id="shutterBtn" aria-label="Shoot photo">📸</button>
    </div>
  </div>

  <!-- PHOTO CHECK (generic, reused after every shot) -->
  <div class="screen center-flex" id="photoCheck">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content center-flex">
      <div class="thumb" id="photoCheckThumb" style="width:220px;height:220px;"></div>
      <p style="font-size:18px;" aria-live="polite">Looks good?</p>
      <div class="actions">
        <button class="btn btn-primary" onclick="photoLooksGood()">Looks good</button>
        <button class="btn btn-secondary" onclick="goBack()">Retake</button>
      </div>
    </div>
  </div>

  <!-- LOOKING -->
  <div class="screen looking-wrap" id="looking">
    <div class="topbar" style="position:absolute;top:0;left:0;right:0;">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="spinner"></div>
    <p aria-live="polite">Looking for matches…</p>
  </div>

  <!-- WHAT'S THIS FOR -->
  <div class="screen" id="whatFor">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content">
      <h2>What's this photo for?</h2>
      <div class="pick-list">
        <button class="btn btn-primary" onclick="chooseJob('add')">Add New Item</button>
        <button class="btn btn-primary" onclick="chooseJob('update')">Update an Item</button>
        <button class="btn btn-primary" onclick="chooseJob('find')">Find an Item</button>
        <button class="btn btn-primary" onclick="chooseJob('sample')">A Sample Came In</button>
      </div>
    </div>
  </div>

  <!-- ADD: BRAND GRID -->
  <div class="screen" id="addBrand">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content">
      <h3>Which brand?</h3>
      <div class="search-bar"><input type="text" id="brandSearch" placeholder="Search brands…" oninput="filterBrands()" aria-label="Search brands"></div>
      <div class="section-label">Recent</div>
      <div class="brand-grid" id="recentBrands">
        <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'York')" onkeydown="onBrandKey(event,this,'York')">York</div>
        <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Kravet')" onkeydown="onBrandKey(event,this,'Kravet')">Kravet</div>
        <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Thibaut')" onkeydown="onBrandKey(event,this,'Thibaut')">Thibaut</div>
      </div>
      <div class="section-label">All Brands</div>
      <div class="brand-grid" id="allBrands">
        <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Brewster')" onkeydown="onBrandKey(event,this,'Brewster')">Brewster</div>
        <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Cole &amp; Son')" onkeydown="onBrandKey(event,this,'Cole &amp; Son')">Cole &amp; Son</div>
        <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Mulberry')" onkeydown="onBrandKey(event,this,'Mulberry')">Mulberry</div>
        <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Other / Unsure')" onkeydown="onBrandKey(event,this,'Other / Unsure')">Other / Unsure</div>
      </div>
      <div class="actions">
        <button class="btn btn-primary" id="saveDraftBtn" disabled onclick="saveDraft()">Save as Draft</button>
      </div>
    </div>
  </div>

  <!-- GENERIC DONE SCREEN -->
  <div class="screen light" id="doneScreen">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content">
      <div class="done-row">
        <div class="thumb" id="doneThumb"></div>
        <div class="done-facts">
          <strong id="doneTitle"></strong><br>
          code <span id="doneCode"></span> · <span id="doneBrand"></span><br>
          <span id="doneAction"></span>
        </div>
      </div>
      <div class="actions">
        <button class="btn btn-light-primary" onclick="goHome()">Do Another</button>
        <button class="btn btn-light-secondary" onclick="goHome()">Home</button>
      </div>
    </div>
  </div>

  <!-- UPDATE: MATCH -->
  <div class="screen" id="updateMatch">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="step-indicator">Step 1 of 2</div>
    <div class="content">
      <h2>Found a Match</h2>
      <div class="swatch blue"></div>
      <div class="item-title">Palm Damask — Sage</div>
      <div class="item-sub">code DW-10423 · York</div>
      <div class="actions">
        <button class="btn btn-primary" onclick="go('updateCam2')">Yes, that's right</button>
        <button class="btn btn-secondary" onclick="showNoMatch('update')">Not this item</button>
      </div>
    </div>
  </div>

  <!-- UPDATE: TAKE NEW PHOTO -->
  <div class="screen" id="updateCam2">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="step-indicator">Step 2 of 2</div>
    <div class="hint" style="position:static;margin:12px 0;">Take the new photo</div>
    <div class="viewfinder" style="position:relative;flex:1;"></div>
    <div class="shutter-wrap" style="position:absolute;bottom:48px;left:0;right:0;">
      <button class="shutter" onclick="showPhotoCheck('UPDATE_SAVE')" aria-label="Shoot new photo">📸</button>
    </div>
  </div>

  <!-- GENERIC NO MATCH -->
  <div class="screen" id="noMatch">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content center-flex">
      <div class="big-emoji">🤔</div>
      <h2 aria-live="polite">No match found</h2>
      <div class="actions">
        <button class="btn btn-primary" onclick="noMatchTryAgain()">Try again</button>
        <button class="btn btn-secondary" onclick="go('typeCode')">Type the code instead</button>
      </div>
    </div>
  </div>

  <!-- GENERIC TYPE CODE -->
  <div class="screen" id="typeCode">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content">
      <h3>Type the item's code</h3>
      <div class="search-bar"><input type="text" id="codeInput" placeholder="e.g. DW-10423" aria-label="Type the item code"></div>
      <div class="actions">
        <button class="btn btn-primary" onclick="codeSubmit()">Find It</button>
      </div>
    </div>
  </div>

  <!-- FIND: MATCH -->
  <div class="screen" id="findMatch">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content">
      <h2>Best Match</h2>
      <div class="swatch"></div>
      <div class="item-title">Palm Damask — Sage</div>
      <div class="item-sub">code DW-10423 · York</div>
      <div class="actions">
        <button class="btn btn-primary" onclick="finishFind()">Yes, that's right</button>
        <button class="btn btn-secondary" onclick="showNoMatch('find')">Not this item</button>
      </div>
    </div>
  </div>

  <!-- SAMPLE: MATCH -->
  <div class="screen" id="sampleMatch">
    <div class="topbar">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="content">
      <h2>Sample Matched</h2>
      <div class="swatch"></div>
      <div class="item-title">Palm Damask — Sage</div>
      <div class="item-sub">code DW-10423 · York</div>
      <div class="actions">
        <button class="btn btn-primary" onclick="markReceived()">Mark It Received</button>
        <button class="btn btn-secondary" onclick="showNoMatch('sample')">Not this item</button>
      </div>
    </div>
  </div>

  <!-- SAMPLE: PRINTING -->
  <div class="screen looking-wrap" id="samplePrinting">
    <div class="topbar" style="position:absolute;top:0;left:0;right:0;">
      <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
      <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
    </div>
    <div class="spinner"></div>
    <p aria-live="polite">Printing your sticker…</p>
  </div>

</div>

<!-- TOAST (undo) -->
<div class="toast" id="toast" role="status" aria-live="polite">
  <span id="toastText">Saved</span>
  <button onclick="undoAction()">Undo</button>
</div>

<script>
  let historyStack = ['home'];
  let selectedBrand = null;
  let currentJob = null;
  let demoNoMatch = false;
  let photoCheckNext = null;
  let noMatchJob = null;
  let toastTimer = null;
  let undoTarget = null;

  function render(id){
    document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
    document.getElementById(id).classList.add('active');
  }

  function go(id){
    historyStack.push(id);
    render(id);
    onEnter(id);
  }

  function goBack(){
    if(historyStack.length > 1){
      historyStack.pop();
      render(historyStack[historyStack.length - 1]);
    } else {
      goHome();
    }
  }

  function goHome(){
    historyStack = ['home'];
    render('home');
  }

  function onEnter(id){
    if(id === 'addBrand'){
      selectedBrand = null;
      document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
      document.getElementById('saveDraftBtn').disabled = true;
      document.getElementById('brandSearch').value = '';
      filterBrands();
    }
  }

  function showPhotoCheck(nextId){
    photoCheckNext = nextId;
    go('photoCheck');
  }

  function photoLooksGood(){
    // remove photoCheck from the history stack so Back skips it cleanly
    historyStack.pop();
    if(photoCheckNext === 'HOME_SHOT'){ afterHomeShot(); return; }
    if(photoCheckNext === 'UPDATE_SAVE'){ updateSave(); return; }
    go(photoCheckNext);
  }

  function chooseJob(job){
    currentJob = job;
    if(job === 'add'){ go('addBrand'); }
    else if(job === 'update'){ demoNoMatch ? showNoMatch('update') : go('updateMatch'); }
    else if(job === 'find'){ demoNoMatch ? showNoMatch('find') : go('findMatch'); }
    else if(job === 'sample'){ demoNoMatch ? showNoMatch('sample') : go('sampleMatch'); }
  }

  function showNoMatch(job){
    noMatchJob = job;
    go('noMatch');
  }

  function noMatchTryAgain(){
    // back out to the camera to reshoot
    historyStack = ['home'];
    render('home');
  }

  function codeSubmit(){
    if(noMatchJob === 'update'){ go('updateCam2'); }
    else if(noMatchJob === 'find'){ finishFind(); }
    else if(noMatchJob === 'sample'){ markReceived(); }
    else { goHome(); }
  }

  function selectBrand(el, name){
    document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
    el.classList.add('selected');
    selectedBrand = name;
    document.getElementById('saveDraftBtn').disabled = false;
  }

  function onBrandKey(evt, el, name){
    if(evt.key === 'Enter' || evt.key === ' '){
      evt.preventDefault();
      selectBrand(el, name);
    }
  }

  function filterBrands(){
    const q = document.getElementById('brandSearch').value.trim().toLowerCase();
    document.querySelectorAll('#allBrands .brand-tile').forEach(t=>{
      t.classList.toggle('hidden', q.length > 0 && !t.textContent.toLowerCase().includes(q));
    });
  }

  function randomCode(){ return 'DW-' + Math.floor(10000 + Math.random() * 89999); }

  function showDone(opts){
    document.getElementById('doneThumb').className = 'thumb' + (opts.blue ? ' blue' : '');
    document.getElementById('doneTitle').textContent = opts.name;
    document.getElementById('doneCode').textContent = opts.code;
    document.getElementById('doneBrand').textContent = opts.brand;
    document.getElementById('doneAction').textContent = opts.action;
    historyStack = ['home', 'doneScreen'];
    render('doneScreen');
  }

  function saveDraft(){
    if(!selectedBrand) return;
    showToast('Saved · Undo', 'addBrand');
    showDone({name:'New Item', code: randomCode(), brand: selectedBrand, action:'Saved as a draft'});
  }

  function updateSave(){
    showToast('Updated · Undo', 'updateMatch');
    showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Updated the photo', blue:true});
  }

  function finishFind(){
    showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Found it'});
  }

  function markReceived(){
    go('samplePrinting');
    var fromScreen = 'samplePrinting';
    setTimeout(function(){
      if(historyStack[historyStack.length - 1] !== fromScreen) return;
      showToast('Saved · Undo', 'sampleMatch');
      showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Marked as received'});
    }, 1100);
  }

  function showToast(text, backTarget){
    document.getElementById('toastText').textContent = text;
    undoTarget = backTarget;
    const t = document.getElementById('toast');
    t.classList.add('show');
    clearTimeout(toastTimer);
    toastTimer = setTimeout(function(){ t.classList.remove('show'); }, 5000);
  }

  function undoAction(){
    document.getElementById('toast').classList.remove('show');
    clearTimeout(toastTimer);
    if(undoTarget){
      historyStack = ['home', undoTarget];
      render(undoTarget);
      // NOTE: do NOT call onEnter() here — undo must restore the confirm
      // screen with the prior selection intact, not reset it (onEnter('addBrand')
      // clears selectedBrand for a fresh forward-navigation entry only).
    }
  }

  document.getElementById('shutterBtn').addEventListener('click', function(){
    showPhotoCheck('HOME_SHOT');
  });

  // after the very first shot on Home, go looking then ask what it's for
  function afterHomeShot(){
    historyStack.push('looking');
    render('looking');
    var fromScreen = 'looking';
    setTimeout(function(){
      if(historyStack[historyStack.length - 1] !== fromScreen) return;
      historyStack.pop();
      go('whatFor');
    }, 700);
  }
</script>
</body>
</html>