← back to NationalPaperHangers

views/public/measure.ejs

358 lines

<%- include('../partials/head', { title }) %>
<%- include('../partials/header') %>
<style>
  /* ── rmx-* overlay styling — shared with /book's room-measure engine.
        Copied (not linked) so /book is never touched by this page. ───── */
  .rmx-overlay{position:fixed;inset:0;z-index:99999;display:flex;flex-direction:column;justify-content:flex-end;pointer-events:none}
  .rmx-card{pointer-events:auto;background:rgba(12,12,12,0.94);color:#fff;border-radius:16px 16px 0 0;padding:18px 20px 22px;box-shadow:0 -8px 30px rgba(0,0,0,0.4);max-width:560px;width:100%;margin:0 auto}
  .rmx-title{font-size:15px;font-weight:700;margin:0 0 8px}
  .rmx-body{font-size:13px;line-height:1.5}
  .rmx-instr{margin:0 0 10px}
  .rmx-step{font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#f5c451;font-weight:700;margin:0 0 4px}
  .rmx-status{margin:8px 0;font-size:12px;font-weight:600}
  .rmx-hit-ok{color:#4ade80}
  .rmx-hit-none{color:#fbbf24}
  .rmx-readout{font-size:20px;font-weight:700;margin:8px 0;font-variant-numeric:tabular-nums}
  .rmx-field{display:block;margin:8px 0}
  .rmx-field input{display:block;width:100%;margin-top:4px;padding:10px;border-radius:8px;border:1px solid #555;background:#1a1a1a;color:#fff;font-size:16px}
  .rmx-crosshair{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);font-size:54px;line-height:1;color:#f5c451;text-shadow:0 0 6px rgba(0,0,0,0.9);pointer-events:none}
  .rmx-actions{display:flex;gap:10px;margin-top:16px}
  .rmx-actions button{flex:1;padding:13px;border-radius:9px;font-size:14px;font-weight:700;cursor:pointer;border:0}
  .rmx-cancel{background:transparent;color:#bbb;border:1px solid #555}
  .rmx-primary{background:#f5c451;color:#0a0a0a}
  .rmx-primary:disabled{opacity:0.45;cursor:not-allowed}

  /* ── measure-kit page ─────────────────────────────────────────────── */
  .mk-hero{max-width:720px;margin:0 auto;padding:8px 0 4px;text-align:center}
  .mk-hero .kicker{font-size:12px;letter-spacing:0.14em;text-transform:uppercase;color:var(--muted,#888);margin:0 0 8px}
  .mk-hero h1{font-family:'Cormorant Garamond',Georgia,serif;font-weight:500;font-size:clamp(30px,6vw,46px);line-height:1.05;margin:0 0 12px}
  .mk-hero .lede{font-size:16px;line-height:1.55;color:var(--muted,#555);max-width:560px;margin:0 auto 18px}
  .mk-cta{display:inline-flex;align-items:center;gap:8px;background:var(--ink,#0a0a0a);color:#fff;border:0;padding:14px 26px;border-radius:999px;font-size:15px;font-weight:600;cursor:pointer}
  .mk-cta:hover{opacity:0.92}
  .mk-cta.is-gold{background:#f5c451;color:#0a0a0a}
  .mk-method-note{font-size:12px;color:var(--muted,#888);margin:10px 0 0}

  .mk-wrap{max-width:720px;margin:26px auto 0}
  .mk-walls{display:flex;flex-direction:column;gap:12px;margin:0 0 16px}
  .mk-wall{border:1px solid var(--border,#d4d2c8);border-radius:12px;padding:14px 16px;background:var(--card-bg,#fff)}
  .mk-wall-head{display:flex;align-items:center;gap:8px;margin:0 0 10px}
  .mk-wall-head strong{flex:1;font-size:14px}
  .mk-wall-area{font-size:13px;color:#16a34a;font-weight:600;font-variant-numeric:tabular-nums}
  .mk-remove{border:0;background:transparent;color:var(--muted,#999);font-size:18px;line-height:1;cursor:pointer;padding:2px 6px}
  .mk-remove:hover{color:#dc2626}
  .mk-dims{display:flex;gap:10px;flex-wrap:wrap}
  .mk-field{flex:1;min-width:120px;font-size:12px;color:var(--muted,#666)}
  .mk-field input,.mk-field select{display:block;width:100%;margin-top:4px;padding:10px;border-radius:8px;border:1px solid var(--border,#ccc);background:var(--bg,#fff);color:var(--ink,#111);font-size:16px}
  .mk-row-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
  .mk-mini{font-size:12px;font-weight:600;border:1px solid var(--border,#ccc);background:transparent;color:var(--ink,#222);padding:7px 12px;border-radius:8px;cursor:pointer}
  .mk-mini.is-primary{background:#f5c451;border-color:#f5c451}
  .mk-deductions{margin-top:10px;border-top:1px dashed var(--border,#e3e1d8);padding-top:10px}
  .mk-deduct{display:flex;gap:8px;align-items:flex-end;margin:0 0 8px}
  .mk-deduct .mk-field{min-width:0}
  .mk-add-wall{display:block;width:100%;border:2px dashed var(--border,#d4d2c8);background:transparent;color:var(--ink,#333);padding:13px;border-radius:12px;font-size:14px;font-weight:600;cursor:pointer;margin:0 0 18px}
  .mk-add-wall:hover{border-color:#9aa39a}

  .mk-calc{border:1px solid var(--border,#d4d2c8);border-radius:12px;padding:16px 18px;background:var(--bg-alt,#f8f7f2);margin:0 0 18px}
  .mk-calc h2{font-size:16px;margin:0 0 12px}
  .mk-calc-grid{display:flex;gap:10px;flex-wrap:wrap}

  .mk-result{border:1px solid var(--ink,#0a0a0a);border-radius:14px;padding:20px 22px;background:var(--card-bg,#fff)}
  .mk-result .total{font-size:13px;color:var(--muted,#666);margin:0 0 4px}
  .mk-result .total b{font-size:22px;color:var(--ink,#0a0a0a);font-variant-numeric:tabular-nums}
  .mk-buy{font-size:18px;font-weight:600;margin:10px 0;line-height:1.4}
  .mk-perwall{font-size:12px;color:var(--muted,#666);margin:0 0 8px;line-height:1.6}
  .mk-flag{font-size:12px;color:#b45309;background:#fef3c7;border-radius:8px;padding:8px 10px;margin:8px 0}
  .mk-handoff{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}
  .mk-handoff a{flex:1;min-width:180px;text-align:center;text-decoration:none;padding:13px 16px;border-radius:10px;font-size:14px;font-weight:600}
  .mk-shop{background:#f5c451;color:#0a0a0a}
  .mk-installer{background:var(--ink,#0a0a0a);color:#fff}
  .mk-disclaimer{font-size:11px;color:var(--muted,#888);line-height:1.5;margin:14px 0 0;border-top:1px solid var(--border,#eee);padding-top:12px}
  .mk-privacy{font-size:12px;color:var(--muted,#888);text-align:center;max-width:560px;margin:18px auto 0;line-height:1.5}

  /* ── P3 product handoff banner ────────────────────────────────────── */
  .mk-product{display:flex;align-items:center;gap:12px;border:1px solid var(--border,#d4d2c8);border-left:4px solid #f5c451;border-radius:10px;padding:12px 16px;margin:0 0 16px;background:var(--card-bg,#fff)}
  .mk-product .mk-product-body{flex:1;min-width:0}
  .mk-product .mk-product-kicker{font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted,#888);margin:0 0 2px}
  .mk-product .mk-product-name{font-size:15px;font-weight:600;color:var(--ink,#0a0a0a);margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .mk-product .mk-product-meta{font-size:12px;color:var(--muted,#777);margin:2px 0 0}
</style>

<section class="mk-hero">
  <p class="kicker">National Paper Hangers · Tools</p>
  <h1>Measure any wall with your phone.</h1>
  <p class="lede">No tape measure, no ladder. Point your phone at the wall, place three taps, and get true-scale width and height — then we'll tell you exactly how much wallpaper to buy.</p>
  <button type="button" class="mk-cta is-gold" id="mk-hero-cta">📐 Measure your first wall</button>
  <p class="mk-method-note" id="mk-method-note"></p>
</section>

<div class="mk-wrap">
  <% if (typeof product !== 'undefined' && product) { %>
  <div class="mk-product" id="mk-product">
    <span aria-hidden="true" style="font-size:22px">🧮</span>
    <div class="mk-product-body">
      <p class="mk-product-kicker">Sizing for<%= product.from ? ' · ' + product.from : '' %></p>
      <p class="mk-product-name"><%= product.name || product.sku || 'Selected product' %></p>
      <p class="mk-product-meta"><%
        var bits = [];
        if (product.sku) bits.push('SKU ' + product.sku);
        if (product.format === 'panels') bits.push('Mural / panel goods');
        else if (product.format === 'euro') bits.push('Euro roll');
        else if (product.format === 'us_standard') bits.push('US standard roll');
        if (product.repeat != null && product.repeat > 0) bits.push(product.repeat + '" pattern repeat');
      %><%= bits.join(' · ') || 'Measure your walls below to size this product.' %></p>
    </div>
  </div>
  <% } %>
  <div class="mk-walls" id="mk-walls"></div>
  <button type="button" class="mk-add-wall" id="mk-add-wall">+ Add another wall</button>

  <div class="mk-calc">
    <h2>How much wallpaper?</h2>
    <div class="mk-calc-grid">
      <label class="mk-field">Product format
        <select id="mk-format">
          <option value="us_standard">US standard roll (sold in double rolls)</option>
          <option value="euro">Euro roll (21" × 33 ft)</option>
          <option value="panels">Mural / panel goods</option>
        </select>
      </label>
      <label class="mk-field" id="mk-repeat-field">Pattern repeat (in)
        <input type="number" id="mk-repeat" min="0" max="120" step="0.5" value="0" inputmode="decimal">
      </label>
      <label class="mk-field" id="mk-waste-field">Waste factor (%)
        <input type="number" id="mk-waste" min="0" max="60" step="1" placeholder="auto" inputmode="numeric">
      </label>
      <label class="mk-field mk-panel-only" id="mk-panelw-field" style="display:none">Panel width (in)
        <input type="number" id="mk-panelw" min="6" max="60" step="1" value="36" inputmode="numeric">
      </label>
    </div>
  </div>

  <div class="mk-result" id="mk-result">
    <p class="total">Total wall area: <b id="mk-total">0 ft²</b></p>
    <p class="mk-buy" id="mk-buy">Add a wall measurement to size your order.</p>
    <p class="mk-perwall" id="mk-perwall"></p>
    <div id="mk-flags"></div>
    <div class="mk-handoff">
      <% if (typeof product !== 'undefined' && product && product.shop) { %>
      <a class="mk-shop" href="<%= product.shop %>" target="_blank" rel="noopener noreferrer">Shop <%= product.name || 'this look' %><%= product.from ? ' — ' + product.from : '' %></a>
      <% } else { %>
      <a class="mk-shop" href="https://designerwallcoverings.com" target="_blank" rel="noopener noreferrer">Shop the look — Designer Wallcoverings</a>
      <% } %>
      <a class="mk-installer" href="/find">Find an installer near you</a>
    </div>
    <p class="mk-disclaimer">These figures are estimates based on standard trade rules of thumb and the dimensions entered. Pattern match, wall condition, and trim losses vary — <strong>always confirm quantities with your installer or the manufacturer before ordering.</strong> National Paper Hangers is a referral and lead-coordination service and does not warrant installation work.</p>
  </div>

  <p class="mk-privacy">🔒 Privacy: measurement happens entirely on your device. Camera and motion access are only requested when you tap to measure, AR camera frames never leave your phone, and nothing you enter here is sent to or stored on our servers.</p>
</div>

<% if (typeof product !== 'undefined' && product) { %>
<script type="application/json" id="mk-product-data"><%- JSON.stringify(product).replace(/</g, '\\u003c') %></script>
<% } %>
<script src="/js/room-measure.js" defer></script>
<script src="/js/measure-kit.js" defer></script>
<script>
(function () {
  'use strict';
  var walls = [];                 // [{ width_ft, height_ft, method, deductions:[] }]
  var nextId = 1;

  var $ = function (id) { return document.getElementById(id); };
  var wallsEl = $('mk-walls');

  // ── P3: product context from a DW/wallco PDP deep-link ──────────────
  // Pre-fills format / repeat / panel width so the buy line matches the
  // product the shopper came from. Parsed from a non-executable JSON block.
  var product = null;
  try {
    var pd = $('mk-product-data');
    if (pd) product = JSON.parse(pd.textContent || 'null');
  } catch (_) { product = null; }

  function applyProduct() {
    if (!product) return;
    if (product.format) $('mk-format').value = product.format;
    if (product.repeat != null) $('mk-repeat').value = product.repeat;
    if (product.panelw != null) $('mk-panelw').value = product.panelw;
  }

  function num(v) { var n = parseFloat(v); return isFinite(n) ? n : 0; }
  function fmtFt(n) { return (Math.round(n * 10) / 10) + ' ft'; }

  // ── render the wall list ────────────────────────────────────────────
  function renderWalls() {
    wallsEl.innerHTML = '';
    walls.forEach(function (wall, i) {
      var area = window.MeasureKit ? window.MeasureKit.wallArea(wall) : { net: 0 };
      var card = document.createElement('div');
      card.className = 'mk-wall';
      var deductHtml = (wall.deductions || []).map(function (d, di) {
        return '<div class="mk-deduct">' +
          '<label class="mk-field">Door/window W (ft)<input type="number" min="0" step="0.5" inputmode="decimal" data-k="dw" data-w="' + i + '" data-d="' + di + '" value="' + (d.w_ft || '') + '"></label>' +
          '<label class="mk-field">H (ft)<input type="number" min="0" step="0.5" inputmode="decimal" data-k="dh" data-w="' + i + '" data-d="' + di + '" value="' + (d.h_ft || '') + '"></label>' +
          '<button type="button" class="mk-remove" data-rmdeduct="' + i + '" data-d="' + di + '" aria-label="Remove deduction">×</button>' +
        '</div>';
      }).join('');

      card.innerHTML =
        '<div class="mk-wall-head">' +
          '<strong>Wall ' + (i + 1) + '</strong>' +
          '<span class="mk-wall-area">' + area.net + ' ft²</span>' +
          '<button type="button" class="mk-remove" data-rmwall="' + i + '" aria-label="Remove wall">×</button>' +
        '</div>' +
        '<div class="mk-dims">' +
          '<label class="mk-field">Width (ft)<input type="number" min="0" step="0.5" inputmode="decimal" data-k="w" data-w="' + i + '" value="' + (wall.width_ft || '') + '"></label>' +
          '<label class="mk-field">Height (ft)<input type="number" min="0" step="0.5" inputmode="decimal" data-k="h" data-w="' + i + '" value="' + (wall.height_ft || '') + '"></label>' +
        '</div>' +
        '<div class="mk-row-actions">' +
          '<button type="button" class="mk-mini is-primary" data-measure="' + i + '" id="mk-measure-' + i + '">📐 Measure with phone</button>' +
          '<button type="button" class="mk-mini" data-adddeduct="' + i + '">+ Subtract a door/window</button>' +
        '</div>' +
        (deductHtml ? '<div class="mk-deductions">' + deductHtml + '</div>' : '');
      wallsEl.appendChild(card);
    });
    // device-aware measure button labels
    if (window.RoomMeasure) {
      window.RoomMeasure.ready.then(function () {
        var lbl = window.RoomMeasure.label();
        walls.forEach(function (_, i) {
          var b = $('mk-measure-' + i);
          if (b) {
            if (lbl) { b.textContent = lbl; b.disabled = false; }
            else { b.textContent = '📐 No phone sensors — type dimensions'; b.disabled = true; }
          }
        });
      });
    }
    recompute();
  }

  // ── calculator ──────────────────────────────────────────────────────
  function recompute() {
    if (!window.MeasureKit) return;
    var MK = window.MeasureKit;
    var total = MK.totalArea(walls);
    $('mk-total').textContent = total + ' ft²';

    var format = $('mk-format').value;
    var repeat = num($('mk-repeat').value);
    var wasteRaw = $('mk-waste').value.trim();
    var waste = wasteRaw === '' ? undefined : (num(wasteRaw) / 100);

    // auto-fill the waste placeholder so the assumption is visible
    $('mk-waste').placeholder = Math.round(MK.defaultWaste(repeat) * 100) + ' (auto)';

    var buyEl = $('mk-buy'), perEl = $('mk-perwall'), flagsEl = $('mk-flags');
    flagsEl.innerHTML = '';

    if (format === 'panels') {
      $('mk-panelw-field').style.display = '';
      $('mk-repeat-field').style.display = 'none';
      $('mk-waste-field').style.display = 'none';
      var pres = MK.panelsForWalls(walls, { panelWidthIn: num($('mk-panelw').value) || 36 });
      buyEl.textContent = total > 0
        ? 'Buy ' + pres.totalPanels + ' panel' + (pres.totalPanels === 1 ? '' : 's') +
          ' (' + pres.panelWidthIn + '" wide) to span all walls.'
        : 'Add a wall measurement to size your order.';
      perEl.innerHTML = pres.perWall.map(function (p) {
        return 'Wall ' + (p.wallIndex + 1) + ': ' + p.panels + ' panel' + (p.panels === 1 ? '' : 's');
      }).join(' · ');
      flagsEl.innerHTML = '<div class="mk-flag">Mural panels must also be ordered tall enough to cover each wall’s height — check the panel length against your tallest wall.</div>';
    } else {
      $('mk-panelw-field').style.display = 'none';
      $('mk-repeat-field').style.display = '';
      $('mk-waste-field').style.display = '';
      var r = MK.rollsForArea(total, { formatId: format, repeatIn: repeat, waste: waste });
      buyEl.textContent = MK.buyLine(r);
      perEl.innerHTML = total > 0
        ? 'Based on ' + total + ' ft² + ' + r.wastePct + '% waste = ' + r.areaWithWaste + ' ft² of paper.'
        : '';
    }
  }

  // ── measure via RoomMeasure engine ──────────────────────────────────
  function measureInto(i) {
    if (!window.RoomMeasure) return;
    window.RoomMeasure.measure().then(function (m) {
      if (m.width_ft != null) walls[i].width_ft = m.width_ft;
      if (m.height_ft != null) walls[i].height_ft = m.height_ft;
      walls[i].method = m.method;
      renderWalls();
      // gyro gives height only — nudge the user to fill width
      if (m.width_ft == null && m.method === 'gyro') {
        var wb = wallsEl.querySelector('input[data-k="w"][data-w="' + i + '"]');
        if (wb) { wb.focus(); }
      }
    }).catch(function (err) {
      if (err && err.message && err.message !== 'cancelled') {
        alert(err.message);
      }
    });
  }

  function addWall() { walls.push({ width_ft: null, height_ft: null, deductions: [] }); renderWalls(); }

  // ── events (delegated) ──────────────────────────────────────────────
  wallsEl.addEventListener('input', function (e) {
    var t = e.target, k = t.getAttribute('data-k');
    if (!k) return;
    var wi = +t.getAttribute('data-w');
    if (k === 'w') walls[wi].width_ft = num(t.value);
    else if (k === 'h') walls[wi].height_ft = num(t.value);
    else if (k === 'dw' || k === 'dh') {
      var di = +t.getAttribute('data-d');
      walls[wi].deductions[di][k === 'dw' ? 'w_ft' : 'h_ft'] = num(t.value);
    }
    // update just the area chips + result without nuking focus
    var chips = wallsEl.querySelectorAll('.mk-wall-area');
    walls.forEach(function (wall, idx) {
      if (chips[idx]) chips[idx].textContent = window.MeasureKit.wallArea(wall).net + ' ft²';
    });
    recompute();
  });

  wallsEl.addEventListener('click', function (e) {
    var t = e.target;
    if (t.hasAttribute('data-measure')) measureInto(+t.getAttribute('data-measure'));
    else if (t.hasAttribute('data-rmwall')) { walls.splice(+t.getAttribute('data-rmwall'), 1); renderWalls(); }
    else if (t.hasAttribute('data-adddeduct')) { walls[+t.getAttribute('data-adddeduct')].deductions.push({ w_ft: null, h_ft: null }); renderWalls(); }
    else if (t.hasAttribute('data-rmdeduct')) {
      walls[+t.getAttribute('data-rmdeduct')].deductions.splice(+t.getAttribute('data-d'), 1); renderWalls();
    }
  });

  $('mk-add-wall').addEventListener('click', addWall);
  $('mk-hero-cta').addEventListener('click', function () {
    if (!walls.length) addWall();
    // if a phone method is available, jump straight into measuring wall 1
    if (window.RoomMeasure) {
      window.RoomMeasure.ready.then(function () {
        if (window.RoomMeasure.method()) measureInto(0);
        else { var b = wallsEl.querySelector('input[data-k="w"]'); if (b) b.focus(); }
      });
    }
  });
  ['mk-format', 'mk-repeat', 'mk-waste', 'mk-panelw'].forEach(function (id) {
    $(id).addEventListener('input', recompute);
    $(id).addEventListener('change', recompute);
  });

  // method note in the hero
  if (window.RoomMeasure) {
    window.RoomMeasure.ready.then(function () {
      var m = window.RoomMeasure.method();
      $('mk-method-note').textContent =
        m === 'webxr' ? 'Your device supports full AR — measure width AND height.'
        : m === 'gyro' ? 'Your device measures wall HEIGHT with its motion sensor; type the width in.'
        : 'This device has no measurement sensors — type your dimensions below (or open this page on your phone).';
    });
  }

  applyProduct();   // prefill format/repeat/panel from PDP deep-link, if any
  addWall();        // start with one wall ready
})();
</script>

<%- include('../partials/footer') %>