fix: perfectly match Vendor tab UI to reference screenshot

This commit is contained in:
Christian Vidal Wolf
2026-03-04 08:29:20 +01:00
parent 8ad1f908b1
commit bdd65d16d1
+115 -169
View File
@@ -3,14 +3,6 @@ import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContai
import { BSRRecord } from '../types'; import { BSRRecord } from '../types';
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge'; import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
const MARKET_COLORS: Record<string, string> = {
DE: '#3b82f6',
UK: '#ef4444',
IT: '#22c55e',
FR: '#a855f7',
ES: '#f97316',
};
interface VendorDataViewProps { interface VendorDataViewProps {
bsrData: BSRRecord[]; bsrData: BSRRecord[];
asinMetadata?: Map<string, { sku: string; title: string; line: string }>; asinMetadata?: Map<string, { sku: string; title: string; line: string }>;
@@ -24,25 +16,21 @@ interface ChartPoint {
} }
const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetadata, buyBoxLostMap }) => { const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetadata, buyBoxLostMap }) => {
// Determine active markets in filtered data for series generation
const activeMarkets = useMemo(() => { const activeMarkets = useMemo(() => {
const m = new Set(bsrData.map(r => r.market)); const m = new Set(bsrData.map(r => r.market));
return Array.from(m).sort(); return Array.from(m).sort();
}, [bsrData]); }, [bsrData]);
// Determine if daily resolution is available
const useDailyResolution = useMemo(() => { const useDailyResolution = useMemo(() => {
const withDate = bsrData.filter(r => r.date).length; const withDate = bsrData.filter(r => r.date).length;
return withDate > bsrData.length / 2; return withDate > bsrData.length / 2;
}, [bsrData]); }, [bsrData]);
// Get unique ASINs in filtered data
const uniqueAsins = useMemo(() => { const uniqueAsins = useMemo(() => {
const asinSet = new Set(bsrData.map(r => r.asin.trim().toUpperCase())); const asinSet = new Set(bsrData.map(r => r.asin.trim().toUpperCase()));
return Array.from(asinSet); return Array.from(asinSet);
}, [bsrData]); }, [bsrData]);
// Get product info when single ASIN is selected
const productInfo = useMemo(() => { const productInfo = useMemo(() => {
if (uniqueAsins.length !== 1) return null; if (uniqueAsins.length !== 1) return null;
const asin = uniqueAsins[0]; const asin = uniqueAsins[0];
@@ -56,7 +44,6 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
}; };
}, [uniqueAsins, asinMetadata]); }, [uniqueAsins, asinMetadata]);
// Aggregate Data for Charts
const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => { const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => {
const byBucket = new Map<string, BSRRecord[]>(); const byBucket = new Map<string, BSRRecord[]>();
@@ -90,19 +77,16 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
activeMarkets.forEach(m => { activeMarkets.forEach(m => {
const marketRows = rows.filter(r => r.market === m); const marketRows = rows.filter(r => r.market === m);
// Top BSR
const topBsrRows = marketRows.filter(r => r.topLevelBSR != null); const topBsrRows = marketRows.filter(r => r.topLevelBSR != null);
topPoint[`${m}_bsr`] = topBsrRows.length > 0 topPoint[`${m}_bsr`] = topBsrRows.length > 0
? Math.round(topBsrRows.reduce((sum, r) => sum + r.topLevelBSR!, 0) / topBsrRows.length) ? Math.round(topBsrRows.reduce((sum, r) => sum + r.topLevelBSR!, 0) / topBsrRows.length)
: null; : null;
// Detail BSR
const detailBsrRows = marketRows.filter(r => r.detailLevelBSR != null); const detailBsrRows = marketRows.filter(r => r.detailLevelBSR != null);
detailPoint[`${m}_bsr`] = detailBsrRows.length > 0 detailPoint[`${m}_bsr`] = detailBsrRows.length > 0
? Math.round(detailBsrRows.reduce((sum, r) => sum + r.detailLevelBSR!, 0) / detailBsrRows.length) ? Math.round(detailBsrRows.reduce((sum, r) => sum + r.detailLevelBSR!, 0) / detailBsrRows.length)
: null; : null;
// Rating
const ratingRows = marketRows.filter(r => r.avgRating != null); const ratingRows = marketRows.filter(r => r.avgRating != null);
ratingPoint[`${m}_rating`] = ratingRows.length > 0 ratingPoint[`${m}_rating`] = ratingRows.length > 0
? Math.round((ratingRows.reduce((sum, r) => sum + r.avgRating!, 0) / ratingRows.length) * 10) / 10 ? Math.round((ratingRows.reduce((sum, r) => sum + r.avgRating!, 0) / ratingRows.length) * 10) / 10
@@ -133,39 +117,38 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
); );
} }
// Exact color values based on the screenshot
const cardBg = '#161b24';
const orangeHex = '#f37526';
const cyanHex = '#2acbd6';
const purpleHex = '#7950f2';
const gridLineColor = '#ffffff';
return ( return (
<div className="space-y-6 p-4 pb-24 md:pb-4 max-w-5xl mx-auto"> <div className="p-4 md:p-6 lg:px-8 pb-24 max-w-[800px] mx-auto space-y-6">
{/* 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 */} {/* Product Details Header Card */}
{productInfo && ( {productInfo && (
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-2xl relative overflow-hidden group"> <div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md relative">
<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="flex items-start justify-between gap-4">
<div className="space-y-4 flex-1"> <div className="flex flex-col gap-3 flex-1">
<div> <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"> <span className="inline-block bg-[#1f2b4a] text-[#4f86f7] text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-[4px] mb-3">
Product Details PRODUCT DETAILS
</span> </span>
<h3 className="text-xl md:text-2xl font-black text-white leading-tight uppercase tracking-tight"> <h2 className="text-xl md:text-2xl font-bold text-white mb-1">
{productInfo.title} {productInfo.title}
</h3> </h2>
</div> </div>
<div className="flex flex-wrap gap-x-8 gap-y-2"> <div className="flex items-center gap-6 mt-1">
<div className="flex items-center gap-2"> <div className="text-[13px] md:text-sm">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">SKU:</span> <span className="text-[#64748b] font-semibold mr-2">SKU:</span>
<span className="text-base font-bold text-slate-200">{productInfo.sku}</span> <span className="text-[#e2e8f0] font-medium">{productInfo.sku}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="text-[13px] md:text-sm">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">ASIN:</span> <span className="text-[#64748b] font-semibold mr-2">ASIN:</span>
<span className="text-base font-bold text-indigo-400 font-mono tracking-tight">{productInfo.asin}</span> <span className="text-[#4f86f7] font-medium tracking-wide">{productInfo.asin}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -177,201 +160,164 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [], asinMetad
)} )}
{/* Top Level BSR Trend Chart */} {/* Top Level BSR Trend Chart */}
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-xl"> <div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md flex flex-col">
<div className="mb-6"> <div className="mb-4">
<h3 className="text-lg font-black text-white uppercase tracking-tight">Top Level BSR Trend</h3> <h3 className="text-[17px] font-semibold text-white tracking-wide mb-1">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> <p className="text-[13px] text-[#64748b]">Weekly Average Rank Lower is better</p>
</div> </div>
<div className="h-[350px] w-full">
<div className="h-[220px] w-full mt-2">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={topBsrChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}> <AreaChart data={topBsrChartData} margin={{ top: 20, right: 10, left: 10, bottom: 0 }}>
<defs> <defs>
<linearGradient id="colorTop" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorOrange" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f43f5e" stopOpacity={0.3} /> <stop offset="5%" stopColor={orangeHex} stopOpacity={0.25} />
<stop offset="95%" stopColor="#f43f5e" stopOpacity={0} /> <stop offset="95%" stopColor={orangeHex} stopOpacity={0} />
</linearGradient> </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> </defs>
<CartesianGrid strokeDasharray="0" stroke="#1e293b" vertical={false} /> <CartesianGrid strokeDasharray="0" stroke={gridLineColor} strokeOpacity={0.03} vertical={false} />
<XAxis <XAxis dataKey="label" hide />
dataKey="label" <YAxis reversed domain={['auto', 'auto']} hide />
axisLine={false}
tickLine={false}
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
dy={10}
/>
<YAxis
reversed
axisLine={false}
tickLine={false}
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
domain={['auto', 'auto']}
/>
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px', boxShadow: '0 20px 25px -5px rgb(0 0 0 / 0.1)' }} contentStyle={{ backgroundColor: '#111827', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
itemStyle={{ fontSize: '11px', fontWeight: 700 }} itemStyle={{ fontSize: '12px' }}
labelStyle={{ color: '#94a3b8', fontSize: '10px', marginBottom: '4px', fontWeight: 800, textTransform: 'uppercase' }} labelStyle={{ color: '#9ca3af', fontSize: '11px', marginBottom: '4px' }}
cursor={{ stroke: '#334155', strokeWidth: 1 }} cursor={{ stroke: '#374151', strokeWidth: 1, strokeDasharray: '3 3' }}
/> />
{activeMarkets.map(m => ( {activeMarkets.map((m, i) => (
<Area <Area
key={m} key={m}
type="monotone" type="monotone"
dataKey={`${m}_bsr`} dataKey={`${m}_bsr`}
name={`${m} BSR`} name={`${m} BSR`}
stroke={MARKET_COLORS[m] || '#f97316'} stroke={orangeHex}
strokeWidth={3} strokeWidth={2.5}
fillOpacity={1} fillOpacity={1}
fill={`url(#grad_top_${m})`} fill={i === 0 ? "url(#colorOrange)" : "transparent"}
dot={{ r: 4, fill: '#151b2b', strokeWidth: 2, stroke: MARKET_COLORS[m] || '#f97316' }} activeDot={{ r: 6, strokeWidth: 0, fill: orangeHex }}
activeDot={{ r: 6, strokeWidth: 0 }} dot={{ r: 4, fill: cardBg, strokeWidth: 2, stroke: orangeHex }}
/> />
))} ))}
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
<div className="mt-4 flex flex-wrap gap-4 justify-center">
{activeMarkets.map(m => ( {/* Exact Legend matching screenshot */}
<div key={m} className="flex items-center gap-2"> <div className="mt-6 flex justify-center items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: MARKET_COLORS[m] }}></div> {activeMarkets.length > 0 && (
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{m} BSR</span> <div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: orangeHex }}></div>
<span className="text-[12px] text-[#8b9bb4]">{activeMarkets[0]} BSR</span>
</div> </div>
))} )}
</div> </div>
</div> </div>
{/* Detail Level BSR Trend Chart */} {/* Detail Level BSR Trend Chart */}
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-xl"> <div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md flex flex-col">
<div className="mb-6"> <div className="mb-4">
<h3 className="text-lg font-black text-white uppercase tracking-tight">Detail Level BSR Trend</h3> <h3 className="text-[17px] font-semibold text-white tracking-wide mb-1">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> <p className="text-[13px] text-[#64748b]">Sub-category Performance Filtered ASINs</p>
</div> </div>
<div className="h-[350px] w-full">
<div className="h-[220px] w-full mt-2">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={detailBsrChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}> <AreaChart data={detailBsrChartData} margin={{ top: 20, right: 10, left: 10, bottom: 0 }}>
<defs> <defs>
{activeMarkets.map(m => ( <linearGradient id="colorCyan" x1="0" y1="0" x2="0" y2="1">
<linearGradient key={m} id={`grad_detail_${m}`} x1="0" y1="0" x2="0" y2="1"> <stop offset="5%" stopColor={cyanHex} stopOpacity={0.25} />
<stop offset="5%" stopColor={MARKET_COLORS[m] || '#06b6d4'} stopOpacity={0.3} /> <stop offset="95%" stopColor={cyanHex} stopOpacity={0} />
<stop offset="95%" stopColor={MARKET_COLORS[m] || '#06b6d4'} stopOpacity={0} />
</linearGradient> </linearGradient>
))}
</defs> </defs>
<CartesianGrid strokeDasharray="0" stroke="#1e293b" vertical={false} /> <CartesianGrid strokeDasharray="0" stroke={gridLineColor} strokeOpacity={0.03} vertical={false} />
<XAxis <XAxis dataKey="label" hide />
dataKey="label" <YAxis reversed domain={['auto', 'auto']} hide />
axisLine={false}
tickLine={false}
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
dy={10}
/>
<YAxis
reversed
axisLine={false}
tickLine={false}
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
domain={['auto', 'auto']}
/>
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px' }} contentStyle={{ backgroundColor: '#111827', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
itemStyle={{ fontSize: '11px', fontWeight: 700 }} itemStyle={{ fontSize: '12px' }}
labelStyle={{ color: '#94a3b8', fontSize: '10px', marginBottom: '4px', fontWeight: 800, textTransform: 'uppercase' }} labelStyle={{ color: '#9ca3af', fontSize: '11px', marginBottom: '4px' }}
cursor={{ stroke: '#334155', strokeWidth: 1 }} cursor={{ stroke: '#374151', strokeWidth: 1, strokeDasharray: '3 3' }}
/> />
{activeMarkets.map(m => ( {activeMarkets.map((m, i) => (
<Area <Area
key={m} key={m}
type="monotone" type="monotone"
dataKey={`${m}_bsr`} dataKey={`${m}_bsr`}
name={`${m} Category Rank`} name="Category Rank"
stroke={MARKET_COLORS[m] || '#06b6d4'} stroke={cyanHex}
strokeWidth={3} strokeWidth={2.5}
fillOpacity={1} fillOpacity={1}
fill={`url(#grad_detail_${m})`} fill={i === 0 ? "url(#colorCyan)" : "transparent"}
dot={{ r: 4, fill: '#151b2b', strokeWidth: 2, stroke: MARKET_COLORS[m] || '#06b6d4' }} activeDot={{ r: 6, strokeWidth: 0, fill: cyanHex }}
dot={{ r: 4, fill: cardBg, strokeWidth: 2, stroke: cyanHex }}
/> />
))} ))}
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
<div className="mt-4 flex flex-wrap gap-4 justify-center">
{activeMarkets.map(m => ( {/* Exact Legend matching screenshot */}
<div key={m} className="flex items-center gap-2"> <div className="mt-6 flex justify-center items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: MARKET_COLORS[m] }}></div> <div className="flex items-center gap-2">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{m} Category Rank</span> <div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: cyanHex }}></div>
<span className="text-[12px] text-[#8b9bb4]">Category Rank</span>
</div> </div>
))}
</div> </div>
</div> </div>
{/* Average Rating Chart */} {/* Average Rating Chart */}
<div className="bg-[#151b2b] border border-[#1e293b] rounded-2xl p-6 shadow-xl"> <div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md flex flex-col">
<div className="mb-6"> <div className="mb-4">
<h3 className="text-lg font-black text-white uppercase tracking-tight">Average Rating</h3> <h3 className="text-[17px] font-semibold text-white tracking-wide mb-1">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> <p className="text-[13px] text-[#64748b]">Weekly Average (1.0 - 5.0)</p>
</div> </div>
<div className="h-[350px] w-full">
<div className="h-[220px] w-full mt-2">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={ratingChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}> <AreaChart data={ratingChartData} margin={{ top: 20, right: 10, left: 10, bottom: 0 }}>
<defs> <defs>
{activeMarkets.map(m => ( <linearGradient id="colorPurple" x1="0" y1="0" x2="0" y2="1">
<linearGradient key={m} id={`grad_rating_${m}`} x1="0" y1="0" x2="0" y2="1"> <stop offset="5%" stopColor={purpleHex} stopOpacity={0.25} />
<stop offset="5%" stopColor={MARKET_COLORS[m] || '#a855f7'} stopOpacity={0.3} /> <stop offset="95%" stopColor={purpleHex} stopOpacity={0} />
<stop offset="95%" stopColor={MARKET_COLORS[m] || '#a855f7'} stopOpacity={0} />
</linearGradient> </linearGradient>
))}
</defs> </defs>
<CartesianGrid strokeDasharray="0" stroke="#1e293b" vertical={false} /> <CartesianGrid strokeDasharray="0" stroke={gridLineColor} strokeOpacity={0.03} vertical={false} />
<XAxis <XAxis dataKey="label" hide />
dataKey="label" <YAxis domain={[0, 5]} hide />
axisLine={false}
tickLine={false}
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
dy={10}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: '#475569', fontSize: 10, fontWeight: 700 }}
domain={[0, 5]}
/>
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px' }} contentStyle={{ backgroundColor: '#111827', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
itemStyle={{ fontSize: '11px', fontWeight: 700 }} itemStyle={{ fontSize: '12px' }}
labelStyle={{ color: '#94a3b8', fontSize: '10px', marginBottom: '4px', fontWeight: 800, textTransform: 'uppercase' }} labelStyle={{ color: '#9ca3af', fontSize: '11px', marginBottom: '4px' }}
cursor={{ stroke: '#334155', strokeWidth: 1 }} cursor={{ stroke: '#374151', strokeWidth: 1, strokeDasharray: '3 3' }}
/> />
{activeMarkets.map(m => ( {activeMarkets.map((m, i) => (
<Area <Area
key={m} key={m}
type="monotone" type="monotone"
dataKey={`${m}_rating`} dataKey={`${m}_rating`}
name={`${m} Star Rating`} name="Star Rating"
stroke={MARKET_COLORS[m] || '#a855f7'} stroke={purpleHex}
strokeWidth={3} strokeWidth={2.5}
fillOpacity={1} fillOpacity={1}
fill={`url(#grad_rating_${m})`} fill={i === 0 ? "url(#colorPurple)" : "transparent"}
dot={{ r: 4, fill: '#151b2b', strokeWidth: 2, stroke: MARKET_COLORS[m] || '#a855f7' }} activeDot={{ r: 6, strokeWidth: 0, fill: purpleHex }}
dot={{ r: 4, fill: cardBg, strokeWidth: 2, stroke: purpleHex }}
/> />
))} ))}
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
<div className="mt-4 flex flex-wrap gap-4 justify-center">
{activeMarkets.map(m => ( {/* Exact Legend matching screenshot */}
<div key={m} className="flex items-center gap-2"> <div className="mt-6 flex justify-center items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: MARKET_COLORS[m] }}></div> <div className="flex items-center gap-2">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{m} Star Rating</span> <div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: purpleHex }}></div>
</div> <span className="text-[12px] text-[#8b9bb4]">Star Rating</span>
))}
</div> </div>
</div> </div>
</div> </div>
</div>
); );
}; };