← back to Carnegie Internal

scripts/build-carnegie.mjs

71 lines

#!/usr/bin/env node
// Build data/products.json (shaped snapshot: {vendor, facets, products}) for the Carnegie
// internal line viewer, straight from dw_unified.carnegie_catalog. $0 local PG read.
import pg from 'pg';
import { writeFileSync } from 'node:fs';
const pool = new pg.Pool({ connectionString: 'postgresql:///dw_unified?host=/tmp' });

// hex -> coarse color bucket (drives the Colors facet + color sort)
function bucket(hex){
  const m=/#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(hex||''); if(!m) return 'Neutral';
  const [r,g,b]=[1,2,3].map(i=>parseInt(m[i],16)); const L=0.2126*r+0.7152*g+0.0722*b;
  if(L>210) return 'White'; if(L<55) return 'Black';
  const mx=Math.max(r,g,b),mn=Math.min(r,g,b);
  if(mx-mn<22) return L>150?'Light':'Gray';
  if(r>=g&&r>=b) return (g>b)?'Warm':'Red';
  if(g>=r&&g>=b) return 'Green';
  return 'Blue';
}

const { rows } = await pool.query(`
  SELECT dw_sku, mfr_sku, pattern_name, pattern_number, color_number, color_name, title,
         price, content, product_type, dw_class, image_url,
         (palette->0->>'hex') AS hex, array_to_string(color_tags,', ') AS colorway,
         color_tags, width, finish, backing, origin, description_text
  FROM carnegie_catalog WHERE dw_sku IS NOT NULL AND image_url IS NOT NULL
  ORDER BY pattern_name, color_number, dw_sku`);

const products = rows.map(r => {
  const cbucket = bucket(r.hex);
  const colorName = (r.color_tags && r.color_tags[0]) || r.color_name || ('Color '+r.color_number);
  return {
    sku: r.dw_sku, dw_sku: r.dw_sku, mfr_sku: r.mfr_sku,
    handle: r.dw_sku,
    series: r.pattern_name,                 // "Patterns" facet
    pattern_number: r.pattern_number,
    color_number: r.color_number,
    color: colorName,                       // "Colorways" facet
    color_bucket: cbucket,                  // "Colors" facet
    hex: r.hex || null,
    title: r.title || ('Carnegie '+r.pattern_name+' '+r.color_number),
    price: r.price != null ? Number(r.price) : null,
    material: r.content || null,            // "Content" facet (kept key 'material' for card reuse)
    type: r.product_type || r.dw_class || null,
    width: r.width || null,
    finish: r.finish || null,
    backing: r.backing || null,
    origin: r.origin || null,
    image: r.image_url,
    swatch: r.image_url,
    body: r.description_text || null,
  };
});

// facets: field -> {value: count}
const F = ['series','color_bucket','color','material','type','width','finish','backing','origin'];
const facets = {}; for (const k of F) facets[k] = {};
for (const p of products) for (const k of F) { const v=p[k]; if(v) facets[k][v]=(facets[k][v]||0)+1; }

// vendor meta — Carnegie is a real customer-facing contract line (not private-label)
const vr = (await pool.query(
  `SELECT vendor_name, vendor_discount_pct, sample_price FROM vendor_registry WHERE lower(vendor_name)='carnegie' LIMIT 1`)).rows[0] || {};
const vendor = { name: 'Carnegie', code: 'carnegie',
  discount_pct: vr.vendor_discount_pct!=null?Number(vr.vendor_discount_pct):null,
  sample_price: vr.sample_price!=null?Number(vr.sample_price):4.25, private_label:false };

const snapshot = { built_at:new Date().toISOString(), source:'carnegie_catalog',
  count:products.length, vendor, facets, products };
writeFileSync(new URL('../data/products.json', import.meta.url), JSON.stringify(snapshot));
console.log(`carnegie snapshot: ${products.length} products, facets: ${F.map(k=>k+'='+Object.keys(facets[k]).length).join(' ')}`);
await pool.end();