← back to Chinoiseriewallpaper

public/js/site.js

336 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);
  });
}

// ── Fields — every toggleable card field [key, label, default-visible] ────────
const CW_FIELDS = [
  ['image',     'Image',       true],
  ['aesthetic', 'Aesthetic',   true],
  ['title',     'Title',       true],
  ['dots',      'Color Dots',  false],
  ['price',     'Price',       true],
  ['sample',    'Sample CTA',  true],
];

function cwLoadFields() {
  const def = {}; CW_FIELDS.forEach(([k,,d]) => def[k] = d);
  try { return Object.assign(def, JSON.parse(localStorage.getItem('cw-fields') || '{}')); } catch(e) { return def; }
}

// ── 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,
  viewMode:  localStorage.getItem('cw-view') || 'grid',
  fields:    cwLoadFields(),
};

// ── 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" data-f="image">
      <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" data-f="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" data-f="aesthetic">${escHtml(aesthetic)}</div>
      <div class="card-title" data-f="title">${escHtml(p.title)}</div>
      <div data-f="dots">${dotsHtml}</div>
      <div class="card-footer">
        <span data-f="price">${priceDisplay}</span>
        <a href="${escHtml(sampleUrl)}" target="_blank" rel="noopener noreferrer" class="card-sample-link" data-f="sample">
          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));
  });
}

// ── Grid / List view toggle ───────────────────────────────────────────────────
function cwSetView() {
  const isList = state.viewMode === 'list';
  grid.classList.toggle('list-view', isList);
  const vg = document.getElementById('cwViewGrid');
  const vl = document.getElementById('cwViewList');
  if (vg) vg.classList.toggle('on', !isList);
  if (vl) vl.classList.toggle('on', isList);
}

const cwViewGrid = document.getElementById('cwViewGrid');
const cwViewList = document.getElementById('cwViewList');
if (cwViewGrid) cwViewGrid.addEventListener('click', () => {
  state.viewMode = 'grid'; localStorage.setItem('cw-view', 'grid'); cwSetView();
});
if (cwViewList) cwViewList.addEventListener('click', () => {
  state.viewMode = 'list'; localStorage.setItem('cw-view', 'list'); cwSetView();
});
cwSetView();

// ── Fields menu ───────────────────────────────────────────────────────────────
function cwApplyFields() {
  CW_FIELDS.forEach(([k]) => document.documentElement.classList.toggle('hf-' + k, !state.fields[k]));
}

function cwBuildFieldMenu() {
  const fpop = document.getElementById('cwFpop');
  if (!fpop) return;
  fpop.innerHTML = CW_FIELDS.map(([k, label]) =>
    `<label><input type="checkbox" data-field="${k}"${state.fields[k] ? ' checked' : ''}> ${label}</label>`
  ).join('');
  fpop.querySelectorAll('input').forEach(cb => cb.addEventListener('change', () => {
    state.fields[cb.dataset.field] = cb.checked;
    localStorage.setItem('cw-fields', JSON.stringify(state.fields));
    cwApplyFields();
  }));
}

const cwFieldsBtn = document.getElementById('cwFieldsBtn');
const cwFpop = document.getElementById('cwFpop');
if (cwFieldsBtn && cwFpop) {
  cwFieldsBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    const open = cwFpop.hidden;
    cwFpop.hidden = !open;
    cwFieldsBtn.setAttribute('aria-expanded', String(open));
  });
  document.addEventListener('click', (e) => {
    if (!cwFpop.hidden && !e.target.closest('.fieldmenu')) {
      cwFpop.hidden = true;
      cwFieldsBtn.setAttribute('aria-expanded', 'false');
    }
  });
}

cwBuildFieldMenu();
cwApplyFields();

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