← 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);
})();