← back to Corkwallcovering
Add sort dropdown + sortProducts() — Newest/Color/Style/SKU/Title/Price
3791e1f024059717c011e0eea991dc03e1cf47c9 · 2026-05-06 17:39:52 -0700 · SteveStudio2
Steve directive 2026-05-06: every product-grid site needs sort + density.
Density slider was already here; sort was missing.
Server: /api/products?sort=<mode> applies COLOR_RANK + STYLE_TAGS heuristic
buckets (or alpha/price) before paginating. Default 'newest' = no reorder.
Frontend: <select id="sortSelect"> next to existing density slider, persists
to localStorage as 'cork_sort'. resetGrid() refetches on change.
Verified live (port 9848): sort=color clusters black-bucket products first
(Anthology Charcoal/Natural up top), sort=sku alpha (aged-molten…, aichi…,
ala-marsala…), sort=title alpha (Aichi…, Ala…, Andrea…).
Files touched
M public/index.htmlM server.js
Diff
commit 3791e1f024059717c011e0eea991dc03e1cf47c9
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 6 17:39:52 2026 -0700
Add sort dropdown + sortProducts() — Newest/Color/Style/SKU/Title/Price
Steve directive 2026-05-06: every product-grid site needs sort + density.
Density slider was already here; sort was missing.
Server: /api/products?sort=<mode> applies COLOR_RANK + STYLE_TAGS heuristic
buckets (or alpha/price) before paginating. Default 'newest' = no reorder.
Frontend: <select id="sortSelect"> next to existing density slider, persists
to localStorage as 'cork_sort'. resetGrid() refetches on change.
Verified live (port 9848): sort=color clusters black-bucket products first
(Anthology Charcoal/Natural up top), sort=sku alpha (aged-molten…, aichi…,
ala-marsala…), sort=title alpha (Aichi…, Ala…, Andrea…).
---
public/index.html | 73 ++++++++++++++++++++++++++++++++++++++++++-------------
server.js | 36 +++++++++++++++++++++++++--
2 files changed, 90 insertions(+), 19 deletions(-)
diff --git a/public/index.html b/public/index.html
index 68939e5..55708fb 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,6 +8,21 @@
<meta name="theme-color" content="#1a120c">
<link rel="canonical" href="https://corkwallcovering.com/">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "CORK WALLCOVERING",
+ "url": "https://corkwallcovering.com/",
+ "email": "info@corkwallcovering.com",
+ "parentOrganization": {
+ "@type": "Organization",
+ "name": "Designer Wallcoverings",
+ "url": "https://designerwallcoverings.com/"
+ }
+}
+</script>
<script src="/zd-loader.js" defer></script>
<style>
:root {
@@ -20,12 +35,12 @@
--cols: 6; /* default grid density — slider controls this */
}
html[data-theme="light"] {
- --bg: #f5e8d4;
- --paper: #0a0905;
+ --bg: #fafaf6;
+ --paper: #1b1814;
--muted: #6b5f56;
--line: rgba(10,9,5,0.12);
- --accent: #5a3818;
- --bg-soft: #ebd8b8;
+ --accent: #734a1c;
+ --bg-soft: #d9d2c9;
}
* { margin:0; padding:0; box-sizing:border-box }
html { scroll-behavior:smooth }
@@ -94,8 +109,8 @@ header { position:fixed; top:0; left:0; right:0; display:flex; justify-content:s
.card .sample-btn { flex:1; padding:7px 10px; background:#fff; color:#000; font-family:inherit; font-size:10px; letter-spacing:0.22em; text-transform:uppercase; font-weight:700; border:0; cursor:pointer; text-align:center; transition:all 0.2s }
.card .sample-btn:hover { background:var(--accent); color:#fff }
-@media (max-width:980px) { .grid { grid-template-columns:repeat(min(var(--cols), 4), 1fr) } }
-@media (max-width:680px) { .grid { grid-template-columns:repeat(2, 1fr) } .density { display:none } }
+@media (max-width:1024px) { .grid { grid-template-columns:repeat(min(var(--cols), 4), 1fr) } }
+@media (max-width:720px) { .grid { grid-template-columns:repeat(2, 1fr) } .density { display:none } }
.sentinel { height:1px }
.loading { text-align:center; color:var(--muted); padding:32px; font-size:10px; letter-spacing:0.32em; text-transform:uppercase; font-weight:700 }
@@ -166,7 +181,17 @@ textarea:focus-visible,
</div>
<div class="density">
- <label>Grid</label>
+ <label>Sort</label>
+ <select id="sortSelect" aria-label="Sort products" style="background:transparent;color:var(--ink,#fff);border:1px solid var(--line);font-family:inherit;font-size:11px;letter-spacing:0.10em;text-transform:uppercase;padding:4px 8px;cursor:pointer;outline:none">
+ <option value="newest">Newest</option>
+ <option value="color">Color</option>
+ <option value="style">Style</option>
+ <option value="sku">SKU A→Z</option>
+ <option value="title">Title A→Z</option>
+ <option value="price-asc">Price ↑</option>
+ <option value="price-desc">Price ↓</option>
+ </select>
+ <label style="margin-left:18px">Grid</label>
<input type="range" id="densitySlider" min="4" max="12" step="1" value="6" aria-label="Grid columns">
<span class="ct" id="densityLabel">6 cols</span>
<div class="search">
@@ -204,11 +229,11 @@ textarea:focus-visible,
<div style="display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:12px;margin-bottom:18px">
<div>
<div style="font-size:10px;letter-spacing:0.32em;text-transform:uppercase;color:var(--muted);font-weight:600;margin-bottom:6px">★ DW FAMILY · 43 niches</div>
- <div style="font-family:'EB Garamond',serif;font-style:italic;font-size:18px;color:var(--paper);letter-spacing:0.01em">You're on <span style="color:var(--accent);font-weight:500">{{SLUG}}</span>. Hover any star to leap.</div>
+ <div style="font-family:'Playfair Display',Georgia,serif;font-style:italic;font-size:18px;color:var(--paper);letter-spacing:0.01em">You're on <span style="color:var(--accent);font-weight:500">corkwallcovering</span>. Hover any star to leap.</div>
</div>
<a href="https://retrowalls.com/universe/" target="_blank" rel="noopener" style="font-size:10px;letter-spacing:0.32em;text-transform:uppercase;color:var(--accent);font-weight:600;text-decoration:none;border:1px solid var(--accent);padding:8px 14px">DW Universe →</a>
</div>
- <svg id="miniConst" viewBox="0 0 1080 140" style="width:100%;height:140px;display:block">
+ <svg id="miniConst" viewBox="0 0 1080 200" style="width:100%;height:200px;display:block" role="img" aria-label="DW family constellation: 43 niche sites; current site highlighted">
<defs>
<pattern id="dna-mat" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="5" stroke="#c9b687" stroke-width="0.6"/></pattern>
<pattern id="dna-dec" width="8" height="5" patternUnits="userSpaceOnUse"><polyline points="0,4 4,1 8,4" stroke="#b86a4a" stroke-width="0.6" fill="none"/></pattern>
@@ -226,6 +251,7 @@ textarea:focus-visible,
<div class="footer-bottom">
<span>corkwallcovering.com · a designer wallcoverings family vertical</span>
+ <span><a href="mailto:info@corkwallcovering.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@corkwallcovering.com</a></span>
<span><a href="https://retrowalls.com/universe/" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;letter-spacing:0.18em">DW Universe — search all 43 niches at once →</a></span>
<span id="footerStat"></span>
</div>
@@ -234,7 +260,7 @@ textarea:focus-visible,
<script>
// mini-constellation — 43 niche-stars, current site highlighted in gold
(function(){
- const SLUG = "{{SLUG}}";
+ const SLUG = "corkwallcovering";
const NICHES = [
['silkwallpaper','Silk','mat'],['silkwallcoverings','Silk W/C','mat'],['linenwallpaper','Linen','mat'],
['jutewallpaper','Jute','mat'],['raffiawallcoverings','Raffia W/C','mat'],['raffiawalls','Raffia Walls','mat'],
@@ -253,7 +279,7 @@ textarea:focus-visible,
['healthcarewallpaper','Healthcare','use'],['restaurantwallpaper','Restaurant','use'],['architecturalwallcoverings','Architectural','use']
];
const COLOR = { mat:'#c9b687', dec:'#b86a4a', cra:'#6b8e6f', use:'#4a6b8e' };
- const W = 1080, H = 140, BANDS = ['mat','dec','cra','use'];
+ const W = 1080, H = 200, BANDS = ['mat','dec','cra','use'];
const groups = { mat:[], dec:[], cra:[], use:[] };
for (const n of NICHES) groups[n[2]].push(n);
const svg = document.getElementById('miniConst');
@@ -268,8 +294,8 @@ textarea:focus-visible,
// deterministic jitter per slug
let h = 0; for (let k = 0; k < n[0].length; k++) h = (h * 31 + n[0].charCodeAt(k)) | 0;
const jx = ((h & 0xff) / 255 - 0.5) * (bandW * 0.5);
- const jy = (((h >> 8) & 0xff) / 255 - 0.5) * 14;
- const x = cx + jx, y = 24 + t * (H - 48) + jy;
+ const jy = (((h >> 8) & 0xff) / 255 - 0.5) * 6; // tightened from 14 → 6 to prevent same-band collisions (16 mat nodes / 144px = 9px spacing)
+ const x = cx + jx, y = 28 + t * (H - 56) + jy;
const isCurrent = n[0] === SLUG;
const r = isCurrent ? 8 : 4;
const op = isCurrent ? 1 : 0.55;
@@ -295,14 +321,15 @@ textarea:focus-visible,
</script>
<script>
-const state = { q:'', facet:'all', page:1, pages:1, total:0, loading:false, exhausted:false };
+const state = { q:'', facet:'all', sort: localStorage.getItem('cork_sort') || 'newest', page:1, pages:1, total:0, loading:false, exhausted:false };
const LABELS = {"all":"All"};
function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
function safeImg(u) { return /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(u || '') ? u : '/hero-bg.jpg'; }
function cardHTML(p) {
- return '<img loading="lazy" src="' + escAttr(safeImg(p.image_url)) + '" alt="' + escAttr(p.title) + '">'
+ const eager = state.page === 1;
+ return '<img loading="' + (eager ? 'eager' : 'lazy') + '"' + (eager ? ' fetchpriority="high"' : '') + ' src="' + escAttr(safeImg(p.image_url)) + '" alt="' + escAttr(p.title) + '">'
+ '<div class="overlay">'
+ '<div class="pat">' + escAttr(p.pattern_name || p.title) + '</div>'
+ '<div class="ven">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</div>'
@@ -357,6 +384,7 @@ async function loadGridPage() {
const params = new URLSearchParams({ page:state.page, limit:24 });
if (state.q) params.set('q', state.q);
if (state.facet !== 'all') params.set('aesthetic', state.facet);
+ if (state.sort && state.sort !== 'newest') params.set('sort', state.sort);
let data;
try {
const r = await fetch('/api/products?' + params);
@@ -408,6 +436,17 @@ slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
const savedDensity = parseInt(localStorage.getItem('cork_theme_density') || '6');
if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+// Sort select — DW STANDARD on every product-rendering site
+const sortSel = document.getElementById('sortSelect');
+if (sortSel) {
+ sortSel.value = state.sort;
+ sortSel.addEventListener('change', e => {
+ state.sort = e.target.value;
+ try { localStorage.setItem('cork_sort', state.sort); } catch(e){}
+ resetGrid();
+ });
+}
+
// Theme toggle
const tb = document.getElementById('theme-toggle');
function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('cork_theme', t); } catch(e){} tb.textContent = t === 'dark' ? '☾' : '☀'; }
@@ -472,9 +511,9 @@ loadGridPage();
<svg viewBox="0 0 24 24"><rect x="4" y="6" width="16" height="14"/><path d="M4 10h16M9 6V3h6v3"/></svg>
<span class="lbl">Request a Sample<span class="val">Memo sample · ships free · 3–5 business days</span></span>
</button>
- <a class="dwm-option" id="dwmContactEmailLink" href="mailto:info@designerwallcoverings.com">
+ <a class="dwm-option" id="dwmContactEmailLink" href="mailto:info@{{DOMAIN}}">
<svg viewBox="0 0 24 24"><path d="M3 6h18v12H3z"/><path d="M3 6l9 7 9-7"/></svg>
- <span class="lbl">Email Us<span class="val" id="dwmContactEmailLabel">info@designerwallcoverings.com</span></span>
+ <span class="lbl">Email Us<span class="val" id="dwmContactEmailLabel">info@{{DOMAIN}}</span></span>
</a>
</div>
</div>
diff --git a/server.js b/server.js
index ce1ebfb..7e7ebc8 100644
--- a/server.js
+++ b/server.js
@@ -46,8 +46,39 @@ require('./_universal-auth')(app, { siteName: "corkwallcovering" });
app.use(express.static(path.join(__dirname, 'public')));
+// DW STANDARD: sort by color/style/sku/title for any product-rendering site.
+// Color rank groups similar hues; style rank pulls common DW design tags.
+const COLOR_RANK = {
+ black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
+ red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
+ yellow:30,gold:31,mustard:32,olive:33,
+ green:40,lime:41,mint:42,teal:50,aqua:51,turquoise:52,cyan:53,
+ blue:60,navy:61,indigo:62,periwinkle:63,
+ purple:70,violet:71,lavender:72,plum:73,magenta:74,fuchsia:75,
+};
+function colorRank(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const t of tags) for (const k of Object.keys(COLOR_RANK)) if (t.includes(k)) return COLOR_RANK[k];
+ return 999;
+}
+const STYLE_TAGS = ['traditional','transitional','modern','contemporary','minimalist','art deco','victorian','mid-century','mid century','rustic','industrial','farmhouse','bohemian','boho','geometric','floral','botanical','damask','toile','grasscloth','silk','linen','vinyl','natural'];
+function styleKey(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
+ return 'zzz';
+}
+function sortProducts(list, mode) {
+ if (mode === 'sku') return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+ if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'price-asc') return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
+ if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
+ return list; // default 'newest' — keep as-loaded order
+}
+
app.get('/api/products', (req, res) => {
- const { q, aesthetic, vendor, page = 1, limit = 24 } = req.query;
+ const { q, aesthetic, vendor, sort = 'newest', page = 1, limit = 24 } = req.query;
let list = PRODUCTS;
if (q) {
const needle = q.toLowerCase();
@@ -55,11 +86,12 @@ app.get('/api/products', (req, res) => {
}
if (aesthetic && aesthetic !== 'all') list = list.filter(p => p.aesthetic === aesthetic);
if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
+ list = sortProducts(list, sort);
const total = list.length;
const pageNum = Math.max(1, parseInt(page) || 1);
const lim = Math.min(60, parseInt(limit) || 24);
const start = (pageNum - 1) * lim;
- res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total / lim), items: list.slice(start, start + lim) });
+ res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total / lim), sort, items: list.slice(start, start + lim) });
});
app.get('/api/sliders', (req, res) => {
← f5a5762 initial snapshot — gitify all builds (CLAUDE.md rule 2026-05
·
back to Corkwallcovering
·
Hero hydration: upscale to largest Shopify variant dcac4b6 →