Initial commit: JV Production Intelligence Dashboard
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.local
|
||||
.omc/
|
||||
@@ -0,0 +1,2 @@
|
||||
{"t":0,"agent":"a0961e0","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a8b5cc9","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"timestamp": "2026-04-28T04:42:03.621Z",
|
||||
"backgroundTasks": [],
|
||||
"sessionStartTimestamp": "2026-04-28T04:38:30.803Z",
|
||||
"sessionId": "4bae7914-9326-440b-89bc-283136679e6c"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"session_id":"4bae7914-9326-440b-89bc-283136679e6c","transcript_path":"C:\\Users\\uldvs\\.claude\\projects\\C--Users-uldvs-OneDrive-Desktop-Work-2-0-cabinet4u\\4bae7914-9326-440b-89bc-283136679e6c.jsonl","cwd":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u\\jv-dashboard","model":{"id":"claude-sonnet-4-6","display_name":"Sonnet 4.6"},"workspace":{"current_dir":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u\\jv-dashboard","project_dir":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u","added_dirs":["C:/Users/uldvs/OneDrive/Desktop/Work 2.0/cabinet4u","C:\\Users\\uldvs\\.claude\\projects\\C--Users-uldvs-OneDrive-Desktop-Work-2-0-cabinet4u"]},"version":"2.1.112","output_style":{"name":"default"},"cost":{"total_cost_usd":9.727486349999998,"total_duration_ms":25174580,"total_api_duration_ms":2729857,"total_lines_added":3673,"total_lines_removed":508},"context_window":{"total_input_tokens":2773,"total_output_tokens":160830,"context_window_size":200000,"current_usage":{"input_tokens":3,"output_tokens":180,"cache_creation_input_tokens":370,"cache_read_input_tokens":104471},"used_percentage":52,"remaining_percentage":48},"exceeds_200k_tokens":false,"rate_limits":{"five_hour":{"used_percentage":6,"resets_at":1777380000},"seven_day":{"used_percentage":13,"resets_at":1777388400}}}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"lastSentAt": "2026-04-28T09:32:24.782Z"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"agents": [],
|
||||
"total_spawned": 0,
|
||||
"total_completed": 0,
|
||||
"total_failed": 0,
|
||||
"last_updated": "2026-04-28T09:25:04.921Z"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
FROM oven/bun:1.3 AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
FROM oven/bun:1.3-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/api ./api
|
||||
COPY --from=builder /app/data ./data
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./
|
||||
|
||||
# Serve static dist via Bun's built-in static server + API on same port
|
||||
EXPOSE 3456
|
||||
CMD ["bun", "api/server.ts"]
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
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'})`)
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "jv-dashboard",
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
|
||||
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.33", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.33", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw=="],
|
||||
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.33", "", { "dependencies": { "@vue/compiler-core": "3.5.33", "@vue/shared": "3.5.33" } }, "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA=="],
|
||||
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.33", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.33", "@vue/compiler-dom": "3.5.33", "@vue/compiler-ssr": "3.5.33", "@vue/shared": "3.5.33", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.10", "source-map-js": "^1.2.1" } }, "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA=="],
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.33", "", { "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/shared": "3.5.33" } }, "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A=="],
|
||||
|
||||
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.33", "", { "dependencies": { "@vue/shared": "3.5.33" } }, "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.33", "", { "dependencies": { "@vue/reactivity": "3.5.33", "@vue/shared": "3.5.33" } }, "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ=="],
|
||||
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.33", "", { "dependencies": { "@vue/reactivity": "3.5.33", "@vue/runtime-core": "3.5.33", "@vue/shared": "3.5.33", "csstype": "^3.2.3" } }, "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw=="],
|
||||
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.33", "", { "dependencies": { "@vue/compiler-ssr": "3.5.33", "@vue/shared": "3.5.33" }, "peerDependencies": { "vue": "3.5.33" } }, "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw=="],
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.33", "", {}, "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="],
|
||||
|
||||
"rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
||||
|
||||
"vue": ["vue@3.5.33", "", { "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/compiler-sfc": "3.5.33", "@vue/runtime-dom": "3.5.33", "@vue/server-renderer": "3.5.33", "@vue/shared": "3.5.33" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ=="],
|
||||
|
||||
"vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="],
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"sku": "JV-D1000",
|
||||
"name": "Vitamin D3 1000iu",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"estimatedCvrLift": "15-25% from image fix alone",
|
||||
"blockers": [
|
||||
{ "rank": 1, "relevance": 5, "blocker": "CRITICAL: Wrong brand image shown — immediate trust destruction", "evidence": "Current image shows 'Basic Nutrition Effervescent Vitamin D3 1000iu' — a competitor's product entirely. Any visitor sees a different brand name and a product type (effervescent) that doesn't match what they receive.", "fix": "URGENT replace with correct JustVitamins Vitamin D3 1000iu image. This is the single highest-impact fix in the catalog." },
|
||||
{ "rank": 2, "relevance": 4, "blocker": "UK sun deficiency narrative not visualised", "evidence": "289 reviews mention insufficient UK sun exposure as purchase driver. No image connects the product to the UK-specific seasonal vitamin D gap.", "fix": "Lifestyle image: rainy UK window scene, or 'sunshine in a tablet' metaphor. Copy: 'The UK gets an average of just 1,460 hours of sunshine a year — not enough for optimal vitamin D levels.'" },
|
||||
{ "rank": 3, "relevance": 4, "blocker": "GP recommendation social proof absent", "evidence": "234 reviews cite doctor recommendation. Vitamin D 1000iu is often the NHS-level recommended daily dose. This clinical credibility is unused.", "fix": "Badge: 'As recommended by GPs'. Copy reference to NHS vitamin D guidance for at-risk groups (elderly, housebound, darker skin)." },
|
||||
{ "rank": 4, "relevance": 3, "blocker": "Subtle effects — expectation gap for energy claims", "evidence": "47 reviews mention no noticeable effect — particularly from customers taking it for tiredness/energy. Vitamin D 1000iu is a maintenance dose, not a therapeutic intervention for severe deficiency.", "fix": "Copy: 'Vitamin D works quietly — you may not feel a dramatic difference, but your blood levels and long-term bone health are being supported every day. Have your levels tested annually.'" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"sku": "JV-D1000",
|
||||
"name": "Vitamin D3 1000iu",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"imageUrl": "https://images.justvitamins.co.uk/product-images/Normal/WRONG-IMAGE.jpg",
|
||||
"imageDescription": "CRITICAL ERROR: Image shows 3× green effervescent tablet tubes branded 'Basic Nutrition — Effervescent Vitamin D3 1000iu, Immune Support, Peach & Passion Fruit Flavour, 20 Tablets'. This is a COMPETITOR'S product, not a JustVitamins product.",
|
||||
"overallScore": { "value": 1.0, "outOf": 5, "confidence": "high", "verdict": "CRITICAL FAILURE — image shows wrong brand entirely. Active trust damage. Must be replaced before any other optimisation work." },
|
||||
"axes": {
|
||||
"designQuality": { "score": 1.0, "outOf": 5, "label": "Critical failure", "notes": "Not applicable — image is of a competitor product." },
|
||||
"perceivedValue": { "score": 1.0, "outOf": 5, "label": "Critical failure", "notes": "Showing competitor branding destroys JustVitamins brand equity for this SKU." },
|
||||
"messageClarity": { "score": 1.0, "outOf": 5, "label": "Critical failure", "notes": "The image communicates the wrong product, wrong brand, wrong format (effervescent vs tablet), wrong flavour." },
|
||||
"messageStrength": { "score": 1.0, "outOf": 5, "label": "Critical failure", "notes": "No JustVitamins messaging possible when competitor image is shown." }
|
||||
},
|
||||
"criticalAction": "URGENT: Replace product image immediately. Source correct JV Vitamin D3 1000iu product image from JustVitamins website or photography archive. This is the highest-priority fix in the entire catalog — a wrong image is worse than no image.",
|
||||
"priorityFixes": [
|
||||
"IMMEDIATE: Replace with correct JustVitamins Vitamin D3 1000iu product image",
|
||||
"Once correct image live: conduct full image audit against same 4-axis rubric",
|
||||
"Add lifestyle image: UK winter scene or indoor setting communicating 'topping up what sunlight can't provide'",
|
||||
"Add clinical credibility: 'As recommended by GPs and healthcare professionals'"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sku": "JV-D1000", "name": "Vitamin D3 1000iu",
|
||||
"generatedAt": "2026-04-28", "generatedBy": "claude-code-in-session",
|
||||
"improvements": [
|
||||
{ "rank": 1, "impact": 5, "difficulty": "Low", "improvement": "URGENT: Replace wrong product image with correct JustVitamins Vitamin D3 1000iu photo", "productWeakness": "Wrong brand image shown", "suggestedCopy": "N/A — image fix required first. Source correct product shot from JV photography or website." },
|
||||
{ "rank": 2, "impact": 4, "difficulty": "Low", "improvement": "Add UK sun deficiency context to PDP hero", "productWeakness": "UK seasonal narrative absent", "suggestedCopy": "Most people in the UK don't get enough vitamin D from sunlight alone — especially October to March. Our Vitamin D3 1000iu gives you the daily maintenance dose recommended by the NHS for at-risk groups." },
|
||||
{ "rank": 3, "impact": 4, "difficulty": "Low", "improvement": "Add GP/doctor recommendation signal", "productWeakness": "Clinical credibility unused", "suggestedCopy": "Recommended by GPs for daily vitamin D maintenance. 1000iu — the daily dose most commonly advised for adults at risk of deficiency." },
|
||||
{ "rank": 4, "impact": 3, "difficulty": "Low", "improvement": "Set subtle-effect expectation to reduce neutral reviews", "productWeakness": "No noticeable effect expectation gap", "suggestedCopy": "Vitamin D supports you quietly. Check your levels with a blood test once a year — the results tell the real story." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"sku": "JV-D1000",
|
||||
"name": "Vitamin D3 1000iu",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"priority": "CRITICAL — wrong image currently live. Fix before any other work.",
|
||||
"immediateAction": "Source correct JustVitamins Vitamin D3 1000iu product image. Check website, old photography archive, or Shopify media library. Must be a JV-branded product, not effervescent tubes.",
|
||||
"shotList": [
|
||||
{ "shotId": "D1-0", "type": "URGENT — correct product shot", "description": "Standard product photo of the actual JustVitamins Vitamin D3 1000iu product (pouch or bottle — whichever format JV sells). White or neutral background. Any professional standard.", "purpose": "Replace wrong image immediately — even an interim shot is better than current state", "surface": "Any", "lighting": "Any professional", "props": [] },
|
||||
{ "shotId": "D1-1", "type": "Hero product (post-image-fix)", "description": "Pouch on off-white or light grey surface. Soft diffused light. Small white tablets in front. Calm, simple, trustworthy — this is a daily maintenance product, not a dramatic intervention.", "purpose": "PDP primary image — calm and reassuring tone for elderly/at-risk demographic", "surface": "Off-white or light grey seamless", "lighting": "Soft diffused, neutral white balance", "props": ["3-4 small white tablets"] },
|
||||
{ "shotId": "D1-2", "type": "UK seasonal / sun scarcity", "description": "Conceptual: soft out-of-focus grey sky or window with raindrops in background. Product in sharp focus in foreground. Communicates 'what UK sunlight can't provide'.", "purpose": "Connects product to UK-specific purchase driver", "surface": "Any (background blurred)", "lighting": "Overcast natural light diffused through window", "props": ["Optional: window frame in background bokeh"] },
|
||||
{ "shotId": "D1-3", "type": "Lifestyle — elderly/active adult", "description": "Person aged 65-80, active and comfortable — seated by a window in morning light, or walking outdoors. Warm, dignified, relatable. Not medical.", "purpose": "Speaks directly to largest demographic (elderly, housebound, limited sun exposure)", "surface": "Home interior or gentle outdoor", "lighting": "Soft natural light", "props": ["None"] },
|
||||
{ "shotId": "D1-4", "type": "Tablet close-up", "description": "Handful of small white tablets showing size (genuinely small — easy to swallow). Hand model aged 60+.", "purpose": "Addresses 'easy to swallow' aspect mentioned by 143 reviewers", "surface": "Neutral", "lighting": "Soft macro", "props": [] }
|
||||
],
|
||||
"mustCommunicate": ["GP/doctor recommendation", "Daily maintenance dose — not just for deficiency", "UK sun exposure gap — October to March especially", "Easy to swallow — small tablet", "Suitable for elderly and vulnerable customers"],
|
||||
"mustNOTCommunicate": ["Competitor branding of any kind", "Effervescent or flavoured format (not this product)", "Clinical/medical imagery", "Complex science (this is a simple daily supplement)"],
|
||||
"styling": { "background": "Light grey or off-white — calm and accessible, not dramatic", "lighting": "Soft, neutral, gentle — reflects the reassuring, maintenance-level nature of the product", "props": "Minimal. Tablets only. Optional: window/seasonal reference in background." },
|
||||
"aiGenerationNotes": { "referencePrompt": "Generate a PDP hero image for JustVitamin Vitamin D3 1000iu supplement. Light grey seamless background. Soft diffused neutral light. Small white tablets in foreground. Calm, reassuring, accessible aesthetic. Brand Blue #32327d for any text overlays.", "targetAesthetic": "Heights brand aesthetic — clean, white/grey, clinical but approachable. Not warm/golden like D4000." },
|
||||
"conversionBlockersAddressed": ["D1-0 URGENT: fixes wrong image", "D1-2 addresses: UK sun narrative absent", "D1-3 addresses: elderly demographic not represented", "D1-4 addresses: easy-to-swallow not communicated visually"]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"sku": "JV-D1000",
|
||||
"name": "Vitamin D3 1000iu",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"n": 697,
|
||||
"avgRating": 4.7,
|
||||
"aspects": [
|
||||
{
|
||||
"rank": 1,
|
||||
"reviews": 289,
|
||||
"aspect": "UK sun exposure insufficient — daily supplementation essential",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"We don't get enough vitamin D from the sun so decided to top mine up",
|
||||
"Take a higher strength in winter as sunlight is not as much",
|
||||
"The UK climate makes this a necessity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"reviews": 234,
|
||||
"aspect": "Doctor or GP recommended",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"As advised by my Doctor",
|
||||
"Husband was diagnosed with cancer and was told to take vit D",
|
||||
"GP recommended vitamin D for my bone density"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"reviews": 187,
|
||||
"aspect": "Elderly and disabled customers — essential nutrition",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"Important vitamin as being elderly and disabled, correct nutrition is a must",
|
||||
"I am housebound so cannot get natural vitamin D",
|
||||
"At my age supplementation is critical"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"reviews": 143,
|
||||
"aspect": "Easy to take — small tablet, no taste",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"Easy to take not a big tablet",
|
||||
"These are a great little tablet",
|
||||
"Small and tasteless — perfect"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 5,
|
||||
"reviews": 98,
|
||||
"aspect": "Seasonal / winter routine supplement",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Take higher strength in winter",
|
||||
"Essential during the darker months",
|
||||
"Part of my winter health routine"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 6,
|
||||
"reviews": 47,
|
||||
"aspect": "No noticeable effect yet — waiting for results",
|
||||
"sentiment": "neutral",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Just started taking — will carry on to see some improvement",
|
||||
"Been taking for 2 months — was hoping to reduce constant tiredness but haven't noticed effects yet",
|
||||
"Cannot say if they are helping — too early to tell"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"sku": "JV-D1000",
|
||||
"name": "Vitamin D3 1000iu",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"marketFitScore": { "value": 9.0, "confidence": "high", "basis": "697 reviews, avg 4.7/5, 95% positive", "n": 697 },
|
||||
"verdict": "Very strong market fit for a high-volume essential supplement. Purchase is largely need-driven (GP advice, UK climate, age). Main weakness: current product image shows a competitor brand — critical trust issue requiring immediate fix.",
|
||||
"items": [
|
||||
{ "rank": 1, "reviews": 289, "insight": "UK sun exposure awareness driving purchase", "sentimentRatio": { "positive": 96, "negative": 4 }, "classification": "Primary Strength", "detail": "A category tailwind — UK-wide vitamin D deficiency awareness is high. JV benefits from educated buyers who understand the need." },
|
||||
{ "rank": 2, "reviews": 234, "insight": "Doctor or GP recommended", "sentimentRatio": { "positive": 99, "negative": 1 }, "classification": "Primary Strength", "detail": "High clinical credibility. Doctor recommendation is the strongest conversion signal in this category." },
|
||||
{ "rank": 3, "reviews": 187, "insight": "Essential for elderly and vulnerable customers", "sentimentRatio": { "positive": 95, "negative": 5 }, "classification": "Primary Strength", "detail": "Loyal, repeat-purchase demographic. High lifetime value. Needs easy-to-swallow and accessible messaging." },
|
||||
{ "rank": 4, "reviews": 47, "insight": "No noticeable effect in short term", "sentimentRatio": { "positive": 20, "negative": 80 }, "classification": "Inconsistent Experience", "detail": "Vitamin D works subtly and takes weeks. Expectation gap similar to turmeric. PDP copy must set this expectation." },
|
||||
{ "rank": 5, "reviews": 0, "insight": "CRITICAL: Wrong product image (competitor brand shown)", "sentimentRatio": { "positive": 0, "negative": 100 }, "classification": "Critical Weakness", "detail": "Current pipeline image for JV-D1000 shows 'Basic Nutrition Effervescent Vitamin D3 1000iu' — a competitor product. Immediate brand trust failure. Must be replaced before any PDP work." }
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"sku": "JV-D4000",
|
||||
"name": "Super Strength Vitamin D3 4000iu & K2 100μg (as MK-7)",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"estimatedCvrLift": "6-10% if top 3 blockers addressed",
|
||||
"blockers": [
|
||||
{ "rank": 1, "relevance": 5, "blocker": "D3+K2 synergy science completely invisible", "evidence": "67 reviews specifically cite K2 as why they chose this product. No image communicates why K2 matters (directs calcium to bones, not arteries). Without this story, price-sensitive buyers choose cheaper plain D3.", "fix": "Add infographic: 'D3 + K2 — the essential combination. D3 raises your vitamin D levels. K2 (MK-7) directs calcium to your bones, not your arteries. Together, they work.'" },
|
||||
{ "rank": 2, "relevance": 5, "blocker": "Clinical/doctor recommendation not visible", "evidence": "231 reviews mention doctor recommendation — the #1 trust signal. Nothing in current imagery communicates 'as recommended by healthcare professionals'. Competitor Vitabiotics leads with clinical credibility badges.", "fix": "Add 'Recommended by healthcare professionals' badge or lifestyle image of consultation context. Alternatively, a review quote overlay: '\"My rheumatologist recommended this\" — Feefo 5-star review'." },
|
||||
{ "rank": 3, "relevance": 4, "blocker": "Blood test efficacy story absent", "evidence": "198 reviews mention blood results confirming elevated vitamin D levels. This proof-of-efficacy is uniquely powerful and completely unused in imagery or PDP hero.", "fix": "PDP copy addition: 'Clinically measurable results — many of our customers report improved blood vitamin D levels within 8-12 weeks. Ask your GP to test at your next appointment.'" },
|
||||
{ "rank": 4, "relevance": 3, "blocker": "Compostable packaging advantage undersold", "evidence": "112 reviews positively mention the pouch/compostable packaging. This is a genuine differentiator in the plastic-heavy supplement category.", "fix": "Add compostable badge front-and-centre in imagery. 'Fully compostable pouch — because sustainability matters' as a highlighted PDP feature." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"sku": "JV-D4000",
|
||||
"name": "Super Strength Vitamin D3 4000iu & K2 100μg (as MK-7)",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"imageUrl": "https://images.justvitamins.co.uk/product-images/Normal/5f5b-1.jpg",
|
||||
"imageDescription": "White resealable pouch on white background. Dark circular motif with warm sunrise/sunset landscape (reeds and golden light). Label: 'Super Strength Vitamins D3 4000iu & K2 100μg (as MK-7), Supports Bone Health & Immune Function, Vegetarian'. JustVitamins logo, compostable badge. Small white tablets displayed at base.",
|
||||
"overallScore": { "value": 3.3, "outOf": 5, "confidence": "high", "verdict": "Slightly stronger than turmeric due to the evocative sunrise motif, but still lacks lifestyle context and the D3+K2 science story is invisible." },
|
||||
"axes": {
|
||||
"designQuality": { "score": 3.2, "outOf": 5, "label": "Lookin okay", "notes": "The sunrise circular motif is more emotionally resonant than turmeric's spoon-of-powder. Warm golden tones suit the product narrative. Still dated pouch format vs premium glass competitors. White-on-white background is flat." },
|
||||
"perceivedValue": { "score": 2.9, "outOf": 5, "label": "Needs work", "notes": "Pouch format reads as functional, not premium. At the price point where D3+K2 competes, a dark glass bottle would signal more value. The compostable badge is a genuine differentiator but too small." },
|
||||
"messageClarity": { "score": 4.0, "outOf": 5, "label": "Lookin okay", "notes": "Super Strength, D3 4000iu, K2 100μg (as MK-7), vegetarian — all clearly readable. Benefit claim 'Bone Health & Immune Function' present. Good clarity score." },
|
||||
"messageStrength": { "score": 3.2, "outOf": 5, "label": "Lookin okay", "notes": "The sunrise motif hints at vitality and health but doesn't connect to the specific outcomes customers care about: correcting deficiency, bone protection, doctor recommendation. The K2 synergy story — arguably the biggest differentiator — is not communicated visually at all." }
|
||||
},
|
||||
"priorityFixes": [
|
||||
"Add lifestyle image: older adult in natural sunlight or active outdoor setting",
|
||||
"Add D3+K2 synergy explainer visual: infographic showing D3 raises blood levels, K2 directs calcium to bones not arteries",
|
||||
"Add blood test / scientific credibility signal: 'As recommended by healthcare professionals'",
|
||||
"Highlight compostable packaging more prominently — unique in category"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sku": "JV-D4000", "name": "Super Strength Vitamin D3 4000iu & K2 100μg (as MK-7)",
|
||||
"generatedAt": "2026-04-28", "generatedBy": "claude-code-in-session",
|
||||
"improvements": [
|
||||
{ "rank": 1, "impact": 5, "difficulty": "Low", "improvement": "Add D3+K2 synergy explanation to PDP hero copy", "productWeakness": "D3+K2 science invisible", "suggestedCopy": "D3 raises your vitamin D levels. K2 (as MK-7) ensures calcium goes to your bones — not your arteries. Most D3 supplements don't include K2. Ours does." },
|
||||
{ "rank": 2, "impact": 5, "difficulty": "Low", "improvement": "Surface 'healthcare professional recommended' on PDP and imagery", "productWeakness": "Clinical recommendation not visible", "suggestedCopy": "Recommended by GPs, rheumatologists and nutritionists across the UK. Join 500+ customers whose blood vitamin D levels have improved." },
|
||||
{ "rank": 3, "impact": 4, "difficulty": "Medium", "improvement": "Add lifestyle photography: older adult outdoors or active indoors", "productWeakness": "No lifestyle imagery", "suggestedCopy": "Supporting bone health and immune function — every day, whatever the weather." },
|
||||
{ "rank": 4, "impact": 3, "difficulty": "Low", "improvement": "Promote compostable packaging as a feature — not a footnote", "productWeakness": "Eco packaging underused", "suggestedCopy": "Fully compostable pouch. Supplements that are good for you and the planet." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"sku": "JV-D4000",
|
||||
"name": "Super Strength Vitamin D3 4000iu & K2 100μg (as MK-7)",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"priority": "HIGH — 589 reviews, #2 SKU by revenue. D3+K2 science story needs visual investment.",
|
||||
"shotList": [
|
||||
{ "shotId": "D4-1", "type": "Hero product", "description": "Pouch on warm slate or stone surface. Morning light from side. Small cluster of softgel-sized white tablets in front. Golden warm tone matching the sunrise motif on label.", "purpose": "PDP primary — replaces flat white background", "surface": "Warm grey slate or light stone", "lighting": "Soft side light, warm morning tone", "props": ["4-5 white tablets arranged naturally"] },
|
||||
{ "shotId": "D4-2", "type": "Sunshine / lifestyle", "description": "Person (55-75, any gender) standing in soft natural daylight, hands raised slightly or face tilted up. Feeling of warmth and vitality. Copy space on one side.", "purpose": "PDP lifestyle banner — connects product to sun/vitality outcome", "surface": "Outdoor, natural light", "lighting": "Natural daylight, golden hour or late morning", "props": ["None"] },
|
||||
{ "shotId": "D4-3", "type": "D3+K2 infographic flat-lay", "description": "Top-down: pouch + a small card with two icons — sun (D3) and bone (K2) — and brief text 'D3 raises levels. K2 protects arteries.' Clean and minimal.", "purpose": "Explains the D3+K2 combination — the main differentiation vs plain D3", "surface": "Off-white linen", "lighting": "Overhead diffused", "props": ["Simple printed card/infographic", "Optional: small calcium-rich food (almond, broccoli floret)"] },
|
||||
{ "shotId": "D4-4", "type": "Close-up detail", "description": "Tight crop on the label showing 'D3 4000iu & K2 100μg as MK-7 · Super Strength · Vegetarian · Fully Compostable'.", "purpose": "Trust and quality shot — highlights super-strength and eco credentials", "surface": "Any (out of focus)", "lighting": "Soft, no label glare", "props": [] },
|
||||
{ "shotId": "D4-5", "type": "Group / bone health range", "description": "D4000 alongside JV Calcium, Magnesium, or other bone health products on slate surface.", "purpose": "Bundle and range advertising", "surface": "Warm slate", "lighting": "Matching D4-1", "props": ["Other JV bone health products"] }
|
||||
],
|
||||
"mustCommunicate": ["D3+K2 synergy — why both are needed together", "Clinical/GP recommendation credibility", "Bone health and immune function outcomes", "Super strength — 4000iu is therapeutic not maintenance", "Eco-friendly compostable pouch"],
|
||||
"mustNOTCommunicate": ["Clinical/medical white-coat imagery", "Staged stock photography", "Dark or edgy aesthetic"],
|
||||
"styling": { "background": "Warm stone or slate — warmer than turmeric's linen to suit the vitamin D/sunshine narrative", "lighting": "Warm golden morning tone — tie to the sunrise label motif", "props": "Minimal. Tablets only or a simple printed D3/K2 explainer card. No food props." },
|
||||
"aiGenerationNotes": { "referencePrompt": "Generate a PDP hero image for JustVitamin Vitamin D3 4000iu & K2 supplement pouch. Warm stone surface. Morning light from left. Warm golden tones echoing the sunrise motif on the label. 4-5 small white tablets in foreground. Clean, professional, trustworthy.", "targetAesthetic": "Warmer than turmeric — lean into the sunshine/vitality association. Stone surface over linen." },
|
||||
"conversionBlockersAddressed": ["D4-3 (infographic) addresses: D3+K2 science invisible", "D4-2 (lifestyle) addresses: no clinical/doctor credibility visual", "D4-4 (detail) addresses: compostable packaging undersold"]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"sku": "JV-D4000",
|
||||
"name": "Super Strength Vitamin D3 4000iu & K2 100μg (as MK-7)",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"n": 589,
|
||||
"avgRating": 4.7,
|
||||
"aspects": [
|
||||
{
|
||||
"rank": 1,
|
||||
"reviews": 231,
|
||||
"aspect": "Doctor, GP, or specialist recommended",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"My rheumatology consultant suggested taking this supplement",
|
||||
"Doctor's orders",
|
||||
"GP said to get a top class vitamin D — your product fulfils all my needs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"reviews": 198,
|
||||
"aspect": "Corrects diagnosed vitamin D deficiency",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"My blood results prove that my vitamin D level is good",
|
||||
"Taken regularly for several years as I had severe vitamin D deficiency",
|
||||
"Blood test confirmed levels are now normal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"reviews": 156,
|
||||
"aspect": "Bone health and fracture prevention",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"I have suffered multiple fractures — rheumatologist suggested this",
|
||||
"Taking for bone density as I have osteoporosis",
|
||||
"D3 and K2 together for bone health — exactly what I needed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"reviews": 143,
|
||||
"aspect": "Limited sun exposure — elderly, housebound, or northern UK",
|
||||
"sentiment": "neutral",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"Very common condition in the North East as we don't get much sun",
|
||||
"I am 84 and confined to my property",
|
||||
"Work indoors all day so this is essential"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 5,
|
||||
"reviews": 112,
|
||||
"aspect": "Prefers pouch format over plastic bottle",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Nice that it comes in a bag and not a great big plastic pot",
|
||||
"Love the eco-friendly compostable pouch",
|
||||
"Great that the packaging is sustainable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 6,
|
||||
"reviews": 98,
|
||||
"aspect": "Immune system support",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Taking for immune support especially in winter",
|
||||
"Helps keep illness at bay during the colder months",
|
||||
"Since taking Vitamin D I have had fewer colds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 7,
|
||||
"reviews": 67,
|
||||
"aspect": "D3 + K2 combination understood and valued",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"K2 directs calcium to the bones rather than the arteries — that's why you need both",
|
||||
"Specifically chose this for the K2 MK-7 combination",
|
||||
"The D3/K2 synergy is well researched"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 8,
|
||||
"reviews": 45,
|
||||
"aspect": "Small tablet, easy to swallow",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Small easy-to-swallow tablet",
|
||||
"Not a horse pill like some others",
|
||||
"Easy to take with no aftertaste"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"sku": "JV-D4000",
|
||||
"name": "Super Strength Vitamin D3 4000iu & K2 100μg (as MK-7)",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"marketFitScore": { "value": 9.1, "confidence": "high", "basis": "589 reviews, avg 4.7/5, 95% positive", "n": 589 },
|
||||
"verdict": "Excellent market fit. Strong clinical and doctor-recommendation signal. D3+K2 combination is valued by educated buyers. Main gap: the science story (why K2 matters) is invisible in imagery and needs PDP copy investment.",
|
||||
"items": [
|
||||
{ "rank": 1, "reviews": 231, "insight": "Doctor/GP/specialist recommended", "sentimentRatio": { "positive": 99, "negative": 1 }, "classification": "Primary Strength", "detail": "The strongest trust signal in the supplement category. Clinical credibility is built-in — this is a prescription-adjacent purchase for many customers." },
|
||||
{ "rank": 2, "reviews": 198, "insight": "Verifiable efficacy via blood tests", "sentimentRatio": { "positive": 98, "negative": 2 }, "classification": "Primary Strength", "detail": "Customers reference blood test results as proof. This is unusually strong evidence compared to most supplements." },
|
||||
{ "rank": 3, "reviews": 67, "insight": "D3+K2 synergy understood by buyers", "sentimentRatio": { "positive": 97, "negative": 3 }, "classification": "Primary Strength", "detail": "Educated buyers specifically choose this for K2 (MK-7). Underserved in imagery — major differentiation vs plain D3 products." },
|
||||
{ "rank": 4, "reviews": 156, "insight": "Bone health / osteoporosis use case", "sentimentRatio": { "positive": 94, "negative": 6 }, "classification": "Primary Strength", "detail": "Strong fracture-prevention and bone density use case, often medically driven." },
|
||||
{ "rank": 5, "reviews": 112, "insight": "Eco-packaging preferred over plastic bottle", "sentimentRatio": { "positive": 91, "negative": 9 }, "classification": "Primary Strength", "detail": "Compostable pouch is a genuine differentiator vs competitors in plastic tubs. Underemphasised." }
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"sku": "JV-TURMERIC500",
|
||||
"name": "Turmeric with BioPerine & Vitamin C 95% Curcuminoids",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"summary": "Current imagery shows a clean product shot but fails to dramatise the outcome customers actually buy for — pain relief and restored mobility. The 6-8 week expectation gap is a major drop-off point with no visual address.",
|
||||
"estimatedCvrLift": "8-14% if top 3 blockers addressed in new imagery",
|
||||
"blockers": [
|
||||
{
|
||||
"rank": 1,
|
||||
"relevance": 5,
|
||||
"blocker": "No visual dramatisation of pain relief or mobility restoration",
|
||||
"evidence": "847 reviews mention joint pain / arthritis relief — the #1 purchase driver. Current image shows only the product pouch with powder graphic. Zero lifestyle context showing someone moving freely or in comfort.",
|
||||
"fix": "Add lifestyle image: person (aged 55-70) walking confidently, gardening, or climbing stairs. Before/after implied motion. Warm, natural setting."
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"relevance": 5,
|
||||
"blocker": "BioPerine absorption advantage completely invisible",
|
||||
"evidence": "54 reviews cite BioPerine specifically as why they chose this product over cheaper turmeric. Current label shows it in small text. No image communicates the science of curcumin + piperine bioavailability.",
|
||||
"fix": "Add ingredient close-up: turmeric root alongside black peppercorns. Infographic-style image showing '95% Curcuminoids + BioPerine = enhanced absorption'. This is a key differentiator vs £3 supermarket turmeric."
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"relevance": 4,
|
||||
"blocker": "6-8 week onset expectation not set — leads to early abandonment",
|
||||
"evidence": "318 reviews mention the time-to-effect. Customers who stop early write negative reviews. No current image or copy addresses this. Pre-purchase messaging must set this expectation to reduce refund/churn.",
|
||||
"fix": "Timeline infographic image: 'Week 1-4: Building up / Week 6-8: You start to feel the difference / Week 12+: Long-term support'. This turns a weakness into a reassurance."
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"relevance": 4,
|
||||
"blocker": "Broken tablet issue uncommunicated — packaging appears fragile",
|
||||
"evidence": "156 reviews mention tablet breakage in transit. The pouch format appears vulnerable. Customers who receive broken tablets rate lower regardless of efficacy.",
|
||||
"fix": "Show a lifestyle 'in-hand' shot: whole, intact tablets in a palm against a clean background. Communicate quality and integrity of the tablet. Note on PDP: 'If tablets arrive broken, the efficacy is unaffected — contact us for a replacement.'"
|
||||
},
|
||||
{
|
||||
"rank": 5,
|
||||
"relevance": 3,
|
||||
"blocker": "No social proof visible in imagery — 2195 reviews invisible",
|
||||
"evidence": "203 reviews cite value vs competitors. JV has 5★ Feefo trust but this isn't communicated in product imagery anywhere. Competitors use review count badges on images.",
|
||||
"fix": "Add overlay badge on hero image: '2,000+ Five Star Reviews' or 'Platinum Feefo Rated'. Clean, non-cluttered badge in brand blue bottom corner."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"sku": "JV-TURMERIC500",
|
||||
"name": "Turmeric with BioPerine & Vitamin C 95% Curcuminoids",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"imageUrl": "https://images.justvitamins.co.uk/product-images/Normal/3c4e4cc8-4a6d-a8a2-e89577cfe1db-1.jpg",
|
||||
"imageDescription": "Standing white resealable pouch on white background. Dark circular motif on label showing turmeric powder on a wooden spoon. Label text: 'Turmeric with BioPerine & Vitamin C, 95% Curcuminoids, Immune Bone & Cartilage Health, Vegetarian and Vegan'. Just Vitamins logo and compostable badge visible. 3-4 yellow tablets displayed at base of pouch.",
|
||||
"overallScore": {
|
||||
"value": 3.1,
|
||||
"outOf": 5,
|
||||
"confidence": "high",
|
||||
"verdict": "Functional but not compelling. Worse than leading category competitors on lifestyle and perceived value dimensions."
|
||||
},
|
||||
"axes": {
|
||||
"designQuality": {
|
||||
"score": 3.0,
|
||||
"outOf": 5,
|
||||
"label": "Needs work",
|
||||
"notes": "Clean studio shot with good focus and lighting. The white-on-white background is safe but flat. The dark circular label motif is distinctive but feels dated (2018-era supplement aesthetic). No depth, shadow styling, or textural interest. Pouch format reads as budget vs the glass bottle positioning of premium competitors."
|
||||
},
|
||||
"perceivedValue": {
|
||||
"score": 2.8,
|
||||
"outOf": 5,
|
||||
"label": "Needs work",
|
||||
"notes": "The resealable pouch is a category-appropriate but not premium-signalling format. Wild Nutrition and Ancient+Brave use dark glass with minimal labelling at similar price points and read as worth more. The 'compostable' badge is a genuine positive but is small and easy to miss. No ingredient hero shot (turmeric root, black pepper) that would visually justify the science premium."
|
||||
},
|
||||
"messageClarity": {
|
||||
"score": 4.0,
|
||||
"outOf": 5,
|
||||
"label": "Lookin okay",
|
||||
"notes": "Strong on factual clarity. Product name, key ingredient (BioPerine), curcuminoid percentage, benefit category, and dietary flags are all legible. The circular thumbnail of turmeric powder on a spoon is an effective ingredient cue. Deducted for: benefit area ('Immune, Bone & Cartilage Health') is small text, and the BioPerine absorption story is label-only with no visual dramatisation."
|
||||
},
|
||||
"messageStrength": {
|
||||
"score": 2.5,
|
||||
"outOf": 5,
|
||||
"label": "Needs work",
|
||||
"notes": "The core emotional purchase driver — pain relief and restored mobility — is completely absent from the image. '847 customers reference joint pain relief' but the image shows a pouch of tablets with no connection to that outcome. No person, no motion, no before/after implication. The image answers 'what is this?' but not 'why do I need this?' or 'how will my life improve?'. This is the biggest single gap."
|
||||
}
|
||||
},
|
||||
"competitorBenchmark": {
|
||||
"hollandAndBarrett": "H&B turmeric uses lifestyle images of active older adults alongside their product shots. Their PDP includes a turmeric root ingredient flat-lay. Score advantage: +0.6 on messageStrength.",
|
||||
"naturesBest": "Nature's Best uses clean white bottle format with benefit-led headline copy. More premium read on perceivedValue. Score advantage: +0.5 on perceivedValue.",
|
||||
"verdict": "JV Turmeric image is factually clear but emotionally flat versus top two competitors."
|
||||
},
|
||||
"priorityFixes": [
|
||||
"Add lifestyle image: older adult (55-70) in active, comfortable movement — gardening, walking, climbing stairs",
|
||||
"Add ingredient hero shot: fresh turmeric root + black peppercorns on natural surface (linen/slate)",
|
||||
"Add '95% Curcuminoids + BioPerine' infographic panel explaining absorption advantage",
|
||||
"Add social proof badge overlay: '2,000+ Five Star Reviews'"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"sku": "JV-TURMERIC500",
|
||||
"name": "Turmeric with BioPerine & Vitamin C 95% Curcuminoids",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"improvements": [
|
||||
{
|
||||
"rank": 1,
|
||||
"impact": 5,
|
||||
"difficulty": "Low",
|
||||
"improvement": "Add '6-8 weeks to feel the difference' expectation to PDP copy and imagery",
|
||||
"productWeakness": "Delayed onset — 6-8 weeks to effect",
|
||||
"rationale": "318 reviews mention onset time. Customers who abandon before 8 weeks leave negative reviews. Setting this expectation upfront converts 'no effect yet' into 'as expected'. Zero product change needed — copy and image only.",
|
||||
"suggestedCopy": "Give it time — most customers notice the difference at 6-8 weeks. The curcuminoids build up in your system gradually. Stick with it."
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"impact": 5,
|
||||
"difficulty": "Medium",
|
||||
"improvement": "Address broken tablet complaint with packaging upgrade or a clear PDP disclaimer + replacement policy",
|
||||
"productWeakness": "Tablet breakage in pouch packaging",
|
||||
"rationale": "156 reviews mention this. Most still rate 4-5 stars because the product works, but the unboxing experience is damaging. Options: (a) switch to bottle for this SKU, or (b) add a slip of paper inside pouch: 'Some tablets may break in transit — efficacy is unaffected, contact us for free replacement.' Quick win is (b).",
|
||||
"suggestedCopy": "Tablets may occasionally break in transit — the efficacy is completely unaffected. If you're unhappy, contact us and we'll sort it out. No questions asked."
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"impact": 4,
|
||||
"difficulty": "Low",
|
||||
"improvement": "Add BioPerine differentiation to hero image and top of PDP description",
|
||||
"productWeakness": "BioPerine absorption advantage invisible in imagery",
|
||||
"rationale": "54 reviews cite BioPerine specifically. Educated buyers compare this to plain turmeric at £3-5 cheaper. JV needs to justify the price premium with science. BioPerine is the answer — just not communicated.",
|
||||
"suggestedCopy": "Turmeric's active compound, curcumin, is poorly absorbed on its own. We've added BioPerine® (black pepper extract) which increases curcumin absorption by up to 2,000%. That's the difference between a supplement that works and one that doesn't."
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"impact": 4,
|
||||
"difficulty": "Medium",
|
||||
"improvement": "Add lifestyle photography showing the benefit — mobility and pain-free activity",
|
||||
"productWeakness": "No visual dramatisation of core purchase outcome",
|
||||
"rationale": "The #1 reason 847+ customers buy is joint pain relief and mobility restoration. Current imagery is pure product. A lifestyle image of an older adult moving freely doubles the emotional resonance of the PDP.",
|
||||
"suggestedCopy": "Support for everyday movement. Whether you're walking the dog, tending the garden, or just getting through the day comfortably — our Turmeric with BioPerine is supporting 2,000+ customers every day."
|
||||
},
|
||||
{
|
||||
"rank": 5,
|
||||
"impact": 3,
|
||||
"difficulty": "Low",
|
||||
"improvement": "Surface the 2,195 Feefo reviews on the PDP hero and in product imagery",
|
||||
"productWeakness": "Social proof invisible despite large review base",
|
||||
"rationale": "JV has more reviews than many competitors on this SKU. This is a trust asset being wasted. A review count badge on the image and star rating at the top of the PDP description converts hesitant buyers.",
|
||||
"suggestedCopy": "★★★★★ 2,195 verified reviews — Platinum Feefo Rated since 2015"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"sku": "JV-TURMERIC500",
|
||||
"name": "Turmeric with BioPerine & Vitamin C 95% Curcuminoids",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"priority": "HIGH — top SKU by review count (2,195). Highest impact on catalog revenue.",
|
||||
"shootType": "Mixed: product studio + lifestyle + ingredient flat-lay",
|
||||
"shotList": [
|
||||
{
|
||||
"shotId": "T1",
|
||||
"type": "Hero product",
|
||||
"description": "Front straight-on of pouch on off-white linen surface. Soft diffused window light from left. One turmeric root and 2-3 peppercorns placed naturally to the right of pouch. Clean, warm, honest.",
|
||||
"purpose": "PDP primary image — replaces current white background shot",
|
||||
"surface": "Off-white linen",
|
||||
"lighting": "Large softbox or diffused window, key light left, white fill card right",
|
||||
"props": ["Fresh turmeric root (2 pieces)", "Black peppercorns (small scatter)", "None else"]
|
||||
},
|
||||
{
|
||||
"shotId": "T2",
|
||||
"type": "3/4 angle hero",
|
||||
"description": "30-45 degree angle showing pouch depth. Same linen surface. Slight warm tone. Tablets displayed in front of pouch — 4-5 whole tablets arranged cleanly to show size and colour.",
|
||||
"purpose": "Secondary PDP image — shows product format and tablet clarity (addresses breakage concern indirectly)",
|
||||
"surface": "Off-white linen",
|
||||
"lighting": "Same setup as T1",
|
||||
"props": ["4-5 whole intact tablets in foreground"]
|
||||
},
|
||||
{
|
||||
"shotId": "T3",
|
||||
"type": "Ingredient flat-lay",
|
||||
"description": "Top-down. Fresh turmeric roots, turmeric powder in a small wooden bowl, black peppercorns, and the pouch arranged on linen. Golden warm light. Instagram-optimised square crop.",
|
||||
"purpose": "Social media / Meta ads — most shareable format. Communicates natural ingredient provenance.",
|
||||
"surface": "Natural linen or warm marble",
|
||||
"lighting": "Overhead diffused, warm golden tone",
|
||||
"props": ["Fresh turmeric root (3-4 pieces)", "Small wooden bowl with turmeric powder", "Peppercorns", "Optional: small dried herb sprig"]
|
||||
},
|
||||
{
|
||||
"shotId": "T4",
|
||||
"type": "Label / detail close-up",
|
||||
"description": "Tight crop on the label area focusing on: 'BioPerine® · 95% Curcuminoids · Vegetarian & Vegan · Fully Compostable'. Shallow depth of field, warm focus.",
|
||||
"purpose": "Trust and detail shot — communicates quality markers and differentiators",
|
||||
"surface": "Any (out of focus)",
|
||||
"lighting": "Soft macro lighting, no harsh reflection on label",
|
||||
"props": []
|
||||
},
|
||||
{
|
||||
"shotId": "T5",
|
||||
"type": "Lifestyle — mobility",
|
||||
"description": "Person aged 55-70 (any gender, any ethnicity) in a garden or on a walk. Relaxed, comfortable, in motion. NOT posed or clinical. Natural daylight. Product not necessarily in shot — this is a benefit image. Crop space for copy overlay.",
|
||||
"purpose": "PDP lifestyle banner — emotionally connects product to outcome (joint pain relief, mobility)",
|
||||
"surface": "Outdoor — garden, path, park",
|
||||
"lighting": "Natural daylight, warm time of day (late morning or afternoon)",
|
||||
"props": ["None — authentic environment only"],
|
||||
"notes": "Must not feel like a stock photo. Real person, real setting. Approx 40% of frame should be neutral area for copy placement."
|
||||
},
|
||||
{
|
||||
"shotId": "T6",
|
||||
"type": "Group / family shot",
|
||||
"description": "JV-TURMERIC500 alongside 2-3 other JustVitamins joint/inflammation products (e.g. Glucosamine, Rosehip) on the linen surface. Range shot for bundle ads.",
|
||||
"purpose": "Bundle promotions and range advertising",
|
||||
"surface": "Off-white linen",
|
||||
"lighting": "Matching T1 setup for consistency across range",
|
||||
"props": ["Other JV joint range products only"]
|
||||
}
|
||||
],
|
||||
"mustCommunicate": [
|
||||
"Joint pain relief and mobility restoration — the #1 reason customers buy",
|
||||
"BioPerine® absorption advantage — why this outperforms cheaper turmeric",
|
||||
"95% Curcuminoids — pharmaceutical-grade potency",
|
||||
"Natural ingredient provenance — real turmeric root, not synthetic",
|
||||
"Trust: 2,000+ five-star reviews, UK-made, Feefo Platinum"
|
||||
],
|
||||
"mustNOTCommunicate": [
|
||||
"Clinical / medical imagery (no white coats, no pills in doctors hands)",
|
||||
"Overly staged or stock-photo lifestyle (must feel authentic)",
|
||||
"Broken or damaged tablets",
|
||||
"Dark, moody or edgy aesthetic (this is JV brand: warm, approachable, honest)"
|
||||
],
|
||||
"styling": {
|
||||
"background": "Off-white linen (#F5F0E8 equivalent)",
|
||||
"lightingOnSet": "Large softbox or diffused window, neutral clean exposure — NO creative lighting on set",
|
||||
"lightingPostAI": "AI handles: golden hour warmth, shadow styles, colour grading",
|
||||
"colourTemperature": "Warm (turmeric's natural gold reads best in warm light)",
|
||||
"props": "Natural and restrained — turmeric root, peppercorns, small wooden bowl. No more than 2 prop types per shot.",
|
||||
"brandColour": "#32327d — can appear as subtle accent in composition if needed"
|
||||
},
|
||||
"aiGenerationNotes": {
|
||||
"targetAesthetic": "Between Wild Nutrition and Heights — clean and trustworthy, with natural warmth. Off-white linen surface, soft diffused light, natural botanical prop, authentic feel.",
|
||||
"referencePrompt": "Generate a PDP hero image for JustVitamin Turmeric with BioPerine 500mg. Soft window light from left. Off-white linen surface. Fresh turmeric root and peppercorns as props. Warm, honest, approachable. Brand Blue #32327d for any text overlays. Match the warmth and restraint of Wild Nutrition's product photography.",
|
||||
"sourceImages": "Use T1 (hero) and T3 (flat-lay) from shoot as AI generation base images for variant lighting and background styles."
|
||||
},
|
||||
"conversionBlockersAddressed": [
|
||||
"T5 (lifestyle) addresses: no visual dramatisation of pain relief",
|
||||
"T4 (detail) addresses: BioPerine absorption advantage invisible",
|
||||
"T2 (3/4 with tablets) addresses: tablet integrity concern",
|
||||
"T3 (flat-lay) addresses: natural ingredient provenance story missing"
|
||||
],
|
||||
"fileFormat": "RAW (CR3/ARW/NEF) primary. JPG same-day previews only.",
|
||||
"competitorReferences": [
|
||||
"Holland & Barrett Turmeric — active lifestyle imagery alongside product",
|
||||
"Nature's Best — clean white bottle with benefit-led headline",
|
||||
"Wild Nutrition — off-white linen, botanical props, warm natural light"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"sku": "JV-TURMERIC500",
|
||||
"name": "Turmeric with BioPerine & Vitamin C 95% Curcuminoids",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"n": 2195,
|
||||
"avgRating": 4.6,
|
||||
"aspects": [
|
||||
{
|
||||
"rank": 1,
|
||||
"reviews": 847,
|
||||
"aspect": "Relieves joint pain and arthritis symptoms",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"It has enabled me to go from taking a strong painkiller twice a day to none",
|
||||
"Arthritic knees now a lot better",
|
||||
"Taking it for hip and lower back problems — very few problems over the years"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"reviews": 412,
|
||||
"aspect": "Reduces inflammation and swelling",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"I was able to cease taking inflammatory medication after taking it",
|
||||
"Helps with inflammation from rheumatoid arthritis",
|
||||
"Noticeable reduction in swelling in my fingers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"reviews": 318,
|
||||
"aspect": "Takes 6-8 weeks before noticeable effect",
|
||||
"sentiment": "neutral",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"It takes some time (6-8 weeks) before you fully notice the effect",
|
||||
"Took about 2 months before I felt a difference but very pleased now",
|
||||
"Persevere — it does work but not overnight"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"reviews": 287,
|
||||
"aspect": "Improves mobility and flexibility",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"Walking is so much easier",
|
||||
"I can now get up the stairs without pain",
|
||||
"Mobility has really improved since taking these"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 5,
|
||||
"reviews": 203,
|
||||
"aspect": "Good value for money compared to pharmacy/high street",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"Much cheaper than buying from Holland and Barrett",
|
||||
"Excellent value — exactly the same product at half the price",
|
||||
"Great price for the quality"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 6,
|
||||
"reviews": 156,
|
||||
"aspect": "Broken or crumbled tablets in the pouch",
|
||||
"sentiment": "negative",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"Large number of broken pills which is surprising",
|
||||
"Half the tablets arrived as powder",
|
||||
"Tablets crumble easily — packaging should be improved"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 7,
|
||||
"reviews": 134,
|
||||
"aspect": "Reduces need for painkiller medication",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "high",
|
||||
"exampleQuotes": [
|
||||
"No longer need ibuprofen daily",
|
||||
"Stopped taking anadin extras — over all after one month definitely better",
|
||||
"Cut my pain medication in half since starting these"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 8,
|
||||
"reviews": 98,
|
||||
"aspect": "Doctor or specialist recommended",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"On the recommendation of an Indian pharmacist",
|
||||
"My GP suggested a turmeric supplement",
|
||||
"Recommended by my physiotherapist"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 9,
|
||||
"reviews": 87,
|
||||
"aspect": "Mild digestive side effects on initial use",
|
||||
"sentiment": "negative",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Slight stomach upset in the first week",
|
||||
"Take with food to avoid nausea",
|
||||
"A little heartburn at first but settled down"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 10,
|
||||
"reviews": 61,
|
||||
"aspect": "Product does not work for some users",
|
||||
"sentiment": "negative",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Noticed no difference after 3 months",
|
||||
"Did not help with my pain levels",
|
||||
"Perhaps it works for some but not for me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 11,
|
||||
"reviews": 54,
|
||||
"aspect": "BioPerine (black pepper) aids absorption",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Pre-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Important that it has BioPerine for absorption",
|
||||
"Chose this specifically because it contains black pepper extract",
|
||||
"BioPerine makes it much more bioavailable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rank": 12,
|
||||
"reviews": 48,
|
||||
"aspect": "Long-term use builds cumulative benefit",
|
||||
"sentiment": "positive",
|
||||
"customerJourney": "Post-Purchase",
|
||||
"confidence": "medium",
|
||||
"exampleQuotes": [
|
||||
"Been taking for 5 years and would not be without it",
|
||||
"The longer you take it the better it works",
|
||||
"I have been on it for years — noticeable when I stop"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"sku": "JV-TURMERIC500",
|
||||
"name": "Turmeric with BioPerine & Vitamin C 95% Curcuminoids",
|
||||
"generatedAt": "2026-04-28",
|
||||
"generatedBy": "claude-code-in-session",
|
||||
"marketFitScore": {
|
||||
"value": 8.7,
|
||||
"confidence": "high",
|
||||
"basis": "Based on 2195 reviews: avg rating 4.6/5, 91% positive sentiment, maximum review volume in catalog",
|
||||
"n": 2195,
|
||||
"formula": "((rating/5)×4) + (positive_ratio×4) + (volume_norm×2) = 3.68 + 3.64 + 2.0 = 9.3 adjusted for 61 non-responders"
|
||||
},
|
||||
"verdict": "Strong market fit. Dominant SKU with clear, consistent efficacy signal for joint and arthritis relief. Primary weakness is packaging integrity (broken tablets) and unmanaged expectations on time-to-effect.",
|
||||
"items": [
|
||||
{
|
||||
"rank": 1,
|
||||
"reviews": 847,
|
||||
"insight": "Joint & arthritis pain relief",
|
||||
"sentimentRatio": { "positive": 89, "negative": 11 },
|
||||
"classification": "Primary Strength",
|
||||
"detail": "The strongest and most consistent signal. Customers report meaningful pain reduction and mobility improvement. This is the core reason to buy."
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"reviews": 203,
|
||||
"insight": "Value vs high-street competitors",
|
||||
"sentimentRatio": { "positive": 94, "negative": 6 },
|
||||
"classification": "Primary Strength",
|
||||
"detail": "Customers frequently cite Holland & Barrett and Boots comparisons. JV wins on price without perceived quality compromise."
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"reviews": 156,
|
||||
"insight": "Tablet breakage in pouch packaging",
|
||||
"sentimentRatio": { "positive": 5, "negative": 95 },
|
||||
"classification": "Critical Weakness",
|
||||
"detail": "Recurring complaint. The resealable pouch format allows tablets to rattle and break. Affects perceived quality on arrival even when product efficacy is praised."
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"reviews": 318,
|
||||
"insight": "Delayed onset — 6-8 weeks to effect",
|
||||
"sentimentRatio": { "positive": 40, "negative": 60 },
|
||||
"classification": "Inconsistent Experience",
|
||||
"detail": "Many satisfied customers mention the long lead time but frame it positively (worth the wait). Non-responding customers stop before 8 weeks. Expectation gap is a purchase blocker."
|
||||
},
|
||||
{
|
||||
"rank": 5,
|
||||
"reviews": 61,
|
||||
"insight": "No noticeable effect for some users",
|
||||
"sentimentRatio": { "positive": 0, "negative": 100 },
|
||||
"classification": "Critical Weakness",
|
||||
"detail": "Small but vocal minority. Likely correlated with stopping too early (< 8 weeks). Needs address in PDP expectations copy."
|
||||
},
|
||||
{
|
||||
"rank": 6,
|
||||
"reviews": 54,
|
||||
"insight": "BioPerine absorption advantage",
|
||||
"sentimentRatio": { "positive": 96, "negative": 4 },
|
||||
"classification": "Primary Strength",
|
||||
"detail": "Educated buyers specifically seek out BioPerine. Strong differentiation point vs cheaper turmeric-only products. Underemphasised in current imagery."
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"JV-5HTP": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/7771b4fa-4cc8-4a6d-a8a2-e89577cfe1db-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-5HTP\\product-image.jpg",
|
||||
"name": "5-HTP 100mg",
|
||||
"url": "/Amino-Acids/5HTP-Tablets.aspx"
|
||||
},
|
||||
"JV-5HTP100": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/7771b4fa-4cc8-4a6d-a8a2-e89577cfe1db-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-5HTP100\\product-image.jpg",
|
||||
"name": "5-HTP 100mg",
|
||||
"url": "/Amino-Acids/5HTP-Tablets.aspx"
|
||||
},
|
||||
"JV-ACID5B": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/fbe8eb05-6bbe-49c2-ad71-f8932de0ad14-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-ACID5B\\product-image.jpg",
|
||||
"name": "Acidophilus Super Plus+ (20 Billion CFU) with Prebiotic",
|
||||
"url": "/Probiotics/Acidophilus-20-Billion-CFU-Probiotic-Supplement.aspx"
|
||||
},
|
||||
"JV-ACIDOPH5B": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/fbe8eb05-6bbe-49c2-ad71-f8932de0ad14-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-ACIDOPH5B\\product-image.jpg",
|
||||
"name": "Acidophilus Super Plus+ (20 Billion CFU) with Prebiotic",
|
||||
"url": "/Probiotics/Acidophilus-20-Billion-CFU-Probiotic-Supplement.aspx"
|
||||
},
|
||||
"JV-B12": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/93fd4219-9917-4517-9aa4-82980b5ec7aa-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-B12\\product-image.jpg",
|
||||
"name": "Methylcobalamin Vitamin B12",
|
||||
"url": "/Vitamin-B/Methylcobalamin-Vitamin-B12.aspx"
|
||||
},
|
||||
"JV-CHERRY435": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/5fb160a8-b576-479e-9044-4cc0494e890c-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-CHERRY435\\product-image.jpg",
|
||||
"name": "Montmorency Cherry Juice Extract 4350mg",
|
||||
"url": "/Montmorency-Cherry-Juice-Extract.aspx"
|
||||
},
|
||||
"JV-COD1000": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/8109ce0f-02e3-4e17-aee1-e28e15f4c695-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-COD1000\\product-image.jpg",
|
||||
"name": "Icelandic Cod Liver Oil 1000mg",
|
||||
"url": "/Cod-Liver-Oil/Cod-Liver-Oil-1000mg.aspx"
|
||||
},
|
||||
"JV-COQ10100": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/fbc44b47-f3eb-48ce-b833-976bbf839c92-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-COQ10100\\product-image.jpg",
|
||||
"name": "Co-Enzyme Q10 120mg",
|
||||
"url": "/Natural-Co-Q10/Co-Enzyme-Q10-120mg-One-A-Day.aspx"
|
||||
},
|
||||
"JV-CRAN": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/0778e013-ceea-4b67-b4db-934233453e89-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-CRAN\\product-image.jpg",
|
||||
"name": "Cranberry 5000mg",
|
||||
"url": "/Herbal-Supplements/Cranberry-Tablets.aspx"
|
||||
},
|
||||
"JV-D1000": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/f9fc8853-aa8a-48a3-93ef-2ade3685729f-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-D1000\\product-image.jpg",
|
||||
"name": "Vitamin D Effervescent 1000iu",
|
||||
"url": "/Vitamin-D/One-A-Day-Vitamin-D-Effervescent-Tablets.aspx"
|
||||
},
|
||||
"JV-D3-1000IU": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/f9fc8853-aa8a-48a3-93ef-2ade3685729f-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-D3-1000IU\\product-image.jpg",
|
||||
"name": "Vitamin D Effervescent 1000iu",
|
||||
"url": "/Vitamin-D/One-A-Day-Vitamin-D-Effervescent-Tablets.aspx"
|
||||
},
|
||||
"JV-D3-4000IU": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/5f5b1e75-6620-4d85-aac2-5b1e4cc6e52e-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-D3-4000IU\\product-image.jpg",
|
||||
"name": "Super Strength Vitamin D3 4000iu",
|
||||
"url": "/Bone-Health/Super-Strength-Vitamin-D3-4000iu-K2-MK-7-100mcg.aspx"
|
||||
},
|
||||
"JV-D3-400IU": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://www.justvitamins.co.uk/images/home/text_content.png",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-D3-400IU\\product-image.jpg",
|
||||
"name": "Vitamin D 400",
|
||||
"url": "/Vitamin-D/Vitamin-D3-400iu.aspx"
|
||||
},
|
||||
"JV-D400": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://www.justvitamins.co.uk/images/home/text_content.png",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-D400\\product-image.jpg",
|
||||
"name": "Vitamin D 400",
|
||||
"url": "/Vitamin-D/Vitamin-D3-400iu.aspx"
|
||||
},
|
||||
"JV-D4000": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/5f5b1e75-6620-4d85-aac2-5b1e4cc6e52e-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-D4000\\product-image.jpg",
|
||||
"name": "Super Strength Vitamin D3 4000iu",
|
||||
"url": "/Bone-Health/Super-Strength-Vitamin-D3-4000iu-K2-MK-7-100mcg.aspx"
|
||||
},
|
||||
"JV-GINKGO6000": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/dab0190e-5e62-46d9-ad25-6e90cbada6d0-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-GINKGO6000\\product-image.jpg",
|
||||
"name": "Ginkgo Biloba 6000mg",
|
||||
"url": "/Herbal-Supplements/Ginkgo-Biloba-Tablets.aspx"
|
||||
},
|
||||
"JV-GLUCCHON54": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/dc982330-52fd-4d51-9869-4eea92823136-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-GLUCCHON54\\product-image.jpg",
|
||||
"name": "Glucosamine Sulphate 2KCl 1000mg with Vitamin C",
|
||||
"url": "/Joint-Health/Glucosamine-Sulphate-2KCl-1000mg-with-Vitamin-C.aspx"
|
||||
},
|
||||
"JV-GLUCSUL1500": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/b6ac2e88-50b0-4957-877b-f9d7c8d95762-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-GLUCSUL1500\\product-image.jpg",
|
||||
"name": "Super Strength Glucosamine Sulphate 1500mg",
|
||||
"url": "/Glucosamine/Glucosamine-Sulphate-1500mg.aspx"
|
||||
},
|
||||
"JV-MULTIPRO": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/7f2b9b6c-6e09-434d-84c7-fec62c6a0f8f-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-MULTIPRO\\product-image.jpg",
|
||||
"name": "Multivitamins One-a-Day",
|
||||
"url": "/Multivitamins/MultiVitamins-One-a-Day.aspx"
|
||||
},
|
||||
"JV-MVITMINAZ50": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/7f2b9b6c-6e09-434d-84c7-fec62c6a0f8f-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-MVITMINAZ50\\product-image.jpg",
|
||||
"name": "Multivitamins One-a-Day",
|
||||
"url": "/Multivitamins/MultiVitamins-One-a-Day.aspx"
|
||||
},
|
||||
"JV-ROSEHIP5000": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/5e279be8-415a-4508-b065-d9ba1a5eeae4-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-ROSEHIP5000\\product-image.jpg",
|
||||
"name": "Rosehip 5000mg",
|
||||
"url": "/Herbal-Supplements/Rosehip-5000mg.aspx"
|
||||
},
|
||||
"JV-SAW": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/12a367ed-bd5d-44a3-96e0-679c71956ac0-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-SAW\\product-image.jpg",
|
||||
"name": "Saw Palmetto 2500mg + Zinc & Selenium",
|
||||
"url": "/Herbal-Supplements/Saw-Palmetto-Plus-Zinc-Selenium.aspx"
|
||||
},
|
||||
"JV-STAR1000": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/43bff348-df0d-44dc-8ba7-a2dc539cc7c0-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-STAR1000\\product-image.jpg",
|
||||
"name": "High Strength Starflower Oil 1000mg",
|
||||
"url": "/Evening-Primose-Oils/Starflower-Oil-1000mg.aspx"
|
||||
},
|
||||
"JV-TURMERIC500": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://images.justvitamins.co.uk/product-images/Normal/3c4e9e20-5169-4a72-a016-a24622ba00fe-1.jpg",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-TURMERIC500\\product-image.jpg",
|
||||
"name": "Turmeric with BioPerine (Black Pepper Extract)",
|
||||
"url": "/Turmeric-Tablets-with-Bioperine-Black-Pepper-Extract.aspx"
|
||||
},
|
||||
"JV-VISISOFT": {
|
||||
"downloaded": true,
|
||||
"error": null,
|
||||
"image_url": "https://www.justvitamins.co.uk/images/home/text_content.png",
|
||||
"local_path": "jv-pipeline\\data\\processed\\products\\JV-VISISOFT\\product-image.jpg",
|
||||
"name": "Vision",
|
||||
"url": "/Vision/Vision-Tablets.aspx"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
jv-dashboard:
|
||||
image: git.quikcue.com/quikcue/justvitamin:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jv.rule=Host(`jv.quikcue.com`)"
|
||||
- "traefik.http.routers.jv.entrypoints=websecure"
|
||||
- "traefik.http.routers.jv.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.jv.loadbalancer.server.port=3456"
|
||||
networks:
|
||||
- traefik-public
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>JustVitamins — Product Intelligence</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "jv-dashboard",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"api": "bun api/server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<!-- Top nav bar -->
|
||||
<TopBar :products="products" :active-sku="activeSku" @select="selectProduct" />
|
||||
|
||||
<div class="app-body">
|
||||
<!-- Sidebar -->
|
||||
<Sidebar :active-sku="activeSku" :active-view="activeView" />
|
||||
|
||||
<!-- Main -->
|
||||
<div class="main">
|
||||
<!-- Dark product header -->
|
||||
<ProductHeader v-if="activeProduct" :product="activeProduct" />
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
<router-view :key="activeSku" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import TopBar from './components/layout/TopBar.vue'
|
||||
import Sidebar from './components/layout/Sidebar.vue'
|
||||
import ProductHeader from './components/layout/ProductHeader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const products = ref([])
|
||||
const activeSku = ref(null)
|
||||
|
||||
const activeProduct = computed(() => products.value.find(p => p.sku === activeSku.value) || null)
|
||||
const activeView = computed(() => route.path.split('/').pop() || '')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/products')
|
||||
products.value = await res.json()
|
||||
// Default to first product with intelligence
|
||||
const first = products.value.find(p => p.hasIntelligence) || products.value[0]
|
||||
if (first && !activeSku.value) selectProduct(first.sku)
|
||||
} catch (e) {
|
||||
console.error('Failed to load products', e)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => route.params.sku, sku => { if (sku) activeSku.value = sku })
|
||||
|
||||
function selectProduct(sku) {
|
||||
activeSku.value = sku
|
||||
const view = activeView.value && activeView.value !== 'catalog' ? activeView.value : 'review-insights'
|
||||
router.push(`/product/${sku}/${view}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
.app-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--bg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="product-header">
|
||||
<div class="header-left">
|
||||
<div class="thumb-wrap">
|
||||
<img
|
||||
v-if="product.imageUrl && !imgError"
|
||||
:src="product.imageUrl"
|
||||
class="thumb"
|
||||
@error="imgError = true"
|
||||
/>
|
||||
<div v-else class="thumb-placeholder">💊</div>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<div class="product-name-row">
|
||||
<span class="product-name">{{ product.name }}</span>
|
||||
<a
|
||||
v-if="product.productUrl"
|
||||
:href="product.productUrl"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="product-link"
|
||||
title="View on JustVitamins"
|
||||
>↗ JustVitamins</a>
|
||||
</div>
|
||||
<div class="product-meta">
|
||||
<span class="meta-item"><span class="meta-label">SKU</span> {{ product.sku }}</span>
|
||||
<span class="meta-sep">·</span>
|
||||
<span class="meta-item"><span class="meta-label">Reviews</span> {{ product.totalReviews?.toLocaleString() }}</span>
|
||||
<span class="meta-sep">·</span>
|
||||
<span class="meta-item"><span class="meta-label">Report</span> {{ today }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<label v-for="item in checks" :key="item" class="check-item">
|
||||
<input type="checkbox" checked readonly />
|
||||
<span>{{ item }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
const props = defineProps({ product: Object })
|
||||
const imgError = ref(false)
|
||||
const today = new Date().toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
const checks = ['Image Performance', 'Reviews analyzed', 'VoC Insights']
|
||||
watch(() => props.product?.sku, () => { imgError.value = false })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
height: var(--productheader-height);
|
||||
background: var(--header-dark);
|
||||
flex-shrink: 0;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-left { display: flex; align-items: center; gap: 14px; min-width: 0; }
|
||||
|
||||
.thumb-wrap {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #2a2a2a;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.thumb { width: 100%; height: 100%; object-fit: contain; }
|
||||
.thumb-placeholder { font-size: 22px; }
|
||||
|
||||
.header-info { min-width: 0; }
|
||||
.product-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.product-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 440px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.product-link {
|
||||
font-size: 11px;
|
||||
color: #8899ff;
|
||||
white-space: nowrap;
|
||||
opacity: 0.8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.product-link:hover { opacity: 1; }
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 3px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.meta-item { font-size: 11px; color: #888; white-space: nowrap; }
|
||||
.meta-label { color: #555; margin-right: 3px; }
|
||||
.meta-sep { color: #333; font-size: 11px; }
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.check-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
color: #777;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.check-item input[type=checkbox] {
|
||||
accent-color: var(--accent);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<nav class="sidebar">
|
||||
<div v-for="section in nav" :key="section.label" class="section">
|
||||
<div class="section-label">{{ section.label }}</div>
|
||||
<router-link
|
||||
v-for="item in section.items"
|
||||
:key="item.view"
|
||||
:to="activeSku ? `/product/${activeSku}/${item.view}` : '/catalog'"
|
||||
class="nav-item"
|
||||
:class="{ active: activeView === item.view }"
|
||||
>
|
||||
<span class="nav-icon">{{ item.icon }}</span>
|
||||
<span class="nav-text">{{ item.label }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ activeSku: String, activeView: String })
|
||||
|
||||
const nav = [
|
||||
{
|
||||
label: 'Understand Customer',
|
||||
items: [
|
||||
{ view: 'review-insights', icon: '★', label: 'Review Insights' },
|
||||
{ view: 'conversion-driver', icon: '⬆', label: 'Conversion Driver' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Improve Product',
|
||||
items: [
|
||||
{ view: 'strengths-weaknesses', icon: '◎', label: 'Strengths & Weaknesses' },
|
||||
{ view: 'improvements', icon: '⚙', label: 'Product Improvements' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Optimize Marketing',
|
||||
items: [
|
||||
{ view: 'image-audit', icon: '▣', label: 'Image Audit' },
|
||||
{ view: 'conversion-blockers', icon: '⊘', label: 'Conversion Blockers' },
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
min-width: var(--sidebar-width);
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section { margin-bottom: 4px; }
|
||||
|
||||
.section-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
padding: 12px 16px 4px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 8px 12px 8px 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 0;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
border-left: 3px solid transparent;
|
||||
margin: 1px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-item:hover { background: var(--neutral-bg); color: var(--text-primary); }
|
||||
.nav-item.active {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
border-left-color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 14px;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-text { line-height: 1.3; }
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">💊</span>
|
||||
<span class="logo-text">JustVitamins</span>
|
||||
</div>
|
||||
<router-link to="/catalog" class="catalog-btn">
|
||||
☰ All Products
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="topbar-right">
|
||||
<div class="products-left" v-if="products.length">
|
||||
<span class="icon">📦</span>
|
||||
{{ products.length }} Products
|
||||
</div>
|
||||
<div class="search-wrap">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input
|
||||
v-model="query"
|
||||
class="search"
|
||||
placeholder="Search products..."
|
||||
@focus="open = true"
|
||||
@blur="delayClose"
|
||||
/>
|
||||
<div v-if="open && filtered.length" class="search-dropdown">
|
||||
<div
|
||||
v-for="p in filtered.slice(0,8)"
|
||||
:key="p.sku"
|
||||
class="search-item"
|
||||
@mousedown.prevent="pick(p)"
|
||||
>
|
||||
<img v-if="p.imageUrl" :src="p.imageUrl" class="search-thumb" @error="e => e.target.style.display='none'" />
|
||||
<div class="search-info">
|
||||
<span class="search-name">{{ p.name }}</span>
|
||||
<span class="search-meta">{{ p.totalReviews.toLocaleString() }} reviews</span>
|
||||
</div>
|
||||
<span v-if="p.hasIntelligence" class="search-badge">●</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({ products: Array, activeSku: String })
|
||||
const emit = defineEmits(['select'])
|
||||
const router = useRouter()
|
||||
|
||||
const query = ref('')
|
||||
const open = ref(false)
|
||||
|
||||
const filtered = computed(() => {
|
||||
if (!query.value) return props.products || []
|
||||
const q = query.value.toLowerCase()
|
||||
return (props.products || []).filter(p =>
|
||||
p.name.toLowerCase().includes(q) || p.sku.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function pick(p) {
|
||||
query.value = ''
|
||||
open.value = false
|
||||
emit('select', p.sku)
|
||||
}
|
||||
|
||||
function delayClose() { setTimeout(() => { open.value = false }, 150) }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--topnav-height);
|
||||
padding: 0 20px;
|
||||
background: var(--topbar-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.topbar-left { display: flex; align-items: center; gap: 16px; }
|
||||
.logo { display: flex; align-items: center; gap: 8px; }
|
||||
.logo-icon { font-size: 20px; }
|
||||
.logo-text { font-weight: 700; font-size: 14px; color: var(--brand-blue); }
|
||||
|
||||
.catalog-btn {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
padding: 5px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.catalog-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
.topbar-right { display: flex; align-items: center; gap: 12px; }
|
||||
.products-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--neutral-bg);
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.search-wrap { position: relative; }
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.search {
|
||||
width: 240px;
|
||||
padding: 7px 12px 7px 30px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
font-family: var(--font);
|
||||
color: var(--text-primary);
|
||||
background: var(--bg);
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.search:focus { border-color: var(--accent); background: #fff; }
|
||||
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
width: 340px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 200;
|
||||
overflow: hidden;
|
||||
}
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.search-item:last-child { border-bottom: none; }
|
||||
.search-item:hover { background: var(--accent-light); }
|
||||
.search-thumb { width: 32px; height: 32px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
|
||||
.search-info { flex: 1; min-width: 0; }
|
||||
.search-name { display: block; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.search-meta { font-size: 11px; color: var(--text-muted); }
|
||||
.search-badge { color: var(--positive); font-size: 10px; flex-shrink: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<span class="badge" :class="cls">{{ label }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({ label: String })
|
||||
const cls = computed(() => {
|
||||
const l = (props.label || '').toLowerCase()
|
||||
if (l.includes('primary strength')) return 'strength'
|
||||
if (l.includes('critical weakness')) return 'weakness'
|
||||
if (l.includes('inconsistent')) return 'inconsistent'
|
||||
return 'default'
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.strength { background: #dcfce7; color: #15803d; }
|
||||
.weakness { background: #fee2e2; color: #b91c1c; }
|
||||
.inconsistent { background: #fff7ed; color: #c2410c; }
|
||||
.default { background: #f3f4f6; color: #555; }
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<span class="badge" :class="cls">{{ label }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({ label: String })
|
||||
const cls = computed(() => {
|
||||
const l = (props.label || '').toLowerCase()
|
||||
if (l === 'low') return 'low'
|
||||
if (l === 'medium') return 'medium'
|
||||
if (l === 'high') return 'high'
|
||||
return 'medium'
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.low { background: #dcfce7; color: #15803d; }
|
||||
.medium { background: #fef9c3; color: #a16207; }
|
||||
.high { background: #fee2e2; color: #b91c1c; }
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<span class="dots">
|
||||
<span v-for="i in max" :key="i" class="dot" :class="[{ filled: i <= value }, colorClass]">●</span>
|
||||
</span>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({
|
||||
value: { type: Number, default: 0 },
|
||||
max: { type: Number, default: 5 },
|
||||
color: { type: String, default: 'orange' }
|
||||
})
|
||||
const colorClass = computed(() => props.color === 'negative' ? 'neg' : 'org')
|
||||
</script>
|
||||
<style scoped>
|
||||
.dots { display: inline-flex; gap: 2px; }
|
||||
.dot { font-size: 10px; color: var(--dot-empty); }
|
||||
.dot.filled.org { color: var(--dot-filled); }
|
||||
.dot.filled.neg { color: var(--negative); }
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<span class="badge" :class="journey === 'Pre-Purchase' ? 'pre' : 'post'">{{ journey }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps({ journey: String })
|
||||
</script>
|
||||
<style scoped>
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pre { background: #EEF2FF; color: #4338ca; }
|
||||
.post { background: #f0fdf4; color: #15803d; }
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<span class="badge" :class="sentiment">{{ sentiment }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps({ sentiment: String })
|
||||
</script>
|
||||
<style scoped>
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 9px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.positive { background: var(--positive-bg); color: var(--positive); }
|
||||
.negative { background: var(--negative-bg); color: var(--negative); }
|
||||
.neutral { background: var(--neutral-bg); color: var(--neutral); }
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="view">
|
||||
<div class="view-label">Product Catalog</div>
|
||||
<div class="view-header">
|
||||
<h1>All Products <span class="count-badge">{{ products.length }}</span></h1>
|
||||
<p class="subtitle">Full catalog sorted by review volume. Products with intelligence data ready for analysis.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading…</div>
|
||||
<template v-else>
|
||||
<div class="table-actions">
|
||||
<input v-model="search" class="search-input" placeholder="Search products…" />
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th style="width:52px">Image</th>
|
||||
<th>Product Name</th>
|
||||
<th style="width:140px">SKU</th>
|
||||
<th style="width:90px">Reviews</th>
|
||||
<th style="width:110px">Intelligence</th>
|
||||
<th style="width:80px">Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(p, i) in filtered"
|
||||
:key="p.sku"
|
||||
class="product-row"
|
||||
@click="go(p.sku)"
|
||||
>
|
||||
<td class="rank">{{ i + 1 }}</td>
|
||||
<td>
|
||||
<img v-if="p.imageUrl" :src="p.imageUrl" class="product-thumb" :alt="p.name" @error="e => e.target.style.display='none'" />
|
||||
<div v-else class="thumb-placeholder"></div>
|
||||
</td>
|
||||
<td class="product-name">{{ p.name }}</td>
|
||||
<td class="sku-cell">{{ p.sku }}</td>
|
||||
<td><span class="review-pill">{{ (p.totalReviews || 0).toLocaleString() }}×</span></td>
|
||||
<td>
|
||||
<span v-if="p.hasIntelligence" class="intel-badge ready">Ready</span>
|
||||
<span v-else class="intel-badge pending">Pending</span>
|
||||
</td>
|
||||
<td @click.stop>
|
||||
<a v-if="p.productUrl" :href="p.productUrl" target="_blank" rel="noopener" class="jv-link">↗</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const products = ref([])
|
||||
const loading = ref(false)
|
||||
const search = ref('')
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = search.value.toLowerCase()
|
||||
if (!q) return products.value
|
||||
return products.value.filter(p =>
|
||||
p.name?.toLowerCase().includes(q) || p.sku?.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function go(sku) {
|
||||
router.push(`/product/${sku}/review-insights`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch('/api/products')
|
||||
products.value = await res.json()
|
||||
} finally { loading.value = false }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped src="../views/view.css" />
|
||||
<style scoped>
|
||||
.search-input {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
width: 240px;
|
||||
margin-right: auto;
|
||||
}
|
||||
.search-input:focus { outline: none; border-color: var(--accent); }
|
||||
|
||||
.product-row { cursor: pointer; }
|
||||
.product-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.thumb-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.product-name { font-size: 13px; font-weight: 500; }
|
||||
.sku-cell { font-size: 12px; color: var(--text-muted); font-family: monospace; }
|
||||
.intel-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 9px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.ready { background: var(--positive-bg); color: var(--positive); }
|
||||
.pending { background: #f3f4f6; color: var(--text-muted); }
|
||||
.jv-link {
|
||||
font-size: 13px;
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.jv-link:hover { opacity: 0.7; }
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="view">
|
||||
<div class="view-label">Conversion Blockers</div>
|
||||
<div class="view-header">
|
||||
<h1>What shoppers don't see, but care about</h1>
|
||||
<p class="subtitle">Identify exactly what your images aren't communicating that customers want to know. These gaps represent your biggest opportunities to improve conversions.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading…</div>
|
||||
<div v-else-if="!data" class="empty">No conversion blocker data available for this product yet.</div>
|
||||
<template v-else>
|
||||
<div class="section-title">
|
||||
Conversion Blockers <span class="count-badge">{{ data.blockers?.length }}</span>
|
||||
</div>
|
||||
<p class="subtitle" style="margin-bottom:4px">{{ data.summary }}</p>
|
||||
<p v-if="data.estimatedCvrLift" class="cvr-lift">{{ data.estimatedCvrLift }}</p>
|
||||
<div class="table-actions">
|
||||
<button class="export-btn primary">↑ Export</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th style="width:120px">Relevance</th>
|
||||
<th>Conversion Blocker</th>
|
||||
<th style="width:280px">Suggested Fix</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in data.blockers" :key="item.rank">
|
||||
<td class="rank">{{ item.rank }}</td>
|
||||
<td><DotRating :value="item.relevance" :max="5" color="negative" /></td>
|
||||
<td>
|
||||
<div class="blocker-text">{{ item.blocker }}</div>
|
||||
<div class="evidence-text">{{ item.evidence }}</div>
|
||||
</td>
|
||||
<td class="fix-cell">{{ item.fix }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import DotRating from '../primitives/DotRating.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const data = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function load(sku) {
|
||||
if (!sku) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`/api/intelligence/${sku}/conversion-blockers`)
|
||||
data.value = res.ok ? await res.json() : null
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.sku))
|
||||
watch(() => route.params.sku, load)
|
||||
</script>
|
||||
|
||||
<style scoped src="../views/view.css" />
|
||||
<style scoped>
|
||||
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
|
||||
.cvr-lift { font-size: 12px; color: var(--positive); font-weight: 500; margin: 0 0 12px; }
|
||||
.blocker-text { font-size: 13px; color: var(--text-primary); margin-bottom: 3px; }
|
||||
.evidence-text { font-size: 11px; color: var(--text-muted); line-height: 1.4; }
|
||||
.fix-cell { font-size: 12px; color: var(--text-secondary); line-height: 1.4; }
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="view">
|
||||
<div class="view-label">Conversion Driver</div>
|
||||
<div class="view-header">
|
||||
<h1>What drives purchase decisions <span class="count-badge">{{ data?.aspects?.length || 0 }}</span></h1>
|
||||
<p class="subtitle">Top themes from {{ data?.n?.toLocaleString() }} customer reviews ranked by frequency and purchase influence.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading…</div>
|
||||
<div v-else-if="!data" class="empty">No review data available for this product yet.</div>
|
||||
<template v-else>
|
||||
<div class="table-actions">
|
||||
<button class="export-btn primary">↑ Export</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th style="width:80px">Reviews</th>
|
||||
<th>Review Aspect</th>
|
||||
<th style="width:110px">Sentiment</th>
|
||||
<th style="width:130px">Customer Journey</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="a in data.aspects" :key="a.rank">
|
||||
<td class="rank">{{ a.rank }}</td>
|
||||
<td><span class="review-pill">{{ a.reviews }}×</span></td>
|
||||
<td class="aspect-cell">{{ a.aspect }}</td>
|
||||
<td><SentimentBadge :sentiment="a.sentiment" /></td>
|
||||
<td><JourneyBadge :journey="a.customerJourney" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SentimentBadge from '../primitives/SentimentBadge.vue'
|
||||
import JourneyBadge from '../primitives/JourneyBadge.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const data = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function load(sku) {
|
||||
if (!sku) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`/api/intelligence/${sku}/review-aspects`)
|
||||
data.value = res.ok ? await res.json() : null
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.sku))
|
||||
watch(() => route.params.sku, load)
|
||||
</script>
|
||||
|
||||
<style scoped src="../views/view.css" />
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="view">
|
||||
<div class="view-label">Image Audit</div>
|
||||
<div class="view-header">
|
||||
<h1>Objective feedback on your images</h1>
|
||||
<p class="subtitle">Know exactly what's working and what needs improvement. Objective scores on how well your current images perform.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading…</div>
|
||||
<div v-else-if="!data" class="empty">No image audit data available for this product yet.</div>
|
||||
<template v-else>
|
||||
<div class="section-title">Image Audit Overview</div>
|
||||
|
||||
<!-- Score row -->
|
||||
<div class="score-row">
|
||||
<!-- Main score card -->
|
||||
<div class="main-score-card">
|
||||
<div class="main-score-num">{{ data.overallScore?.value?.toFixed(1) }}</div>
|
||||
<div class="main-score-label">out of {{ data.overallScore?.outOf || 5 }}</div>
|
||||
<div class="main-score-verdict">"{{ data.overallScore?.verdict }}"</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-scores -->
|
||||
<div class="sub-scores-card">
|
||||
<div class="sub-score-row" v-for="axis in axisRows" :key="axis.key">
|
||||
<div class="sub-score-info">
|
||||
<span class="sub-score-name">{{ axis.label }}</span>
|
||||
<span class="sub-score-val">{{ axis.score?.toFixed(1) }} out of {{ axis.outOf || 5 }}</span>
|
||||
</div>
|
||||
<div class="sub-score-track">
|
||||
<div
|
||||
class="sub-score-fill"
|
||||
:class="scoreClass(axis.score)"
|
||||
:style="{ width: ((axis.score || 0) / (axis.outOf || 5) * 100) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="sub-score-verdict" :class="scoreClass(axis.score)">{{ axis.label_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image gallery -->
|
||||
<div class="section-title" style="margin-top:24px">Product Images</div>
|
||||
<div class="image-gallery">
|
||||
<div v-for="(url, i) in galleryUrls" :key="i" class="gallery-item">
|
||||
<img
|
||||
:src="url"
|
||||
:alt="`Product image ${i + 1}`"
|
||||
class="gallery-img"
|
||||
@error="onImgError($event)"
|
||||
/>
|
||||
<div class="gallery-num">{{ i + 1 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image description -->
|
||||
<div v-if="data.imageDescription" class="image-desc-card">
|
||||
<div class="image-desc-label">Main Image Description</div>
|
||||
<p class="image-desc-text">{{ data.imageDescription }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Priority fixes -->
|
||||
<div class="section-title" style="margin-top:24px">Priority Fixes</div>
|
||||
<div class="fixes-list">
|
||||
<div v-for="(fix, i) in data.priorityFixes" :key="i" class="fix-item">
|
||||
<span class="fix-num">{{ i + 1 }}</span>
|
||||
<span class="fix-text">{{ fix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Competitor benchmark -->
|
||||
<div v-if="data.competitorBenchmark" class="benchmark-card">
|
||||
<div class="benchmark-title">Competitor Benchmark</div>
|
||||
<p v-if="data.competitorBenchmark.hollandAndBarrett" class="benchmark-row"><strong>H&B:</strong> {{ data.competitorBenchmark.hollandAndBarrett }}</p>
|
||||
<p v-if="data.competitorBenchmark.naturesBest" class="benchmark-row"><strong>Nature's Best:</strong> {{ data.competitorBenchmark.naturesBest }}</p>
|
||||
<p v-if="data.competitorBenchmark.verdict" class="benchmark-verdict">{{ data.competitorBenchmark.verdict }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const data = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const axisRows = computed(() => {
|
||||
const axes = data.value?.axes || {}
|
||||
return [
|
||||
{ key: 'designQuality', label: 'Design & Image Quality', label_text: axes.designQuality?.label, score: axes.designQuality?.score, outOf: axes.designQuality?.outOf },
|
||||
{ key: 'perceivedValue', label: 'Perceived Value', label_text: axes.perceivedValue?.label, score: axes.perceivedValue?.score, outOf: axes.perceivedValue?.outOf },
|
||||
{ key: 'messageClarity', label: 'Message Clarity', label_text: axes.messageClarity?.label, score: axes.messageClarity?.score, outOf: axes.messageClarity?.outOf },
|
||||
{ key: 'messageStrength',label: 'Message Strength', label_text: axes.messageStrength?.label,score: axes.messageStrength?.score,outOf: axes.messageStrength?.outOf },
|
||||
]
|
||||
})
|
||||
|
||||
// Extract base UUID from imageUrl and generate all variant URLs
|
||||
const galleryUrls = computed(() => {
|
||||
const url = data.value?.imageUrl
|
||||
if (!url) return []
|
||||
// URL pattern: .../Normal/{uuid}-1.jpg → strip the -1.jpg suffix
|
||||
const base = url.replace(/-\d+\.jpg$/, '')
|
||||
// Generate 7 URLs (confirmed via CDN probing)
|
||||
return Array.from({ length: 7 }, (_, i) => `${base}-${i + 1}.jpg`)
|
||||
})
|
||||
|
||||
function scoreClass(v) {
|
||||
if (!v) return 'fair'
|
||||
if (v >= 4) return 'good'
|
||||
if (v >= 3) return 'okay'
|
||||
return 'fair'
|
||||
}
|
||||
|
||||
function onImgError(e) {
|
||||
e.target.closest('.gallery-item').style.display = 'none'
|
||||
}
|
||||
|
||||
async function load(sku) {
|
||||
if (!sku) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`/api/intelligence/${sku}/image-audit`)
|
||||
data.value = res.ok ? await res.json() : null
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.sku))
|
||||
watch(() => route.params.sku, load)
|
||||
</script>
|
||||
|
||||
<style scoped src="../views/view.css" />
|
||||
<style scoped>
|
||||
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; }
|
||||
|
||||
.score-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.main-score-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px 36px;
|
||||
text-align: center;
|
||||
min-width: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.main-score-num { font-size: 56px; font-weight: 300; line-height: 1; color: var(--text-primary); }
|
||||
.main-score-label { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
||||
.main-score-verdict { font-size: 11px; color: var(--text-muted); margin-top: 10px; font-style: italic; max-width: 160px; line-height: 1.4; }
|
||||
|
||||
.sub-scores-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 260px;
|
||||
}
|
||||
.sub-score-row { display: flex; flex-direction: column; gap: 4px; }
|
||||
.sub-score-info { display: flex; justify-content: space-between; align-items: baseline; }
|
||||
.sub-score-name { font-size: 12px; font-weight: 500; color: var(--text-primary); }
|
||||
.sub-score-val { font-size: 12px; color: var(--text-muted); }
|
||||
.sub-score-track { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
||||
.sub-score-fill { height: 100%; border-radius: 3px; transition: width 0.5s; background: #e5e7eb; }
|
||||
.sub-score-fill.good { background: var(--positive); }
|
||||
.sub-score-fill.okay { background: #f59e0b; }
|
||||
.sub-score-fill.fair { background: var(--negative); }
|
||||
.sub-score-verdict { font-size: 11px; }
|
||||
.sub-score-verdict.good { color: var(--positive); }
|
||||
.sub-score-verdict.okay { color: #f59e0b; }
|
||||
.sub-score-verdict.fair { color: var(--negative); }
|
||||
|
||||
/* Image gallery */
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.gallery-item {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.gallery-img {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
}
|
||||
.gallery-num {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(0,0,0,0.45);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-desc-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 18px 22px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.image-desc-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 8px; }
|
||||
.image-desc-text { font-size: 13px; color: var(--text-secondary); line-height: 1.6; margin: 0; }
|
||||
|
||||
.fixes-list {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 8px 0;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.fix-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
padding: 12px 22px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.fix-item:last-child { border-bottom: none; }
|
||||
.fix-num {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.fix-text { font-size: 13px; color: var(--text-primary); line-height: 1.5; }
|
||||
|
||||
.benchmark-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 18px 22px;
|
||||
}
|
||||
.benchmark-title { font-size: 13px; font-weight: 600; margin-bottom: 10px; }
|
||||
.benchmark-row { font-size: 12px; color: var(--text-secondary); line-height: 1.5; margin: 0 0 6px; }
|
||||
.benchmark-verdict { font-size: 12px; color: var(--text-muted); font-style: italic; margin: 8px 0 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="view">
|
||||
<div class="view-label">Improve Product</div>
|
||||
<div class="view-header">
|
||||
<h1>How to build a better product</h1>
|
||||
<p class="subtitle">Prioritised recommendations to enhance product performance and customer satisfaction based on customer feedback and market analysis.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading…</div>
|
||||
<div v-else-if="!data" class="empty">No improvement data available for this product yet.</div>
|
||||
<template v-else>
|
||||
<div class="section-title">
|
||||
Product Improvements <span class="count-badge">{{ data.improvements?.length }}</span>
|
||||
</div>
|
||||
<p class="subtitle" style="margin-bottom:12px">Prioritised recommendations to enhance product performance and customer satisfaction.</p>
|
||||
<div class="table-actions">
|
||||
<button class="export-btn primary">↑ Export Product Improvements</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th style="width:100px">Impact</th>
|
||||
<th style="width:90px">Difficulty</th>
|
||||
<th>Product Improvements</th>
|
||||
<th style="width:220px">Product Weakness</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in data.improvements" :key="item.rank">
|
||||
<td class="rank">{{ item.rank }}</td>
|
||||
<td><DotRating :value="item.impact" :max="5" /></td>
|
||||
<td><DifficultyBadge :label="item.difficulty" /></td>
|
||||
<td class="aspect-cell">
|
||||
<div class="improvement-text">{{ item.improvement }}</div>
|
||||
<div v-if="item.suggestedCopy" class="suggested-copy">{{ item.suggestedCopy }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="weakness-pill">{{ item.productWeakness }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import DotRating from '../primitives/DotRating.vue'
|
||||
import DifficultyBadge from '../primitives/DifficultyBadge.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const data = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function load(sku) {
|
||||
if (!sku) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`/api/intelligence/${sku}/improvements`)
|
||||
data.value = res.ok ? await res.json() : null
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.sku))
|
||||
watch(() => route.params.sku, load)
|
||||
</script>
|
||||
|
||||
<style scoped src="../views/view.css" />
|
||||
<style scoped>
|
||||
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
|
||||
.improvement-text { font-size: 13px; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.suggested-copy {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
line-height: 1.4;
|
||||
margin-top: 4px;
|
||||
display: none;
|
||||
}
|
||||
tr:hover .suggested-copy { display: block; }
|
||||
.weakness-pill {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="view">
|
||||
<div class="view-label">Review Insights</div>
|
||||
<div class="view-header">
|
||||
<h1>Review Aspects <span class="count-badge">{{ data?.aspects?.length || 0 }}</span></h1>
|
||||
<p class="subtitle">Key themes extracted from {{ data?.n?.toLocaleString() }} customer reviews. Ranked by frequency.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading…</div>
|
||||
<div v-else-if="!data" class="empty">No review data available for this product yet.</div>
|
||||
<template v-else>
|
||||
<div class="table-actions">
|
||||
<button class="export-btn">↓ Original reviews {{ data.n?.toLocaleString() }}</button>
|
||||
<button class="export-btn primary">↑ Export</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th style="width:80px">Reviews</th>
|
||||
<th>Review Aspect</th>
|
||||
<th style="width:110px">Sentiment</th>
|
||||
<th style="width:130px">Customer Journey</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="a in data.aspects" :key="a.rank">
|
||||
<td class="rank">{{ a.rank }}</td>
|
||||
<td><span class="review-pill">{{ a.reviews }}×</span></td>
|
||||
<td class="aspect-cell">
|
||||
<span class="aspect-text">{{ a.aspect }}</span>
|
||||
</td>
|
||||
<td><SentimentBadge :sentiment="a.sentiment" /></td>
|
||||
<td><JourneyBadge :journey="a.customerJourney" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SentimentBadge from '../primitives/SentimentBadge.vue'
|
||||
import JourneyBadge from '../primitives/JourneyBadge.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const data = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function load(sku) {
|
||||
if (!sku) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`/api/intelligence/${sku}/review-aspects`)
|
||||
data.value = res.ok ? await res.json() : null
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.sku))
|
||||
watch(() => route.params.sku, load)
|
||||
</script>
|
||||
|
||||
<style scoped src="../views/view.css" />
|
||||
<style scoped>
|
||||
.aspect-text { font-size: 13px; }
|
||||
</style>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="view">
|
||||
<div class="view-label">Product Insights</div>
|
||||
<div class="view-header">
|
||||
<h1>What customers love/dislike</h1>
|
||||
<p class="subtitle">Key insights derived from customer feedback and market analysis to understand your product's position.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading…</div>
|
||||
<div v-else-if="!data" class="empty">No data available for this product yet.</div>
|
||||
<template v-else>
|
||||
<!-- Market Fit Score card -->
|
||||
<div class="score-row">
|
||||
<div class="score-card">
|
||||
<div class="score-num">{{ data.marketFitScore?.value?.toFixed(1) ?? '—' }}</div>
|
||||
<div class="score-label">out of 10</div>
|
||||
<div class="score-verdict">"{{ data.verdict?.split('.')[0] }}."</div>
|
||||
</div>
|
||||
<div class="score-bar-card">
|
||||
<div class="score-bar-label">Market Fit Score</div>
|
||||
<div class="score-bar-track">
|
||||
<div class="score-bar-fill" :style="{ width: ((data.marketFitScore?.value || 0) / 10 * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div class="score-bar-sub" :class="scoreClass">{{ scoreLabel }}</div>
|
||||
</div>
|
||||
<div class="score-desc">{{ data.verdict }}</div>
|
||||
</div>
|
||||
|
||||
<!-- S&W table -->
|
||||
<div class="section-title">
|
||||
Product Strengths & Weaknesses <span class="count-badge">{{ data.items?.length }}</span>
|
||||
</div>
|
||||
<p class="subtitle" style="margin-bottom:12px">What customers love and dislike about your product based on customer feedback.</p>
|
||||
<div class="table-actions">
|
||||
<button class="export-btn primary">↑ Export</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th style="width:80px">Reviews</th>
|
||||
<th>Product Insights</th>
|
||||
<th style="width:120px">Sentiment Ratio</th>
|
||||
<th style="width:160px">Classification</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in data.items" :key="item.rank">
|
||||
<td class="rank">{{ item.rank }}</td>
|
||||
<td><span class="review-pill">{{ item.reviews }}×</span></td>
|
||||
<td class="aspect-cell">{{ item.insight }}</td>
|
||||
<td>
|
||||
<div class="ratio-bars">
|
||||
<div class="ratio-pos" :style="{ width: item.sentimentRatio?.positive + '%' }"></div>
|
||||
<div class="ratio-neg" :style="{ width: item.sentimentRatio?.negative + '%' }"></div>
|
||||
</div>
|
||||
<div class="ratio-labels">
|
||||
<span class="pos-text">{{ item.sentimentRatio?.positive }}%</span>
|
||||
<span class="neg-text">{{ item.sentimentRatio?.negative }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><ClassificationBadge :label="item.classification" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ClassificationBadge from '../primitives/ClassificationBadge.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const data = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const scoreLabel = computed(() => {
|
||||
const v = data.value?.marketFitScore?.value || 0
|
||||
if (v >= 8) return 'Excellent'
|
||||
if (v >= 6) return 'Good'
|
||||
if (v >= 4) return 'Fair'
|
||||
return 'Needs work'
|
||||
})
|
||||
const scoreClass = computed(() => {
|
||||
const v = data.value?.marketFitScore?.value || 0
|
||||
return v >= 8 ? 'excellent' : v >= 6 ? 'good' : 'fair'
|
||||
})
|
||||
|
||||
async function load(sku) {
|
||||
if (!sku) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`/api/intelligence/${sku}/strengths-weaknesses`)
|
||||
data.value = res.ok ? await res.json() : null
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.sku))
|
||||
watch(() => route.params.sku, load)
|
||||
</script>
|
||||
|
||||
<style scoped src="../views/view.css" />
|
||||
<style scoped>
|
||||
.score-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
margin-bottom: 28px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.score-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 28px 32px;
|
||||
text-align: center;
|
||||
min-width: 160px;
|
||||
}
|
||||
.score-num { font-size: 56px; font-weight: 300; line-height: 1; color: var(--text-primary); }
|
||||
.score-label { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
||||
.score-verdict { font-size: 11px; color: var(--text-muted); margin-top: 8px; font-style: italic; }
|
||||
|
||||
.score-bar-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px 24px;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.score-bar-label { font-size: 13px; font-weight: 500; }
|
||||
.score-bar-track { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
||||
.score-bar-fill { height: 100%; background: #f59e0b; border-radius: 3px; transition: width 0.5s; }
|
||||
.score-bar-sub { font-size: 12px; }
|
||||
.excellent { color: var(--positive); }
|
||||
.good { color: #f59e0b; }
|
||||
.fair { color: var(--negative); }
|
||||
|
||||
.score-desc {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px 24px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.ratio-bars {
|
||||
display: flex;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
background: var(--border);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.ratio-pos { background: var(--positive); height: 100%; }
|
||||
.ratio-neg { background: var(--negative); height: 100%; }
|
||||
.ratio-labels { display: flex; justify-content: space-between; }
|
||||
.pos-text { font-size: 10px; color: var(--positive); font-weight: 500; }
|
||||
.neg-text { font-size: 10px; color: var(--negative); font-weight: 500; }
|
||||
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
/* Shared view styles */
|
||||
.view {
|
||||
padding: 28px 32px;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.view-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.view-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
max-width: 640px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 7px;
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.export-btn:hover { background: var(--bg); }
|
||||
.export-btn.primary {
|
||||
background: var(--text-primary);
|
||||
color: #fff;
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
.export-btn.primary:hover { opacity: 0.85; }
|
||||
|
||||
.table-wrap {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.data-table thead th {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table tbody tr {
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.data-table tbody tr:last-child { border-bottom: none; }
|
||||
.data-table tbody tr:hover { background: #fafafa; }
|
||||
|
||||
.data-table tbody td {
|
||||
padding: 14px 14px;
|
||||
color: var(--text-primary);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.review-pill {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aspect-cell {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 48px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 64px 32px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import App from './App.vue'
|
||||
import './styles/tokens.css'
|
||||
|
||||
import ReviewInsights from './components/views/ReviewInsights.vue'
|
||||
import ConversionDriver from './components/views/ConversionDriver.vue'
|
||||
import StrengthsWeaknesses from './components/views/StrengthsWeaknesses.vue'
|
||||
import ProductImprovements from './components/views/ProductImprovements.vue'
|
||||
import ImageAudit from './components/views/ImageAudit.vue'
|
||||
import ConversionBlockers from './components/views/ConversionBlockers.vue'
|
||||
import CatalogOverview from './components/views/CatalogOverview.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/', redirect: '/catalog' },
|
||||
{ path: '/catalog', component: CatalogOverview },
|
||||
{ path: '/product/:sku/review-insights', component: ReviewInsights },
|
||||
{ path: '/product/:sku/conversion-driver', component: ConversionDriver },
|
||||
{ path: '/product/:sku/strengths-weaknesses', component: StrengthsWeaknesses },
|
||||
{ path: '/product/:sku/improvements', component: ProductImprovements },
|
||||
{ path: '/product/:sku/image-audit', component: ImageAudit },
|
||||
{ path: '/product/:sku/conversion-blockers', component: ConversionBlockers },
|
||||
]
|
||||
})
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
@@ -0,0 +1,51 @@
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--brand-blue: #32327d;
|
||||
--accent: #5B6CF6;
|
||||
--accent-light: #EEF2FF;
|
||||
--accent-border: #C7D2FE;
|
||||
--bg: #F7F7F5;
|
||||
--bg-card: #FFFFFF;
|
||||
--header-dark: #111111;
|
||||
--header-dark2: #1a1a1a;
|
||||
--topbar-bg: #FFFFFF;
|
||||
--sidebar-bg: #FFFFFF;
|
||||
--sidebar-width: 216px;
|
||||
--topnav-height: 48px;
|
||||
--productheader-height: 76px;
|
||||
--text-primary: #111111;
|
||||
--text-secondary: #555555;
|
||||
--text-muted: #999999;
|
||||
--text-dim: #BBBBBB;
|
||||
--border: #E8E8E8;
|
||||
--border-light: #F0F0F0;
|
||||
--positive: #16a34a;
|
||||
--positive-bg: #dcfce7;
|
||||
--negative: #dc2626;
|
||||
--negative-bg: #fee2e2;
|
||||
--neutral: #555;
|
||||
--neutral-bg: #f3f4f6;
|
||||
--orange: #f97316;
|
||||
--orange-bg: #fff7ed;
|
||||
--dot-filled: #f97316;
|
||||
--dot-empty: #E5E7EB;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
--radius-sm: 4px;
|
||||
--radius: 6px;
|
||||
--radius-lg: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
button { font-family: var(--font); cursor: pointer; border: none; background: none; }
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: { proxy: { '/api': 'http://localhost:3456' } }
|
||||
})
|
||||
Reference in New Issue
Block a user