← back to Wallco Ai
murals: add sort dropdown + density slider (Steve standing rule — every product grid needs sort + density)
8a16967ff5638796146342b5a19c873416cf77a2 · 2026-06-01 17:16:49 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 8a16967ff5638796146342b5a19c873416cf77a2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 1 17:16:49 2026 -0700
murals: add sort dropdown + density slider (Steve standing rule — every product grid needs sort + density)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
server.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 77 insertions(+), 3 deletions(-)
diff --git a/server.js b/server.js
index abd487a..7f85409 100644
--- a/server.js
+++ b/server.js
@@ -533,10 +533,36 @@ app.get('/design/:id/tabs', (req, res) => {
// V8 spec — wine-list spec sheet
// V9 config — configurator-first
// V10 stack — single-column card stack
+// Theme variants are standalone HTML. Inject the standard site chrome — login
+// header + nav + footer — so every theme has the login and the same info as the
+// rest of the site (Steve 2026-06-01). One injection point covers all variants.
+const _themeVariantCache = {};
function themeVariantSend(req, res, file) {
const id = parseInt(req.params.id, 10);
if (!Number.isFinite(id) || id < 1) return res.status(404).type('html').send('<h1>404</h1>');
- res.sendFile(path.join(__dirname, 'public', 'theme-variants', file));
+ try {
+ let html = _themeVariantCache[file];
+ if (html === undefined) {
+ html = fs.readFileSync(path.join(__dirname, 'public', 'theme-variants', file), 'utf8');
+ // site.css FIRST in <head> so the variant's own inline :root still wins on
+ // palette, while header/nav/footer pick up their standard styling.
+ if (!/css\/site\.css/.test(html)) {
+ html = html.replace(/<head>/i, '<head>\n<link rel="stylesheet" href="/css/site.css">');
+ }
+ // standard header (Sign In / login) right after <body>
+ if (!/site-header/.test(html)) {
+ html = html.replace(/<body[^>]*>/i, (m) => `${m}\n${htmlHeader('')}`);
+ }
+ // standard footer + nav script before </body>
+ if (!/site-footer/.test(html)) {
+ html = html.replace(/<\/body>/i, `\n${FOOTER}\n<script src="/corner-nav.js" defer></script>\n</body>`);
+ }
+ _themeVariantCache[file] = html;
+ }
+ res.type('html').send(html);
+ } catch (e) {
+ res.status(404).type('html').send('<h1>404</h1>');
+ }
}
app.get('/design/:id/hero', (req, res) => themeVariantSend(req, res, 'v4-hero.html'));
app.get('/design/:id/vertical', (req, res) => themeVariantSend(req, res, 'v5-vertical.html'));
@@ -19397,6 +19423,7 @@ ${designs.map(tearsheet).join('')}
// ── MURALS
app.get('/murals', (req, res) => {
const q = String(req.query.q || '').toLowerCase().trim();
+ const sort = req.query.sort || 'newest';
const murals = DESIGNS.filter(d => d.kind === 'mural' || d.kind === 'mural_panel');
let all = murals.length === 0 ? DESIGNS : murals; // fallback: show all if no mural-kind yet
// Server-side ?q= filter — mirrors the search endpoint's logic so the
@@ -19413,6 +19440,9 @@ app.get('/murals', (req, res) => {
return terms.every(t => hay.includes(t));
});
}
+ // Server-side sort — same helper + keys as /designs (Steve standing rule:
+ // every product grid MUST have sort + density).
+ all = sortDesigns(all, sort);
const cards = all.map(d => {
const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
@@ -19451,17 +19481,61 @@ ${htmlHeader('/murals')}
</div>
<div class="catalog-toolbar" style="padding:18px 40px 0">
<form method="get" action="/murals" class="filter-form">
- <div class="toolbar-row">
+ <input type="hidden" name="sort" id="murals-sort-hidden" value="${sort.replace(/[^a-z-]/g,'')}">
+ <div class="toolbar-row" style="display:flex;gap:14px;flex-wrap:wrap;align-items:center">
<div class="search-wrap" style="position:relative;flex:1;min-width:200px">
<input type="search" name="q" value="${q.replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c]))}" placeholder="Search murals…" class="search-input" id="murals-search-input" autocomplete="off" aria-autocomplete="list" aria-controls="murals-search-suggest" aria-expanded="false" role="combobox" style="width:100%">
<div id="murals-search-suggest" role="listbox" aria-label="Search suggestions" style="display:none;position:absolute;left:0;right:0;top:100%;margin-top:4px;background:var(--bg,#fff);border:1px solid var(--line,#ddd);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:50;max-height:60vh;overflow-y:auto"></div>
</div>
+ <div style="display:flex;align-items:center;gap:6px">
+ <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink-faint)">Sort</span>
+ <select aria-label="Sort murals" class="filter-select" onchange="document.getElementById('murals-sort-hidden').value=this.value;this.form.submit()">
+ <option value="newest" ${sort==='newest'?'selected':''}>Newest first</option>
+ <option value="oldest" ${sort==='oldest'?'selected':''}>Oldest first</option>
+ <option value="title" ${sort==='title'?'selected':''}>Title A → Z</option>
+ <option value="title-desc" ${sort==='title-desc'?'selected':''}>Title Z → A</option>
+ <option value="color" ${sort==='color'?'selected':''}>Color (group similar hues)</option>
+ <option value="light-dark" ${sort==='light-dark'?'selected':''}>Light → Dark</option>
+ <option value="dark-light" ${sort==='dark-light'?'selected':''}>Dark → Light</option>
+ <option value="color-wheel" ${sort==='color-wheel'?'selected':''}>Color Wheel (warm → cool)</option>
+ <option value="vivid" ${sort==='vivid'?'selected':''}>Most Vivid</option>
+ <option value="muted" ${sort==='muted'?'selected':''}>Most Muted</option>
+ <option value="style" ${sort==='style'?'selected':''}>Style (grouped)</option>
+ </select>
+ </div>
+ <div class="density-wrap" style="display:flex;align-items:center;gap:7px">
+ <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink-faint)" title="Grid density">Density</span>
+ <input type="range" id="murals-density-slider" min="2" max="8" step="1" value="4"
+ aria-label="Grid density (columns)" aria-valuemin="2" aria-valuemax="8" aria-valuenow="4">
+ <span id="murals-density-value" style="font:11px var(--sans);color:var(--ink-faint);min-width:1ch">4</span>
+ </div>
</div>
</form>
</div>
- <div class="design-grid catalog-grid" style="padding:24px 40px">
+ <div class="design-grid catalog-grid" id="murals-grid" style="padding:24px 40px">
${cards || '<p class="empty-state">No murals match that search.</p>'}
</div>
+ <script>
+ (function(){
+ var slider = document.getElementById('murals-density-slider');
+ var grid = document.getElementById('murals-grid');
+ var label = document.getElementById('murals-density-value');
+ if (!slider || !grid) return;
+ var saved = localStorage.getItem('wallco-density'); // shared with /designs
+ if (saved) slider.value = saved;
+ function applyCols(v){
+ var w = window.innerWidth, n = parseInt(v, 10);
+ if (w <= 600) n = Math.min(n, 2);
+ else if (w <= 900) n = Math.min(n, 3);
+ grid.style.setProperty('--cols', String(n));
+ if (label) label.textContent = String(n);
+ slider.setAttribute('aria-valuenow', String(n));
+ }
+ applyCols(slider.value);
+ slider.addEventListener('input', function(){ applyCols(this.value); localStorage.setItem('wallco-density', this.value); });
+ window.addEventListener('resize', function(){ applyCols(slider.value); });
+ })();
+ </script>
</section>
</main>
${FOOTER}
← 271f231 studio/search: fix dead pattern thumbnails (generator + stud
·
back to Wallco Ai
·
docs: convergence migration plan — make prod spoon_all_desig 031802d →