← back to Trade Portal
public/index.html
526 lines
<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
var _fetch = window.fetch.bind(window);
var cleanBase = function () { return location.origin + location.pathname; };
window.fetch = function (input, init) {
try {
if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
input = new URL(input, cleanBase()).href;
} else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
input = new Request(new URL(input.url, cleanBase()).href, input);
}
} catch (_) { /* fall through to native */ }
return _fetch(input, init);
};
})();
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>To the Trade — Designer Wallcoverings</title>
<style>
:root{--cols:4;--accent:#1a3a5c;--gold:#b8962e}
*{box-sizing:border-box}
body{margin:0;font:15px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:#f7f5f2}
.topbar{background:var(--accent);color:#fff;padding:10px 24px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.topbar h1{margin:0;font-size:15px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;flex:1}
.topbar a{color:#cce0ff;font-size:13px;text-decoration:none;cursor:pointer}
.topbar a:hover{color:#fff}
.trade-badge{background:var(--gold);color:#fff;font-size:11px;font-weight:700;padding:2px 8px;border-radius:3px;letter-spacing:.05em;text-transform:uppercase}
#session-info{font-size:13px;color:#cce0ff;display:flex;align-items:center;gap:10px}
/* ── TEST-MODE banner ── */
.test-banner{background:#7b3f00;color:#ffe8cc;font-size:12px;padding:7px 24px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.test-banner strong{font-size:12px;letter-spacing:.04em}
.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid #e2ddd7;background:#fff;position:sticky;top:0;z-index:10}
.controls h2{margin:0;font-size:13px;font-weight:600;color:#666;flex:1}
label{font-size:12px;color:#666;display:flex;gap:6px;align-items:center}
select,input[type=range]{font:inherit}
.count{font-size:12px;color:#999}
.nav-links{display:flex;gap:14px}
.nav-links a{font-size:13px;color:var(--accent);text-decoration:none;font-weight:500;cursor:pointer}
.nav-links a:hover{text-decoration:underline}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px;padding:24px}
.card{background:#fff;border:1px solid #e2ddd7;border-radius:8px;overflow:hidden;transition:box-shadow .15s;display:flex;flex-direction:column}
.card:hover{box-shadow:0 4px 16px rgba(0,0,0,.1)}
.card img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block;background:#eee}
.card-body{padding:10px 12px;flex:1;display:flex;flex-direction:column}
.card-title{font-size:13px;font-weight:600;margin:0 0 2px;line-height:1.3}
.card-vendor{font-size:11px;color:#888;margin:0 0 4px}
.card-type{font-size:11px;color:#aaa}
.card-sku{font-size:10px;color:#bbb;margin-top:2px}
.price-row{margin-top:8px;display:flex;flex-direction:column;gap:3px}
.price-retail{font-size:13px;color:#444}
.price-retail .plabel{font-size:10px;color:#999;text-transform:uppercase;letter-spacing:.04em}
.price-trade-wrap{margin-top:4px}
.price-trade{font-size:14px;font-weight:700;color:var(--accent);background:#eaf2ff;padding:3px 7px;border-radius:4px;display:inline-block}
.price-trade-label{font-size:10px;color:var(--gold);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
.price-est-note{font-size:10px;color:#999;margin-top:2px;font-style:italic}
.trade-locked{font-size:12px;color:#bbb;font-style:italic;margin-top:4px}
.kravet-badge{font-size:10px;background:#fef3e2;color:var(--gold);border:1px solid #f5d68a;padding:1px 5px;border-radius:3px;display:inline-block;margin-top:3px}
/* ── Cart / Buy button area ── */
.card-actions{margin-top:auto;padding-top:10px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}
.qty-input{width:48px;padding:5px 6px;border:1px solid #ddd;border-radius:4px;font-size:12px;text-align:center}
.btn-buy{display:inline-block;padding:6px 12px;background:var(--accent);color:#fff;font-size:12px;font-weight:600;border-radius:5px;text-decoration:none;border:none;cursor:pointer;white-space:nowrap}
.btn-buy:hover{background:#0d2440}
.btn-buy-disabled{display:inline-block;padding:6px 12px;background:#e0e0e0;color:#999;font-size:12px;font-weight:600;border-radius:5px;cursor:default;white-space:nowrap}
.checkout-caveat{font-size:10px;color:#999;margin-top:4px;font-style:italic;line-height:1.4}
.price-on-request{font-size:13px;color:#777;font-style:italic;margin-top:4px}
.btn-view-dw{display:inline-block;padding:6px 12px;background:#fff;color:var(--accent);font-size:12px;font-weight:600;border-radius:5px;text-decoration:none;border:1px solid var(--accent);white-space:nowrap}
.btn-view-dw:hover{background:#eaf2ff}
/* ── Trade CTA banner ── */
.empty{padding:60px;text-align:center;color:#999}
#trade-cta{background:linear-gradient(135deg,#1a3a5c,#0d2440);color:#fff;padding:20px 24px;display:flex;align-items:center;gap:20px;flex-wrap:wrap}
#trade-cta h3{margin:0 0 4px;font-size:15px}
#trade-cta p{margin:0;font-size:13px;color:#bcd6f5}
#trade-cta .cta-actions{display:flex;gap:10px;flex-wrap:wrap;margin-left:auto}
.btn{padding:9px 18px;border-radius:5px;font-size:13px;font-weight:600;cursor:pointer;border:none;text-decoration:none;display:inline-block}
.btn-primary{background:var(--gold);color:#fff}
.btn-primary:hover{background:#9a7a22}
.btn-outline{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}
.btn-outline:hover{background:rgba(255,255,255,.1)}
.btn-sm{padding:6px 14px;font-size:12px}
/* ── List view ── */
.grid.list{display:flex;flex-direction:column;gap:6px;padding:12px 24px 60px}
.grid.list .card{flex-direction:row;align-items:stretch;border-radius:6px}
.grid.list .card img{width:80px;height:80px;aspect-ratio:1/1;flex:0 0 80px;object-fit:cover}
.grid.list .card-body{flex:1;flex-direction:row;flex-wrap:wrap;align-items:center;gap:12px;padding:8px 12px;display:flex}
.grid.list .card-body .card-title{flex:1;min-width:160px}
.grid.list .card-body .card-vendor{min-width:100px}
.grid.list .card-body .price-row{margin-top:0}
.grid.list .card-body .card-actions{margin-top:0;padding-top:0}
.grid.list .card-body .card-sku{margin-top:0}
/* ── View toggle ── */
.tp-view-btn{background:#fff;border:1px solid #e2ddd7;border-radius:5px;padding:6px 11px;cursor:pointer;font-size:12px;font-family:inherit;color:#666}
.tp-view-btn:hover{border-color:var(--accent)}
.tp-view-btn.active{border-color:var(--accent);color:var(--accent)}
/* ── Fields ▾ popover ── */
.tp-fieldmenu{position:relative}
.tp-fbtn{background:#fff;border:1px solid #e2ddd7;border-radius:5px;padding:6px 11px;cursor:pointer;font-size:12px;font-family:inherit;color:#666;white-space:nowrap}
.tp-fbtn:hover{border-color:var(--accent);color:var(--accent)}
.tp-fpop{position:absolute;top:calc(100% + 6px);right:0;background:#fff;border:1px solid #e2ddd7;border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);padding:8px 4px;z-index:30;min-width:200px}
.tp-fpop[hidden]{display:none}
.tp-fpop label{display:flex;align-items:center;gap:9px;padding:7px 14px;font-size:12px;color:#666;cursor:pointer;white-space:nowrap}
.tp-fpop label:hover{background:#f5f5f5;color:#333}
.tp-fpop input{accent-color:var(--accent);margin:0}
/* field-hide classes (grid only) */
body.tphf-img .grid:not(.list) .card img{display:none}
body.tphf-title .grid:not(.list) .card-title{display:none}
body.tphf-vendor .grid:not(.list) .card-vendor{display:none}
body.tphf-type .grid:not(.list) .card-type{display:none}
body.tphf-sku .grid:not(.list) .card-sku{display:none}
body.tphf-price .grid:not(.list) .price-row{display:none}
body.tphf-actions .grid:not(.list) .card-actions{display:none}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:100;display:none;align-items:center;justify-content:center}
.modal-overlay.open{display:flex}
.modal{background:#fff;border-radius:10px;width:100%;max-width:400px;padding:28px;position:relative}
.modal h2{margin:0 0 16px;font-size:18px;color:var(--accent)}
.modal-close{position:absolute;top:14px;right:16px;background:none;border:none;font-size:20px;cursor:pointer;color:#999}
.field{margin-bottom:14px}
.field label{display:block;font-size:12px;font-weight:600;color:#666;margin-bottom:4px;text-transform:uppercase;letter-spacing:.04em}
.field input,.field select,.field textarea{width:100%;padding:9px 10px;border:1px solid #ddd;border-radius:5px;font:inherit;font-size:14px}
.field input:focus,.field select:focus,.field textarea:focus{outline:2px solid var(--accent);border-color:transparent}
.field textarea{resize:vertical;min-height:70px}
.login-hint{font-size:12px;color:#999;margin-top:16px;padding:10px;background:#f5f5f5;border-radius:5px}
.login-hint code{background:#e8e8e8;padding:1px 4px;border-radius:3px;font-size:11px}
.error-msg{color:#c0392b;font-size:13px;margin-top:8px;display:none}
#apply-page{display:none;padding:32px 24px;max-width:680px;margin:0 auto}
#apply-page h2{color:var(--accent);margin:0 0 6px}
#apply-page .subtitle{color:#666;font-size:14px;margin:0 0 24px}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.form-grid .full{grid-column:1/-1}
.apply-success{display:none;padding:24px;background:#eafaf1;border:1px solid #a9dfbf;border-radius:8px;color:#196f3d;margin-top:16px}
.back-link{color:var(--accent);cursor:pointer;font-size:13px;margin-bottom:20px;display:inline-block;background:none;border:none;padding:0;font:inherit}
</style>
<link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<div class="topbar">
<h1>Designer Wallcoverings — To the Trade</h1>
<div id="session-info">
<span id="session-name" style="display:none"></span>
<span id="trade-badge-top" class="trade-badge" style="display:none">Trade Account</span>
<a id="btn-login" onclick="openLogin()">Trade Login</a>
<a id="btn-logout" onclick="doLogout()" style="display:none">Log Out</a>
<a href="/admin" style="color:#5a8ab5;font-size:11px;margin-left:6px">Admin</a>
</div>
</div>
<!-- TEST-MODE banner -->
<div class="test-banner">
<strong>TEST</strong>
— "Buy on Shopify" hands off to the <strong>live</strong> Shopify checkout at designerwallcoverings.com.
Some trade lines require a customer login at checkout (store setting).
PayPal shows when enabled in Shopify Payments (store admin toggle).
</div>
<div id="trade-cta">
<div>
<h3>Access Net / Trade Pricing</h3>
<p>Interior designers, architects & decorators unlock trade pricing on all lines.</p>
</div>
<div class="cta-actions">
<button class="btn btn-outline btn-sm" onclick="openLogin()">Trade Login</button>
<button class="btn btn-primary btn-sm" onclick="showApply()">Apply for Trade Access</button>
</div>
</div>
<div id="grid-page">
<div class="controls">
<h2 id="page-heading">All Products</h2>
<div class="nav-links">
<a onclick="showApply()">Apply for Trade Account</a>
</div>
<label>Sort
<select id="sort">
<option value="newest">Newest</option>
<option value="title">Title A→Z</option>
<option value="sku">SKU A→Z</option>
<option value="price-asc">Price ↑</option>
<option value="price-desc">Price ↓</option>
<option value="vendor">Vendor</option>
<option value="color">Color</option>
<option value="type">Product Type</option>
</select>
</label>
<label>Density <input id="density" type="range" min="2" max="6" value="4"></label>
<button class="tp-view-btn" id="tp-viewgrid" title="Grid view">⊞ Grid</button>
<button class="tp-view-btn" id="tp-viewlist" title="List view">☰ List</button>
<div class="tp-fieldmenu">
<button class="tp-fbtn" id="tp-fieldbtn">Fields ▾</button>
<div class="tp-fpop" id="tp-fpop" hidden>
<label><input type="checkbox" data-tpf="img" checked> Image</label>
<label><input type="checkbox" data-tpf="title" checked> Title</label>
<label><input type="checkbox" data-tpf="vendor" checked> Vendor</label>
<label><input type="checkbox" data-tpf="type" checked> Product type</label>
<label><input type="checkbox" data-tpf="sku" checked> SKU</label>
<label><input type="checkbox" data-tpf="price" checked> Price</label>
<label><input type="checkbox" data-tpf="actions" checked> Buy / actions</label>
</div>
</div>
<span class="count" id="count"></span>
</div>
<div class="grid" id="grid"><div class="empty">Loading…</div></div>
</div>
<div id="apply-page">
<button class="back-link" onclick="showGrid()">← Back to catalog</button>
<h2>Apply for Trade Access</h2>
<p class="subtitle">Complete the form below. We review applications within 1–2 business days.</p>
<form id="apply-form">
<div class="form-grid">
<div class="field">
<label>Company Name *</label>
<input name="company_name" required placeholder="Studio Name or Firm">
</div>
<div class="field">
<label>Contact Name *</label>
<input name="contact_name" required placeholder="Your full name">
</div>
<div class="field">
<label>Business Email *</label>
<input name="email" type="email" required placeholder="you@studio.com">
</div>
<div class="field">
<label>Phone *</label>
<input name="phone" type="tel" required placeholder="(555) 000-0000">
</div>
<div class="field">
<label>Resale / Trade Cert # *</label>
<input name="trade_cert" required placeholder="CA-1234567 or equivalent">
</div>
<div class="field">
<label>Business Type</label>
<select name="business_type">
<option value="">Select…</option>
<option>Interior Designer</option>
<option>Architect</option>
<option>Decorator</option>
<option>Contractor / Installer</option>
<option>Home Stager</option>
<option>Other</option>
</select>
</div>
<div class="field full">
<label>Website / Portfolio</label>
<input name="website" type="url" placeholder="https://yourstudio.com">
</div>
<div class="field full">
<label>How did you hear about us?</label>
<input name="how_heard" placeholder="Referral, Instagram, trade show…">
</div>
<div class="field full">
<label>Additional Notes</label>
<textarea name="notes" placeholder="Anything else you'd like us to know"></textarea>
</div>
</div>
<div class="error-msg" id="apply-error"></div>
<button class="btn btn-primary" type="submit" style="margin-top:16px;width:100%;padding:12px">Submit Trade Application</button>
</form>
<div class="apply-success" id="apply-success">
<strong>Application received!</strong> We'll review your trade credentials and follow up at your email within 1–2 business days.
<br><br><button class="back-link" onclick="showGrid()">← Return to catalog</button>
</div>
</div>
<!-- Login modal -->
<div class="modal-overlay" id="login-modal">
<div class="modal">
<button class="modal-close" onclick="closeLogin()">×</button>
<h2>Trade Account Login</h2>
<form id="login-form">
<div class="field">
<label>Email</label>
<input id="login-email" type="email" required placeholder="trade@yourstudio.com" autocomplete="email">
</div>
<div class="field">
<label>Password</label>
<input id="login-pass" type="password" required placeholder="Password" autocomplete="current-password">
</div>
<div class="error-msg" id="login-error"></div>
<button class="btn btn-primary" type="submit" style="width:100%;margin-top:4px">Log In</button>
</form>
<div class="login-hint">
<strong>Demo credentials:</strong><br>
Email: <code>trade@demo.com</code> Password: <code>trade2024</code>
</div>
<div style="margin-top:14px;font-size:12px;color:#999;text-align:center">
No account? <a href="#" onclick="closeLogin();showApply();return false" style="color:var(--accent)">Apply for trade access</a>
</div>
</div>
</div>
<script>
const esc = s => (s||'').replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const $id = id => document.getElementById(id);
// Sort + density: persist in localStorage (house rule)
const sortSel = $id('sort'), densInp = $id('density');
sortSel.value = localStorage.getItem('tp-sort') || 'newest';
densInp.value = localStorage.getItem('tp-cols') || '4';
document.documentElement.style.setProperty('--cols', densInp.value);
sortSel.onchange = () => { localStorage.setItem('tp-sort', sortSel.value); loadProducts(); };
densInp.oninput = () => {
document.documentElement.style.setProperty('--cols', densInp.value);
localStorage.setItem('tp-cols', densInp.value);
};
// Session state
let tradeSession = false;
async function refreshSession() {
const r = await fetch('/api/session');
const d = await r.json();
tradeSession = d.trade;
if (d.trade) {
$id('session-name').textContent = `${d.name} (${d.company})`;
$id('session-name').style.display = 'inline';
$id('trade-badge-top').style.display = 'inline';
$id('btn-login').style.display = 'none';
$id('btn-logout').style.display = 'inline';
$id('trade-cta').style.display = 'none';
} else {
$id('session-name').style.display = 'none';
$id('trade-badge-top').style.display = 'none';
$id('btn-login').style.display = 'inline';
$id('btn-logout').style.display = 'none';
$id('trade-cta').style.display = 'flex';
}
return d;
}
// Products
async function loadProducts() {
const r = await fetch('/api/products?sort=' + encodeURIComponent(sortSel.value));
const d = await r.json();
$id('count').textContent = d.count + ' products';
$id('page-heading').textContent = d.trade ? 'All Products — Trade Pricing Unlocked' : 'All Products';
$id('grid').innerHTML = d.products.length
? d.products.map(p => cardHTML(p, d.trade)).join('')
: '<div class="empty">No products found.</div>';
}
// Build cart URL client-side for qty changes: https://domain/cart/<variant_id>:<qty>
function buildCartUrl(baseCartUrl, qty) {
if (!baseCartUrl) return null;
// base already has variant_id:1 — swap the qty
return baseCartUrl.replace(/:(\d+)$/, ':' + (parseInt(qty)||1));
}
function cardHTML(p, isTrade) {
const isKravet = p.pricing_rule === 'kravet-map';
const isEstTrade = p.pricing_rule === 'standard-est-25pct';
let priceHTML = '';
if (p.sample_only) {
// sample_only: no numeric price — show pre-formatted display string + "on request" note
const displayStr = esc(p.price_display || 'Roll price on request');
priceHTML = `<div class="price-row">
<div class="price-retail"><span class="plabel">Roll Price </span><span class="price-on-request">${displayStr}</span></div>
${isTrade ? '<div class="price-est-note">Trade pricing also on request for this line.</div>' : ''}
</div>`;
} else if (isTrade && p.trade != null) {
// Logged-in trade user, buyable product with a real price
const retailFmt = esc(p.price_display || (p.retail != null ? `$${p.retail.toFixed(2)}` : '—'));
priceHTML = `<div class="price-row">
<div class="price-retail"><span class="plabel">Retail </span>${retailFmt}</div>
<div class="price-trade-wrap">
<div class="price-trade-label">Your Trade Price</div>
<div class="price-trade">$${p.trade.toFixed(2)}</div>
${isKravet ? '<div class="kravet-badge">MAP Pricing (Kravet family)</div>' : ''}
${isEstTrade ? '<div class="price-est-note">est. — trade discount applied at checkout via account/code</div>' : ''}
</div>
</div>`;
} else {
// Public visitor, buyable product
const retailFmt = esc(p.price_display || (p.retail != null ? `$${p.retail.toFixed(2)}` : '—'));
priceHTML = `<div class="price-row">
<div class="price-retail"><span class="plabel">Retail </span>${retailFmt}</div>
<div class="trade-locked">🔒 Trade price — login to reveal</div>
</div>`;
}
// Cart / Buy button — only for buyable products
const qtyId = 'qty-' + esc(p.sku || p.handle || Math.random().toString(36).slice(2));
let cartHTML = '';
if (p.sample_only) {
// sample_only: "View on DW" link, never a cart button
const productUrl = esc(p.product_url || '');
cartHTML = productUrl
? `<div class="card-actions">
<a class="btn-view-dw" href="${productUrl}" target="_blank" rel="noopener noreferrer">View on DW ↗</a>
</div>`
: '';
} else if (p.buyable && p.cart_url) {
// buyable: qty + buy button
const tradeCheckoutNote = isTrade
? `<div class="checkout-caveat">ⓘ Checkout charges the store price. Trade pricing applied via account or discount code at checkout — Phase 2 gap. <em>?discount=CODE</em> param accepted.</div>`
: '';
cartHTML = `<div class="card-actions">
<input class="qty-input" id="${esc(qtyId)}" type="number" min="1" max="999" value="1" title="Quantity">
<a class="btn-buy" id="buy-${esc(p.sku||p.handle)}"
href="${esc(p.cart_url)}"
target="_blank" rel="noopener noreferrer"
onclick="this.href=buildCartUrl('${esc(p.cart_url)}', document.getElementById('${esc(qtyId)}').value); return true;"
title="Opens Shopify hosted checkout">Buy on Shopify ↗</a>
</div>
${tradeCheckoutNote}`;
} else {
cartHTML = `<div class="card-actions"><span class="btn-buy-disabled">No variant</span></div>`;
}
return `<div class="card">
<img src="${esc(p.image||'')}" alt="${esc(p.title||'')}" loading="lazy">
<div class="card-body">
<div class="card-title">${esc(p.title||'Untitled')}</div>
<div class="card-vendor">${esc(p.vendor||'')}</div>
<div class="card-type">${esc(p.product_type||'')}</div>
${priceHTML}
<div class="card-sku">${esc(p.sku||'')}</div>
${cartHTML}
</div>
</div>`;
}
// Login
function openLogin() { $id('login-modal').classList.add('open'); $id('login-error').style.display='none'; }
function closeLogin() { $id('login-modal').classList.remove('open'); }
$id('login-modal').onclick = e => { if (e.target === $id('login-modal')) closeLogin(); };
$id('login-form').onsubmit = async e => {
e.preventDefault();
const err = $id('login-error');
err.style.display = 'none';
try {
const r = await fetch('/api/login', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ email: $id('login-email').value, password: $id('login-pass').value })
});
const d = await r.json();
if (d.ok) { closeLogin(); await refreshSession(); loadProducts(); }
else { err.textContent = d.error || 'Login failed'; err.style.display = 'block'; }
} catch { err.textContent = 'Network error'; err.style.display = 'block'; }
};
async function doLogout() {
await fetch('/api/logout', { method: 'POST' });
await refreshSession();
loadProducts();
}
// Apply form
function showApply() {
$id('grid-page').style.display = 'none';
$id('apply-page').style.display = 'block';
$id('apply-success').style.display = 'none';
$id('apply-form').style.display = 'block';
}
function showGrid() {
$id('apply-page').style.display = 'none';
$id('grid-page').style.display = 'block';
}
$id('apply-form').onsubmit = async e => {
e.preventDefault();
const err = $id('apply-error');
err.style.display = 'none';
const body = Object.fromEntries(new FormData(e.target).entries());
try {
const r = await fetch('/api/apply', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const d = await r.json();
if (d.ok) { $id('apply-form').style.display = 'none'; $id('apply-success').style.display = 'block'; }
else { err.textContent = d.error || 'Submission error'; err.style.display = 'block'; }
} catch { err.textContent = 'Network error'; err.style.display = 'block'; }
};
// ── VIEW TOGGLE ──────────────────────────────────────────────────
const tpGrid = $id('grid');
function setTpView(v) {
tpGrid.classList.toggle('list', v === 'list');
$id('tp-viewgrid').classList.toggle('active', v === 'grid');
$id('tp-viewlist').classList.toggle('active', v === 'list');
$id('density').disabled = v === 'list';
$id('density').parentElement.style.opacity = v === 'list' ? '.35' : '1';
try { localStorage.setItem('tp-view', v); } catch(_) {}
}
$id('tp-viewgrid').addEventListener('click', () => setTpView('grid'));
$id('tp-viewlist').addEventListener('click', () => setTpView('list'));
setTpView(localStorage.getItem('tp-view') || 'grid');
// ── FIELDS MENU ──────────────────────────────────────────────────
const TPF_DEF = { img:true, title:true, vendor:true, type:true, sku:true, price:true, actions:true };
let TPF = Object.assign({}, TPF_DEF);
try { const _s = localStorage.getItem('tp-fields'); if (_s) TPF = Object.assign({}, TPF_DEF, JSON.parse(_s)); } catch(_) {}
function applyTpFields() {
for (const k of Object.keys(TPF_DEF)) document.body.classList.toggle('tphf-'+k, !TPF[k]);
document.querySelectorAll('#tp-fpop input[data-tpf]').forEach(cb => { cb.checked = !!TPF[cb.dataset.tpf]; });
try { localStorage.setItem('tp-fields', JSON.stringify(TPF)); } catch(_) {}
}
document.querySelectorAll('#tp-fpop input[data-tpf]').forEach(cb => {
cb.addEventListener('change', () => { TPF[cb.dataset.tpf] = cb.checked; applyTpFields(); });
});
applyTpFields();
$id('tp-fieldbtn').addEventListener('click', e => { e.stopPropagation(); $id('tp-fpop').hidden = !$id('tp-fpop').hidden; });
document.addEventListener('click', e => { if (!e.target.closest('.tp-fieldmenu')) { const p = $id('tp-fpop'); if(p) p.hidden = true; } });
// Init
(async () => { await refreshSession(); await loadProducts(); })();
</script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>