← 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=>({'&':'&','<':'<','>':'>','"':'"'}[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>