107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
import { join } from 'path'
|
|
|
|
const PORT = 3456
|
|
const PIPELINE_ROOT = process.env.PIPELINE_ROOT ||
|
|
join(import.meta.dir, '..', 'data', 'pipeline')
|
|
const INTEL_ROOT = join(import.meta.dir, '..', 'data', 'intelligence')
|
|
const DIST_ROOT = join(import.meta.dir, '..', 'dist')
|
|
const IS_PROD = process.env.NODE_ENV === 'production'
|
|
|
|
const cors = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
async function readJson(path: string) {
|
|
const f = Bun.file(path)
|
|
if (!(await f.exists())) return null
|
|
return f.json()
|
|
}
|
|
|
|
async function getProducts() {
|
|
const catalog = await readJson(join(PIPELINE_ROOT, 'catalog.json')) as any[]
|
|
const images = await readJson(join(PIPELINE_ROOT, 'product-images.json')) as Record<string, any>
|
|
|
|
return catalog.map((p: any) => {
|
|
const img = images?.[p.sku] || {}
|
|
const intelPath = join(INTEL_ROOT, p.sku, 'review-aspects.json')
|
|
return {
|
|
sku: p.sku,
|
|
name: img.name || p.sku,
|
|
totalReviews: p.total_reviews || 0,
|
|
variants: p.variants || [],
|
|
imageUrl: img.image_url || null,
|
|
productUrl: img.url ? `https://www.justvitamins.co.uk${img.url}` : null,
|
|
hasIntelligence: Bun.file(intelPath).exists()
|
|
}
|
|
}).sort((a: any, b: any) => b.totalReviews - a.totalReviews)
|
|
}
|
|
|
|
async function serveStatic(pathname: string): Promise<Response | null> {
|
|
if (!IS_PROD) return null
|
|
// Strip query string, decode
|
|
const clean = decodeURIComponent(pathname.split('?')[0])
|
|
const filePath = join(DIST_ROOT, clean === '/' ? 'index.html' : clean)
|
|
const f = Bun.file(filePath)
|
|
if (await f.exists()) return new Response(f)
|
|
// SPA fallback — serve index.html for all non-file routes
|
|
const index = Bun.file(join(DIST_ROOT, 'index.html'))
|
|
if (await index.exists()) return new Response(index, { headers: { 'Content-Type': 'text/html' } })
|
|
return null
|
|
}
|
|
|
|
const server = Bun.serve({
|
|
port: PORT,
|
|
async fetch(req) {
|
|
const url = new URL(req.url)
|
|
const path = url.pathname
|
|
|
|
if (req.method === 'OPTIONS') return new Response(null, { headers: cors })
|
|
|
|
// API routes
|
|
if (path.startsWith('/api/')) {
|
|
// GET /api/products
|
|
if (path === '/api/products') {
|
|
const products = await getProducts()
|
|
const resolved = await Promise.all(
|
|
products.map(async (p: any) => ({ ...p, hasIntelligence: await p.hasIntelligence }))
|
|
)
|
|
return new Response(JSON.stringify(resolved), { headers: cors })
|
|
}
|
|
|
|
// GET /api/intelligence/:sku/:view
|
|
const intelMatch = path.match(/^\/api\/intelligence\/([^/]+)\/([^/]+)$/)
|
|
if (intelMatch) {
|
|
const [, sku, view] = intelMatch
|
|
if (view === 'all') {
|
|
const views = ['review-aspects', 'strengths-weaknesses', 'conversion-blockers', 'improvements', 'image-audit', 'photo-brief']
|
|
const result: Record<string, any> = {}
|
|
for (const v of views) {
|
|
const data = await readJson(join(INTEL_ROOT, sku, `${v}.json`))
|
|
if (data) result[v.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase())] = data
|
|
}
|
|
return Object.keys(result).length
|
|
? new Response(JSON.stringify(result), { headers: cors })
|
|
: new Response(JSON.stringify({ error: 'No intelligence data' }), { status: 404, headers: cors })
|
|
}
|
|
const data = await readJson(join(INTEL_ROOT, sku, `${view}.json`))
|
|
return data
|
|
? new Response(JSON.stringify(data), { headers: cors })
|
|
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
|
|
}
|
|
|
|
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
|
|
}
|
|
|
|
// Static file serving in production
|
|
const static_ = await serveStatic(path)
|
|
if (static_) return static_
|
|
|
|
return new Response('Not found', { status: 404 })
|
|
}
|
|
})
|
|
|
|
console.log(`JV Dashboard running on http://localhost:${PORT} (${IS_PROD ? 'production' : 'dev'})`)
|