← back to Wallco Ai

public/theme-variants/theme-variant-nav.js

233 lines

/* theme-variant-nav.js — ADMIN-ONLY PDP theme switchers for the 17 variants.
 *
 * Drop-in (ABSOLUTE path — variants are served at /design/:id/:slug):
 *   <script src="/theme-variants/theme-variant-nav.js" defer></script>
 *
 * ADMIN GATE: everything below shows ONLY when localStorage.dwAdmin === '1'
 * (set via ?admin=1, cleared via ?admin=0). For shoppers we HIDE the static
 * .switcher bar entirely so the internal theme machinery is never exposed.
 *
 * For admins, two switchers:
 *  (1) theme dropdown — turns each variant's .switcher bar ("Theme: X ← Classic")
 *      into a working <select> of Classic + all 17 variants. Classic routes via
 *      ?classic=1 (server forces the monolith) so it works even when a variant
 *      is the live default; variants route via /design/:id/<slug>.
 *  (2) ‹ prev / next › stepper pill (upper-right). Alt+←/→ also step.
 * Marks the LIVE theme (from /api/pdp-theme). Self-styled · idempotent · 0 deps.
 */
(function () {
  'use strict';
  if (window.__themeVariantNavLoaded) return;
  window.__themeVariantNavLoaded = true;

  // ── ADMIN GATE ─────────────────────────────────────────────────────────
  // Resolve admin first. Shoppers get the .switcher bar hidden and nothing else.
  var isAdmin = false;
  try {
    var q = new URLSearchParams(location.search);
    if (q.get('admin') === '1') localStorage.setItem('dwAdmin', '1');
    if (q.get('admin') === '0') localStorage.removeItem('dwAdmin');
    isAdmin = localStorage.getItem('dwAdmin') === '1';
  } catch (e) { isAdmin = false; }

  if (!isAdmin) {
    // Hide the internal theme-switcher bar from shoppers (it's static HTML in
    // every variant). Injected as early as the deferred script runs.
    try {
      var hide = document.createElement('style');
      hide.id = 'tvn-hide-switcher';
      hide.textContent = '.switcher{display:none !important}';
      (document.head || document.documentElement).appendChild(hide);
    } catch (e) {}
    return;
  }

  // ordered route slugs (match server /design/:id/:slug + smoke test) → display name
  var THEMES = [
    ['compact',    'Compact'],
    ['bento',      'Bento'],
    ['tabs',       'Tabs'],
    ['hero',       'Hero'],
    ['vertical',   'Vertical'],
    ['drawer',     'Drawer'],
    ['editorial',  'Editorial'],
    ['spec',       'Spec'],
    ['config',     'Config'],
    ['stack',      'Stack'],
    ['best',       'Best'],
    ['roomsplit',  'Room Split'],
    ['roomdrawer', 'Room Drawer'],
    ['roomzine',   'Room Editorial'],
    ['roomsticky', 'Room Sticky'],
    ['roommin',    'Room Minimal'],
    ['roomshow',   'Room Showcase'],
  ];
  var SLUGS = THEMES.map(function (t) { return t[0]; });

  // Detect current context.
  //  • /design/:id/:slug  → route mode, slug from URL.
  //  • /design/:id (bare) → route mode too, but the server serves the LIVE
  //    theme (or ?theme=) with NO slug in the URL, so we can't read it from the
  //    path. Mark slug=null and resolve the "current" theme from /api/pdp-theme
  //    (or ?theme=) once it's known, in tvnInit() / the pill fetch.
  //  • /theme-variants/vN-slug.html → file mode (dev).
  var mRoute = location.pathname.match(/^\/design\/([^/]+)\/([^/?#]+)/);
  var mBare  = location.pathname.match(/^\/design\/([^/?#]+)\/?$/);
  var mode, designId, slug, idx;
  if (mRoute) {
    mode = 'route'; designId = mRoute[1]; slug = mRoute[2];
    idx = SLUGS.indexOf(slug);
  } else if (mBare) {
    mode = 'route'; designId = mBare[1]; slug = null; idx = -1; // resolved async
  } else {
    mode = 'file';
    var base = location.pathname.split('/').pop().replace(/\.html?$/i, ''); // vN-slug
    slug = base.replace(/^v\d+-/, '');
    idx = SLUGS.indexOf(slug);
  }
  if (idx === -1) idx = 0; // safe synchronous default; bare URL overridden once live theme is known

  // Resolve the served theme on a bare /design/:id (no slug in URL): prefer an
  // explicit ?theme= preview, else the live theme. Call after LIVE_IDX is set.
  function resolveBareIdx() {
    if (slug !== null) return;
    var tp = new URLSearchParams(location.search).get('theme');
    var ti = tp ? SLUGS.indexOf(tp) : -1;
    if (ti !== -1) idx = ti;
    else if (LIVE_IDX !== -1) idx = LIVE_IDX;
  }

  // map slug -> the vN-slug filename (for file mode)
  var FILEMAP = {
    compact:'v1-compact', bento:'v2-bento', tabs:'v3-tabs', hero:'v4-hero', vertical:'v5-vertical',
    drawer:'v6-drawer', editorial:'v7-editorial', spec:'v8-spec', config:'v9-config', stack:'v10-stack',
    best:'v11-best', roomsplit:'v12-roomsplit', roomdrawer:'v13-roomdrawer', roomzine:'v14-roomzine',
    roomsticky:'v15-roomsticky', roommin:'v16-roommin', roomshow:'v17-roomshow',
  };

  function urlFor(i) {
    var s = SLUGS[(i % SLUGS.length + SLUGS.length) % SLUGS.length];
    if (mode === 'route') return '/design/' + encodeURIComponent(designId) + '/' + s + location.search;
    return (FILEMAP[s] || ('v' + (i + 1) + '-' + s)) + '.html' + location.search;
  }
  function go(d) { location.href = urlFor(idx + d); }

  var LIVE_IDX = -1;

  // ───────────────────────── (1) theme dropdown (admin) ─────────────────────
  function classicUrl() {
    if (mode !== 'route') return '/';                 // file-mode dev: no monolith
    var p = new URLSearchParams(location.search);
    p.set('classic', '1');
    return '/design/' + encodeURIComponent(designId) + '?' + p.toString();
  }
  function buildDropdown() {
    var sw = document.querySelector('header .switcher') || document.querySelector('.switcher');
    if (!sw || sw.querySelector('.tvn-dd')) return;   // no bar, or already built
    if (!document.getElementById('tvn-dd-css')) {
      var st = document.createElement('style');
      st.id = 'tvn-dd-css';
      st.textContent = '.switcher .tvn-dd{font:inherit;font-size:11px;line-height:1.4;color:var(--ink,#1a1611);'
        + 'background:transparent;border:1px solid rgba(0,0,0,.28);border-radius:6px;padding:2px 6px;'
        + 'cursor:pointer;max-width:230px;vertical-align:middle}';
      document.head.appendChild(st);
    }
    var sel = document.createElement('select');
    sel.className = 'tvn-dd';
    sel.setAttribute('aria-label', 'Switch PDP theme');
    var oc = document.createElement('option');
    oc.value = '__classic__';
    oc.textContent = 'Classic (full PDP)';
    sel.appendChild(oc);
    THEMES.forEach(function (t, i) {
      var o = document.createElement('option');
      o.value = t[0];
      o.textContent = 'V' + (i + 1) + ' · ' + t[1] + (i === LIVE_IDX ? '  • live' : '');
      if (i === idx) o.selected = true;
      sel.appendChild(o);
    });
    sel.addEventListener('change', function () {
      location.href = (sel.value === '__classic__') ? classicUrl() : urlFor(SLUGS.indexOf(sel.value));
    });
    // Repurpose the existing "← Classic" link (inline href is the broken bare
    // URL). Force it through classicUrl() so it works even if the page's own
    // inline script resets the href afterwards.
    var back = sw.querySelector('#back-to-classic');
    if (back) {
      back.href = classicUrl();
      back.addEventListener('click', function (e) { e.preventDefault(); location.href = classicUrl(); });
    }
    sw.appendChild(document.createTextNode(' '));
    sw.appendChild(sel);
  }
  function tvnInit() {
    // Resolve live theme (to flag "• live"), then build. Build regardless of
    // fetch outcome — the dropdown must work even if /api/pdp-theme is down.
    fetch('/api/pdp-theme', { credentials: 'same-origin' })
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (j) {
        var s = j && typeof j.theme === 'string' ? j.theme.toLowerCase().trim() : '';
        var li = SLUGS.indexOf(s);
        if (li !== -1) LIVE_IDX = li;
      })
      .catch(function () {})
      .then(function () { resolveBareIdx(); buildDropdown(); });
  }
  if (document.body) tvnInit();
  else document.addEventListener('DOMContentLoaded', tvnInit);

  // ───────────────── (2) ‹ prev / next › stepper pill (admin) ────────────────
  // ---- styles ----
  var css = document.createElement('style');
  css.textContent = [
    '#tvn{position:fixed;top:14px;right:16px;z-index:99999;display:flex;align-items:center;gap:2px;',
      'background:rgba(12,12,12,.86);backdrop-filter:blur(8px);border:1px solid rgba(245,241,232,.20);',
      'border-radius:999px;padding:3px 4px;font-family:-apple-system,system-ui,sans-serif;box-shadow:0 6px 22px rgba(0,0,0,.45)}',
    '#tvn button{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;background:transparent;',
      'color:#f5f1e8;font-size:16px;line-height:1;cursor:pointer;border-radius:50%;transition:background .15s}',
    '#tvn button:hover{background:rgba(245,241,232,.16)}',
    '#tvn #tvn-label{font-size:11px;letter-spacing:.03em;color:#f5f1e8;min-width:130px;text-align:center;padding:0 8px;white-space:nowrap;opacity:.95}',
    '#tvn #tvn-label b{font-weight:600}',
    '#tvn .tvn-live{display:inline-block;width:6px;height:6px;border-radius:50%;background:#3fd09a;margin-left:6px;vertical-align:middle}',
  ].join('');
  document.head.appendChild(css);

  function label() {
    var name = THEMES[idx][1];
    var live = (idx === LIVE_IDX) ? '<span class="tvn-live" title="Live theme"></span>' : '';
    return '<b>' + name + '</b> · ' + (idx + 1) + '/' + THEMES.length + live;
  }

  function build() {
    var el = document.createElement('div');
    el.id = 'tvn';
    el.setAttribute('aria-label', 'Theme navigator (admin)');
    el.innerHTML =
      '<button type="button" id="tvn-prev" aria-label="Previous theme" title="Previous theme (Alt+←)">‹</button>' +
      '<span id="tvn-label">' + label() + '</span>' +
      '<button type="button" id="tvn-next" aria-label="Next theme" title="Next theme (Alt+→)">›</button>';
    document.body.appendChild(el);
    document.getElementById('tvn-prev').addEventListener('click', function () { go(-1); });
    document.getElementById('tvn-next').addEventListener('click', function () { go(1); });
    document.addEventListener('keydown', function (e) {
      if (e.target && /input|textarea|select/i.test(e.target.tagName)) return;
      if (e.altKey && e.key === 'ArrowLeft')  { e.preventDefault(); go(-1); }
      if (e.altKey && e.key === 'ArrowRight') { e.preventDefault(); go(1); }
    });
    // mark the live theme (server resolves null -> "best" and returns a slug)
    fetch('/api/pdp-theme', { credentials: 'same-origin' })
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (j) {
        var s = j && typeof j.theme === 'string' ? j.theme.toLowerCase().trim() : '';
        var li = SLUGS.indexOf(s);
        if (li !== -1) LIVE_IDX = li;
        resolveBareIdx(); // bare /design/:id has no slug → current = live/?theme
        var l = document.getElementById('tvn-label'); if (l) l.innerHTML = label();
      })
      .catch(function () {});
  }

  if (document.body) build();
  else document.addEventListener('DOMContentLoaded', build);
})();