[object Object]

← back to Ffepurchasing

stage2: wire microsite catalog + admin CRUD

0de1c6c1d7d5d282ae251e1bdfbf32ee8c7d4446 · 2026-05-19 10:17:26 -0700 · Steve Abrams

Files touched

Diff

commit 0de1c6c1d7d5d282ae251e1bdfbf32ee8c7d4446
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 10:17:26 2026 -0700

    stage2: wire microsite catalog + admin CRUD
---
 server.js | 65 ++++++++++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 41 insertions(+), 24 deletions(-)

diff --git a/server.js b/server.js
index 2aed233..1abed6d 100644
--- a/server.js
+++ b/server.js
@@ -10,16 +10,10 @@ const fs = require('fs');
 const PORT = process.env.PORT || 9883;
 const DW_SHOPIFY = 'https://designerwallcoverings.com';
 const __SITE = path.basename(__dirname);
-let DATA_RAW;
-try {
-  const raw = fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8');
-  DATA_RAW = JSON.parse(raw);
-  if (!Array.isArray(DATA_RAW)) throw new Error('products.json must be an array');
-} catch (e) {
-  console.error(`[${__SITE}] FATAL: could not load products.json — ${e.message}`);
-  console.error(`[${__SITE}] Starting with empty catalog. Run pull script to populate data/products.json.`);
-  DATA_RAW = [];
-}
+const catalog = require('../_shared/admin-catalog');
+const siteCfg = JSON.parse(fs.readFileSync(path.join(__dirname, 'site.config.json'), 'utf8'));
+const SITE_SLUG = siteCfg.slug || __SITE;
+const SITE_RAILS = Array.isArray(siteCfg.rails) ? siteCfg.rails : [];
 
 function isJunk(p) {
   if (!p.image_url || !p.image_url.trim()) return true;
@@ -32,9 +26,10 @@ function isJunk(p) {
   return false;
 }
 
-const PRODUCTS = DATA_RAW.filter(p => !isJunk(p));
-const DROPPED = DATA_RAW.length - PRODUCTS.length;
-console.log(`Loaded ${DATA_RAW.length}, kept ${PRODUCTS.length}, dropped ${DROPPED}`);
+// Catalog now lives in dw_unified.microsite_products — populated async at
+// startup (see bottom of file). Falls back to data/products.json if PG is down.
+let PRODUCTS = [];
+let DROPPED = 0;
 
 // graphics-loop pass 12: niche keyword filter — only show products that fit this site's niche
 const NICHE_POS = ["contract","commercial","hospitality","hotel","restaurant","class a","type-ii","trade","wholesale","specifier"];
@@ -44,12 +39,8 @@ function nicheFit(p) {
   if (NICHE_NEG.some(n => blob.includes(n.toLowerCase()))) return false;
   return NICHE_POS.length === 0 || NICHE_POS.some(k => blob.includes(k.toLowerCase()));
 }
-const PRODUCTS_NICHE = PRODUCTS.filter(nicheFit);
-const NICHE_DROPPED = PRODUCTS.length - PRODUCTS_NICHE.length;
-console.log(`Niche filter: kept ${PRODUCTS_NICHE.length} of ${PRODUCTS.length}, dropped ${NICHE_DROPPED}`);
-// Replace PRODUCTS reference with the niche-filtered list for all downstream handlers.
-const PRODUCTS_ORIG = PRODUCTS;
-Object.defineProperty(global, '__products_niche_applied', { value: true });
+// Niche-filtered list, populated at startup alongside PRODUCTS.
+let PRODUCTS_NICHE = [];
 
 
 const app = express();
@@ -81,6 +72,13 @@ function sortProducts(list, mode) {
   if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
   if (mode === 'price-asc')  return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
   if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
+  // List-view sortable columns — every column sortable in both directions.
+  if (mode === 'title-desc')     return [...list].sort((a,b) => String(b.title || '').localeCompare(String(a.title || '')));
+  if (mode === 'sku-desc')       return [...list].sort((a,b) => String(b.sku || b.handle || '').localeCompare(String(a.sku || a.handle || '')));
+  if (mode === 'vendor')         return [...list].sort((a,b) => String(a.vendor || '').localeCompare(String(b.vendor || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+  if (mode === 'vendor-desc')    return [...list].sort((a,b) => String(b.vendor || '').localeCompare(String(a.vendor || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+  if (mode === 'aesthetic')      return [...list].sort((a,b) => String(a.aesthetic || '').localeCompare(String(b.aesthetic || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+  if (mode === 'aesthetic-desc') return [...list].sort((a,b) => String(b.aesthetic || '').localeCompare(String(a.aesthetic || '')) || String(a.title || '').localeCompare(String(b.title || '')));
   return list;
 }
 
@@ -112,9 +110,8 @@ app.get('/api/products', (req, res) => {
 });
 
 app.get('/api/sliders', (req, res) => {
-  const SLIDER_AESTHETICS = ["mural","luxury","botanical","chinoiserie","deco","natural"];
   const out = [];
-  for (const a of SLIDER_AESTHETICS) {
+  for (const a of SITE_RAILS) {
     const items = PRODUCTS_NICHE.filter(p => p.aesthetic === a).slice(0, 12);
     if (items.length >= 4) out.push({ aesthetic: a, items });
   }
@@ -182,6 +179,26 @@ ${urls.map(u => `  <url><loc>https://ffepurchasing.com${u}</loc><changefreq>week
   res.type('application/xml').send(xml);
 });
 
-app.listen(PORT, '127.0.0.1', () => {
-  console.log(`ffepurchasing listening on http://127.0.0.1:${PORT}`);
-});
+// Admin catalog CRUD — /admin/catalog (basic-auth) + /api/admin/* REST.
+catalog.mount(app, { siteSlug: SITE_SLUG, rails: SITE_RAILS });
+
+// Load the catalog from dw_unified, then start listening. PG down → JSON fallback.
+(async () => {
+  try {
+    const rows = await catalog.getProducts(SITE_SLUG);
+    PRODUCTS = rows.filter(p => !isJunk(p));
+    DROPPED = rows.length - PRODUCTS.length;
+    console.log(`[${__SITE}] loaded ${rows.length} from dw_unified, kept ${PRODUCTS.length}, dropped ${DROPPED}`);
+  } catch (e) {
+    console.error(`[${__SITE}] 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 : []).filter(p => !isJunk(p));
+    } catch (_) { PRODUCTS = []; }
+  }
+  PRODUCTS_NICHE = PRODUCTS.filter(nicheFit);
+  console.log(`[${__SITE}] niche filter: kept ${PRODUCTS_NICHE.length} of ${PRODUCTS.length}`);
+  app.listen(PORT, '127.0.0.1', () => {
+    console.log(`ffepurchasing listening on http://127.0.0.1:${PORT}`);
+  });
+})();

← 72acf4a hero-4grid: relocate json from data/ to public/ for Express  ·  back to Ffepurchasing  ·  fix site.config.json — correct slug/identity (was copy-paste e92af14 →