← back to Quadrille Showroom

scripts/build-showroom-data.js

146 lines

#!/usr/bin/env node
/**
 * build-showroom-data.js
 * Builds data/showroom-products.json for the Quadrille House Wallcovering Showroom.
 *
 * Source of truth: the Quadrille House snapshot at
 *   ~/Projects/quadrille-house-site/data/house.json
 * (which is itself built from the local dw_unified mirror via that project's
 *  scripts/build-house.js). We filter to product_type === 'Wallcovering'
 * (~883: China Seas + Grasscloth — the wallpaper arm of the Quadrille house)
 * and map each record into the shape the wing engine (public/js/showroom.js)
 * expects.
 *
 * Standalone-friendly: reads the sibling house.json if present; if it's
 * missing, prints the dw_unified query to run instead (no hard PG dependency).
 *
 * Cost: $0 (local file read).
 */
const fs = require('fs');
const path = require('path');

const OUT = path.join(__dirname, '..', 'data', 'showroom-products.json');
const HOUSE_CANDIDATES = [
  path.join(process.env.HOME, 'Projects', 'quadrille-house-site', 'data', 'house.json'),
  path.join(__dirname, '..', 'data', 'house.json'), // optional vendored copy
];

function loadHouse() {
  for (const p of HOUSE_CANDIDATES) {
    if (fs.existsSync(p)) {
      console.log('[build] reading', p);
      const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
      return raw.products || raw;
    }
  }
  console.error('[build] ERROR: could not find house.json in any of:\n  ' + HOUSE_CANDIDATES.join('\n  '));
  console.error('[build] Rebuild it first:  node ~/Projects/quadrille-house-site/scripts/build-house.js');
  process.exit(1);
}

// Pull a leading number out of a dimension string like '54"' -> 54. null-safe.
function inches(v) {
  if (v == null) return null;
  if (typeof v === 'number') return v;
  const m = String(v).match(/([\d.]+)/);
  return m ? parseFloat(m[1]) : null;
}

function firstImage(p) {
  if (Array.isArray(p.images) && p.images.length) return p.images[0];
  return p.swatch || p.room || null;
}

// ---- Line-standard spec resolver -------------------------------------------
// The upstream house.json carries width/repeat/match fields, but they're null
// for the entire China Seas line. The specs are nonetheless known: China Seas
// is Quadrille's hand-screen-printed wallpaper arm — a 27"-wide line with a
// typical 27" × 27" repeat and a straight match. Grasscloth is a natural-fiber
// 36"-wide line with a free (random) match and no pattern repeat. We fill the
// real dimensional specs here (preferring any genuine upstream value) so the
// detail panel reads true AND the engine tiles at correct architectural scale
// (computeRepeat uses real tile size when rawWidthKnown is set).
function isGrasscloth(p) {
  const hay = `${p.book || ''} ${p.series || ''} ${p.title || ''} ${p.composition || ''}`.toLowerCase();
  return /grass\s*cloth|grasscloth/.test(hay);
}

function resolveSpecs(p) {
  const upW = inches(p.width), upR = inches(p.repeat);
  if (isGrasscloth(p)) {
    return {
      width: upW || 36,                       // grasscloth railroads at 36"
      repeat: upR || null,                    // natural fiber — no printed repeat
      match_type: p.match_type || 'Free match (random)',
      material: p.composition || 'Natural grasscloth on paper backing',
      finish: p.finish || 'Woven natural fiber',
      specs_known: true,
    };
  }
  // Default: China Seas hand screen print
  return {
    width: upW || 27,                         // 27" screen-print width
    repeat: upR || 27,                        // typical 27" vertical repeat (27" × 27")
    match_type: p.match_type || 'Straight match',
    material: p.composition || 'Hand screen-printed on paper',
    finish: p.finish || 'Hand screen print',
    specs_known: true,
  };
}

// Engine-shape adapter (see normalizedProduct in showroom.js).
function toWing(p) {
  const img = firstImage(p);
  // China Seas bakes pattern+color into the title; series is uniformly "Pattern".
  const name = (p.series && p.series !== 'Pattern' && p.series) || p.display_name || p.title || 'Pattern';
  const s = resolveSpecs(p);
  return {
    pattern_name: name,
    color_name: p.color && p.color !== p.title ? p.color : '',
    sku: p.sku || p.handle || '',
    vendor: p.brand || 'Quadrille House',
    image: img,
    width: s.width,                    // resolved: 27" China Seas / 36" Grasscloth
    repeat: s.repeat,                  // resolved: 27" China Seas / none for Grasscloth
    match_type: s.match_type,          // Straight match / Free match
    finish: s.finish,                  // Hand screen print / Woven natural fiber
    specs_known: s.specs_known,        // tells the engine the tile size is real (not guessed)
    collection: p.book || '',
    material: s.material,
    store_url: p.store_url || '',
    cta_mode: p.cta_mode || 'quote',
    hue: typeof p.hue === 'number' ? p.hue : null,
    color_bucket: p.color_bucket || null,
    // distinct room shot only (519/883 reuse the swatch as "room" — treat those as none)
    room: p.room && p.room !== p.swatch ? p.room : null,
    title: p.title || name,
    published_at: p.published_at || null,
  };
}

function main() {
  const all = loadHouse();
  const wc = all.filter(p => p.product_type === 'Wallcovering').map(toWing).filter(p => p.image);

  // Stable default order: book, then title.
  wc.sort((a, b) => (a.collection || '').localeCompare(b.collection || '') || (a.title || '').localeCompare(b.title || ''));

  const byBrand = {};
  wc.forEach(p => { byBrand[p.vendor] = (byBrand[p.vendor] || 0) + 1; });
  const distinctRoom = wc.filter(p => p.room).length;

  const payload = {
    built_at: new Date().toISOString(),
    count: wc.length,
    brands: byBrand,
    distinct_room_images: distinctRoom,
    products: wc,
  };
  fs.writeFileSync(OUT, JSON.stringify(payload, null, 0));
  console.log(`[build] wrote ${wc.length} wallcoverings -> ${OUT}`);
  console.log('[build] brands:', byBrand);
  console.log(`[build] distinct room images: ${distinctRoom}/${wc.length} (rest are flat swatches — gen-assets can render rooms)`);
}

main();