← back to Atomic50 Onboard

server.js

112 lines

/* Atomic 50 — a Designer Wallcoverings editorial lookbook.
   PUBLIC, QUOTE-ONLY line: pressed-tin ceiling/wall-panel tiles + moldings.
   Data = data/products.json (self-contained snapshot of dw_unified.atomic50_catalog).
   NO prices anywhere. Every product CTA = "Request a Quote" + "Order a Sample ($4.25)".
   Designer Wallcoverings is the HOUSE BRAND; Atomic 50 is the featured collection. */
const express = require('express');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT || 9964;
const DATA = path.join(__dirname, 'data', 'products.json');

const app = express();
app.use(require('compression')());
app.use(express.json({ limit: '1mb' }));

// unauthenticated health probe (deploy smoke test + uptime monitors)
app.get('/healthz', (_req, res) => res.json({ ok: true, products: SNAP.products.length }));

let SNAP = { products: [], facets: {} };
let LIGHT = [];
function load() {
  SNAP = JSON.parse(fs.readFileSync(DATA, 'utf8'));
  // grid stays light — drop body_html + the full 40+ image array; KEEP the
  // capped `swatches` set (≤12) so cards can hover-cycle finishes. PDP fetches full.
  LIGHT = SNAP.products.map(({ body_html, images, ...p }) => p);
  console.log(`[atomic50] loaded ${SNAP.products.length} products`);
}
load();
fs.watchFile(DATA, { interval: 5000 }, (cur, prev) => {
  if (cur.mtimeMs !== prev.mtimeMs) { try { load(); } catch (e) { console.error('[atomic50] reload failed:', e.message); } }
});

// House identity — Designer Wallcoverings is the brand; Atomic 50 is the line.
const CONFIG = {
  house: 'Designer Wallcoverings',
  houseUrl: 'https://www.designerwallcoverings.com',
  nav: [
    { label: 'The Collection', href: '#collection' },
    { label: 'Designer Wallcoverings', href: 'https://www.designerwallcoverings.com' },
  ],
  vendor: 'Atomic 50',
  line: 'Atomic 50',
  wordmark: 'Designer Wallcoverings',
  eyebrow: 'A Designer Wallcoverings Collection',
  kicker: 'Pressed Tin · Made in the USA · ASTM E84 Class A',
  tagline: 'Pressed-tin ceiling tiles, wall panels & moldings — curated by Designer Wallcoverings.',
  title: 'Atomic 50 — Pressed-Tin Ceilings · A Designer Wallcoverings Collection',
  metaDescription: 'The Atomic 50 pressed-tin ceiling & wall-panel collection at Designer Wallcoverings. American-made T1 tin-plated steel, ASTM E84 Class A. To the trade — request a quote or order a sample.',
  slug: 'atomic50',
  quoteOnly: true,
  samplePrice: 4.25,
  palette: { bg: '#efece6', paper: '#fbf9f4', ink: '#20211d', accent: '#7a6a4c', gold: '#9a7c3f', line: '#ddd6c9', taupe: '#a89e8a' }, // aged pewter / raw tin
  about: {
    paragraphs: [
      'Atomic 50 presses genuine American tin ceilings the old way — pattern by pattern, stamped from T1-grade tin-plated steel with 25% recycled content. The Victorian medallions, Art Deco geometries, and ornate floral reliefs in this collection are the same motifs that crowned turn-of-the-century storefronts and parlors, made now for ceilings, accent walls, wainscots, and backsplashes.',
      'Each tile ships in raw, unfinished tin — a soft, light-catching metal that patinas beautifully — or powder-coated to any of 40+ colors. Every panel is ASTM E84 Class A rated for commercial and residential specification alike.',
    ],
    collab: 'The Atomic 50 collection, presented in collaboration with Designer Wallcoverings.',
  },
};

app.get('/api/config', (_req, res) => res.json(CONFIG));
app.get('/api/products', (_req, res) => res.json({ count: LIGHT.length, products: LIGHT }));
app.get('/api/facets', (_req, res) => res.json(SNAP.facets));
app.get('/api/product/:handle', (req, res) => {
  const p = SNAP.products.find((x) => x.handle === req.params.handle);
  if (!p) return res.status(404).json({ error: 'not found' });
  res.json(p);
});

// "Pairs well with" — same/adjacent hue, different pattern. Up to 6.
app.get('/api/pairs/:handle', (req, res) => {
  const p = SNAP.products.find((x) => x.handle === req.params.handle);
  if (!p) return res.status(404).json({ error: 'not found' });
  const hueDist = (a, b) => { if (a == null || b == null) return 180; const d = Math.abs(a - b) % 360; return d > 180 ? 360 - d : d; };
  const scored = SNAP.products.filter((x) => x.handle !== p.handle && x.pattern !== p.pattern).map((x) => {
    let s = 0;
    if (x.color_bucket && x.color_bucket === p.color_bucket) s += 40;
    s += Math.max(0, 30 - hueDist(x.hue, p.hue) / 2);
    if (x.product_type === p.product_type) s += 10;
    return { x, s };
  }).sort((a, b) => b.s - a.s);
  const seen = new Set(), out = [];
  for (const o of scored) { if (seen.has(o.x.pattern)) continue; seen.add(o.x.pattern); out.push(o.x); if (out.length === 6) break; }
  res.json({ pairs: out });
});

// Quote + Sample requests — logged to data/requests.jsonl (purchasing picks up).
// NO prices; a sample is the $4.25 memo. Never emails a client name to the vendor.
const REQ_MSG = {
  quote: 'Quote request logged — Designer Wallcoverings will follow up with pricing and lead time.',
  sample: 'Sample request logged — your $4.25 memo will be prepared and shipped.',
};
app.post('/api/request', (req, res) => {
  const { sku, name, email, note } = req.body || {};
  const type = ['quote', 'sample'].includes(req.body && req.body.type) ? req.body.type : 'quote';
  if (!sku || !email) return res.status(400).json({ ok: false, error: 'sku and email required' });
  const rec = { at: new Date().toISOString(), type, sku, name: name || '', email, note: note || '', ip: req.ip };
  try {
    fs.appendFileSync(path.join(__dirname, 'data', 'requests.jsonl'), JSON.stringify(rec) + '\n');
  } catch (e) { return res.status(500).json({ ok: false, error: 'log failed' }); }
  res.json({ ok: true, message: REQ_MSG[type] });
});

app.use(express.static(path.join(__dirname, 'public')));
app.get('/product/:handle', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'product.html')));

const server = app.listen(PORT, () => {
  console.log(`Atomic 50 landing → http://127.0.0.1:${server.address().port}`);
});