← 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); });