← back to Reidwitlin Landing

server.js

123 lines

/* Reid Witlin editorial vendor-landing — Designer Wallcoverings house brand.
   INTERNAL DATA ONLY: catalog is dw_unified.rwltd_catalog (NOT on Shopify).
   Self-contained: reads data/products.json. Links resolve to this site's OWN
   internal PDPs and a memo-sample/inquiry CTA — never Shopify, never rwltd.com. */
try { require('dotenv').config(); } catch { /* dotenv optional; pm2 env block also sets these */ }
const express = require('express');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT || 0; // 0 = OS-assigned free port
const DATA      = path.join(__dirname, 'data', 'products.json');
const INQUIRIES = path.join(__dirname, 'data', 'inquiries.jsonl');

const app = express();

// HTTP Basic Auth — internal-only gate (username + password). Override via
// BASIC_AUTH="user:pass"; defaults to the DW standard admin / DW2024! (same as astek).
const [AUTH_USER, AUTH_PASS] = (process.env.BASIC_AUTH || 'admin:DW2024!').split(':');
app.use((req, res, next) => {
  const hdr = req.headers.authorization || '';
  const [scheme, b64] = hdr.split(' ');
  if (scheme === 'Basic' && b64) {
    const [u, p] = Buffer.from(b64, 'base64').toString('utf8').split(':');
    if (u === AUTH_USER && p === AUTH_PASS) return next();
  }
  res.set('WWW-Authenticate', 'Basic realm="Reid Witlin - Designer Wallcoverings (internal)"');
  return res.status(401).send('Authentication required.');
});

app.use(express.json({ limit: '256kb' }));

let SNAP = { products: [], facets: { total: 0, books: [], series: [], colors: [] } };
function load() {
  SNAP = JSON.parse(fs.readFileSync(DATA, 'utf8'));
  console.log(`[reidwitlin] loaded ${SNAP.products.length} products`);
}
load();

// House identity — Designer Wallcoverings is the brand; Reid Witlin is the featured private-label line.
const CONFIG = {
  house: 'Designer Wallcoverings',
  houseUrl: 'https://www.designerwallcoverings.com',
  nav: [
    { label: 'The Collection', href: '#collections' },
    { label: 'Designer Wallcoverings', href: 'https://www.designerwallcoverings.com' },
  ],
  vendor: 'Reid Witlin',
  line: 'Reid Witlin',
  wordmark: 'Designer Wallcoverings',
  eyebrow: 'A Designer Wallcoverings Collection',
  kicker: 'Made-to-Order · To the Trade',
  tagline: 'Artful, made-to-order wallcoverings — the Reid Witlin line, curated by Designer Wallcoverings.',
  booksHeading: 'The Reid Witlin Collection',
  title: 'Reid Witlin — A Designer Wallcoverings Collection',
  metaDescription: 'The Reid Witlin private-label wallcovering collection at Designer Wallcoverings. To the trade — order a memo sample before specifying.',
  slug: 'reidwitlin',
  palette: null,
  about: {
    paragraphs: [
      'Reid Witlin is a Designer Wallcoverings private-label line — an artist’s catalog of painterly, expressive, made-to-order wallcoverings. From tonal textures and modern abstracts to statement patterns, each design is offered across a deep run of colorways and printed to order for interiors that ask to be remembered.',
      'The collection spans hundreds of colorways developed for both residential and hospitality specification, each finished for surface and light.',
    ],
    collab: 'The Reid Witlin collection, a Designer Wallcoverings private label.',
  },
};

// vendorMeta (phone / our account # / discount / pricing model) rides in from
// data/products.json — rebuilt by scripts/build-rwltd-data.js.
app.get('/api/config', (_req, res) => res.json({ ...CONFIG, vendorMeta: SNAP.vendor || null }));
app.get('/api/products', (_req, res) => res.json({ count: SNAP.products.length, products: SNAP.products }));
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);
});

// Pure helper — circular hue distance in degrees (0–180). Null hue → neutral 180.
const hueDist = (a, b) => { if (a == null || b == null) return 180; const d = Math.abs(a - b) % 360; return d > 180 ? 360 - d : d; };

// "Pairs well with" — same/adjacent color family, different pattern (contrast of scale). 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 scored = SNAP.products.filter(x => x.handle !== p.handle && x.series !== p.series).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);
    s += 20; // always a different pattern (filtered above) — reward scale contrast
    if (x.book && x.book === p.book) s += 8;
    return { x, s };
  }).sort((a, b) => b.s - a.s);
  // de-dup by series so pairs aren't 6 colorways of one pattern
  const seen = new Set(), out = [];
  for (const o of scored) { if (seen.has(o.x.series)) continue; seen.add(o.x.series); out.push(o.x); if (out.length === 6) break; }
  res.json({ pairs: out });
});

// Internal purchasing requests — Request Memo / Check Stock / Get Price.
// REQ-numbered in dw_unified.vendor_requests; stock/price auto-email the vendor
// via george-gmail when an email is on file (account # only, never a client name).
const { mountVendorRequests } = require('./lib/vendor-requests');
mountVendorRequests(app, { vendorCode: 'rwltd', getVendor: () => SNAP.vendor || {}, dataDir: path.join(__dirname, 'data') });

// Legacy inquiry endpoint — kept for anything still posting here; logs only.
app.post('/api/inquiry', (req, res) => {
  const { sku, name, email, note } = req.body || {};
  if (!sku || !email) return res.status(400).json({ ok: false, error: 'sku and email required' });
  const rec = { at: new Date().toISOString(), sku, name: name || '', email, note: note || '', ip: req.ip };
  try {
    fs.appendFileSync(INQUIRIES, JSON.stringify(rec) + '\n');
  } catch (e) { return res.status(500).json({ ok: false, error: 'log failed' }); }
  res.json({ ok: true, message: 'Memo-sample request received. A Designer Wallcoverings specialist will follow up.' });
});

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(`Reid Witlin landing → http://127.0.0.1:${server.address().port}`);
});