← 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// ── 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();