← back to New Items Dashboard

public/index.html

597 lines

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>🆕 NEW Items - Wines & Handbags</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: #333;
      min-height: 100vh;
      padding: 20px;
    }

    .container {
      max-width: 1400px;
      margin: 0 auto;
    }

    header {
      text-align: center;
      color: white;
      margin-bottom: 30px;
    }

    h1 {
      font-size: 3em;
      margin-bottom: 10px;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
    }

    .subtitle {
      font-size: 1.2em;
      opacity: 0.9;
    }

    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: 20px;
      margin-bottom: 30px;
    }

    .stat-card {
      background: white;
      padding: 20px;
      border-radius: 12px;
      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
      text-align: center;
    }

    .stat-card h3 {
      color: #667eea;
      font-size: 0.9em;
      text-transform: uppercase;
      margin-bottom: 10px;
      letter-spacing: 1px;
    }

    .stat-value {
      font-size: 2.5em;
      font-weight: bold;
      color: #333;
    }

    .stat-label {
      font-size: 0.9em;
      color: #666;
      margin-top: 5px;
    }

    .tabs {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
    }

    .tab {
      flex: 1;
      padding: 15px 30px;
      background: rgba(255,255,255,0.2);
      border: none;
      border-radius: 8px;
      color: white;
      font-size: 1.1em;
      cursor: pointer;
      transition: all 0.3s;
      font-weight: 600;
    }

    .tab:hover {
      background: rgba(255,255,255,0.3);
    }

    .tab.active {
      background: white;
      color: #667eea;
      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
    }

    .content {
      background: white;
      border-radius: 12px;
      padding: 30px;
      box-shadow: 0 8px 16px rgba(0,0,0,0.1);
      min-height: 500px;
    }

    .tab-content {
      display: none;
    }

    .tab-content.active {
      display: block;
    }

    .items-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(var(--card-min, 300px), 1fr));
      gap: 20px;
      margin-top: 20px;
    }

    .grid-controls {
      display: flex;
      gap: 16px;
      align-items: center;
      flex-wrap: wrap;
      padding: 12px 14px;
      background: #f9fafb;
      border: 1px solid #e5e7eb;
      border-radius: 8px;
      margin-bottom: 8px;
    }

    .grid-controls label {
      font-size: 0.85em;
      color: #374151;
      font-weight: 600;
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .grid-controls select {
      font-size: 0.9em;
      padding: 6px 10px;
      border: 1px solid #d1d5db;
      border-radius: 6px;
      background: white;
      color: #1f2937;
      cursor: pointer;
    }

    .grid-controls input[type="range"] {
      width: 160px;
      cursor: pointer;
    }

    .grid-controls .density-val {
      font-size: 0.8em;
      color: #6b7280;
      min-width: 48px;
      font-variant-numeric: tabular-nums;
    }

    .item-card {
      border: 2px solid #e5e7eb;
      border-radius: 8px;
      padding: 15px;
      transition: all 0.3s;
      position: relative;
      overflow: hidden;
    }

    .item-card:hover {
      border-color: #667eea;
      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
      transform: translateY(-2px);
    }

    .new-badge {
      position: absolute;
      top: 10px;
      right: 10px;
      background: #10b981;
      color: white;
      padding: 4px 12px;
      border-radius: 20px;
      font-size: 0.75em;
      font-weight: bold;
      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
    }

    .item-image {
      width: 100%;
      height: 200px;
      object-fit: cover;
      border-radius: 6px;
      margin-bottom: 15px;
      background: #f3f4f6;
    }

    .item-title {
      font-size: 1.1em;
      font-weight: 600;
      margin-bottom: 10px;
      color: #1f2937;
      line-height: 1.4;
    }

    .item-details {
      display: flex;
      flex-direction: column;
      gap: 8px;
      font-size: 0.9em;
      color: #6b7280;
    }

    .item-detail {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .item-detail strong {
      color: #374151;
    }

    .price {
      font-size: 1.3em;
      font-weight: bold;
      color: #667eea;
      margin-top: 10px;
    }

    .source-tag {
      display: inline-block;
      background: #f3f4f6;
      padding: 4px 10px;
      border-radius: 4px;
      font-size: 0.85em;
      color: #6b7280;
      margin-top: 10px;
    }

    .empty-state {
      text-align: center;
      padding: 60px 20px;
      color: #6b7280;
    }

    .empty-state h3 {
      font-size: 1.5em;
      margin-bottom: 10px;
      color: #374151;
    }

    .loading {
      text-align: center;
      padding: 60px 20px;
      color: #6b7280;
      font-size: 1.2em;
    }

    .last-updated {
      text-align: center;
      margin-top: 20px;
      color: #6b7280;
      font-size: 0.9em;
    }

    @media (max-width: 768px) {
      h1 { font-size: 2em; }
      .items-grid { grid-template-columns: 1fr; }
      .tabs { flex-direction: column; }
    }
  </style>
<!-- Meta Pixel — INERT until set-fb-pixel.sh is run (does NOT load fbevents.js or hit Meta until flipped) -->
<script>
(function(){
  var PIXEL_ID = '{{FACEBOOK_PIXEL_ID}}';
  // Inert until flipped — the brace check prevents BOTH the loader IIFE
  // AND the init call from running, so fbevents.js is never fetched.
  if (PIXEL_ID.indexOf('{{') !== -1) return;
  !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
  n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
  n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
  t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
  document,'script','https://connect.facebook.net/en_US/fbevents.js');
  fbq('init', PIXEL_ID);
  fbq('track', 'PageView');
})();
</script>
<!-- noscript fallback is added by set-fb-pixel.sh on flip (omitted here to avoid unflipped img phone-home) -->
<!-- End Meta Pixel -->
</head>
<body>
  <div class="container">
    <header>
      <h1>🆕 NEW Items Dashboard</h1>
      <p class="subtitle">Latest Wines & Handbags Just Listed</p>
    </header>

    <div class="stats" id="stats">
      <div class="stat-card">
        <h3>🍷 New Wines</h3>
        <div class="stat-value" id="newWinesCount">-</div>
        <div class="stat-label">Just discovered</div>
      </div>
      <div class="stat-card">
        <h3>👜 New Handbags</h3>
        <div class="stat-value" id="newHandbagsCount">-</div>
        <div class="stat-label">Just listed</div>
      </div>
      <div class="stat-card">
        <h3>📦 Total Wines Tracked</h3>
        <div class="stat-value" id="totalWines">-</div>
        <div class="stat-label">All time</div>
      </div>
      <div class="stat-card">
        <h3>👝 Total Handbags</h3>
        <div class="stat-value" id="totalHandbags">-</div>
        <div class="stat-label">In database</div>
      </div>
    </div>

    <div class="tabs">
      <button class="tab active" onclick="showTab('wines')">🍷 New Wines</button>
      <button class="tab" onclick="showTab('handbags')">👜 New Handbags</button>
    </div>

    <div class="content">
      <div id="wines" class="tab-content active">
        <div class="grid-controls">
          <label>Sort:
            <select id="wines-sort">
              <option value="newest">Newest</option>
              <option value="vendor">Vendor / Source</option>
              <option value="series">Series / Vintage</option>
              <option value="sku">SKU A→Z</option>
              <option value="title">Title A→Z</option>
            </select>
          </label>
          <label>Density:
            <input type="range" id="wines-density" min="180" max="480" step="20" value="300">
            <span class="density-val" id="wines-density-val">300px</span>
          </label>
        </div>
        <div id="wines-grid"><div class="loading">Loading new wines...</div></div>
      </div>
      <div id="handbags" class="tab-content">
        <div class="grid-controls">
          <label>Sort:
            <select id="handbags-sort">
              <option value="newest">Newest</option>
              <option value="vendor">Brand</option>
              <option value="series">Model</option>
              <option value="sku">SKU A→Z</option>
              <option value="title">Title A→Z</option>
            </select>
          </label>
          <label>Density:
            <input type="range" id="handbags-density" min="180" max="480" step="20" value="300">
            <span class="density-val" id="handbags-density-val">300px</span>
          </label>
        </div>
        <div id="handbags-grid"><div class="loading">Loading new handbags...</div></div>
      </div>
    </div>

    <div class="last-updated" id="lastUpdated"></div>
  </div>

  <script>
    let winesData = [];
    let handbagsData = [];

    // ---- localStorage keys per Steve's standing rule: namespace by site+surface
    const LS = {
      winesSort: 'newItemsDash.wines.sort',
      winesDensity: 'newItemsDash.wines.density',
      handbagsSort: 'newItemsDash.handbags.sort',
      handbagsDensity: 'newItemsDash.handbags.density',
    };

    // ---- Hydrate sort + density from localStorage BEFORE first fetch
    function hydrateControls() {
      const wSort = localStorage.getItem(LS.winesSort) || 'newest';
      const wDen = parseInt(localStorage.getItem(LS.winesDensity) || '300', 10);
      const hSort = localStorage.getItem(LS.handbagsSort) || 'newest';
      const hDen = parseInt(localStorage.getItem(LS.handbagsDensity) || '300', 10);

      const wSortEl = document.getElementById('wines-sort');
      const wDenEl = document.getElementById('wines-density');
      const hSortEl = document.getElementById('handbags-sort');
      const hDenEl = document.getElementById('handbags-density');

      if (wSortEl) wSortEl.value = wSort;
      if (wDenEl) wDenEl.value = wDen;
      if (hSortEl) hSortEl.value = hSort;
      if (hDenEl) hDenEl.value = hDen;

      applyDensity('wines', wDen);
      applyDensity('handbags', hDen);
    }

    function applyDensity(which, px) {
      const grid = document.getElementById(which + '-grid');
      if (grid) grid.style.setProperty('--card-min', px + 'px');
      const lbl = document.getElementById(which + '-density-val');
      if (lbl) lbl.textContent = px + 'px';
    }

    function showTab(tab) {
      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
      document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));

      event.target.classList.add('active');
      document.getElementById(tab).classList.add('active');
    }

    async function loadStats() {
      try {
        const res = await fetch('/api/stats');
        const data = await res.json();

        if (data.success) {
          document.getElementById('totalWines').textContent = data.wines.total.toLocaleString();
          document.getElementById('totalHandbags').textContent = data.handbags.total.toLocaleString();
        }
      } catch (error) {
        console.error('Error loading stats:', error);
      }
    }

    async function loadWines() {
      try {
        const sort = (document.getElementById('wines-sort') || {}).value || localStorage.getItem(LS.winesSort) || 'newest';
        const res = await fetch('/api/new-wines?sort=' + encodeURIComponent(sort));
        const data = await res.json();

        winesData = data.wines || [];
        document.getElementById('newWinesCount').textContent = winesData.length;

        const container = document.getElementById('wines-grid');

        if (winesData.length === 0) {
          container.innerHTML = `
            <div class="empty-state">
              <h3>No new wines yet</h3>
              <p>${data.message || 'The crawler runs daily at 6:00 AM. Check back tomorrow!'}</p>
            </div>
          `;
          return;
        }

        container.innerHTML = `
          <div class="items-grid">
            ${winesData.map(wine => `
              <div class="item-card">
                <span class="new-badge">NEW</span>
                <div class="item-title">${wine.name || 'Unknown Wine'}</div>
                <div class="item-details">
                  ${wine.vintage ? `<div class="item-detail"><span>Vintage:</span> <strong>${wine.vintage}</strong></div>` : ''}
                  ${wine.rating ? `<div class="item-detail"><span>Rating:</span> <strong>⭐ ${wine.rating}/100</strong></div>` : ''}
                  ${wine.availability ? `<div class="item-detail"><span>Stock:</span> <strong>${wine.availability}</strong></div>` : ''}
                </div>
                ${wine.price ? `<div class="price">$${wine.price.toFixed(2)}</div>` : ''}
                <span class="source-tag">${wine.source || 'Unknown'}</span>
              </div>
            `).join('')}
          </div>
        `;

        // Re-apply density now that the grid was re-rendered
        applyDensity('wines', parseInt(localStorage.getItem(LS.winesDensity) || '300', 10));

        if (data.timestamp) {
          document.getElementById('lastUpdated').textContent =
            `Last crawl: ${new Date(data.timestamp).toLocaleString()}`;
        }
      } catch (error) {
        console.error('Error loading wines:', error);
        document.getElementById('wines-grid').innerHTML = `
          <div class="empty-state">
            <h3>Error loading wines</h3>
            <p>Please try again later</p>
          </div>
        `;
      }
    }

    async function loadHandbags() {
      try {
        const sort = (document.getElementById('handbags-sort') || {}).value || localStorage.getItem(LS.handbagsSort) || 'newest';
        const res = await fetch('/api/new-handbags?sort=' + encodeURIComponent(sort));
        const data = await res.json();

        handbagsData = data.handbags || [];
        document.getElementById('newHandbagsCount').textContent = handbagsData.length;

        const container = document.getElementById('handbags-grid');

        if (handbagsData.length === 0) {
          container.innerHTML = `
            <div class="empty-state">
              <h3>No new handbags yet</h3>
              <p>The crawler runs daily at 6:00 AM. Check back tomorrow!</p>
            </div>
          `;
          return;
        }

        container.innerHTML = `
          <div class="items-grid">
            ${handbagsData.map(bag => `
              <div class="item-card">
                <span class="new-badge">NEW</span>
                ${bag.image_url ? `<img src="${bag.image_url}" class="item-image" alt="${bag.title}" onerror="this.style.display='none'">` : ''}
                <div class="item-title">${bag.title || bag.brand + ' ' + (bag.model || 'Bag')}</div>
                <div class="item-details">
                  ${bag.brand ? `<div class="item-detail"><span>Brand:</span> <strong>${bag.brand}</strong></div>` : ''}
                  ${bag.condition ? `<div class="item-detail"><span>Condition:</span> <strong>${bag.condition}</strong></div>` : ''}
                  ${bag.listing_type ? `<div class="item-detail"><span>Type:</span> <strong>${bag.listing_type}</strong></div>` : ''}
                </div>
                <div class="price">
                  ¥${bag.price_jpy.toLocaleString()}
                  ${bag.price_usd ? `<span style="font-size:0.8em; color:#6b7280;">($${bag.price_usd.toFixed(2)})</span>` : ''}
                </div>
                <span class="source-tag">${bag.source || 'Unknown'}</span>
              </div>
            `).join('')}
          </div>
        `;

        applyDensity('handbags', parseInt(localStorage.getItem(LS.handbagsDensity) || '300', 10));
      } catch (error) {
        console.error('Error loading handbags:', error);
        document.getElementById('handbags-grid').innerHTML = `
          <div class="empty-state">
            <h3>Error loading handbags</h3>
            <p>Please try again later</p>
          </div>
        `;
      }
    }

    // ---- Wire up sort + density change handlers
    function wireControls() {
      const wSort = document.getElementById('wines-sort');
      const wDen = document.getElementById('wines-density');
      const hSort = document.getElementById('handbags-sort');
      const hDen = document.getElementById('handbags-density');

      if (wSort) wSort.addEventListener('change', () => {
        localStorage.setItem(LS.winesSort, wSort.value);
        loadWines();
      });
      if (wDen) wDen.addEventListener('input', () => {
        const px = parseInt(wDen.value, 10);
        localStorage.setItem(LS.winesDensity, String(px));
        applyDensity('wines', px);
      });
      if (hSort) hSort.addEventListener('change', () => {
        localStorage.setItem(LS.handbagsSort, hSort.value);
        loadHandbags();
      });
      if (hDen) hDen.addEventListener('input', () => {
        const px = parseInt(hDen.value, 10);
        localStorage.setItem(LS.handbagsDensity, String(px));
        applyDensity('handbags', px);
      });
    }

    // ---- Boot: hydrate FIRST, then fetch, then wire handlers
    hydrateControls();
    wireControls();
    loadStats();
    loadWines();
    loadHandbags();

    // Refresh every 5 minutes
    setInterval(() => {
      loadStats();
      loadWines();
      loadHandbags();
    }, 5 * 60 * 1000);
  </script>
</body>
</html>