mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:55:23 +02:00
feat: add Top 50 filter button and badges to Ads Performance tab
This commit is contained in:
@@ -531,7 +531,7 @@ const App: React.FC = () => {
|
|||||||
{view === 'table' && <DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
|
{view === 'table' && <DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
|
||||||
{view === 'weekly' && <WeeklyGrid data={combinedAdsData} top50Ranking={top50Ranking2025} onDrillDown={handleSkuDrillDown} />}
|
{view === 'weekly' && <WeeklyGrid data={combinedAdsData} top50Ranking={top50Ranking2025} onDrillDown={handleSkuDrillDown} />}
|
||||||
{view === 'movers' && <TopMovers data={filteredData} />}
|
{view === 'movers' && <TopMovers data={filteredData} />}
|
||||||
{view === 'ads' && <AdsPerformance data={combinedAdsData} filters={filters} />}
|
{view === 'ads' && <AdsPerformance data={combinedAdsData} filters={filters} top50Ranking={top50Ranking2025} />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,12 +4,38 @@ import { CombinedKPIs, FilterState } from '../types';
|
|||||||
interface AdsPerformanceProps {
|
interface AdsPerformanceProps {
|
||||||
data: CombinedKPIs[];
|
data: CombinedKPIs[];
|
||||||
filters: FilterState;
|
filters: FilterState;
|
||||||
|
top50Ranking?: {
|
||||||
|
eu: Map<string, number>;
|
||||||
|
uk: Map<string, number>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
||||||
|
|
||||||
const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
// Top 50 Badge Component
|
||||||
|
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 AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters, top50Ranking }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||||
|
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' }>({
|
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' }>({
|
||||||
key: 'cost',
|
key: 'cost',
|
||||||
direction: 'desc'
|
direction: 'desc'
|
||||||
@@ -137,7 +163,22 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
|
|
||||||
// 3. Filter and Sort
|
// 3. Filter and Sort
|
||||||
const filteredAndSorted = useMemo(() => {
|
const filteredAndSorted = useMemo(() => {
|
||||||
let result = aggregatedByAsin.filter(item =>
|
let result = aggregatedByAsin;
|
||||||
|
|
||||||
|
// Top 50 Filter
|
||||||
|
if (showOnlyTop50 && top50Ranking) {
|
||||||
|
result = result.filter(item => {
|
||||||
|
const asin = item.asin.trim().toUpperCase();
|
||||||
|
if (top50Mode === 'eu') {
|
||||||
|
return top50Ranking.eu.has(asin);
|
||||||
|
} else {
|
||||||
|
return top50Ranking.uk.has(asin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Filter
|
||||||
|
result = result.filter(item =>
|
||||||
item.asin.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
item.asin.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
item.sku.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
item.sku.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
item.title.toLowerCase().includes(searchTerm.toLowerCase())
|
item.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
@@ -150,7 +191,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [aggregatedByAsin, searchTerm, sortConfig]);
|
}, [aggregatedByAsin, searchTerm, sortConfig, showOnlyTop50, top50Ranking, top50Mode]);
|
||||||
|
|
||||||
const handleSort = (key: SortKey) => {
|
const handleSort = (key: SortKey) => {
|
||||||
setSortConfig(prev => ({
|
setSortConfig(prev => ({
|
||||||
@@ -213,6 +254,41 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Top 50 Filter Toggle */}
|
||||||
|
{top50Ranking && (
|
||||||
|
(top50Ranking.eu.size || 0) > 0 ||
|
||||||
|
(top50Ranking.uk.size || 0) > 0
|
||||||
|
) && (
|
||||||
|
<div className="flex bg-slate-950/50 p-1 rounded-xl border border-white/10 shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowOnlyTop50(!showOnlyTop50)}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${showOnlyTop50
|
||||||
|
? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-lg shadow-amber-500/20'
|
||||||
|
: 'text-slate-400 hover:text-amber-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-sm">🏆</span>
|
||||||
|
Top 50
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showOnlyTop50 && (
|
||||||
|
<div className="flex ml-1 p-0.5 bg-slate-900 rounded-lg border border-white/5">
|
||||||
|
<button
|
||||||
|
onClick={() => setTop50Mode('eu')}
|
||||||
|
className={`px-2 py-1 rounded-md text-[10px] font-black uppercase tracking-widest transition-all ${top50Mode === 'eu' ? 'bg-indigo-500 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'}`}
|
||||||
|
>
|
||||||
|
EU
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setTop50Mode('uk')}
|
||||||
|
className={`px-2 py-1 rounded-md text-[10px] font-black uppercase tracking-widest transition-all ${top50Mode === 'uk' ? 'bg-blue-500 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'}`}
|
||||||
|
>
|
||||||
|
UK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search ASIN, SKU or Title..."
|
placeholder="Search ASIN, SKU or Title..."
|
||||||
@@ -245,11 +321,27 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/[0.03]">
|
<tbody className="divide-y divide-white/[0.03]">
|
||||||
{filteredAndSorted.map((row: any) => (
|
{filteredAndSorted.map((row: any) => {
|
||||||
|
const asin = row.asin.trim().toUpperCase();
|
||||||
|
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||||
|
|
||||||
|
if (top50Ranking) {
|
||||||
|
const euRank = top50Ranking.eu.get(asin);
|
||||||
|
const ukRank = top50Ranking.uk.get(asin);
|
||||||
|
if (euRank) ranks.push({ rank: euRank, label: 'EU', theme: 'indigo' });
|
||||||
|
if (ukRank) ranks.push({ rank: ukRank, label: 'UK', theme: 'blue' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<tr key={row.id} className="hover:bg-white/[0.02] transition-colors group">
|
<tr key={row.id} className="hover:bg-white/[0.02] transition-colors group">
|
||||||
<td className="p-4 py-3">
|
<td className="p-4 py-3">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{ranks.map((r, i) => (
|
||||||
|
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
|
||||||
|
))}
|
||||||
<span className="text-white font-bold text-sm tracking-tight">{row.asin}</span>
|
<span className="text-white font-bold text-sm tracking-tight">{row.asin}</span>
|
||||||
|
</div>
|
||||||
<span className="text-[10px] uppercase font-black text-indigo-400 tracking-tighter">SKU: {row.sku}</span>
|
<span className="text-[10px] uppercase font-black text-indigo-400 tracking-tighter">SKU: {row.sku}</span>
|
||||||
<span className="text-[10px] text-slate-400 truncate max-w-[220px] leading-tight" title={row.title}>{row.title}</span>
|
<span className="text-[10px] text-slate-400 truncate max-w-[220px] leading-tight" title={row.title}>{row.title}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,7 +361,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
<TableCell value={row.tacos} prevValue={row.prevData?.tacos} format="percent" inverse />
|
<TableCell value={row.tacos} prevValue={row.prevData?.tacos} format="percent" inverse />
|
||||||
<TableCell value={row.glanceViews || 0} prevValue={row.prevData?.glanceViews} format="number" />
|
<TableCell value={row.glanceViews || 0} prevValue={row.prevData?.glanceViews} format="number" />
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user