← back to Watches
src/components/EnhancedDashboard.jsx
395 lines
import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
import { Doughnut, Bar, Scatter } from 'react-chartjs-2';
import {
Chart as ChartJS,
ArcElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend
} from 'chart.js';
import { FiWatch, FiTrendingUp, FiDollarSign, FiDatabase, FiAward } from 'react-icons/fi';
import { StatCardSkeleton } from './SkeletonLoader';
ChartJS.register(
ArcElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend
);
/**
* Enhanced dashboard with advanced data visualizations and interactive charts
*/
function EnhancedDashboard({ statistics, watches, onWatchSelect, loading }) {
const chartData = useMemo(() => {
if (!watches || watches.length === 0) return null;
// Series distribution
const seriesMap = {};
watches.forEach(watch => {
seriesMap[watch.series] = (seriesMap[watch.series] || 0) + 1;
});
const seriesDistribution = {
labels: Object.keys(seriesMap),
datasets: [{
data: Object.values(seriesMap),
backgroundColor: [
'#C8102E',
'#FFD700',
'#1A1A2E',
'#C0C0C0',
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0'
],
borderWidth: 2,
borderColor: '#fff'
}]
};
// Price distribution by year
const yearPriceMap = {};
watches.forEach(watch => {
const year = watch.yearIntroduced;
const currentPrice = watch.priceHistory[watch.priceHistory.length - 1]?.price || 0;
if (!yearPriceMap[year]) yearPriceMap[year] = [];
yearPriceMap[year].push(currentPrice);
});
const yearLabels = Object.keys(yearPriceMap).sort();
const avgPrices = yearLabels.map(year => {
const prices = yearPriceMap[year];
return prices.reduce((sum, p) => sum + p, 0) / prices.length;
});
const priceByYear = {
labels: yearLabels,
datasets: [{
label: 'Average Current Price',
data: avgPrices,
backgroundColor: '#C8102E',
borderColor: '#C8102E',
borderWidth: 2
}]
};
// Appreciation vs Price scatter
const scatterData = watches.map(watch => {
const first = watch.priceHistory[0].price;
const last = watch.priceHistory[watch.priceHistory.length - 1].price;
const appreciation = ((last - first) / first) * 100;
return {
x: last,
y: appreciation,
label: watch.model
};
});
const appreciationScatter = {
datasets: [{
label: 'Watches',
data: scatterData,
backgroundColor: '#C8102E',
borderColor: '#1A1A2E',
borderWidth: 2,
pointRadius: 6,
pointHoverRadius: 8
}]
};
return { seriesDistribution, priceByYear, appreciationScatter };
}, [watches]);
if (loading || !statistics) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map(i => <StatCardSkeleton key={i} />)}
</div>
</div>
);
}
const totalDataPoints = watches.reduce((sum, w) => sum + w.priceHistory.length, 0);
const avgCurrentPrice = watches.reduce((sum, w) => {
return sum + (w.priceHistory[w.priceHistory.length - 1]?.price || 0);
}, 0) / watches.length;
return (
<div className="space-y-6">
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="Total Watches"
value={statistics.totalWatches}
icon={FiWatch}
color="from-blue-500 to-blue-600"
delay={0}
/>
<StatCard
title="Watch Series"
value={statistics.totalSeries}
icon={FiDatabase}
color="from-green-500 to-green-600"
delay={0.1}
/>
<StatCard
title="Avg Appreciation"
value={`${statistics.averageAppreciation}%`}
icon={FiTrendingUp}
color="from-purple-500 to-purple-600"
delay={0.2}
/>
<StatCard
title="Avg Price"
value={`$${Math.round(avgCurrentPrice).toLocaleString()}`}
icon={FiDollarSign}
color="from-orange-500 to-orange-600"
delay={0.3}
/>
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Series Distribution */}
<ChartCard title="Collection Distribution" delay={0.4}>
<Doughnut
data={chartData.seriesDistribution}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
padding: 15,
font: { size: 11 }
}
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}}
/>
</ChartCard>
{/* Price by Year */}
<ChartCard title="Average Price by Year" delay={0.5}>
<Bar
data={chartData.priceByYear}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (context) => `$${context.parsed.y.toLocaleString()}`
}
}
},
scales: {
y: {
ticks: {
callback: (value) => `$${(value / 1000).toFixed(0)}K`
}
}
}
}}
/>
</ChartCard>
</div>
{/* Appreciation vs Price Scatter */}
<ChartCard title="Price vs Appreciation Analysis" delay={0.6}>
<Scatter
data={chartData.appreciationScatter}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (context) => {
const point = context.raw;
return [
`${point.label}`,
`Price: $${point.x.toLocaleString()}`,
`Appreciation: ${point.y.toFixed(2)}%`
];
}
}
}
},
scales: {
x: {
title: { display: true, text: 'Current Price ($)' },
ticks: {
callback: (value) => `$${(value / 1000).toFixed(0)}K`
}
},
y: {
title: { display: true, text: 'Appreciation (%)' },
ticks: {
callback: (value) => `${value}%`
}
}
}
}}
/>
</ChartCard>
{/* Top Performers */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
>
<div className="flex items-center space-x-3 mb-6">
<FiAward className="text-3xl text-omega-red" />
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">
Top Performing Watches
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{statistics.topPerformers.map((watch, index) => (
<motion.div
key={watch.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7 + index * 0.1 }}
className="flex items-center justify-between p-4 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-600 rounded-lg hover:shadow-md transition-all cursor-pointer group"
onClick={() => {
const fullWatch = watches.find(w => w.id === watch.id);
if (fullWatch) onWatchSelect(fullWatch);
}}
>
<div className="flex items-center space-x-4">
<div className="text-3xl font-bold text-omega-red w-12 text-center">
#{index + 1}
</div>
<div>
<h3 className="font-semibold text-gray-800 dark:text-white group-hover:text-omega-red transition-colors">
{watch.model}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{watch.series}</p>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-green-600">
+{watch.appreciation}%
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
${watch.currentPrice.toLocaleString()}
</p>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Collection Summary */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
Collection Overview
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{getSeriesBreakdown(watches).map((series, index) => (
<motion.div
key={series.name}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.8 + index * 0.05 }}
className="p-4 bg-gradient-to-br from-omega-navy to-omega-red text-white rounded-lg hover:shadow-lg transition-shadow"
>
<h3 className="text-sm font-bold mb-2 opacity-90">{series.name}</h3>
<p className="text-3xl font-bold">{series.count}</p>
<p className="text-xs opacity-80">models tracked</p>
</motion.div>
))}
</div>
</motion.div>
</div>
);
}
function StatCard({ title, value, icon: Icon, color, delay }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
whileHover={{ scale: 1.02, y: -5 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 hover:shadow-xl transition-all"
>
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm font-medium">{title}</p>
<p className="text-3xl font-bold text-gray-800 dark:text-white mt-2">{value}</p>
</div>
<div className={`bg-gradient-to-br ${color} w-16 h-16 rounded-full flex items-center justify-center shadow-lg`}>
<Icon className="text-white text-2xl" />
</div>
</div>
</motion.div>
);
}
function ChartCard({ title, children, delay }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
>
<h3 className="text-lg font-bold text-gray-800 dark:text-white mb-4">{title}</h3>
<div className="h-64 md:h-80">
{children}
</div>
</motion.div>
);
}
function getSeriesBreakdown(watches) {
const seriesMap = {};
watches.forEach(watch => {
seriesMap[watch.series] = (seriesMap[watch.series] || 0) + 1;
});
return Object.entries(seriesMap)
.map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count);
}
export default EnhancedDashboard;