← 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();