← back to CelebritySignatures
Accounts + save-your-placement: zero-dep email/password auth + per-account saved mural placements; injected account.js UI
11d1aad7e20af1a5943b12187d6a51d1a128fcf6 · 2026-06-18 18:59:17 -0700 · Steve
Files touched
M .gitignoreA public/account.jsM server.js
Diff
commit 11d1aad7e20af1a5943b12187d6a51d1a128fcf6
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jun 18 18:59:17 2026 -0700
Accounts + save-your-placement: zero-dep email/password auth + per-account saved mural placements; injected account.js UI
---
.gitignore | 3 +
public/account.js | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 160 ++++++++++++++++++++++++++++++++++++++++-------
3 files changed, 323 insertions(+), 22 deletions(-)
diff --git a/.gitignore b/.gitignore
index fe8c64e..16b3568 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ tmp_collage_cache/
.venv/
data/mural-orders.json
output/*.tif
+data/users.json
+data/saves.json
+data/sessions.json
diff --git a/public/account.js b/public/account.js
new file mode 100644
index 0000000..91a2873
--- /dev/null
+++ b/public/account.js
@@ -0,0 +1,182 @@
+/* Celebrity Signature Murals — accounts + save-your-placement.
+ Self-contained, injected by server.js into /murals so murals.html stays untouched.
+ Reads the wall-studio's existing globals (cur, pos, wallDims, selectMural, drawScene). */
+(function () {
+ 'use strict';
+ const $ = (s, r = document) => r.querySelector(s);
+ const el = (t, props = {}, html) => { const e = document.createElement(t); Object.assign(e, props); if (html != null) e.innerHTML = html; return e; };
+ const api = (p, opt = {}) => fetch(p, { credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, ...opt }).then(r => r.json());
+ let me = null;
+
+ // ---- styles ----
+ document.head.appendChild(el('style', {}, `
+ .acct-bar{position:fixed;top:14px;right:18px;z-index:60;display:flex;gap:8px;align-items:center;font:14px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
+ .acct-bar .pill{background:#fff;border:1px solid #e6e3dc;border-radius:999px;padding:8px 15px;cursor:pointer;color:#1a1a1a;box-shadow:0 1px 4px rgba(0,0,0,.06)}
+ .acct-bar .pill.dark{background:#1a1a1a;color:#fff;border-color:#1a1a1a}
+ .acct-menu{position:absolute;top:42px;right:0;background:#fff;border:1px solid #e6e3dc;border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.14);overflow:hidden;display:none;min-width:170px}
+ .acct-menu.open{display:block}
+ .acct-menu button{display:block;width:100%;text-align:left;border:0;background:#fff;padding:11px 16px;font:inherit;cursor:pointer;color:#1a1a1a}
+ .acct-menu button:hover{background:#f7f5f0}
+ .acct-save{display:inline-flex;align-items:center;gap:7px;border:1px solid #1a1a1a;background:#1a1a1a;color:#fff;font:14px/1 inherit;padding:10px 16px;border-radius:9px;cursor:pointer;margin-top:6px}
+ .acct-save.saved{background:#1d7a36;border-color:#1d7a36}
+ .acct-ovl{position:fixed;inset:0;background:rgba(20,18,14,.45);z-index:70;display:none;align-items:center;justify-content:center}
+ .acct-ovl.open{display:flex}
+ .acct-modal{background:#fff;border-radius:14px;width:min(380px,92vw);padding:24px;font:15px/1.5 -apple-system,sans-serif;box-shadow:0 20px 60px rgba(0,0,0,.3)}
+ .acct-modal h3{font-family:Georgia,serif;margin:0 0 4px;font-size:21px}
+ .acct-modal p{color:#6b6b6b;margin:0 0 16px;font-size:13px}
+ .acct-modal label{display:block;font-size:12px;color:#6b6b6b;margin:0 0 10px}
+ .acct-modal input{width:100%;box-sizing:border-box;margin-top:5px;padding:10px 12px;border:1px solid #e6e3dc;border-radius:8px;font:inherit}
+ .acct-modal .go{width:100%;border:0;background:#1a1a1a;color:#fff;padding:12px;border-radius:9px;font:inherit;font-size:15px;cursor:pointer;margin-top:6px}
+ .acct-modal .alt{text-align:center;margin-top:12px;font-size:13px;color:#6b6b6b}
+ .acct-modal .alt a{color:#1d7a36;cursor:pointer;text-decoration:underline}
+ .acct-msg{font-size:13px;margin-top:10px;min-height:16px}
+ .acct-msg.err{color:#b3261e}.acct-msg.ok{color:#1d7a36}
+ .acct-draw{position:fixed;top:0;right:0;height:100%;width:min(380px,94vw);background:#f7f5f0;z-index:65;box-shadow:-8px 0 30px rgba(0,0,0,.18);transform:translateX(100%);transition:transform .25s;overflow:auto;padding:22px}
+ .acct-draw.open{transform:none}
+ .acct-draw h3{font-family:Georgia,serif;margin:0 0 14px;font-size:20px;display:flex;justify-content:space-between;align-items:center}
+ .acct-draw h3 .x{cursor:pointer;color:#6b6b6b;font-size:22px;border:0;background:0}
+ .save-card{background:#fff;border:1px solid #e6e3dc;border-radius:12px;overflow:hidden;margin-bottom:14px}
+ .save-card img{width:100%;height:120px;object-fit:cover;display:block;background:#fff;border-bottom:1px solid #eee}
+ .save-card .b{padding:11px 13px}
+ .save-card .t{font-family:Georgia,serif;font-size:15px}
+ .save-card .m{font-size:12px;color:#6b6b6b;margin:3px 0 9px}
+ .save-card .row{display:flex;gap:8px}
+ .save-card .row button{flex:1;border:1px solid #e6e3dc;background:#fff;border-radius:7px;padding:7px;font:inherit;font-size:13px;cursor:pointer}
+ .save-card .row button.load{border-color:#1a1a1a;background:#1a1a1a;color:#fff}
+ .acct-empty{color:#6b6b6b;font-size:14px;text-align:center;padding:30px 10px}
+ .acct-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1a1a1a;color:#fff;padding:11px 20px;border-radius:999px;z-index:80;font:14px sans-serif;opacity:0;transition:opacity .2s}
+ .acct-toast.show{opacity:1}
+ `));
+
+ // ---- elements ----
+ const bar = el('div', { className: 'acct-bar' });
+ const acctPill = el('button', { className: 'pill' }, 'Sign in');
+ const menu = el('div', { className: 'acct-menu' });
+ menu.appendChild(el('button', { onclick: openSaves }, '🖼 My saved murals'));
+ menu.appendChild(el('button', { onclick: logout }, 'Sign out'));
+ bar.appendChild(acctPill); bar.appendChild(menu); document.body.appendChild(bar);
+
+ const ovl = el('div', { className: 'acct-ovl' });
+ const modal = el('div', { className: 'acct-modal' });
+ ovl.appendChild(modal); document.body.appendChild(ovl);
+ ovl.addEventListener('click', e => { if (e.target === ovl) ovl.classList.remove('open'); });
+
+ const draw = el('div', { className: 'acct-draw' });
+ document.body.appendChild(draw);
+ const toast = el('div', { className: 'acct-toast' }); document.body.appendChild(toast);
+ const flash = m => { toast.textContent = m; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 1900); };
+
+ acctPill.onclick = () => { if (me) menu.classList.toggle('open'); else openAuth('login'); };
+ document.addEventListener('click', e => { if (!bar.contains(e.target)) menu.classList.remove('open'); });
+
+ // ---- save button into the studio ----
+ function mountSaveBtn() {
+ const studio = $('#studio'); if (!studio || $('#acctSaveBtn')) return;
+ const btn = el('button', { id: 'acctSaveBtn', className: 'acct-save' }, '💾 Save this placement to my account');
+ btn.onclick = savePlacement;
+ const anchor = $('#readout') || $('.stage-wrap', studio) || studio;
+ anchor.parentNode.insertBefore(btn, anchor.nextSibling);
+ }
+
+ // ---- read current placement from the studio's globals (resilient) ----
+ function snapshot() {
+ try {
+ if (typeof cur === 'undefined' || !cur) return null;
+ const wd = (typeof wallDims === 'function') ? wallDims() : { W: +($('#wallW') || {}).value || 0, H: +($('#wallH') || {}).value || 0 };
+ const p = (typeof pos !== 'undefined' && pos) ? pos : { x: 0, y: 0 };
+ return { mural: cur.slug, mural_title: cur.title, wall_w: wd.W, wall_h: wd.H,
+ placement: { from_left_ft: +(+p.x).toFixed(2), off_floor_ft: +(+p.y).toFixed(2) },
+ scene: (typeof curScene !== 'undefined') ? curScene : '' };
+ } catch (e) { return null; }
+ }
+
+ async function savePlacement() {
+ const snap = snapshot();
+ if (!snap) return flash('Pick a mural in the studio first');
+ if (!me) { openAuth('login', savePlacement); return; }
+ const r = await api('/api/saves', { method: 'POST', body: JSON.stringify(snap) });
+ if (r.ok) { const b = $('#acctSaveBtn'); b.classList.add('saved'); b.textContent = '✓ Saved to your account'; setTimeout(() => { b.classList.remove('saved'); b.innerHTML = '💾 Save this placement to my account'; }, 2200); flash('Saved to your account'); }
+ else flash(r.error || 'Could not save');
+ }
+
+ // ---- auth modal ----
+ function openAuth(mode, after) {
+ modal.innerHTML = '';
+ const isLogin = mode === 'login';
+ modal.appendChild(el('h3', {}, isLogin ? 'Sign in' : 'Create your account'));
+ modal.appendChild(el('p', {}, 'Save mural placements to your account and find them anytime.'));
+ const f = el('form');
+ if (!isLogin) f.appendChild(el('label', {}, 'Name<input name="name" autocomplete="name">'));
+ f.appendChild(el('label', {}, 'Email<input name="email" type="email" required autocomplete="email">'));
+ f.appendChild(el('label', {}, 'Password<input name="password" type="password" required minlength="6" autocomplete="' + (isLogin ? 'current-password' : 'new-password') + '">'));
+ const go = el('button', { className: 'go', type: 'submit' }, isLogin ? 'Sign in' : 'Create account');
+ f.appendChild(go);
+ const msg = el('div', { className: 'acct-msg' });
+ f.appendChild(msg);
+ const alt = el('div', { className: 'alt' }, isLogin ? 'No account? <a>Create one</a>' : 'Have an account? <a>Sign in</a>');
+ alt.querySelector('a').onclick = () => openAuth(isLogin ? 'signup' : 'login', after);
+ f.appendChild(alt);
+ f.onsubmit = async e => {
+ e.preventDefault(); msg.className = 'acct-msg'; msg.textContent = 'Working…';
+ const body = JSON.stringify({ name: f.name ? f.name.value : '', email: f.email.value, password: f.password.value });
+ const r = await api('/api/auth/' + (isLogin ? 'login' : 'signup'), { method: 'POST', body });
+ if (r.ok) { me = r.user; refreshBar(); ovl.classList.remove('open'); flash('Signed in as ' + me.email); if (after) after(); }
+ else { msg.className = 'acct-msg err'; msg.textContent = r.error || 'Something went wrong'; }
+ };
+ modal.appendChild(f);
+ ovl.classList.add('open');
+ setTimeout(() => { const em = f.querySelector('input[type=email]'); em && em.focus(); }, 50);
+ }
+
+ async function logout() { await api('/api/auth/logout', { method: 'POST' }); me = null; refreshBar(); menu.classList.remove('open'); flash('Signed out'); }
+
+ // ---- my saved murals drawer ----
+ async function openSaves() {
+ menu.classList.remove('open');
+ draw.innerHTML = '';
+ const h = el('h3', {}, 'My saved murals');
+ const x = el('button', { className: 'x' }, '×'); x.onclick = () => draw.classList.remove('open'); h.appendChild(x);
+ draw.appendChild(h);
+ draw.classList.add('open');
+ const r = await api('/api/saves');
+ if (!r.ok) { draw.appendChild(el('div', { className: 'acct-empty' }, 'Please sign in.')); return; }
+ if (!r.saves.length) { draw.appendChild(el('div', { className: 'acct-empty' }, 'No saved murals yet.<br>Place a mural on your wall and tap “Save”.')); return; }
+ r.saves.forEach(s => {
+ const card = el('div', { className: 'save-card' });
+ card.appendChild(el('img', { src: '/output/portrait-' + s.mural + '.png', loading: 'lazy', onerror: function () { this.src = '/output/collage-' + s.mural + '.png'; } }));
+ const b = el('div', { className: 'b' });
+ b.appendChild(el('div', { className: 't' }, s.mural_title || s.mural));
+ const pl = s.placement ? `${s.placement.from_left_ft} ft from left · ${s.placement.off_floor_ft} ft off floor` : '';
+ b.appendChild(el('div', { className: 'm' }, `${s.wall_w || '?'}×${s.wall_h || '?'} ft wall · ${pl}`));
+ const row = el('div', { className: 'row' });
+ const load = el('button', { className: 'load' }, 'Load'); load.onclick = () => loadSave(s);
+ const del = el('button', {}, 'Delete'); del.onclick = async () => { await api('/api/saves?id=' + s.id, { method: 'DELETE' }); openSaves(); };
+ row.appendChild(load); row.appendChild(del); b.appendChild(row); card.appendChild(b); draw.appendChild(card);
+ });
+ }
+
+ function loadSave(s) {
+ try {
+ const code = (typeof CAT !== 'undefined' && CAT) ? (CAT.murals.find(m => m.slug === s.mural) || {}).code : null;
+ if (code && $('#selMural')) $('#selMural').value = code;
+ if (s.wall_w && $('#wallW')) $('#wallW').value = s.wall_w;
+ if (s.wall_h && $('#wallH')) $('#wallH').value = s.wall_h;
+ if (typeof selectMural === 'function' && code) selectMural(code);
+ if (typeof pos !== 'undefined' && s.placement) { pos.x = s.placement.from_left_ft; pos.y = s.placement.off_floor_ft; }
+ if (typeof drawScene === 'function') drawScene();
+ draw.classList.remove('open');
+ const st = $('#studio'); st && st.scrollIntoView({ behavior: 'smooth' });
+ flash('Loaded “' + (s.mural_title || s.mural) + '”');
+ } catch (e) { flash('Could not load that placement'); }
+ }
+
+ function refreshBar() {
+ if (me) { acctPill.classList.add('dark'); acctPill.textContent = '👤 ' + me.email.split('@')[0]; }
+ else { acctPill.classList.remove('dark'); acctPill.textContent = 'Sign in'; }
+ }
+
+ // ---- init ----
+ api('/api/auth/me').then(r => { me = (r && r.ok) ? r.user : null; refreshBar(); });
+ const tryMount = () => { mountSaveBtn(); if (!$('#acctSaveBtn')) setTimeout(tryMount, 400); };
+ if (document.readyState !== 'loading') tryMount(); else document.addEventListener('DOMContentLoaded', tryMount);
+})();
diff --git a/server.js b/server.js
index d3d6f97..d98b54b 100644
--- a/server.js
+++ b/server.js
@@ -1,12 +1,16 @@
#!/usr/bin/env node
-// Zero-dependency static server for the CelebritySignatures grid.
+// Zero-dependency server for the CelebritySignatures grid + murals storefront.
+// Adds lightweight email/password accounts (scrypt + cookie sessions, JSON-backed)
+// so a visitor can SAVE a mural placement they set in the wall studio.
import { createServer } from 'node:http';
import { readFile, writeFile } from 'node:fs/promises';
import { extname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
+import { scryptSync, randomBytes, timingSafeEqual, createHash } from 'node:crypto';
const ROOT = fileURLToPath(new URL('.', import.meta.url));
const PORT = process.env.PORT || 9920;
+const DATA = join(ROOT, 'data');
const TYPES = {
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
'.json': 'application/json', '.svg': 'image/svg+xml',
@@ -14,42 +18,154 @@ const TYPES = {
'.webp': 'image/webp', '.gif': 'image/gif', '.ico': 'image/x-icon',
};
-const ORDERS = join(ROOT, 'data', 'mural-orders.json');
+// ---- tiny JSON store --------------------------------------------------------
+async function load(name, fallback) { try { return JSON.parse(await readFile(join(DATA, name), 'utf8')); } catch { return fallback; } }
+async function store(name, val) { await writeFile(join(DATA, name), JSON.stringify(val, null, 2)); }
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let raw = '';
+ req.on('data', c => { raw += c; if (raw.length > 2e5) reject(new Error('too large')); });
+ req.on('end', () => { try { resolve(raw ? JSON.parse(raw) : {}); } catch { reject(new Error('bad json')); } });
+ req.on('error', reject);
+ });
+}
+function sendJSON(res, code, obj, headers = {}) { res.writeHead(code, { 'Content-Type': 'application/json', ...headers }); res.end(JSON.stringify(obj)); }
+
+// ---- auth helpers -----------------------------------------------------------
+const COOKIE = 'cs_sess';
+function hashPw(pw, salt) { return scryptSync(pw, salt, 64).toString('hex'); }
+function verifyPw(pw, salt, hash) {
+ const a = Buffer.from(hashPw(pw, salt), 'hex'), b = Buffer.from(hash, 'hex');
+ return a.length === b.length && timingSafeEqual(a, b);
+}
+function parseCookies(req) {
+ const out = {}; (req.headers.cookie || '').split(';').forEach(p => { const i = p.indexOf('='); if (i > 0) out[p.slice(0, i).trim()] = decodeURIComponent(p.slice(i + 1).trim()); });
+ return out;
+}
+async function currentUser(req) {
+ const sessions = await load('sessions.json', {});
+ const tok = parseCookies(req)[COOKIE];
+ const s = tok && sessions[tok];
+ if (!s) return null;
+ const users = await load('users.json', []);
+ return users.find(u => u.id === s.userId) || null;
+}
+async function newSession(userId) {
+ const sessions = await load('sessions.json', {});
+ const tok = randomBytes(24).toString('hex');
+ sessions[tok] = { userId, at: new Date().toISOString() };
+ await store('sessions.json', sessions);
+ return tok;
+}
+const setCookie = tok => `${COOKIE}=${tok}; HttpOnly; Path=/; SameSite=Lax; Max-Age=2592000`;
+const clearCookie = `${COOKIE}=; HttpOnly; Path=/; Max-Age=0`;
+const pubUser = u => ({ email: u.email, name: u.name || '' });
createServer(async (req, res) => {
try {
- let path = decodeURIComponent(req.url.split('?')[0]);
-
- // POST: capture a made-to-order mural request → append to data/mural-orders.json
- if (req.method === 'POST' && path === '/api/mural-order') {
- let raw = '';
- for await (const c of req) { raw += c; if (raw.length > 1e5) { res.writeHead(413).end('too large'); return; } }
- let body; try { body = JSON.parse(raw || '{}'); } catch { res.writeHead(400, { 'Content-Type': 'application/json' }).end('{"ok":false,"error":"bad json"}'); return; }
- if (!body.name || !body.email) { res.writeHead(400, { 'Content-Type': 'application/json' }).end('{"ok":false,"error":"name and email required"}'); return; }
- let list = []; try { list = JSON.parse(await readFile(ORDERS, 'utf8')); } catch {}
+ const url = new URL(req.url, 'http://x');
+ let path = decodeURIComponent(url.pathname);
+ const M = req.method;
+
+ // ===== AUTH =====
+ if (path === '/api/auth/signup' && M === 'POST') {
+ const b = await readBody(req);
+ const email = String(b.email || '').trim().toLowerCase();
+ const pw = String(b.password || '');
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return sendJSON(res, 400, { ok: false, error: 'invalid email' });
+ if (pw.length < 6) return sendJSON(res, 400, { ok: false, error: 'password must be at least 6 characters' });
+ const users = await load('users.json', []);
+ if (users.some(u => u.email === email)) return sendJSON(res, 409, { ok: false, error: 'account already exists — sign in instead' });
+ const salt = randomBytes(16).toString('hex');
+ const user = { id: createHash('sha256').update(email + Date.now()).digest('hex').slice(0, 16), email, name: String(b.name || '').slice(0, 80), salt, hash: hashPw(pw, salt), at: new Date().toISOString() };
+ users.push(user); await store('users.json', users);
+ const tok = await newSession(user.id);
+ return sendJSON(res, 200, { ok: true, user: pubUser(user) }, { 'Set-Cookie': setCookie(tok) });
+ }
+ if (path === '/api/auth/login' && M === 'POST') {
+ const b = await readBody(req);
+ const email = String(b.email || '').trim().toLowerCase();
+ const users = await load('users.json', []);
+ const u = users.find(x => x.email === email);
+ if (!u || !verifyPw(String(b.password || ''), u.salt, u.hash)) return sendJSON(res, 401, { ok: false, error: 'wrong email or password' });
+ const tok = await newSession(u.id);
+ return sendJSON(res, 200, { ok: true, user: pubUser(u) }, { 'Set-Cookie': setCookie(tok) });
+ }
+ if (path === '/api/auth/logout' && M === 'POST') {
+ const sessions = await load('sessions.json', {});
+ const tok = parseCookies(req)[COOKIE];
+ if (tok) { delete sessions[tok]; await store('sessions.json', sessions); }
+ return sendJSON(res, 200, { ok: true }, { 'Set-Cookie': clearCookie });
+ }
+ if (path === '/api/auth/me' && M === 'GET') {
+ const u = await currentUser(req);
+ return sendJSON(res, 200, u ? { ok: true, user: pubUser(u) } : { ok: true, user: null });
+ }
+
+ // ===== SAVED PLACEMENTS (per account) =====
+ if (path === '/api/saves') {
+ const u = await currentUser(req);
+ if (!u) return sendJSON(res, 401, { ok: false, error: 'sign in to save' });
+ const all = await load('saves.json', []);
+ if (M === 'GET') return sendJSON(res, 200, { ok: true, saves: all.filter(s => s.userId === u.id).sort((a, b) => b.at.localeCompare(a.at)) });
+ if (M === 'POST') {
+ const b = await readBody(req);
+ if (!b.mural) return sendJSON(res, 400, { ok: false, error: 'mural required' });
+ const id = randomBytes(8).toString('hex');
+ const rec = { id, userId: u.id, at: new Date().toISOString(),
+ mural: String(b.mural).slice(0, 60), mural_title: String(b.mural_title || '').slice(0, 120),
+ wall_w: +b.wall_w || null, wall_h: +b.wall_h || null,
+ placement: b.placement && typeof b.placement === 'object' ? { from_left_ft: +b.placement.from_left_ft || 0, off_floor_ft: +b.placement.off_floor_ft || 0 } : null,
+ scene: String(b.scene || '').slice(0, 40), label: String(b.label || '').slice(0, 80) };
+ all.push(rec); await store('saves.json', all);
+ return sendJSON(res, 200, { ok: true, save: rec });
+ }
+ if (M === 'DELETE') {
+ const id = url.searchParams.get('id');
+ const kept = all.filter(s => !(s.id === id && s.userId === u.id));
+ await store('saves.json', kept);
+ return sendJSON(res, 200, { ok: true, removed: all.length - kept.length });
+ }
+ }
+
+ // ===== made-to-order request (existing) =====
+ if (path === '/api/mural-order' && M === 'POST') {
+ const b = await readBody(req);
+ if (!b.name || !b.email) return sendJSON(res, 400, { ok: false, error: 'name and email required' });
+ const u = await currentUser(req);
+ const list = await load('mural-orders.json', []);
const id = (list.at(-1)?.id || 1000) + 1;
- list.push({ id, at: new Date().toISOString(), ...body });
- await writeFile(ORDERS, JSON.stringify(list, null, 2));
- res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: true, id }));
- return;
+ list.push({ id, at: new Date().toISOString(), account: u ? u.email : null, ...b });
+ await store('mural-orders.json', list);
+ return sendJSON(res, 200, { ok: true, id });
}
+ // ===== static + page routes =====
if (path === '/') path = '/public/index.html';
if (path === '/murals') path = '/public/murals.html';
+ if (path === '/account.js') path = '/public/account.js';
if (path === '/api/signatures') path = '/data/celebrity_signatures.json';
if (path === '/api/murals-catalog') path = '/data/murals-catalog.json';
- // Short QR target: /k/<code> -> the named "key" mural. Single-letter codes keep
- // the encoded URL tiny so the centre QR stays low-density and scans cleanly off a
- // printed wall. Full-slug (/k/<cat>) still works as a fallback.
const SHORT = { d: 'declaration-of-independence', p: 'politics', s: 'sports', h: 'hollywood', c: 'movies-classic', t: 'tv' };
- const k = path.match(/^\/k\/([a-z0-9-]+)$/);
- if (k) path = `/output/collage-${SHORT[k[1]] || k[1]}-named.png`;
+ const km = path.match(/^\/k\/([a-z0-9-]+)$/);
+ if (km) path = `/output/collage-${SHORT[km[1]] || km[1]}-named.png`;
+
const file = join(ROOT, path);
if (!file.startsWith(ROOT)) { res.writeHead(403).end('forbidden'); return; }
+
+ // Inject the account/save widget into the murals page WITHOUT editing murals.html.
+ if (path === '/public/murals.html') {
+ let html = await readFile(file, 'utf8');
+ html = html.replace('</body>', '<script src="/account.js"></script>\n</body>');
+ res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
+ res.end(html); return;
+ }
+
const body = await readFile(file);
res.writeHead(200, { 'Content-Type': TYPES[extname(file)] || 'application/octet-stream', 'Cache-Control': 'no-cache' });
res.end(body);
- } catch {
+ } catch (e) {
+ if (e.message === 'bad json' || e.message === 'too large') { sendJSON(res, 400, { ok: false, error: e.message }); return; }
res.writeHead(404, { 'Content-Type': 'text/plain' }).end('not found');
}
-}).listen(PORT, () => console.log(`CelebritySignatures grid → http://localhost:${PORT}`));
+}).listen(PORT, () => console.log(`CelebritySignatures → http://localhost:${PORT}`));
← a27e4a9 data: recategorize FDR Sports→Politics + rebuild affected co
·
back to CelebritySignatures
·
snapshot before removing alive celebrities 3c6f492 →