← back to Trade Portal
public/index.html
424 lines
<!doctype html>
<html lang="en">
<head>
<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}
.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>
</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>
</select>
</label>
<label>Density <input id="density" type="range" min="2" max="6" value="4"></label>
<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'; }
};
// Init
(async () => { await refreshSession(); await loadProducts(); })();
</script>
</body>
</html>