mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:45:23 +02:00
feat: redesign Vendor tab with premium AreaCharts and updated UI
This commit is contained in:
+216
-104
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from 'recharts';
|
||||
import React, { useMemo } from 'react';
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { BSRRecord } from '../types';
|
||||
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||
|
||||
@@ -8,7 +8,7 @@ const MARKET_COLORS: Record<string, string> = {
|
||||
UK: '#ef4444',
|
||||
IT: '#22c55e',
|
||||
FR: '#a855f7',
|
||||
ES: '#f59e0b',
|
||||
ES: '#f97316',
|
||||
};
|
||||
|
||||
interface VendorDataViewProps {
|
||||
@@ -30,7 +30,7 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
|
||||
return Array.from(m).sort();
|
||||
}, [bsrData]);
|
||||
|
||||
// Determine if daily resolution is available (>= half the records have a date)
|
||||
// Determine if daily resolution is available
|
||||
const useDailyResolution = useMemo(() => {
|
||||
const withDate = bsrData.filter(r => r.date).length;
|
||||
return withDate > bsrData.length / 2;
|
||||
@@ -56,9 +56,8 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
|
||||
};
|
||||
}, [uniqueAsins, asinMetadata]);
|
||||
|
||||
// Aggregate Data for Charts — daily if dates available, else weekly
|
||||
// Aggregate Data for Charts
|
||||
const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => {
|
||||
// Group by date string (YYYY-MM-DD) or by week number
|
||||
const byBucket = new Map<string, BSRRecord[]>();
|
||||
|
||||
bsrData.forEach(r => {
|
||||
@@ -76,14 +75,12 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
|
||||
sortedKeys.forEach(key => {
|
||||
const rows = byBucket.get(key)!;
|
||||
|
||||
// Human-readable label
|
||||
let label: string;
|
||||
if (useDailyResolution && key.includes('-')) {
|
||||
// YYYY-MM-DD → "DD MMM"
|
||||
const d = new Date(key + 'T12:00:00Z');
|
||||
label = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
|
||||
} else {
|
||||
label = key; // e.g. "W08"
|
||||
label = key;
|
||||
}
|
||||
|
||||
const topPoint: ChartPoint = { label, sortKey: key };
|
||||
@@ -136,40 +133,43 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
|
||||
);
|
||||
}
|
||||
|
||||
const resolutionLabel = useDailyResolution ? 'Daily Avg' : 'Weekly Avg';
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 pb-24 md:pb-4">
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
<span className="text-slate-500 text-xs ml-auto">{bsrData.length.toLocaleString()} records filtered</span>
|
||||
<div className="space-y-6 p-4 pb-24 md:pb-4 max-w-5xl mx-auto">
|
||||
{/* Header Info */}
|
||||
<div className="flex flex-wrap gap-3 items-center justify-between">
|
||||
<h2 className="text-2xl font-black text-slate-100 uppercase tracking-tight">Vendor Analytics</h2>
|
||||
<span className="bg-slate-800 text-slate-400 text-[10px] px-2 py-1 rounded font-bold uppercase tracking-wider border border-slate-700">
|
||||
{bsrData.length.toLocaleString()} records filtered
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Product Info Card - Only shown when single ASIN is selected */}
|
||||
{/* Product Info Card */}
|
||||
{productInfo && (
|
||||
<div className="bg-gradient-to-r from-indigo-500/10 to-purple-500/10 border border-indigo-500/30 rounded-xl p-4 animate-fade-in">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest bg-indigo-500/20 px-2 py-0.5 rounded">
|
||||
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-indigo-500"></div>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-4 flex-1">
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest bg-indigo-500/10 px-2 py-1 rounded mb-2 inline-block">
|
||||
Product Details
|
||||
</span>
|
||||
<h3 className="text-xl md:text-2xl font-black text-white leading-tight uppercase tracking-tight">
|
||||
{productInfo.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500 font-bold uppercase tracking-wider">SKU:</span>
|
||||
<span className="text-sm font-mono font-bold text-slate-200">{productInfo.sku}</span>
|
||||
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">SKU:</span>
|
||||
<span className="text-base font-bold text-slate-200">{productInfo.sku}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500 font-bold uppercase tracking-wider">ASIN:</span>
|
||||
<span className="text-sm font-mono font-bold text-indigo-400">{productInfo.asin}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-xs text-slate-500 font-bold uppercase tracking-wider mt-0.5">Title:</span>
|
||||
<span className="text-sm font-medium text-slate-300">{productInfo.title}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">ASIN:</span>
|
||||
<span className="text-base font-bold text-indigo-400 font-mono tracking-tight">{productInfo.asin}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex-shrink-0 pt-1">
|
||||
<BuyBoxWarningBadge asin={productInfo.asin} buyBoxLostMap={buyBoxLostMap} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,87 +177,199 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
|
||||
)}
|
||||
|
||||
{/* Top Level BSR Trend Chart */}
|
||||
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
|
||||
<h3 className="text-lg font-bold text-slate-200 mb-4">Top Level BSR Trend ({resolutionLabel})</h3>
|
||||
<p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p>
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={topBsrChartData} barGap={4} barCategoryGap="20%">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
|
||||
<XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
|
||||
<YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} domain={['auto', 'auto']} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
||||
labelStyle={{ color: '#e2e8f0' }}
|
||||
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
|
||||
/>
|
||||
<Legend />
|
||||
{activeMarkets.map(m => (
|
||||
<Bar
|
||||
key={m}
|
||||
dataKey={`${m}_bsr`}
|
||||
name={`${m} BSR`}
|
||||
fill={MARKET_COLORS[m] || '#6b7280'}
|
||||
radius={[4, 4, 0, 0]}
|
||||
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-xl">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-black text-white uppercase tracking-tight">Top Level BSR Trend</h3>
|
||||
<p className="text-xs text-slate-500 font-medium">Weekly Average Rank • <span className="text-orange-500/80">Lower is better</span></p>
|
||||
</div>
|
||||
<div className="h-[350px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={topBsrChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorTop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#f43f5e" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#f43f5e" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
{activeMarkets.map(m => (
|
||||
<linearGradient key={m} id={`grad_top_${m}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={MARKET_COLORS[m] || '#f97316'} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={MARKET_COLORS[m] || '#f97316'} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="0" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
|
||||
dy={10}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<YAxis
|
||||
reversed
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
|
||||
domain={['auto', 'auto']}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px', boxShadow: '0 20px 25px -5px rgb(0 0 0 / 0.1)' }}
|
||||
itemStyle={{ fontSize: '11px', fontWeight: 700 }}
|
||||
labelStyle={{ color: '#94a3b8', fontSize: '10px', marginBottom: '4px', fontWeight: 800, textTransform: 'uppercase' }}
|
||||
cursor={{ stroke: '#334155', strokeWidth: 1 }}
|
||||
/>
|
||||
{activeMarkets.map(m => (
|
||||
<Area
|
||||
key={m}
|
||||
type="monotone"
|
||||
dataKey={`${m}_bsr`}
|
||||
name={`${m} BSR`}
|
||||
stroke={MARKET_COLORS[m] || '#f97316'}
|
||||
strokeWidth={3}
|
||||
fillOpacity={1}
|
||||
fill={`url(#grad_top_${m})`}
|
||||
dot={{ r: 4, fill: '#151b2b', strokeWidth: 2, stroke: MARKET_COLORS[m] || '#f97316' }}
|
||||
activeDot={{ r: 6, strokeWidth: 0 }}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-4 justify-center">
|
||||
{activeMarkets.map(m => (
|
||||
<div key={m} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: MARKET_COLORS[m] }}></div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{m} BSR</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail Level BSR Trend Chart */}
|
||||
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
|
||||
<h3 className="text-lg font-bold text-slate-200 mb-4">Detail Level BSR Trend ({resolutionLabel})</h3>
|
||||
<p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p>
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={detailBsrChartData} barGap={4} barCategoryGap="20%">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
|
||||
<XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
|
||||
<YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} domain={['auto', 'auto']} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
||||
labelStyle={{ color: '#e2e8f0' }}
|
||||
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
|
||||
/>
|
||||
<Legend />
|
||||
{activeMarkets.map(m => (
|
||||
<Bar
|
||||
key={m}
|
||||
dataKey={`${m}_bsr`}
|
||||
name={`${m} BSR`}
|
||||
fill={MARKET_COLORS[m] || '#6b7280'}
|
||||
radius={[4, 4, 0, 0]}
|
||||
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-xl">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-black text-white uppercase tracking-tight">Detail Level BSR Trend</h3>
|
||||
<p className="text-xs text-slate-500 font-medium">Sub-category Performance • <span className="text-cyan-500/80">Filtered ASINs</span></p>
|
||||
</div>
|
||||
<div className="h-[350px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={detailBsrChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
{activeMarkets.map(m => (
|
||||
<linearGradient key={m} id={`grad_detail_${m}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={MARKET_COLORS[m] || '#06b6d4'} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={MARKET_COLORS[m] || '#06b6d4'} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="0" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
|
||||
dy={10}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<YAxis
|
||||
reversed
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
|
||||
domain={['auto', 'auto']}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px' }}
|
||||
itemStyle={{ fontSize: '11px', fontWeight: 700 }}
|
||||
labelStyle={{ color: '#94a3b8', fontSize: '10px', marginBottom: '4px', fontWeight: 800, textTransform: 'uppercase' }}
|
||||
cursor={{ stroke: '#334155', strokeWidth: 1 }}
|
||||
/>
|
||||
{activeMarkets.map(m => (
|
||||
<Area
|
||||
key={m}
|
||||
type="monotone"
|
||||
dataKey={`${m}_bsr`}
|
||||
name={`${m} Category Rank`}
|
||||
stroke={MARKET_COLORS[m] || '#06b6d4'}
|
||||
strokeWidth={3}
|
||||
fillOpacity={1}
|
||||
fill={`url(#grad_detail_${m})`}
|
||||
dot={{ r: 4, fill: '#151b2b', strokeWidth: 2, stroke: MARKET_COLORS[m] || '#06b6d4' }}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-4 justify-center">
|
||||
{activeMarkets.map(m => (
|
||||
<div key={m} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: MARKET_COLORS[m] }}></div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{m} Category Rank</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Average Rating Chart */}
|
||||
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
|
||||
<h3 className="text-lg font-bold text-slate-200 mb-4">Average Rating ({resolutionLabel})</h3>
|
||||
<p className="text-xs text-slate-500 mb-3">Averaged across filtered ASINs per market.</p>
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={ratingChartData} barGap={4} barCategoryGap="20%">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
|
||||
<XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
|
||||
<YAxis domain={[0, 5]} tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
||||
labelStyle={{ color: '#e2e8f0' }}
|
||||
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
|
||||
/>
|
||||
<Legend />
|
||||
{activeMarkets.map(m => (
|
||||
<Bar
|
||||
key={m}
|
||||
dataKey={`${m}_rating`}
|
||||
name={`${m} Rating`}
|
||||
fill={MARKET_COLORS[m] || '#6b7280'}
|
||||
radius={[4, 4, 0, 0]}
|
||||
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-xl">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-black text-white uppercase tracking-tight">Average Rating</h3>
|
||||
<p className="text-xs text-slate-500 font-medium">Weekly Average • <span className="text-purple-500/80">1.0 - 5.0 Stars</span></p>
|
||||
</div>
|
||||
<div className="h-[350px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={ratingChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
{activeMarkets.map(m => (
|
||||
<linearGradient key={m} id={`grad_rating_${m}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={MARKET_COLORS[m] || '#a855f7'} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={MARKET_COLORS[m] || '#a855f7'} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="0" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
|
||||
dy={10}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
|
||||
domain={[0, 5]}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px' }}
|
||||
itemStyle={{ fontSize: '11px', fontWeight: 700 }}
|
||||
labelStyle={{ color: '#94a3b8', fontSize: '10px', marginBottom: '4px', fontWeight: 800, textTransform: 'uppercase' }}
|
||||
cursor={{ stroke: '#334155', strokeWidth: 1 }}
|
||||
/>
|
||||
{activeMarkets.map(m => (
|
||||
<Area
|
||||
key={m}
|
||||
type="monotone"
|
||||
dataKey={`${m}_rating`}
|
||||
name={`${m} Star Rating`}
|
||||
stroke={MARKET_COLORS[m] || '#a855f7'}
|
||||
strokeWidth={3}
|
||||
fillOpacity={1}
|
||||
fill={`url(#grad_rating_${m})`}
|
||||
dot={{ r: 4, fill: '#151b2b', strokeWidth: 2, stroke: MARKET_COLORS[m] || '#a855f7' }}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-4 justify-center">
|
||||
{activeMarkets.map(m => (
|
||||
<div key={m} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: MARKET_COLORS[m] }}></div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{m} Star Rating</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user