← back to Claude Webdev Accelerator
templates/server.js
42 lines
// Storefront starter — zero-dep Node http. Serves data/products.json with SERVER-SIDE sort
// (house rule: every product grid gets sort + a density slider). Optional basic auth via BASIC_AUTH.
const http = require('http'), fs = require('fs'), path = require('path');
const PORT = parseInt(process.env.PORT || '3900', 10);
const BASIC_AUTH = process.env.BASIC_AUTH || ''; // "user:pass" to gate; empty = open
const DIR = __dirname;
function load() { try { return JSON.parse(fs.readFileSync(path.join(DIR, 'data', 'products.json'), 'utf8')); } catch { return []; } }
function authed(req) {
if (!BASIC_AUTH) return true;
const m = (req.headers.authorization || '').match(/^Basic\s+(.+)$/i);
if (!m) return false; try { return Buffer.from(m[1], 'base64').toString() === BASIC_AUTH; } catch { return false; }
}
// Sort modes mirror the DW sort-skill: newest, title/sku A→Z, price ↑↓, light→dark (by dominant hex).
function luminance(hex) { if (!/^#?[0-9a-f]{6}$/i.test(hex || '')) return 999; const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
return 0.2126 * r + 0.7152 * g + 0.0722 * b; }
function sortProducts(items, mode) {
const a = items.slice();
switch (mode) {
case 'title': return a.sort((x, y) => (x.title || '').localeCompare(y.title || ''));
case 'sku': return a.sort((x, y) => (x.sku || x.handle || '').localeCompare(y.sku || y.handle || ''));
case 'price-asc': return a.sort((x, y) => (x.price || 0) - (y.price || 0));
case 'price-desc': return a.sort((x, y) => (y.price || 0) - (x.price || 0));
case 'light': return a.sort((x, y) => luminance(y.hex) - luminance(x.hex));
case 'dark': return a.sort((x, y) => luminance(x.hex) - luminance(y.hex));
default: return a; // 'newest' = natural order
}
}
http.createServer((req, res) => {
if (!authed(req)) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="preview"' }); return res.end('auth required'); }
const u = new URL(req.url, 'http://x');
if (u.pathname === '/api/products') {
const out = sortProducts(load(), u.searchParams.get('sort') || 'newest');
res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ count: out.length, products: out }));
}
const f = u.pathname === '/' ? 'index.html' : u.pathname.replace(/^\//, '');
const fp = path.join(DIR, 'public', path.basename(f));
if (fs.existsSync(fp)) { res.writeHead(200, { 'Content-Type': f.endsWith('.html') ? 'text/html' : 'text/plain' }); return res.end(fs.readFileSync(fp)); }
res.writeHead(404); res.end('not found');
}).listen(PORT, function () { console.log('[' + path.basename(DIR) + '] http://localhost:' + this.address().port); });