← back to Dw Domain Fleet

shared/catalog.js

79 lines

/**
 * catalog.js — loads data/catalog.json once, exposes niche-filtered slices.
 */
const fs = require('fs');
const path = require('path');

const RAW = (() => {
  try {
    const d = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'catalog.json'), 'utf8'));
    return Array.isArray(d) ? d : [];
  } catch (e) {
    console.error('[catalog] FATAL: could not load catalog.json —', e.message);
    return [];
  }
})();

// --- junk-product defense (dw-site-build standing rule) ---
const HOUSEHOLD = /lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i;
function isJunk(p) {
  if (!p.image_url || !p.image_url.trim()) return true;
  if (!p.handle) return true;
  if (HOUSEHOLD.test(p.title || '')) return true;
  if (/^\d{13,}$/.test(p.sku || '')) return true;
  if (/display.?variant/i.test((p.tags || []).join(' '))) return true;
  return false;
}

const CLEAN = RAW.filter(p => !isJunk(p));

// --- product-type normalization ---
function normType(t) {
  if (!t) return 'Other';
  const x = String(t).replace(/s$/i, '').trim();
  if (/wallcovering/i.test(x)) return 'Wallcovering';
  if (/wallpaper/i.test(x)) return 'Wallcovering';
  if (/mural/i.test(x)) return 'Mural';
  if (/fabric/i.test(x)) return 'Fabric';
  if (/trim/i.test(x)) return 'Trim';
  return x;
}

/**
 * niche filter — positive keywords (any-match) minus negative keywords (any-match).
 * Matches against title + tags + product_type. If pos is empty → all clean products.
 */
function nicheSlice({ pos = [], neg = [], types = [], limit = 4000 }) {
  const P = pos.map(s => s.toLowerCase());
  const N = neg.map(s => s.toLowerCase());
  const T = types.map(s => s.toLowerCase());
  const out = [];
  for (const p of CLEAN) {
    const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ') + ' ' + (p.product_type || '')).toLowerCase();
    if (N.length && N.some(n => blob.includes(n))) continue;
    if (T.length && !T.includes(normType(p.product_type).toLowerCase())) continue;
    if (P.length && !P.some(k => blob.includes(k))) continue;
    out.push(p);
    if (out.length >= limit) break;
  }
  return out;
}

/**
 * siteExtras — per-site catalog overlay. Returns the products in
 * data/extras/<slug>.json (or [] if none). Used to add site-exclusive products
 * (e.g. the AI-generated LA toile line on asseeninla) WITHOUT writing to the
 * shared Shopify feed and WITHOUT being clobbered by pull-catalog. Each extra is
 * a normal product object (title, handle, image_url, sku, product_type, tags,
 * price, created_at) and may carry an optional buy_url for the sample CTA.
 */
function siteExtras(slug) {
  if (!slug) return [];
  try {
    const d = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'extras', slug + '.json'), 'utf8'));
    return Array.isArray(d) ? d.filter(p => p && p.image_url && p.handle) : [];
  } catch { return []; }
}

module.exports = { RAW, CLEAN, isJunk, normType, nicheSlice, siteExtras, count: CLEAN.length };