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