feat: implement country-aware Top 50 ranking

- App.tsx: Update top50Ranking2025 to calculate ranks based on active country filters
- App.tsx: Add split Pan-EU vs UK ranking when no filters are active
- WeeklyGrid.tsx: Add support for multiple ranking badges (EU/UK)
- WeeklyGrid.tsx: Enhance Top50Badge with labels and color themes
- WeeklyGrid.tsx: Update sorting and filtering logic for new ranking structure
This commit is contained in:
Christian Vidal Wolf
2026-01-22 13:01:04 +01:00
parent 8005a99a48
commit 0214509ffe
2 changed files with 102 additions and 34 deletions
+67 -16
View File
@@ -5,7 +5,12 @@ import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor'
interface WeeklyGridProps {
data: CombinedKPIs[];
top50Ranking?: Map<string, number>; // Static ranking of Top 50 ASINs by 2025 Sell Out
top50Ranking?: {
type: 'filtered' | 'dual';
overall?: Map<string, number>;
eu?: Map<string, number>;
uk?: Map<string, number>;
};
}
type SortConfig = {
@@ -28,14 +33,24 @@ const useDebounce = (value: string, delay: number) => {
};
// Top 50 Badge Component
const Top50Badge: React.FC<{ rank: number }> = ({ rank }) => (
<span
className="inline-flex items-center justify-center px-1.5 py-0.5 rounded text-[9px] font-black bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-sm border border-amber-400/50"
title={`Top ${rank} Best Seller 2025`}
>
🏆 {rank}
</span>
);
const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'blue' | 'indigo' }> = ({ rank, label, theme = 'amber' }) => {
const themeClasses = {
amber: 'from-amber-500 to-orange-500 border-amber-400/50',
blue: 'from-blue-500 to-cyan-500 border-blue-400/50',
indigo: 'from-indigo-500 to-purple-500 border-indigo-400/50',
};
return (
<span
className={`inline-flex items-center justify-center px-1.5 py-0.5 rounded text-[9px] font-black bg-gradient-to-r ${themeClasses[theme]} text-white shadow-sm border`}
title={`Top ${rank} Best Seller 2025 ${label ? `(${label})` : ''}`}
>
<span className="mr-0.5">🏆</span>
{label && <span className="mr-0.5 opacity-90">{label}</span>}
{rank}
</span>
);
};
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking }) => {
// Pivot data - memoized
@@ -96,7 +111,14 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking }) => {
// Top 50 Filter
if (showOnlyTop50 && top50Ranking) {
result = result.filter(r => top50Ranking.has(r.asin.trim().toUpperCase()));
result = result.filter(r => {
const asin = r.asin.trim().toUpperCase();
if (top50Ranking.type === 'filtered') {
return top50Ranking.overall?.has(asin);
} else {
return top50Ranking.eu?.has(asin) || top50Ranking.uk?.has(asin);
}
});
}
// Search Filter
@@ -150,8 +172,21 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking }) => {
result.sort((a, b) => {
if (sortConfig.metric === 'rank') {
const rankA = top50Ranking?.get(a.asin.trim().toUpperCase()) || 999;
const rankB = top50Ranking?.get(b.asin.trim().toUpperCase()) || 999;
const asinA = a.asin.trim().toUpperCase();
const asinB = b.asin.trim().toUpperCase();
let rankA = 999;
let rankB = 999;
if (top50Ranking?.type === 'filtered') {
rankA = top50Ranking.overall?.get(asinA) || 999;
rankB = top50Ranking.overall?.get(asinB) || 999;
} else if (top50Ranking?.type === 'dual') {
// In dual mode, prioritize EU rank, then UK rank
rankA = top50Ranking.eu?.get(asinA) || top50Ranking.uk?.get(asinA) || 999;
rankB = top50Ranking.eu?.get(asinB) || top50Ranking.uk?.get(asinB) || 999;
}
return direction === 'asc' ? rankA - rankB : rankB - rankA;
}
const valA = a[metricKey][weekKey] || 0;
@@ -261,8 +296,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking }) => {
}
}}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all border ${showOnlyTop50
? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white border-amber-400 shadow-lg shadow-amber-500/20'
: 'bg-slate-950/50 text-slate-400 border-white/10 hover:border-amber-500/50 hover:text-amber-400'
? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white border-amber-400 shadow-lg shadow-amber-500/20'
: 'bg-slate-950/50 text-slate-400 border-white/10 hover:border-amber-500/50 hover:text-amber-400'
}`}
>
<span className="text-sm">🏆</span>
@@ -387,13 +422,29 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking }) => {
<tbody className="divide-y divide-white/5">
{paginatedRows.length > 0 ? (
paginatedRows.map((row) => {
const top50Rank = top50Ranking?.get(row.asin.trim().toUpperCase());
const asin = row.asin.trim().toUpperCase();
const ranks = [];
if (top50Ranking) {
if (top50Ranking.type === 'filtered') {
const rank = top50Ranking.overall?.get(asin);
if (rank) ranks.push({ rank, label: '', theme: 'amber' as const });
} else {
const euRank = top50Ranking.eu?.get(asin);
const ukRank = top50Ranking.uk?.get(asin);
if (euRank) ranks.push({ rank: euRank, label: 'EU', theme: 'indigo' as const });
if (ukRank) ranks.push({ rank: ukRank, label: 'UK', theme: 'blue' as const });
}
}
return (
<tr key={row.id} className="hover:bg-white/[0.02] transition-colors group">
<td className="p-3 py-2 sticky left-0 z-10 bg-slate-900 group-hover:bg-slate-800 border-r border-white/10">
<div className="flex flex-col">
<div className="flex items-center gap-2 mb-0.5">
{top50Rank && <Top50Badge rank={top50Rank} />}
{ranks.map((r, i) => (
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
))}
<span className="text-xs font-black text-indigo-400 uppercase tracking-tighter truncate max-w-[120px]">{row.sku || '-'}</span>
<span className="text-[10px] font-bold text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded border border-white/5">{row.asin}</span>
</div>