← 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 &mdash; 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>
  &mdash; "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 &amp; 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&#x2192;Z</option>
        <option value="sku">SKU A&#x2192;Z</option>
        <option value="price-asc">Price &#x2191;</option>
        <option value="price-desc">Price &#x2193;</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&hellip;</div></div>
</div>

<div id="apply-page">
  <button class="back-link" onclick="showGrid()">&larr; Back to catalog</button>
  <h2>Apply for Trade Access</h2>
  <p class="subtitle">Complete the form below. We review applications within 1&ndash;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&hellip;</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&hellip;">
      </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&ndash;2 business days.
    <br><br><button class="back-link" onclick="showGrid()">&larr; Return to catalog</button>
  </div>
</div>

<!-- Login modal -->
<div class="modal-overlay" id="login-modal">
  <div class="modal">
    <button class="modal-close" onclick="closeLogin()">&times;</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> &nbsp; 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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. &mdash; 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">&#128274; Trade price &mdash; 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 &#8599;</a>
         </div>`
      : '';
  } else if (p.buyable && p.cart_url) {
    // buyable: qty + buy button
    const tradeCheckoutNote = isTrade
      ? `<div class="checkout-caveat">&#9432; Checkout charges the store price. Trade pricing applied via account or discount code at checkout &mdash; 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 &#8599;</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>