Files
justvitamin/scripts/generate-metaobject-svg-assets.ts
Omair Saleh 056c47581f feat: editorial review dashboard + elite-grade pilot batch (5 SKUs)
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that
presents the 4-section PDP pilot batch back to Umar, compliance, and the board
in an editorial format. Adds the full data layer that drives it: 5 source-backed
per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed
evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated
product FAQs, and the Matrixify-ready CSV exports for Lewis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:50:09 +08:00

102 lines
5.2 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import { dirname, join } from 'path'
import { ROOT, readJson, writeJson } from './lib/intel-utils'
const assetRoot = process.env.ASSET_ROOT || join(ROOT, '..', '..', 'jv-assets')
const outRoot = join(ROOT, 'content_population_exports')
const assetManifestPath = join(outRoot, 'asset_manifest_validation.json')
const generatedAt = new Date().toISOString()
type AssetRow = {
Kind: string
Owner: string
Field: string
Filename: string
Exists: string
}
function titleFromFilename(filename: string) {
return filename
.replace(/\.(svg|png|jpg|jpeg)$/i, '')
.replace(/^(benefit|dietary|goal)-/i, '')
.replace(/-/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase())
}
function iconPath(filename: string) {
const f = filename.toLowerCase()
if (f.includes('vegan') || f.includes('vegetarian')) return '<path d="M256 400c-52-52-88-119-88-170 0-67 54-121 121-121 26 0 51 8 72 23-7 116-50 208-105 268Z"/><path d="M255 397c16-91 47-158 101-221"/>'
if (f.includes('bone')) return '<path d="M172 191c-23-23-23-61 0-84s61-23 84 0l149 149c23 23 23 61 0 84s-61 23-84 0L172 191Z"/><path d="M119 244c-22-22-22-58 0-80s58-22 80 0l149 149c22 22 22 58 0 80s-58 22-80 0L119 244Z"/>'
if (f.includes('joint') || f.includes('mobility') || f.includes('arthritis')) return '<path d="M150 324c70-28 112-73 124-137"/><path d="M238 124c57 12 99 63 99 124 0 70-57 127-127 127-37 0-71-16-94-42"/><path d="M317 177h58v58"/>'
if (f.includes('doctor') || f.includes('gp') || f.includes('specialist')) return '<path d="M256 116v280"/><path d="M116 256h280"/><rect x="136" y="136" width="240" height="240" rx="48"/>'
if (f.includes('blood') || f.includes('efficacy')) return '<path d="M256 104s89 98 89 168a89 89 0 1 1-178 0c0-70 89-168 89-168Z"/><path d="M220 280h72"/>'
if (f.includes('d3') || f.includes('sun')) return '<circle cx="256" cy="256" r="76"/><path d="M256 80v56M256 376v56M80 256h56M376 256h56M132 132l40 40M340 340l40 40M380 132l-40 40M172 340l-40 40"/>'
if (f.includes('turmeric')) return '<path d="M163 310c47-96 116-146 207-151-7 93-57 162-151 207-35 17-73-20-56-56Z"/><path d="M202 329l111-111"/><circle cx="182" cy="188" r="42"/>'
if (f.includes('bioperine') || f.includes('absorption')) return '<circle cx="198" cy="287" r="39"/><circle cx="284" cy="221" r="31"/><circle cx="336" cy="314" r="27"/><path d="M146 380c83-23 151-74 216-163"/>'
if (f.includes('value') || f.includes('competitor')) return '<path d="M132 256h248"/><path d="M256 132v248"/><path d="M160 180h68v68h-68zM284 264h68v68h-68z"/>'
if (f.includes('elderly') || f.includes('vulnerable')) return '<circle cx="256" cy="160" r="48"/><path d="M160 400c12-82 52-132 96-132s84 50 96 132"/><path d="M340 276l44 124"/>'
return '<circle cx="256" cy="256" r="116"/><path d="M196 260l42 42 86-104"/>'
}
function svg(filename: string) {
const title = titleFromFilename(filename)
return `<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
<title id="title">${title}</title>
<desc id="desc">Just Vitamins migration metaobject asset generated from the approved dashboard filename.</desc>
<rect width="512" height="512" rx="112" fill="#f5f7ff"/>
<circle cx="256" cy="256" r="178" fill="#ffffff" stroke="#dfe4ff" stroke-width="10"/>
<g fill="none" stroke="#32327d" stroke-width="28" stroke-linecap="round" stroke-linejoin="round">
${iconPath(filename)}
</g>
</svg>
`
}
const manifest = readJson(assetManifestPath, null)
const required: AssetRow[] = Array.isArray(manifest?.required) ? manifest.required : []
const metaobjects = required
.filter(row => row.Kind === 'metaobject' && row.Filename && row.Filename.toLowerCase().endsWith('.svg'))
.sort((a, b) => a.Filename.localeCompare(b.Filename))
mkdirSync(assetRoot, { recursive: true })
const rows = []
for (const row of metaobjects) {
const target = join(assetRoot, row.Filename)
const existed = existsSync(target)
if (!existed) {
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, svg(row.Filename), 'utf8')
}
rows.push({
owner: row.Owner,
field: row.Field,
filename: row.Filename,
path: target,
status: existed ? 'preserved_existing' : 'generated_svg_asset'
})
}
writeJson(join(outRoot, 'metaobject_svg_asset_generation.json'), {
generatedAt,
assetRoot,
totalMetaobjectSvgAssets: rows.length,
generated: rows.filter(row => row.status === 'generated_svg_asset').length,
preserved: rows.filter(row => row.status === 'preserved_existing').length,
rows
})
const esc = (value: any) => /[",\n\r]/.test(String(value ?? '')) ? `"${String(value ?? '').replace(/"/g, '""')}"` : String(value ?? '')
await Bun.write(
join(outRoot, 'metaobject_svg_asset_generation.csv'),
[
'owner,field,filename,path,status',
...rows.map(row => ['owner', 'field', 'filename', 'path', 'status'].map(key => esc((row as any)[key])).join(','))
].join('\n') + '\n'
)
console.log(`Metaobject SVG assets represented: ${rows.length}`)
console.log(`Generated: ${rows.filter(row => row.status === 'generated_svg_asset').length}`)
console.log(`Preserved existing: ${rows.filter(row => row.status === 'preserved_existing').length}`)