mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 15:45:24 +02:00
Performance optimizations: lazy rendering, row memoization, and data processing loops
This commit is contained in:
+120
-77
@@ -56,6 +56,95 @@ const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'bl
|
||||
);
|
||||
};
|
||||
|
||||
const WeeklyRow: React.FC<{
|
||||
row: WeeklyPivotRow;
|
||||
weeks: string[];
|
||||
onDrillDown?: (sku: string) => void;
|
||||
stockMap?: Map<string, number>;
|
||||
top50Ranking?: { eu: Map<string, number>; uk: Map<string, number> };
|
||||
top50Mode: 'eu' | 'uk';
|
||||
sortConfig: SortConfig;
|
||||
renderGrowth: (current: number, previous: number) => React.ReactNode;
|
||||
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth }) => {
|
||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||
const asin = row.asin.trim().toUpperCase();
|
||||
if (top50Ranking) {
|
||||
if (top50Mode === 'eu') {
|
||||
const rank = top50Ranking.eu.get(asin);
|
||||
if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' });
|
||||
} else {
|
||||
const rank = top50Ranking.uk.get(asin);
|
||||
if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr 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">
|
||||
{ranks.map((r, i) => (
|
||||
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
|
||||
))}
|
||||
<span
|
||||
onClick={() => onDrillDown?.(row.sku)}
|
||||
className={`text-xs font-black uppercase tracking-tighter truncate max-w-[120px] transition-all
|
||||
${onDrillDown ? 'text-indigo-400 cursor-pointer hover:text-indigo-300 hover:underline' : 'text-indigo-400/70'}`}
|
||||
title={onDrillDown ? `Click to see Ads detail for ${row.sku}` : ''}
|
||||
>
|
||||
{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>
|
||||
<div className="flex items-start gap-2 mb-1">
|
||||
<span className="text-[11px] text-white/70 truncate w-[190px] leading-tight" title={row.title}>{row.title}</span>
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
||||
</div>
|
||||
</td>
|
||||
{weeks.map((week, idx) => {
|
||||
const val = row.unitsByWeek[week] || 0;
|
||||
const prevVal = row.unitsByWeek[weeks[idx + 1]] || 0;
|
||||
const spend = row.spendByWeek[week] || 0;
|
||||
const prevSpend = row.spendByWeek[weeks[idx + 1]] || 0;
|
||||
return (
|
||||
<td key={week} className={`p-3 py-2 text-center border-r border-white/5 align-middle ${sortConfig?.key === week ? 'bg-white/[0.01]' : ''}`}>
|
||||
<div className="flex flex-col items-center justify-center gap-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`text-sm font-bold ${val > 0 ? (sortConfig?.key === week && sortConfig.metric === 'units' ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}>
|
||||
{val > 0 ? val.toLocaleString('de-DE') : '-'}
|
||||
</span>
|
||||
{val > 0 && renderGrowth(val, prevVal)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
{spend > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
|
||||
€{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
||||
</span>
|
||||
{renderGrowth(spend, prevSpend)}
|
||||
</div>
|
||||
)}
|
||||
{row.gvByWeek?.[week] > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`text-[9px] font-black tracking-tighter ${sortConfig?.key === week && sortConfig.metric === 'gv' ? 'text-teal-300' : 'text-teal-500/70'}`}>
|
||||
GV: {row.gvByWeek[week].toLocaleString('de-DE')}
|
||||
</span>
|
||||
{renderGrowth(row.gvByWeek[week], row.gvByWeek?.[weeks[idx + 1]] || 0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange }) => {
|
||||
// Pivot data - memoized
|
||||
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
||||
@@ -70,6 +159,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
||||
const [growthThreshold, setGrowthThreshold] = useState(10);
|
||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
|
||||
const [displayCount, setDisplayCount] = useState(50);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Default sort: most recent week, descending, units
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
|
||||
@@ -492,87 +583,39 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{paginatedRows.length > 0 ? (
|
||||
paginatedRows.map((row) => {
|
||||
const asin = row.asin.trim().toUpperCase();
|
||||
const ranks = [];
|
||||
|
||||
if (top50Ranking) {
|
||||
if (top50Mode === 'eu') {
|
||||
const rank = top50Ranking.eu.get(asin);
|
||||
if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' as const });
|
||||
} else {
|
||||
const rank = top50Ranking.uk.get(asin);
|
||||
if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' as const });
|
||||
}
|
||||
}
|
||||
|
||||
{sortedRows.length > 0 ? (
|
||||
(() => {
|
||||
const displayRows = sortedRows.slice(0, displayCount);
|
||||
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">
|
||||
{ranks.map((r, i) => (
|
||||
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
|
||||
))}
|
||||
<span
|
||||
onClick={() => onDrillDown?.(row.sku)}
|
||||
className={`text-xs font-black uppercase tracking-tighter truncate max-w-[120px] transition-all
|
||||
${onDrillDown ? 'text-indigo-400 cursor-pointer hover:text-indigo-300 hover:underline' : 'text-indigo-400/70'}`}
|
||||
title={onDrillDown ? `Click to see Ads detail for ${row.sku}` : ''}
|
||||
<>
|
||||
{displayRows.map(row => (
|
||||
<WeeklyRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
weeks={weeks}
|
||||
onDrillDown={onDrillDown}
|
||||
stockMap={stockMap}
|
||||
top50Ranking={top50Ranking}
|
||||
top50Mode={top50Mode}
|
||||
sortConfig={sortConfig}
|
||||
renderGrowth={renderGrowth}
|
||||
/>
|
||||
))}
|
||||
{displayCount < sortedRows.length && (
|
||||
<tr>
|
||||
<td colSpan={weeks.length + 1} className="p-6 text-center bg-slate-900/50 backdrop-blur-sm border-t border-white/5">
|
||||
<button
|
||||
onClick={() => setDisplayCount(prev => prev + 100)}
|
||||
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-black uppercase tracking-widest shadow-xl transition-all active:scale-95 border border-indigo-400/30"
|
||||
>
|
||||
{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>
|
||||
<div className="flex items-start gap-2 mb-1">
|
||||
<span className="text-[11px] text-white/70 truncate w-[190px] leading-tight" title={row.title}>{row.title}</span>
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
||||
</div>
|
||||
</td>
|
||||
{weeks.map((week, idx) => {
|
||||
const val = row.unitsByWeek[week] || 0;
|
||||
const prevVal = row.unitsByWeek[weeks[idx + 1]] || 0;
|
||||
const spend = row.spendByWeek[week] || 0;
|
||||
const prevSpend = row.spendByWeek[weeks[idx + 1]] || 0;
|
||||
return (
|
||||
<td key={week} className={`p-3 py-2 text-center border-r border-white/5 align-middle ${sortConfig?.key === week ? 'bg-white/[0.01]' : ''}`}>
|
||||
<div className="flex flex-col items-center justify-center gap-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`text-sm font-bold ${val > 0 ? (sortConfig?.key === week && sortConfig.metric === 'units' ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}>
|
||||
{val > 0 ? val.toLocaleString('de-DE') : '-'}
|
||||
</span>
|
||||
{val > 0 && renderGrowth(val, prevVal)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
{spend > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
|
||||
€{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
||||
</span>
|
||||
{renderGrowth(spend, prevSpend)}
|
||||
</div>
|
||||
)}
|
||||
{row.gvByWeek?.[week] > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`text-[9px] font-black tracking-tighter ${sortConfig?.key === week && sortConfig.metric === 'gv' ? 'text-teal-300' : 'text-teal-500/70'}`}>
|
||||
GV: {row.gvByWeek[week].toLocaleString('de-DE')}
|
||||
</span>
|
||||
{renderGrowth(row.gvByWeek[week], row.gvByWeek?.[weeks[idx + 1]] || 0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
Load More SKUs ({sortedRows.length - displayCount} remaining)
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})
|
||||
})()
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={weeks.length + 1} className="p-10 text-center text-slate-500 italic text-base">
|
||||
|
||||
Reference in New Issue
Block a user