[object Object]

← back to Naturaltextilewallpaper

stage2: wire naturaltextilewallpaper onto shared catalog (hand-tailored)

393ab0e1fd60ea5536f7355a0060af0db4ea65c8 · 2026-05-19 11:07:43 -0700 · Steve Abrams

Files touched

Diff

commit 393ab0e1fd60ea5536f7355a0060af0db4ea65c8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 11:07:43 2026 -0700

    stage2: wire naturaltextilewallpaper onto shared catalog (hand-tailored)
---
 server.js        | 75 +++++++++++++++++++++++++++++++++++++++++++++++---------
 site.config.json | 15 ++++++++++++
 2 files changed, 78 insertions(+), 12 deletions(-)

diff --git a/server.js b/server.js
index 93bdf16..ee4eb0e 100644
--- a/server.js
+++ b/server.js
@@ -5,10 +5,12 @@
  * Design: Loro Piana heirloom mill aesthetic — warm cream/oatmeal/charcoal, Futura-feel sans.
  * Port: 9835
  */
+try { require('dotenv').config({ path: require('path').join(__dirname, '.env') }); } catch (e) {}
 const express = require('express');
 const helmet = require('helmet');
 const path = require('path');
 const fs = require('fs');
+const catalog = require('../_shared/admin-catalog');
 
 const PORT = process.env.PORT || 9835;
 const DW = 'https://designerwallcoverings.com';
@@ -17,15 +19,13 @@ const SITE_DOMAIN = 'naturaltextilewallpaper.com';
 const SITE_EMAIL = 'info@naturaltextilewallpaper.com';
 const GA_ID = process.env.GA_ID || '';
 
-// --- Data load ----------------------------------------------------------
-let DATA_RAW;
+// --- Site config (slug + rails for the shared catalog module) -----------
+let siteCfg = {};
 try {
-  DATA_RAW = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
-  if (!Array.isArray(DATA_RAW)) throw new Error('products.json must be an array');
-} catch(e) {
-  console.error(`[NTW] FATAL: products.json load failed — ${e.message}`);
-  DATA_RAW = [];
-}
+  siteCfg = JSON.parse(fs.readFileSync(path.join(__dirname, 'site.config.json'), 'utf8'));
+} catch (e) { console.error(`[NTW] site.config.json read failed — ${e.message}`); }
+const SITE_SLUG  = siteCfg.slug || path.basename(__dirname);
+const SITE_RAILS = Array.isArray(siteCfg.rails) ? siteCfg.rails : [];
 
 // --- Junk filter --------------------------------------------------------
 function isJunk(p) {
@@ -40,9 +40,6 @@ function isJunk(p) {
   return false;
 }
 
-const PRODUCTS = DATA_RAW.filter(p => !isJunk(p));
-console.log(`[NTW] Loaded ${DATA_RAW.length} → kept ${PRODUCTS.length} after junk filter`);
-
 // --- Material labels (the 7 filter pills) --------------------------------
 const MATERIALS = [
   { key: 'linen',      label: 'Linen' },
@@ -56,6 +53,42 @@ const MATERIALS = [
   { key: 'natural',    label: 'Natural' },
 ];
 
+// --- Material derivation ------------------------------------------------
+// The dw_unified.microsite_products read-path does NOT carry a `material`
+// column (nor `min_price`/`images`). Derive `material` from tags/title/type,
+// and backfill `min_price`/`images` so the rest of this server is untouched.
+const MAT_KEYS = MATERIALS.map(m => m.key);
+function deriveMaterial(p) {
+  if (p.material && MAT_KEYS.includes(p.material)) return p.material;
+  const blob = ((p.title || '') + ' ' + (p.product_type || '') + ' ' +
+    (Array.isArray(p.tags) ? p.tags.join(' ') : '')).toLowerCase();
+  // most specific first; 'woven'/'natural' are catch-alls
+  for (const k of ['grasscloth','seagrass','linen','jute','raffia','sisal','hemp','bamboo','abaca']) {
+    if (blob.includes(k)) {
+      if (k === 'seagrass') return 'grasscloth';
+      if (k === 'abaca')    return 'jute';
+      return k;
+    }
+  }
+  if (/\bwoven\b/.test(blob)) return 'woven';
+  return 'natural';
+}
+function normalizeProduct(p) {
+  const tags = Array.isArray(p.tags) ? p.tags
+    : (typeof p.tags === 'string' ? p.tags.split(',').map(t => t.trim()).filter(Boolean) : []);
+  const out = { ...p, tags };
+  out.material = deriveMaterial(out);
+  if (out.max_price == null && p.price != null) out.max_price = Number(p.price);
+  if (out.min_price == null) out.min_price = out.max_price != null ? Number(out.max_price) : 0;
+  if (!Array.isArray(out.images) || !out.images.length) {
+    out.images = out.image_url ? [out.image_url] : [];
+  }
+  return out;
+}
+
+// PRODUCTS is loaded async at startup from dw_unified (JSON fallback if PG down).
+let PRODUCTS = [];
+
 // --- Sort ---------------------------------------------------------------
 const COLOR_RANK = {
   black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
@@ -545,4 +578,22 @@ function escHtml(s) {
   return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
 }
 
-app.listen(PORT, () => console.log(`[NTW] Running on port ${PORT} — ${PRODUCTS.length} products`));
+// --- Admin catalog CRUD — /admin/catalog (basic-auth) + /api/admin/* -----
+catalog.mount(app, { siteSlug: SITE_SLUG, rails: SITE_RAILS });
+
+// --- Async startup: load catalog from dw_unified, then listen -----------
+(async () => {
+  try {
+    const rows = await catalog.getProducts(SITE_SLUG);
+    PRODUCTS = rows.map(normalizeProduct).filter(p => !isJunk(p));
+    console.log(`[NTW] Loaded ${rows.length} from dw_unified → kept ${PRODUCTS.length} after junk filter`);
+  } catch (e) {
+    console.error(`[NTW] PG catalog load failed (${e.message}) — falling back to data/products.json`);
+    try {
+      const raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
+      PRODUCTS = (Array.isArray(raw) ? raw : []).map(normalizeProduct).filter(p => !isJunk(p));
+      console.log(`[NTW] Fallback: loaded ${PRODUCTS.length} from data/products.json`);
+    } catch (_) { PRODUCTS = []; }
+  }
+  app.listen(PORT, () => console.log(`[NTW] Running on port ${PORT} — ${PRODUCTS.length} products`));
+})();
diff --git a/site.config.json b/site.config.json
new file mode 100644
index 0000000..8a4b35f
--- /dev/null
+++ b/site.config.json
@@ -0,0 +1,15 @@
+{
+  "slug": "naturaltextilewallpaper",
+  "siteName": "Naturaltextilewallpaper",
+  "domain": "naturaltextilewallpaper.com",
+  "heroHeadline": "NATURALTEXTILEWALLPAPER",
+  "rails": [
+    "commercial",
+    "vinyl",
+    "textured",
+    "architectural",
+    "fire",
+    "rated"
+  ],
+  "_generated": true
+}

← d25f742 Untrack server.js.pre-sort-skill snapshot; broaden .gitignor  ·  back to Naturaltextilewallpaper  ·  Phase 2: mobile-ready + tap-to-reveal 9fe6fb9 →