feat: redesign dashboard layout and colors to match mockup

This commit is contained in:
Christian Vidal Wolf
2026-06-17 10:50:18 +02:00
parent 47e2513b79
commit 41ad8d3598
+56 -92
View File
@@ -22,6 +22,16 @@ interface DashboardProps {
const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9'];
const getYearColor = (year: string | number) => {
const yStr = year.toString();
if (yStr === '2024') return '#6366f1'; // Indigo
if (yStr === '2025') return '#ec4899'; // Pink
if (yStr === '2026') return '#10b981'; // Emerald
const yInt = parseInt(yStr);
if (isNaN(yInt)) return '#6366f1';
return COLORS[yInt % COLORS.length];
};
// Reusable Expandable Card Component
const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; className?: string }> = ({ title, children, className }) => {
const [isExpanded, setIsExpanded] = useState(false);
@@ -98,11 +108,9 @@ const MultiYearKPICard: React.FC<{
const sortedYears = [...availableYears].sort((a, b) => parseInt(b) - parseInt(a));
const formatValue = (val: number) => {
if (metric === 'sellOut') {
if (metric === 'sellOut') {
return `${val.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
}
}
return val.toLocaleString('de-DE');
};
@@ -110,10 +118,10 @@ const MultiYearKPICard: React.FC<{
const displayTitle = contextData ? `${title} (Selected Item)` : title;
return (
<div className="bg-gradient-to-br from-surface to-slate-900 border border-border rounded-xl p-3 md:p-6 flex flex-col justify-center">
<h3 className="text-xs md:text-sm font-medium text-slate-400 mb-2 md:mb-3">{displayTitle}</h3>
<div className="bg-slate-900/40 border border-slate-800 rounded-xl p-5 md:p-6 flex flex-col justify-center shadow-lg">
<h3 className="text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wider mb-4">{displayTitle}</h3>
{sortedYears.length === 0 && <p className="text-3xl font-bold text-white">0</p>}
{sortedYears.length === 0 && <p className="text-2xl font-bold text-white">0</p>}
{sortedYears.length > 0 && (
<div className="space-y-4">
@@ -134,7 +142,7 @@ const MultiYearKPICard: React.FC<{
const pct = ((currentValue - prevValue) / prevValue) * 100;
growthElement = (
<span className={`text-xs ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
@@ -146,7 +154,7 @@ const MultiYearKPICard: React.FC<{
const pct = ((contextValue - prevContextValue) / prevContextValue) * 100;
contextGrowthElement = (
<span className={`text-[10px] ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
@@ -154,22 +162,18 @@ const MultiYearKPICard: React.FC<{
}
return (
<div key={year} className="flex flex-col border-b border-slate-800 pb-2 last:border-0">
<div className="flex justify-between items-end">
<div className="flex flex-col">
<span className="text-xs text-slate-500 font-mono mb-0.5">{year}</span>
<div className="flex items-center">
<span className="text-xl font-bold text-slate-200 leading-none">
<div key={year} className="flex flex-col border-b border-slate-800/80 pb-3 last:border-0 last:pb-0">
<span className="text-xs text-slate-500 font-medium mb-1">{year}</span>
<div className="flex items-baseline">
<span className="text-2xl font-bold text-white tracking-tight leading-none">
{formatValue(currentValue)}
</span>
{growthElement}
</div>
</div>
</div>
{/* Context Row (Product Line Total) */}
{contextData && contextValue > 0 && (
<div className="mt-1 flex flex-wrap items-center justify-between bg-slate-800/50 p-2 rounded text-xs">
<div className="mt-2 flex flex-wrap items-center justify-between bg-slate-850/50 p-2 rounded text-xs border border-slate-800">
<span className="text-slate-400 mr-2">Total Product Line:</span>
<div className="flex items-center gap-1">
<span className="text-slate-300 font-medium">{formatValue(contextValue)}</span>
@@ -232,7 +236,7 @@ const SeasonalityTooltip = ({ active, payload, label, metric }: any) => {
const pct = ((currVal - prevVal) / prevVal) * 100;
growthEl = (
<span className={`text-xs ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
@@ -282,7 +286,7 @@ const ComparisonTooltip = ({ active, payload, label, metric }: any) => {
const pct = ((currVal - prevVal) / prevVal) * 100;
growthEl = (
<span className={`text-xs ml-2 font-bold ${pct >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
{pct >= 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}%
</span>
);
}
@@ -361,62 +365,35 @@ const GrowthTable: React.FC<{
<thead className="bg-slate-950 text-xs uppercase font-semibold text-slate-500 sticky top-0 z-10 shadow-sm">
<tr>
<th
className="px-4 py-3 bg-slate-950 min-w-[150px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
className="px-4 py-3 bg-slate-950 min-w-[120px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('line')}
>
<div className="flex items-center">Product Line {getSortIndicator('line')}</div>
<div className="flex items-center font-bold">Product Line {getSortIndicator('line')}</div>
</th>
{/* Sell Out Columns */}
<th
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('previousYearSellOut')}
>
<div className="flex items-center justify-end">Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}</div>
<div className="flex items-center justify-end font-bold">Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
onClick={() => requestSort('currentYearSellOut')}
>
<div className="flex items-center justify-end">Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}</div>
<div className="flex items-center justify-end font-bold">Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('sellOutGrowthValue')}
>
<div className="flex items-center justify-end">SO Diff {getSortIndicator('sellOutGrowthValue')}</div>
<div className="flex items-center justify-end font-bold">SO Diff {getSortIndicator('sellOutGrowthValue')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('sellOutGrowthPercentage')}
>
<div className="flex items-center justify-end">SO Growth % {getSortIndicator('sellOutGrowthPercentage')}</div>
</th>
{/* Units Columns */}
<th
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('previousYearUnits')}
>
<div className="flex items-center justify-end">Units {periods.previous} {getSortIndicator('previousYearUnits')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
onClick={() => requestSort('currentYearUnits')}
>
<div className="flex items-center justify-end">Units {periods.current} {getSortIndicator('currentYearUnits')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('unitsGrowthValue')}
>
<div className="flex items-center justify-end">Units Diff {getSortIndicator('unitsGrowthValue')}</div>
</th>
<th
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
onClick={() => requestSort('unitsGrowthPercentage')}
>
<div className="flex items-center justify-end">Units Growth % {getSortIndicator('unitsGrowthPercentage')}</div>
<div className="flex items-center justify-end font-bold">SO Growth % {getSortIndicator('sellOutGrowthPercentage')}</div>
</th>
</tr>
</thead>
@@ -426,34 +403,21 @@ const GrowthTable: React.FC<{
<tr key={idx} className="hover:bg-slate-800/50">
<td className="px-4 py-2 font-medium">{item.line}</td>
{/* Sell Out Columns */}
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
<td className={`px-4 py-2 text-right font-medium ${item.sellOutGrowthValue >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{item.sellOutGrowthValue > 0 ? '+' : ''}{item.sellOutGrowthValue.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</td>
<td className="px-4 py-2 text-right">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.sellOutGrowthPercentage >= 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
{item.sellOutGrowthPercentage.toFixed(0)}%
</span>
</td>
{/* Units Columns */}
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearUnits.toLocaleString('de-DE')}</td>
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearUnits.toLocaleString('de-DE')}</td>
<td className={`px-4 py-2 text-right font-medium ${item.unitsGrowthValue >= 0 ? 'text-violet-400' : 'text-orange-400'}`}>
{item.unitsGrowthValue > 0 ? '+' : ''}{item.unitsGrowthValue.toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 text-right">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.unitsGrowthPercentage >= 0 ? 'bg-violet-500/10 text-violet-400' : 'bg-orange-500/10 text-orange-400'}`}>
{item.unitsGrowthPercentage.toFixed(0)}%
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-bold ${item.sellOutGrowthPercentage >= 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
{item.sellOutGrowthPercentage >= 0 ? '+' : ''}{item.sellOutGrowthPercentage.toFixed(0)}%
</span>
</td>
</tr>
))
) : (
<tr>
<td colSpan={9} className="px-4 py-6 text-center text-slate-500 italic">
<td colSpan={5} className="px-4 py-6 text-center text-slate-500 italic">
Insufficient data to calculate {type}. (Select at least 2 distinct years/periods)
</td>
</tr>
@@ -463,7 +427,7 @@ const GrowthTable: React.FC<{
</div>
</ExpandableCard>
);
}
};
const Dashboard: React.FC<DashboardProps> = ({
data,
@@ -550,16 +514,16 @@ const Dashboard: React.FC<DashboardProps> = ({
return (
<div className="p-3 md:p-6 space-y-4 md:space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
{/* Ads Performance Section - Condensed Layout */}
{/* Ads Performance Section - Overhauled Layout to match Mockup */}
{adsKPIsByYear && availableAdsYears.length > 0 && (
<div className="bg-gradient-to-br from-fuchsia-950/20 to-indigo-950/30 border border-fuchsia-500/30 rounded-2xl p-4 animate-fade-in shadow-xl">
<div className="flex items-center gap-2 mb-4">
<div className="bg-slate-900/40 border border-slate-800 rounded-2xl p-5 animate-fade-in shadow-xl">
<div className="flex items-center gap-2 mb-4 border-b border-slate-800/80 pb-3">
<div className="relative">
<span className="absolute inset-0 bg-fuchsia-400 rounded-full animate-ping opacity-20"></span>
<span className="relative block w-2 h-2 rounded-full bg-fuchsia-400 shadow-[0_0_8px_rgba(232,121,249,0.8)]"></span>
</div>
<h3 className="text-xs font-black text-fuchsia-400 uppercase tracking-widest">Advertising Performance</h3>
<div className="ml-auto px-2 py-0.5 bg-slate-900/50 rounded-full border border-fuchsia-500/10 text-[9px] text-fuchsia-300/70 font-bold uppercase">
<div className="ml-auto px-2.5 py-0.5 bg-slate-950 rounded border border-slate-800 text-[10px] text-slate-400 font-bold uppercase tracking-wider">
{adsData.length.toLocaleString('de-DE')} Records
</div>
</div>
@@ -579,25 +543,25 @@ const Dashboard: React.FC<DashboardProps> = ({
: (isPositive ? 'text-emerald-400' : 'text-red-400');
return (
<span className={`text-[9px] font-black ml-1 ${colorClass} whitespace-nowrap`}>
<span className={`text-[10px] font-bold ml-1 ${colorClass} whitespace-nowrap`}>
{isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(0)}%
</span>
);
};
return (
<div key={year} className="relative bg-slate-950/40 rounded-xl p-3 border border-white/5">
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] font-black text-slate-400 font-mono bg-slate-900 px-1.5 py-0.5 rounded">{year}</span>
<div className="h-[1px] flex-1 bg-gradient-to-r from-slate-800 to-transparent"></div>
<div key={year} className="relative bg-slate-950/30 rounded-xl p-4 border border-white/5">
<div className="flex items-center gap-2 mb-3">
<span className="text-[10px] font-black text-slate-400 font-mono bg-slate-900 px-2 py-0.5 rounded border border-slate-800">{year}</span>
<div className="h-[1px] flex-1 bg-gradient-to-r from-slate-800/80 to-transparent"></div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{/* Ad Spend */}
<div className="group">
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-tighter block mb-0.5">Spend</span>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Spend</span>
<div className="flex items-baseline overflow-hidden">
<span className="text-sm font-black text-fuchsia-400 truncate">
<span className="text-base font-bold text-fuchsia-400 truncate">
{kpi.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderGrowth(kpi.totalSpend, prevKpi?.totalSpend)}
@@ -605,9 +569,9 @@ const Dashboard: React.FC<DashboardProps> = ({
</div>
{/* Attributed Sales */}
<div className="group">
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-tighter block mb-0.5">Sales</span>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">Sales</span>
<div className="flex items-baseline overflow-hidden">
<span className="text-sm font-black text-emerald-400 truncate">
<span className="text-base font-bold text-emerald-400 truncate">
{kpi.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{renderGrowth(kpi.attributedSales, prevKpi?.attributedSales)}
@@ -615,9 +579,9 @@ const Dashboard: React.FC<DashboardProps> = ({
</div>
{/* ACOS */}
<div className="group">
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-tighter block mb-0.5">ACOS</span>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">ACOS</span>
<div className="flex items-baseline overflow-hidden">
<span className={`text-sm font-black truncate ${kpi.acos <= 30 ? 'text-emerald-400' : kpi.acos <= 50 ? 'text-amber-400' : 'text-red-400'}`}>
<span className="text-base font-bold text-emerald-400 truncate">
{kpi.acos.toFixed(1)}%
</span>
{renderGrowth(kpi.acos, prevKpi?.acos, true)}
@@ -625,9 +589,9 @@ const Dashboard: React.FC<DashboardProps> = ({
</div>
{/* ROAS */}
<div className="group">
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-tighter block mb-0.5">ROAS</span>
<span className="text-[9px] font-semibold text-slate-500 uppercase tracking-wider block mb-1">ROAS</span>
<div className="flex items-baseline overflow-hidden">
<span className={`text-sm font-black truncate ${kpi.roas >= 3 ? 'text-emerald-400' : kpi.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}>
<span className="text-base font-bold text-emerald-400 truncate">
{kpi.roas.toFixed(2)}x
</span>
{renderGrowth(kpi.roas, prevKpi?.roas)}
@@ -659,7 +623,7 @@ const Dashboard: React.FC<DashboardProps> = ({
/>
</div>
{/* Main Grid */}
{/* Main Grid matching Mockup Columns */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column */}
@@ -704,7 +668,7 @@ const Dashboard: React.FC<DashboardProps> = ({
key={year}
dataKey={`${year}_${top10Metric === 'sellOut' ? 'value' : 'units'}`}
name={year}
fill={COLORS[index % COLORS.length]}
fill={getYearColor(year)}
radius={[0, 4, 4, 0]}
/>
))}
@@ -762,7 +726,7 @@ const Dashboard: React.FC<DashboardProps> = ({
key={year}
dataKey={year}
name={year}
fill={COLORS[index % COLORS.length]}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
/>
))}
@@ -808,7 +772,7 @@ const Dashboard: React.FC<DashboardProps> = ({
type="monotone"
dataKey={year}
name={year}
stroke={COLORS[index % COLORS.length]}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
@@ -856,7 +820,7 @@ const Dashboard: React.FC<DashboardProps> = ({
type="monotone"
dataKey={year}
name={year}
stroke={COLORS[index % COLORS.length]}
stroke={getYearColor(year)}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6 }}
@@ -882,7 +846,7 @@ const Dashboard: React.FC<DashboardProps> = ({
key={year}
dataKey={year}
name={year}
fill={COLORS[index % COLORS.length]}
fill={getYearColor(year)}
radius={[4, 4, 0, 0]}
/>
))}