← back to Hollywood Wallcoverings
Custom Creator gallery: full-catalog search + category filter + pagination
24bd0dd4b5ca37f6d7fa46fff2d5eb74f1b04272 · 2026-06-21 06:58:33 -0700 · Steve
Browse all 11,936 wallco.ai designs (was capped at newest 600). Server-side
search (title/handle/category/style), category dropdown with counts, sort
(newest/color/style/sku/title) across the whole catalog, and Load-more
pagination (120/page). New /api/categories facet endpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M customcreator.jsM customcreator/gallery.html
Diff
commit 24bd0dd4b5ca37f6d7fa46fff2d5eb74f1b04272
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Jun 21 06:58:33 2026 -0700
Custom Creator gallery: full-catalog search + category filter + pagination
Browse all 11,936 wallco.ai designs (was capped at newest 600). Server-side
search (title/handle/category/style), category dropdown with counts, sort
(newest/color/style/sku/title) across the whole catalog, and Load-more
pagination (120/page). New /api/categories facet endpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
customcreator.js | 51 +++++++++++++++++++++++++++----
customcreator/gallery.html | 75 ++++++++++++++++++++++++++--------------------
2 files changed, 87 insertions(+), 39 deletions(-)
diff --git a/customcreator.js b/customcreator.js
index d737019..4517069 100644
--- a/customcreator.js
+++ b/customcreator.js
@@ -27,7 +27,7 @@ const DRAFT_TOKEN = process.env.SHOPIFY_DRAFT_TOKEN || process.env.SHOPIFY_ADMIN
const RATE_SQM = 83.38, SAMPLE = 4.25, CM_PER_IN = 2.54;
const MIN_CM = 50, MAX_CM = 1200;
const HOUSE = 'Made to Measure'; // customer-facing vendor label (never "wallco.ai")
-const GALLERY_LIMIT = 600; // newest-N shown in the browse grid (catalog is ~12k)
+const GALLERY_LIMIT = 120; // page size for the browse grid (catalog is ~12k, paginated)
function ftIn(inch) { let f = Math.floor(inch / 12), i = Math.round(inch - f * 12); if (i === 12) { f++; i = 0; } return `${f}′ ${i}″`; }
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
@@ -42,6 +42,36 @@ try {
const imgUrl = file => '/CustomCreator/img/' + file;
+// Category facets (sorted by count) for the browse dropdown.
+const CATS = (() => {
+ const m = new Map();
+ for (const d of DESIGNS) { const c = (d.category || '').trim(); if (c) m.set(c, (m.get(c) || 0) + 1); }
+ return [...m.entries()].sort((a, b) => b[1] - a[1]).map(([category, count]) => ({ category, count }));
+})();
+
+// Server-side sort (full-catalog, not just a page). Cached per mode so 12k isn't re-sorted each request.
+function colorKey(hex) {
+ if (!hex) return 500;
+ const m = String(hex).replace('#', '').match(/.{2}/g); if (!m || m.length < 3) return 500;
+ const [r, g, b] = m.map(x => parseInt(x, 16));
+ const mx = Math.max(r, g, b), mn = Math.min(r, g, b), l = (mx + mn) / 510, d = mx - mn;
+ if (d < 18) return l > 0.8 ? 0 : (l < 0.18 ? 900 : 400 + Math.round((1 - l) * 80));
+ let h = mx === r ? ((g - b) / d) % 6 : mx === g ? (b - r) / d + 2 : (r - g) / d + 4; h *= 60; if (h < 0) h += 360;
+ return 50 + Math.round(h / 5);
+}
+const _sortCache = new Map();
+function sorted(mode) {
+ if (_sortCache.has(mode)) return _sortCache.get(mode);
+ const a = DESIGNS.slice(); // DESIGNS is already newest-first
+ if (mode === 'title') a.sort((x, y) => String(x.title).localeCompare(String(y.title)));
+ else if (mode === 'style') a.sort((x, y) => String(x.style || '~').localeCompare(String(y.style || '~')) || String(x.title).localeCompare(String(y.title)));
+ else if (mode === 'sku') a.sort((x, y) => String(x.handle).localeCompare(String(y.handle)));
+ else if (mode === 'color') a.sort((x, y) => colorKey(x.hex) - colorKey(y.hex) || String(x.title).localeCompare(String(y.title)));
+ // 'newest' = DESIGNS order (no sort)
+ _sortCache.set(mode, a);
+ return a;
+}
+
function toCard(d) {
return { handle: d.handle, sku: d.handle, title: d.title, image: imgUrl(d.file), createdAt: d.created_at || '', style: d.style || 'Original', colorHex: d.hex || '', colorName: '', vendor: HOUSE };
}
@@ -118,13 +148,22 @@ function mount(app, express) {
app.get(['/CustomCreator', '/CustomCreator/'], sendHtml('gallery.html'));
app.get('/CustomCreator/configure', sendHtml('configure.html'));
- // Browse grid — newest GALLERY_LIMIT (optionally ?category= / ?limit=).
+ // Category facets for the browse dropdown.
+ app.get('/CustomCreator/api/categories', (req, res) => res.json({ total: DESIGNS.length, categories: CATS }));
+
+ // Browse grid — full-catalog search/filter/sort with pagination.
+ // ?q= (title contains) · ?category= · ?sort=newest|title|style|sku|color · ?offset= · ?limit=
app.get('/CustomCreator/api/collection', (req, res) => {
- const cat = (req.query.category || '').toLowerCase();
- const limit = Math.min(parseInt(req.query.limit, 10) || GALLERY_LIMIT, 2000);
- let rows = DESIGNS;
+ const q = String(req.query.q || '').trim().toLowerCase();
+ const cat = String(req.query.category || '').trim().toLowerCase();
+ const sort = ['newest', 'title', 'style', 'sku', 'color'].includes(req.query.sort) ? req.query.sort : 'newest';
+ const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
+ const limit = Math.min(Math.max(1, parseInt(req.query.limit, 10) || GALLERY_LIMIT), 600);
+ let rows = sorted(sort);
if (cat) rows = rows.filter(d => String(d.category).toLowerCase() === cat);
- res.json(rows.slice(0, limit).map(toCard));
+ if (q) rows = rows.filter(d => String(d.title).toLowerCase().includes(q) || String(d.handle).toLowerCase().includes(q) || String(d.category).toLowerCase().includes(q) || String(d.style).toLowerCase().includes(q));
+ const total = rows.length;
+ res.json({ items: rows.slice(offset, offset + limit).map(toCard), total, offset, limit });
});
// Single design (by handle or numeric id).
diff --git a/customcreator/gallery.html b/customcreator/gallery.html
index c5a7335..6cd6430 100644
--- a/customcreator/gallery.html
+++ b/customcreator/gallery.html
@@ -75,6 +75,10 @@
<div class="bar" id="grid">
<div class="count" id="count">Loading patterns…</div>
<div class="ctrls">
+ <div class="ctrl"><input id="q" type="search" placeholder="Search designs…" style="font-family:var(--sans);font-size:13px;border:1px solid var(--line);border-radius:3px;padding:7px 12px;min-width:170px"></div>
+ <div class="ctrl"><label for="cat">Category</label>
+ <select id="cat"><option value="">All</option></select>
+ </div>
<div class="ctrl"><label for="sort">Sort</label>
<select id="sort">
<option value="newest">Newest</option>
@@ -88,53 +92,58 @@
</div>
</div>
<div class="grid" id="cards"></div>
+ <div style="text-align:center;margin-top:34px">
+ <button id="more" style="display:none;font-family:var(--sans);font-size:13px;letter-spacing:.06em;border:1px solid var(--ink);background:var(--paper);color:var(--ink);padding:13px 34px;border-radius:3px;cursor:pointer">Load more</button>
+ </div>
</div>
<script>
-let DATA=[];
const $=id=>document.getElementById(id);
const esc=s=>String(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const PAGE=120;
+let offset=0,total=0,loading=false,first=true;
-function colorKey(hex){
- if(!hex) return 500;
- const m=hex.replace('#','').match(/.{2}/g); if(!m||m.length<3) return 500;
- const [r,g,b]=m.map(x=>parseInt(x,16));
- const mx=Math.max(r,g,b),mn=Math.min(r,g,b),l=(mx+mn)/510,d=mx-mn;
- if(d<18) return l>0.8?0:(l<0.18?900:400+Math.round((1-l)*80)); // white..gray..black
- let h=mx===r?((g-b)/d)%6:mx===g?(b-r)/d+2:(r-g)/d+4; h*=60; if(h<0)h+=360;
- return 50+Math.round(h/5); // warm→cool by hue, between white(0) and dark(900)
-}
-function sortData(mode){
- const a=[...DATA];
- if(mode==='newest') a.sort((x,y)=>(y.createdAt||'').localeCompare(x.createdAt||''));
- else if(mode==='color') a.sort((x,y)=>colorKey(x.colorHex)-colorKey(y.colorHex)||x.title.localeCompare(y.title));
- else if(mode==='style') a.sort((x,y)=>(x.style||'~').localeCompare(y.style||'~')||x.title.localeCompare(y.title));
- else if(mode==='sku') a.sort((x,y)=>(x.sku||'').localeCompare(y.sku||''));
- else if(mode==='title') a.sort((x,y)=>x.title.localeCompare(y.title));
- return a;
-}
-function render(){
- const mode=$('sort').value; const rows=sortData(mode);
- $('cards').innerHTML=rows.map(p=>`
+function cardHtml(p){return `
<a class="card" href="/CustomCreator/configure?handle=${encodeURIComponent(p.handle)}">
<div class="card-img" style="--bgimg:url('${esc(p.image)}')"></div>
<div class="card-name">${esc(p.title)}</div>
- <div class="card-sub">${esc(p.style||'Mural')}${p.colorName?' · '+esc(p.colorName):''}</div>
- </a>`).join('');
- $('count').textContent=rows.length+' mural patterns';
+ <div class="card-sub">${esc(p.style||'Original')}${p.colorName?' · '+esc(p.colorName):''}</div>
+ </a>`;}
+
+function query(reset){
+ if(loading) return; loading=true;
+ if(reset){offset=0;$('cards').innerHTML='';}
+ const u=new URLSearchParams({q:$('q').value.trim(),category:$('cat').value,sort:$('sort').value,offset,limit:PAGE});
+ $('count').textContent='Loading…';
+ fetch('/CustomCreator/api/collection?'+u).then(r=>r.json()).then(d=>{
+ total=d.total; const items=d.items||[];
+ $('cards').insertAdjacentHTML('beforeend',items.map(cardHtml).join(''));
+ offset+=items.length;
+ $('count').textContent=total? (offset+' of '+total.toLocaleString()+' designs') : 'No designs match.';
+ $('more').style.display=offset<total?'inline-block':'none';
+ if(first&&items[0]){$('startBtn').href='/CustomCreator/configure?handle='+encodeURIComponent(items[0].handle);first=false;}
+ loading=false;
+ }).catch(()=>{$('count').textContent='Could not load designs.';loading=false;});
+}
+
+function loadCategories(){
+ fetch('/CustomCreator/api/categories').then(r=>r.json()).then(d=>{
+ const cap=s=>String(s||'').replace(/\b\w/g,c=>c.toUpperCase());
+ (d.categories||[]).forEach(c=>{const o=document.createElement('option');o.value=c.category;o.textContent=cap(c.category.replace(/-/g,' '))+' ('+c.count.toLocaleString()+')';$('cat').appendChild(o);});
+ }).catch(()=>{});
}
+
function init(){
- // restore persisted controls
const sv=localStorage.getItem('cc-sort'); if(sv)$('sort').value=sv;
+ const cv=localStorage.getItem('cc-cat'); // applied after categories load
const dv=localStorage.getItem('cc-density'); if(dv){$('density').value=dv;document.documentElement.style.setProperty('--card',dv+'px');}
- $('sort').onchange=()=>{localStorage.setItem('cc-sort',$('sort').value);render();};
+ let t; $('q').oninput=()=>{clearTimeout(t);t=setTimeout(()=>query(true),280);};
+ $('cat').onchange=()=>{localStorage.setItem('cc-cat',$('cat').value);query(true);};
+ $('sort').onchange=()=>{localStorage.setItem('cc-sort',$('sort').value);query(true);};
$('density').oninput=()=>{document.documentElement.style.setProperty('--card',$('density').value+'px');localStorage.setItem('cc-density',$('density').value);};
- // first pattern → personalize the hero CTA
- fetch('/CustomCreator/api/collection').then(r=>r.json()).then(list=>{
- DATA=list||[];
- if(DATA[0]) $('startBtn').href='/CustomCreator/configure?handle='+encodeURIComponent(DATA[0].handle);
- render();
- }).catch(()=>{$('count').textContent='Could not load patterns.';});
+ $('more').onclick=()=>query(false);
+ loadCategories();
+ query(true);
}
init();
</script>
← 16691ff Custom Creator: re-source to wallco.ai designs ONLY (drop Re
·
back to Hollywood Wallcoverings
·
Custom Creator: self-host design images via id-keyed write-t fae2b73 →