056c47581f
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>
102 lines
5.2 KiB
TypeScript
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}`)
|