← back to Patterndesignlab

public/index.html

173 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Pattern Design Lab — seamless patterns, licensed</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header class="top"><div class="wrap">
  <a class="brand" href="/">Pattern Design<span class="dot">.</span>Lab <small>seamless originals, licensed</small></a>
  <form class="searchbox" onsubmit="return doSearch(event)">
    <input id="q" type="search" placeholder="Search patterns, styles, colorways…" autocomplete="off">
    <button class="go" type="submit" aria-label="search">→</button>
  </form>
  <nav class="navlinks"><a href="/">Explore</a><a href="/trends">Trends</a><a href="/designer-signup">Sell your designs</a><a href="/admin">Admin</a></nav>
</div></header>

<div class="wrap">
  <section class="hero">
    <h1>Original seamless patterns, ready to license.</h1>
    <p>A curated marketplace of owned, settlement-passed designs — every tile is truly seamless. Preview any pattern on a wall before you license it.</p>
  </section>

  <div class="shell">
    <aside class="filters" id="filters"><!-- facets injected --></aside>

    <main>
      <div class="bar">
        <span class="count" id="count">…</span>
        <div><label for="sort">Sort</label>
          <select id="sort" onchange="applyControls()">
            <option value="newest">Newest</option>
            <option value="color">Color</option>
            <option value="style">Style</option>
            <option value="sku_az">SKU A→Z</option>
            <option value="title_az">Title A→Z</option>
            <option value="price_up">Price ↑</option>
            <option value="price_dn">Price ↓</option>
          </select>
        </div>
        <div><label for="density">Density</label>
          <input id="density" type="range" min="2" max="6" step="1" value="4" oninput="setDensity(this.value)">
        </div>
      </div>
      <div class="grid" id="grid"></div>
      <div class="empty" id="empty" hidden>No patterns match those filters. <a href="#" onclick="clearAll();return false">Clear filters</a></div>
      <div class="loadmore-wrap" id="loadmoreWrap" hidden>
        <button class="loadmore" id="loadmore" onclick="loadMore()">Load more</button>
        <span class="loadmore-status" id="loadmoreStatus"></span>
      </div>
    </main>
  </div>
</div>
<footer><div class="wrap">Pattern Design Lab · owned, settlement-passed originals only · local preview build</div></footer>

<script>
const state = { q:'', sort:'newest', class:'', style:'', colorway:'', motif:'', technique:'', seamless:'', pmin:'', pmax:'' };
const PAGE_LIMIT = 48;
let page = 1, total = 0, loaded = 0, loading = false;

// ---- persisted controls (localStorage per DW spec) ----
function setDensity(v){ document.documentElement.style.setProperty('--cols', v); localStorage.setItem('pdl.density', v); const el=document.getElementById('density'); if(el) el.value=v; }
function loadControls(){
  const d = localStorage.getItem('pdl.density'); if(d) setDensity(d);
  const s = localStorage.getItem('pdl.sort'); if(s){ state.sort=s; document.getElementById('sort').value=s; }
}
function applyControls(){ state.sort=document.getElementById('sort').value; localStorage.setItem('pdl.sort', state.sort); load(); }

function doSearch(e){ e.preventDefault(); state.q=document.getElementById('q').value.trim(); load(); return false; }
function clearAll(){ Object.assign(state,{q:'',class:'',style:'',colorway:'',motif:'',technique:'',seamless:'',pmin:'',pmax:''}); document.getElementById('q').value=''; load(); }

function fmtPrice(lo,hi){ if(lo==null) return 'Inquire'; return hi&&hi!=lo ? '$'+lo+'–$'+hi : '$'+lo; }

function cardEl(d){
  const el = document.createElement('a'); el.className='card'; el.href='/design/'+d.slug;
  el.innerHTML = `
    <div class="thumb"><div class="tile" style="background-image:url('${d.img}')"></div>
      ${d.seamless?'<span class="badge">Seamless</span>':'<span class="badge">Mural</span>'}</div>
    <div class="cardbody">
      <h3>${esc(d.title)}</h3>
      <div class="meta"><span class="swatch" style="background:${d.dominant_hex||'#ccc'}"></span>
        <span>${esc(d.style||'')}</span><span>·</span><span>${esc(d.colorway||'')}</span></div>
      <div class="byline">by ${esc(d.designer_name)}</div>
      <div class="price">${fmtPrice(d.price_min,d.price_max)}</div>
    </div>`;
  return el;
}

// build the query string for the CURRENT filters + a given page
function queryFor(pg){
  const p = new URLSearchParams();
  for(const k of ['q','sort','class','style','colorway','motif','technique','seamless','pmin','pmax']) if(state[k]) p.set(k,state[k]);
  p.set('page', pg); p.set('limit', PAGE_LIMIT);
  return p.toString();
}

// load() = fresh query (filters/sort/search changed) → reset to page 1, replace grid
async function load(){ const qi=document.getElementById('q'); if(qi) state.q=qi.value.trim(); page=1; await fetchPage(false); renderFacets(); }

// loadMore() = append the next page, keeping the current grid + filters
async function loadMore(){ if(loading || loaded>=total) return; page++; await fetchPage(true); }

async function fetchPage(append){
  if(loading) return; loading = true;
  const btn = document.getElementById('loadmore');
  if(append && btn){ btn.disabled=true; btn.textContent='Loading…'; }
  try {
    const r = await fetch('/api/designs?'+queryFor(page)); const j = await r.json();
    const grid = document.getElementById('grid');
    if(!append){ grid.innerHTML=''; loaded=0; }
    total = j.total|0;
    for(const d of j.designs){ grid.appendChild(cardEl(d)); loaded++; }
    document.getElementById('count').textContent = total + (total===1?' pattern':' patterns');
    document.getElementById('empty').hidden = total>0;
    const wrap = document.getElementById('loadmoreWrap');
    const more = j.hasMore && loaded<total;
    wrap.hidden = total===0;                       // show the status/button row whenever we have results
    if(btn){ btn.hidden = !more; }                 // button only while more pages remain
    document.getElementById('loadmoreStatus').textContent =
      total>0 ? `Showing ${loaded} of ${total}` : '';
  } finally {
    loading=false;
    if(btn){ btn.disabled=false; btn.textContent='Load more'; }
  }
}

function esc(s){ return (s==null?'':String(s)).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }

let FACETS=null;
async function loadFacets(){ FACETS = await (await fetch('/api/facets')).json(); renderFacets(); }
function facetGroup(title,key,rows){
  return `<div class="fgroup"><h4>${title}</h4>`+
    rows.map(o=>`<div class="opt ${state[key]===String(o.value)?'active':''}" onclick="toggleFacet('${key}','${esc(String(o.value)).replace(/'/g,"\\'")}')">
      <span>${esc(o.value)}</span><span class="n">${o.n}</span></div>`).join('')+`</div>`;
}
function renderFacets(){
  if(!FACETS) return;
  const f = document.getElementById('filters');
  const p = FACETS.price||{};
  const c = FACETS.collection||{};
  f.innerHTML =
    `<div class="fgroup"><h4>Collection</h4><div class="seg seg-collection">
      <button class="${state.class===''?'active':''}" onclick="setClass('')">All${c.all!=null?` <span class="n">${c.all}</span>`:''}</button>
      <button class="${state.class==='credible'?'active':''}" onclick="setClass('credible')">Design-forward${c.credible!=null?` <span class="n">${c.credible}</span>`:''}</button>
      <button class="${state.class==='novelty'?'active':''}" onclick="setClass('novelty')">Playful${c.novelty!=null?` <span class="n">${c.novelty}</span>`:''}</button></div></div>`+
    facetGroup('Style','style',FACETS.style)+
    facetGroup('Colorway','colorway',FACETS.colorway)+
    facetGroup('Motif','motif',FACETS.motif)+
    facetGroup('Technique','technique',FACETS.technique)+
    `<div class="fgroup"><h4>Seamless</h4><div class="seg">
      <button class="${state.seamless===''?'active':''}" onclick="setSeamless('')">All</button>
      <button class="${state.seamless==='yes'?'active':''}" onclick="setSeamless('yes')">Yes</button>
      <button class="${state.seamless==='no'?'active':''}" onclick="setSeamless('no')">No</button></div></div>`+
    `<div class="fgroup"><h4>Price (from)</h4><div class="pricewrap">
      <input id="pmin" type="number" placeholder="${p.lo??''}" value="${state.pmin}" oninput="state.pmin=this.value">
      <span>–</span>
      <input id="pmax" type="number" placeholder="${p.hi??''}" value="${state.pmax}" oninput="state.pmax=this.value"></div>
      <button class="clearall" onclick="load()">Apply price</button></div>`+
    `<button class="clearall" onclick="clearAll()">Clear all filters</button>`;
}
function toggleFacet(key,val){ state[key] = state[key]===val ? '' : val; load(); }
function setSeamless(v){ state.seamless=v; load(); }
function setClass(v){ state.class=v; load(); }

// honor an inbound ?q= search (e.g. from the detail-page search bar)
const urlq = new URLSearchParams(location.search).get('q');
if(urlq){ state.q = urlq; const qi = document.getElementById('q'); if(qi) qi.value = urlq; }
loadControls(); loadFacets(); load();
</script>
</body>
</html>