← back to Chinoiseriewallpaper

public/js/site.js

261 lines

/* Chinoiserie Wallpaper — site.js
 * Handles: dark/light toggle, product grid, search, sort, aesthetic filter,
 *          density slider, pagination, memo-sample CTAs.
 */
'use strict';

// ── Theme toggle ─────────────────────────────────────────────────────────────
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
  themeToggle.addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme') || 'light';
    const next    = current === 'light' ? 'dark' : 'light';
    document.documentElement.setAttribute('data-theme', next);
    localStorage.setItem('cw-theme', next);
  });
}

// ── State ─────────────────────────────────────────────────────────────────────
const state = {
  q:         '',
  aesthetic: localStorage.getItem('cw-aesthetic') || 'all',
  sort:      localStorage.getItem('cw-sort')      || 'newest',
  page:      1,
  limit:     60,
  cols:      parseInt(localStorage.getItem('cw-cols'), 10) || 3,
  total:     0,
  pages:     0,
};

// ── DOM refs ──────────────────────────────────────────────────────────────────
const grid          = document.getElementById('productGrid');
const pagination    = document.getElementById('pagination');
const resultsCount  = document.getElementById('resultsCount');
const searchInput   = document.getElementById('searchInput');
const aestheticFilter = document.getElementById('aestheticFilter');
const sortSelect    = document.getElementById('sortSelect');
const densitySlider = document.getElementById('densitySlider');

// ── Fetch + render ────────────────────────────────────────────────────────────
async function fetchAndRender() {
  // Show loading shimmer
  grid.innerHTML = '';
  for (let i = 0; i < state.limit; i++) {
    const s = document.createElement('div');
    s.className = 'skeleton-card';
    grid.appendChild(s);
  }

  const params = new URLSearchParams({
    page:      state.page,
    limit:     state.limit,
    sort:      state.sort,
    aesthetic: state.aesthetic,
  });
  if (state.q) params.set('q', state.q);

  let data;
  try {
    const res = await fetch(`/api/products?${params}`);
    data = await res.json();
  } catch (e) {
    grid.innerHTML = '<div class="empty-state"><p>Unable to load products.</p><small>Please refresh the page.</small></div>';
    return;
  }

  state.total = data.total;
  state.pages = data.pages;

  // Results count
  const startItem = (state.page - 1) * state.limit + 1;
  const endItem   = Math.min(state.page * state.limit, data.total);
  resultsCount.textContent = data.total === 0
    ? 'No patterns found'
    : `Showing ${startItem}–${endItem} of ${data.total} patterns`;

  // Render cards
  grid.setAttribute('data-cols', state.cols);
  grid.innerHTML = '';

  if (!data.items || data.items.length === 0) {
    grid.innerHTML = '<div class="empty-state"><p>No patterns found.</p><small>Try a different search or motif filter.</small></div>';
    renderPagination(0, 0);
    return;
  }

  for (const p of data.items) {
    grid.appendChild(buildCard(p));
  }

  renderPagination(state.total, state.pages);
  window.scrollTo({ top: document.getElementById('collection').offsetTop - 80, behavior: 'smooth' });
}

// Touch device = no hover. 1st tap reveals the card overlay (actions), so the
// memo-sample / view links are reachable on phones without a hover state.
const TOUCH = window.matchMedia('(hover: none)').matches;

function buildCard(p) {
  const card = document.createElement('div');
  card.className = 'product-card';

  const sampleUrl = p.sample_url || `https://designerwallcoverings.com/products/${p.handle}#sample`;
  const productUrl = p.product_url || `https://designerwallcoverings.com/products/${p.handle}`;
  const aesthetic = (p.aesthetic || 'chinoiserie').charAt(0).toUpperCase() + (p.aesthetic || 'chinoiserie').slice(1);

  // Price display
  let priceDisplay = '';
  if (p.price && p.price > 0) {
    priceDisplay = `<span class="card-price">$${Number(p.price).toFixed(2)}</span>`;
  }

  // Color dots — up to 4 per card, real-pixel hex preferred
  const dots = Array.isArray(p.color_dots) ? p.color_dots.slice(0, 4) : [];
  const dotsHtml = dots.length
    ? `<div class="dots" aria-label="Dominant colors">${dots.map(h => `<span class="dot" style="background:${escHtml(h)}"></span>`).join('')}</div>`
    : '';

  card.innerHTML = `
    <div class="card-image-wrap">
      <img
        src="${escHtml(p.image_url)}"
        alt="${escHtml(p.title)}"
        loading="lazy"
        onerror="this.closest('.card-image-wrap').style.background='var(--line)';this.style.display='none';"
      >
      <div class="card-overlay">
        <div class="card-overlay-actions">
          <a href="${escHtml(sampleUrl)}" target="_blank" rel="noopener noreferrer" class="btn-sample">
            Order Memo Sample (free)
          </a>
          <a href="${escHtml(productUrl)}" target="_blank" rel="noopener noreferrer" class="btn-view">
            View Full Details
          </a>
        </div>
      </div>
    </div>
    <div class="card-body">
      <div class="card-aesthetic">${escHtml(aesthetic)}</div>
      <div class="card-title">${escHtml(p.title)}</div>
      ${dotsHtml}
      <div class="card-footer">
        ${priceDisplay}
        <a href="${escHtml(sampleUrl)}" target="_blank" rel="noopener noreferrer" class="card-sample-link">
          Free Sample
        </a>
      </div>
    </div>
  `;

  if (TOUCH) {
    card.addEventListener('click', (e) => {
      // Let taps on the revealed overlay's links pass through normally.
      if (e.target.closest('.card-overlay')) return;
      if (!card.classList.contains('revealed')) {
        e.preventDefault();
        document.querySelectorAll('.product-card.revealed').forEach(c => c.classList.remove('revealed'));
        card.classList.add('revealed');
      }
    });
  }

  return card;
}

function escHtml(str) {
  return String(str || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

// ── Pagination ────────────────────────────────────────────────────────────────
function renderPagination(total, pages) {
  pagination.innerHTML = '';
  if (pages <= 1) return;

  const maxButtons = 7;
  const cur = state.page;

  const addBtn = (label, page, active = false) => {
    const btn = document.createElement('button');
    btn.textContent = label;
    if (active) btn.classList.add('active');
    btn.addEventListener('click', () => {
      state.page = page;
      fetchAndRender();
    });
    pagination.appendChild(btn);
  };

  // Prev
  if (cur > 1) addBtn('‹', cur - 1);

  // Page numbers
  let start = Math.max(1, cur - Math.floor(maxButtons / 2));
  let end   = Math.min(pages, start + maxButtons - 1);
  start     = Math.max(1, end - maxButtons + 1);

  if (start > 1) { addBtn('1', 1); if (start > 2) pagination.appendChild(elSpan('…')); }
  for (let i = start; i <= end; i++) addBtn(i, i, i === cur);
  if (end < pages) { if (end < pages - 1) pagination.appendChild(elSpan('…')); addBtn(pages, pages); }

  // Next
  if (cur < pages) addBtn('›', cur + 1);
}

function elSpan(text) {
  const s = document.createElement('span');
  s.style.cssText = 'display:flex;align-items:center;padding:0 6px;color:var(--ink-faint);font-size:13px;';
  s.textContent = text;
  return s;
}

// ── Event listeners ───────────────────────────────────────────────────────────
let searchTimer;
if (searchInput) {
  searchInput.addEventListener('input', () => {
    clearTimeout(searchTimer);
    searchTimer = setTimeout(() => {
      state.q    = searchInput.value.trim();
      state.page = 1;
      fetchAndRender();
    }, 280);
  });
}

if (aestheticFilter) {
  aestheticFilter.value = state.aesthetic;
  aestheticFilter.addEventListener('change', () => {
    state.aesthetic = aestheticFilter.value;
    state.page      = 1;
    localStorage.setItem('cw-aesthetic', state.aesthetic);
    fetchAndRender();
  });
}

if (sortSelect) {
  sortSelect.value = state.sort;
  sortSelect.addEventListener('change', () => {
    state.sort = sortSelect.value;
    state.page = 1;
    localStorage.setItem('cw-sort', state.sort);
    fetchAndRender();
  });
}

// Density slider — maps 1→4 columns
if (densitySlider) {
  densitySlider.value = state.cols;
  grid.setAttribute('data-cols', state.cols);
  densitySlider.addEventListener('input', () => {
    state.cols = parseInt(densitySlider.value, 10);
    grid.setAttribute('data-cols', state.cols);
    localStorage.setItem('cw-cols', String(state.cols));
  });
}

// ── Initial load ──────────────────────────────────────────────────────────────
fetchAndRender();