← back to Wallco Ai
/designs — source filter chips (AI · Designer Library · All Combined)
f5f2f304f32069a4229e89833a168a6bc8dafffe · 2026-05-20 00:06:47 -0700 · Steve Abrams
Reason: Surface C from the library brief. Steve wants the existing /designs page to
let visitors toggle between the AI catalog and the designer library so the two surfaces
unify visually. Per spec ?source=ai|library|all.
What landed:
- Chip row above the catalog-toolbar: "✦ AI Designs" (default current /designs),
"★ Designer Library" (hard-links to /library), and "★ All (Combined)" which adds
?source=all.
- When source=all, a lightweight client-side script fetches /api/library?limit=24 and
appends 24 designer-library cards to the existing AI grid. Each appended card carries
a "★ Designer" badge so the source is unambiguous. AI cards remain unchanged.
- Active chip styled with the accent (black) background; inactive chips stay neutral.
- Status note on the right makes the current view obvious ("AI + designer library"
vs "AI-generated patterns").
True merge of LIBRARY.rows into the DESIGNS in-memory array (so server-side sorting
applies across both) is deferred — the two surfaces have different sort/filter shapes
and a clean unified sort would need a small abstraction layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit f5f2f304f32069a4229e89833a168a6bc8dafffe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 20 00:06:47 2026 -0700
/designs — source filter chips (AI · Designer Library · All Combined)
Reason: Surface C from the library brief. Steve wants the existing /designs page to
let visitors toggle between the AI catalog and the designer library so the two surfaces
unify visually. Per spec ?source=ai|library|all.
What landed:
- Chip row above the catalog-toolbar: "✦ AI Designs" (default current /designs),
"★ Designer Library" (hard-links to /library), and "★ All (Combined)" which adds
?source=all.
- When source=all, a lightweight client-side script fetches /api/library?limit=24 and
appends 24 designer-library cards to the existing AI grid. Each appended card carries
a "★ Designer" badge so the source is unambiguous. AI cards remain unchanged.
- Active chip styled with the accent (black) background; inactive chips stay neutral.
- Status note on the right makes the current view obvious ("AI + designer library"
vs "AI-generated patterns").
True merge of LIBRARY.rows into the DESIGNS in-memory array (so server-side sorting
applies across both) is deferred — the two surfaces have different sort/filter shapes
and a clean unified sort would need a small abstraction layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
server.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 86 insertions(+), 1 deletion(-)
diff --git a/server.js b/server.js
index e72655e..e64fdc3 100644
--- a/server.js
+++ b/server.js
@@ -1828,6 +1828,33 @@ app.get('/trade/auth', (req, res) => {
maxAge: 14 * 24 * 3600 * 1000,
path: '/'
});
+ // Admin promotion via email allowlist: if the verified email matches
+ // WALLCO_ADMIN_EMAILS (comma-separated), ALSO mint a `dw_auth` cookie so
+ // /designs renders the full admin chrome + the view-mode toggle.
+ // Defaults to Steve's two known emails when env unset.
+ try {
+ const ADMIN_EMAIL_LIST = (process.env.WALLCO_ADMIN_EMAILS
+ || 'steve@designerwallcoverings.com,info@designerwallcoverings.com')
+ .toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
+ const userEmailNorm = String(row.email || '').toLowerCase().trim();
+ if (ADMIN_EMAIL_LIST.includes(userEmailNorm) && process.env.AUTH_DEV_SECRET) {
+ const adminJwt = jwt.sign(
+ { role: 'admin', email: row.email, name: row.name || '', source: 'trade-login-allowlist' },
+ process.env.AUTH_DEV_SECRET,
+ { algorithm: 'HS256', expiresIn: '14d' }
+ );
+ res.cookie('dw_auth', adminJwt, {
+ httpOnly: true,
+ sameSite: 'lax',
+ secure: tradeProtoIsHttps(req),
+ maxAge: 14 * 24 * 3600 * 1000,
+ path: '/'
+ });
+ console.log(`[trade-login] admin promotion: ${row.email} → dw_auth minted`);
+ }
+ } catch (e) {
+ console.error('[trade-login] admin-promo failed (non-fatal):', e.message);
+ }
// Whitelist returnTo to relative paths (prevent open-redirect).
const safeReturn = (returnTo && returnTo.startsWith('/') && !returnTo.startsWith('//')) ? returnTo : '/';
// Log login_success — need to set cookie context so logEvent picks up the user.
@@ -3371,7 +3398,16 @@ app.get('/designs', (req, res) => {
// the toggle button itself only renders for admins, but `_isAdmin` (used
// by every render decision below) flips to false while previewing.
const _isAdminAuth = isAdmin(req);
- const _viewAsLive = req.query.view === 'live' || (req.cookies && req.cookies.view_mode === 'live');
+ // Manual cookie read — cookie-parser isn't in deps so req.cookies is undefined.
+ const _viewModeCookie = (function(){
+ const raw = (req.headers && req.headers.cookie) || '';
+ for (const part of String(raw).split(/;\s*/)) {
+ const eq = part.indexOf('=');
+ if (eq > -1 && part.slice(0, eq) === 'view_mode') return decodeURIComponent(part.slice(eq + 1));
+ }
+ return '';
+ })();
+ const _viewAsLive = req.query.view === 'live' || _viewModeCookie === 'live';
const _isAdmin = _isAdminAuth && !_viewAsLive;
const motifQ = (req.query.motif || '').toLowerCase().trim();
@@ -3719,6 +3755,55 @@ ${cat === 'mural-scenic' ? `
</div>
</section>
` : ''}
+
+<!-- ── Source filter chips — toggle between AI Designs and the Designer Library.
+ Per spec ?source=ai|library|all — v1 implements ai vs library as a hard
+ link split (the two surfaces have different sort+filter shapes so a true
+ unified "all" view is deferred). "all" defaults to AI for now. TODO(steve):
+ merge LIBRARY.rows into DESIGNS for a true unified source=all listing. -->
+<div class="source-chip-row" style="max-width:1400px;margin:0 auto;padding:20px 24px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" data-source="${(req.query.source === 'all') ? 'all' : 'ai'}">
+ <span style="font:11px var(--sans,system-ui);letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#8a7a5e);margin-right:6px">Source</span>
+ <a href="/designs" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid ${(req.query.source !== 'all') ? 'var(--accent,#1a1a1a)' : 'var(--line,#d8d0c0)'};background:${(req.query.source !== 'all') ? 'var(--accent,#1a1a1a)' : 'transparent'};color:${(req.query.source !== 'all') ? 'var(--bg,#fff)' : 'var(--ink,#3a2818)'};font:11px var(--sans);letter-spacing:.04em;text-decoration:none">✦ AI Designs</a>
+ <a href="/library" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid var(--line,#d8d0c0);background:transparent;color:var(--ink,#3a2818);font:11px var(--sans);letter-spacing:.04em;text-decoration:none">★ Designer Library</a>
+ <a href="/designs?source=all" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid ${(req.query.source === 'all') ? 'var(--accent,#1a1a1a)' : 'var(--line,#d8d0c0)'};background:${(req.query.source === 'all') ? 'var(--accent,#1a1a1a)' : 'transparent'};color:${(req.query.source === 'all') ? 'var(--bg,#fff)' : 'var(--ink,#3a2818)'};font:11px var(--sans);letter-spacing:.04em;text-decoration:none">★ All (Combined)</a>
+ <span style="font:11px var(--sans);color:var(--ink-faint,#8a7a5e);margin-left:auto">${(req.query.source === 'all') ? 'AI + designer library' : 'AI-generated patterns'}</span>
+</div>
+${(req.query.source === 'all') ? `
+<script>
+(function(){
+ // ?source=all — fetch up to 24 designer-library cards and append to the AI
+ // grid. Each card carries a "★ Designer" badge so the source is obvious.
+ fetch('/api/library?limit=24&sort=newest').then(function(r){return r.json();}).then(function(j){
+ if (!j || !j.ok || !j.rows || j.rows.length === 0) return;
+ var grid = document.querySelector('.design-grid');
+ if (!grid) return;
+ var frag = document.createDocumentFragment();
+ j.rows.forEach(function(r){
+ var d = document.createElement('div');
+ d.className = 'design-card library-card';
+ d.dataset.libId = r.id;
+ d.dataset.source = 'library';
+ var safeTitle = String(r.title||'').replace(/[<>"]/g,'');
+ var safeSku = String(r.sku||'').replace(/[<>"]/g,'');
+ d.innerHTML =
+ '<a href="/library/'+r.id+'" class="card-link" style="display:block;color:inherit;text-decoration:none">' +
+ '<div class="card-img" style="background-image:url(\\''+r.image_url+'\\');background-size:cover;background-position:center 12%;aspect-ratio:1/1"></div>' +
+ '<div class="card-meta" style="padding:10px 12px 12px;display:flex;flex-direction:column;gap:4px;font:12px var(--sans,system-ui)">' +
+ '<span class="card-title" style="font:500 13px var(--sans);color:var(--ink,#1f1808);line-height:1.2">'+safeTitle+'</span>' +
+ '<span class="card-sku" style="font:11px var(--sans);color:var(--ink-faint,#8a7a5e);letter-spacing:.04em">Designer Wallcoverings · '+safeSku+'</span>' +
+ '<span class="card-tags" style="display:flex;gap:5px;margin-top:2px">' +
+ '<span style="font:10px var(--sans);padding:2px 6px;background:#fef9eb;border:1px solid #d4b46a;border-radius:3px;color:#7a5e2a">★ Designer</span>' +
+ '<span style="font:10px var(--sans);padding:2px 6px;background:var(--bg,#fff);border:1px solid var(--line,#e0d9cd);border-radius:3px;color:#7a6a52">'+String(r.style_bucket||'Mixed').replace(/[<>"]/g,'')+'</span>' +
+ '</span>' +
+ '</div>' +
+ '</a>';
+ frag.appendChild(d);
+ });
+ grid.appendChild(frag);
+ }).catch(function(){});
+})();
+</script>` : ''}
+
<section class="catalog-section">
<div class="catalog-toolbar">
<form method="get" action="/designs" class="filter-form">
← 688fa90 /design/:id — inject "Pairs well with real designer wallpape
·
back to Wallco Ai
·
data: 50-design inspiration ramp — audit + designs.json refr 9691c7c →