← back to Reno Visualizer

public/admin.html

240 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Reno Visualizer — Admin</title>
<style>
:root {
  --brand: #2a4a3e;
  --accent: #c8a86a;
  --border: #e4e0d8;
  --surface: #fafaf8;
  --card-bg: #fff;
  --muted: #7a7870;
  --cols: 2;
}
*,*::before,*::after { box-sizing: border-box; margin:0; padding:0; }
body { font: 14px/1.5 -apple-system,'Segoe UI',Roboto,sans-serif; color: #1a1a18; background: var(--surface); }

.nav {
  display: flex; align-items: center; gap: 16px; padding: 12px 24px;
  background: var(--brand); color: #fff; position: sticky; top: 0; z-index: 100;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.nav h1 { font-size: 16px; font-weight: 700; }
.nav .badge { font-size: 11px; background: rgba(255,255,255,0.15); border-radius: 12px; padding: 2px 10px; }
.nav-spacer { flex: 1; }
.nav a { color: rgba(255,255,255,0.75); font-size: 12px; text-decoration: none; }
.nav a:hover { color: #fff; }

.toolbar {
  display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
  padding: 14px 24px; background: #fff; border-bottom: 1px solid var(--border);
  position: sticky; top: 52px; z-index: 10;
}
.toolbar label { font-size: 12px; color: var(--muted); display: flex; gap: 6px; align-items: center; }
.toolbar select { font: 13px inherit; padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px; }
.toolbar input[type=range] { width: 80px; }
.toolbar .spacer { flex: 1; }
.count-badge { font-size: 11px; color: var(--muted); background: var(--surface); border: 1px solid var(--border);
  padding: 3px 10px; border-radius: 12px; }

.grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: 16px;
  padding: 24px;
}

.card {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
  overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.card-header {
  display: flex; gap: 12px; align-items: flex-start; padding: 14px 16px 10px;
  border-bottom: 1px solid var(--border); background: var(--surface);
}
.card-pattern-thumb {
  width: 56px; height: 56px; object-fit: cover; border-radius: 6px;
  flex-shrink: 0; background: #e8e4dc; border: 1px solid var(--border);
}
.card-meta { flex: 1; min-width: 0; }
.card-meta h3 { font-size: 14px; font-weight: 700; color: var(--brand); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-meta .sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
.card-meta .sku { font-size: 11px; font-family: monospace; background: #f0ede8; border-radius: 4px; padding: 1px 5px; }
.card-body { padding: 12px 16px; }
.card-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.card-row:last-child { margin-bottom: 0; }
.card-key { font-size: 11px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
.card-val { font-size: 12px; color: #1a1a18; text-align: right; max-width: 60%; word-break: break-word; }
.card-val.email { color: var(--brand); font-weight: 600; }
.card-val.interest { background: #f0f5f0; color: var(--brand); border-radius: 10px; padding: 2px 8px; font-size: 11px; }

/* HARD RULE: created date+time always visible on every admin card */
.card-when {
  display: flex; align-items: center; gap: 5px;
  font-size: 11px; color: var(--muted); margin-top: 10px;
  padding-top: 8px; border-top: 1px solid var(--border);
}
.card-when .clock { opacity: 0.7; }
.card-id { font-size: 10px; font-family: monospace; color: #b0b0a8; margin-left: auto; }

.empty-state { padding: 60px 24px; text-align: center; color: var(--muted); font-size: 15px; }
.empty-state .big { font-size: 40px; display: block; margin-bottom: 12px; }

.loading { text-align: center; padding: 40px; color: var(--muted); }
.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #e0e0d8;
  border-top-color: var(--brand); border-radius: 50%; animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>

<nav class="nav">
  <h1>Reno Visualizer Admin</h1>
  <span class="badge" id="nav-count">0 leads</span>
  <span class="nav-spacer"></span>
  <a href="/">← Back to Visualizer</a>
</nav>

<div class="toolbar">
  <label>Sort
    <select id="sort-sel" onchange="loadLeads()">
      <option value="newest">Newest first</option>
      <option value="oldest">Oldest first</option>
    </select>
  </label>
  <label>Density <input type="range" id="density-slider" min="1" max="4" value="2" oninput="onDensityChange()"></label>
  <span class="spacer"></span>
  <span class="count-badge" id="count-badge">Loading…</span>
</div>

<div id="grid-wrap">
  <div class="loading"><span class="spinner"></span></div>
</div>

<script>
const $ = s => document.querySelector(s);
function esc(s) { return (s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }

/* HARD RULE — created date+time on every card, both human readable + ISO in title= */
function fmtDate(iso) {
  if (!iso) return '—';
  try {
    return new Date(iso).toLocaleString(undefined, {
      year: 'numeric', month: 'short', day: 'numeric',
      hour: 'numeric', minute: '2-digit'
    });
  } catch { return iso; }
}

function interestLabel(v) {
  return { order: 'Order rolls', sample: 'Request sample', consult: 'Design consult', quote: 'Get a quote' }[v] || v || '—';
}

function renderCard(lead) {
  return `<div class="card">
    <div class="card-header">
      ${lead.image_url
        ? `<img class="card-pattern-thumb" src="${esc(lead.image_url)}" alt="${esc(lead.pattern_title||'')}" onerror="this.style.background='#e8e4dc'">`
        : `<div class="card-pattern-thumb" style="background:${esc(lead.pattern_color||'#e8e4dc')};display:flex;align-items:center;justify-content:center;font-size:22px;">🖼</div>`
      }
      <div class="card-meta">
        <h3>${esc(lead.email)}</h3>
        ${lead.pattern_title ? `<div class="sub">${esc(lead.pattern_title)}</div>` : ''}
        ${lead.pattern_sku ? `<div class="sub"><span class="sku">${esc(lead.pattern_sku)}</span></div>` : ''}
      </div>
    </div>
    <div class="card-body">
      <div class="card-row">
        <span class="card-key">Room</span>
        <span class="card-val">${esc(lead.room_label||'—')}</span>
      </div>
      <div class="card-row">
        <span class="card-key">Vendor</span>
        <span class="card-val">${esc(lead.pattern_vendor||'—')}</span>
      </div>
      <div class="card-row">
        <span class="card-key">Interest</span>
        <span class="card-val interest">${esc(interestLabel(lead.interest))}</span>
      </div>
      ${lead.message ? `<div class="card-row" style="align-items:flex-start;">
        <span class="card-key">Notes</span>
        <span class="card-val" style="font-style:italic;color:#5a5a50;">${esc(lead.message)}</span>
      </div>` : ''}
      ${lead.pattern_shopify_url ? `<div class="card-row">
        <span class="card-key">Product</span>
        <span class="card-val"><a href="${esc(lead.pattern_shopify_url)}" target="_blank" rel="noopener noreferrer" style="color:var(--brand);font-size:11px;">View on DW →</a></span>
      </div>` : ''}
      <!-- HARD RULE: created date+time, full ISO in title= -->
      <div class="card-when">
        <span class="clock">🕓</span>
        <span title="${esc(lead.created_at||'')}">${fmtDate(lead.created_at)}</span>
        <span class="card-id">${esc(lead.id||'')}</span>
      </div>
    </div>
  </div>`;
}

async function loadLeads() {
  const sort = $('#sort-sel').value;
  try {
    const r = await fetch('/api/leads?sort=' + encodeURIComponent(sort));
    if (r.status === 401) {
      document.body.innerHTML = '<div style="text-align:center;padding:80px;font-size:16px;color:#666;">Access denied — admin auth required.</div>';
      return;
    }
    const d = await r.json();
    const leads = d.leads || [];
    $('#nav-count').textContent = leads.length + ' lead' + (leads.length !== 1 ? 's' : '');
    $('#count-badge').textContent = leads.length + ' captured looks';

    if (!leads.length) {
      $('#grid-wrap').innerHTML = `<div class="empty-state">
        <span class="big">📋</span>
        No leads yet — they'll appear here after someone shops a look.
      </div>`;
      return;
    }

    // Enrich with pattern image from cached products list
    const products = window._products || [];
    const bySkuImg = {};
    const bySkuColor = {};
    products.forEach(p => { bySkuImg[p.sku] = p.image_url || p.image || ''; bySkuColor[p.sku] = p.hex || ''; });

    const cards = leads.map(lead => {
      lead.image_url = lead.pattern_sku ? (bySkuImg[lead.pattern_sku] || '') : '';
      lead.pattern_color = lead.pattern_sku ? (bySkuColor[lead.pattern_sku] || '#e8e4dc') : '#e8e4dc';
      return renderCard(lead);
    }).join('');

    $('#grid-wrap').innerHTML = `<div class="grid" id="leads-grid">${cards}</div>`;
  } catch(e) {
    $('#grid-wrap').innerHTML = '<div class="empty-state">Error loading leads: ' + esc(String(e)) + '</div>';
  }
}

function onDensityChange() {
  const v = $('#density-slider').value;
  localStorage.setItem('admin-cols', v);
  document.documentElement.style.setProperty('--cols', v);
}

// Restore density
(function() {
  const v = localStorage.getItem('admin-cols') || '2';
  $('#density-slider').value = v;
  document.documentElement.style.setProperty('--cols', v);
})();

// Load products for enriching leads
fetch('/api/products').then(r=>r.json()).then(d => {
  window._products = d.products || [];
  loadLeads();
}).catch(() => loadLeads());
</script>
</body>
</html>