feat: implement QOL features (Stats, Search, Dram of the Day)
This commit is contained in:
99
src/components/StatsDashboard.tsx
Normal file
99
src/components/StatsDashboard.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { TrendingUp, CreditCard, Star, Home, BarChart3 } from 'lucide-react';
|
||||
|
||||
interface Bottle {
|
||||
id: string;
|
||||
purchase_price?: number | null;
|
||||
status: 'sealed' | 'open' | 'sampled' | 'empty';
|
||||
distillery?: string;
|
||||
tastings?: { rating: number }[];
|
||||
}
|
||||
|
||||
interface StatsDashboardProps {
|
||||
bottles: Bottle[];
|
||||
}
|
||||
|
||||
export default function StatsDashboard({ bottles }: StatsDashboardProps) {
|
||||
const stats = useMemo(() => {
|
||||
const activeBottles = bottles.filter(b => b.status !== 'empty');
|
||||
const totalValue = bottles.reduce((sum, b) => sum + (Number(b.purchase_price) || 0), 0);
|
||||
|
||||
const ratings = bottles.flatMap(b => b.tastings?.map(t => t.rating) || []);
|
||||
const avgRating = ratings.length > 0
|
||||
? Math.round(ratings.reduce((sum, r) => sum + r, 0) / ratings.length)
|
||||
: 0;
|
||||
|
||||
const distilleries = bottles
|
||||
.filter(b => b.distillery)
|
||||
.reduce((acc, b) => {
|
||||
acc[b.distillery!] = (acc[b.distillery!] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const topDistillery = Object.entries(distilleries).sort((a, b) => b[1] - a[1])[0]?.[0] || 'N/A';
|
||||
|
||||
return {
|
||||
totalValue,
|
||||
activeCount: activeBottles.length,
|
||||
avgRating,
|
||||
topDistillery,
|
||||
totalCount: bottles.length
|
||||
};
|
||||
}, [bottles]);
|
||||
|
||||
const statItems = [
|
||||
{
|
||||
label: 'Gesamtwert',
|
||||
value: stats.totalValue.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }),
|
||||
icon: CreditCard,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-50 dark:bg-green-900/20'
|
||||
},
|
||||
{
|
||||
label: 'In der Bar',
|
||||
value: stats.activeCount,
|
||||
icon: Home,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-50 dark:bg-blue-900/20'
|
||||
},
|
||||
{
|
||||
label: 'Ø Bewertung',
|
||||
value: `${stats.avgRating}/100`,
|
||||
icon: Star,
|
||||
color: 'text-amber-600',
|
||||
bg: 'bg-amber-50 dark:bg-amber-900/20'
|
||||
},
|
||||
{
|
||||
label: 'Top Brennerei',
|
||||
value: stats.topDistillery,
|
||||
icon: BarChart3,
|
||||
color: 'text-purple-600',
|
||||
bg: 'bg-purple-50 dark:bg-purple-900/20'
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full grid grid-cols-2 md:grid-cols-4 gap-4 mb-8 h-fit animate-in fade-in slide-in-from-top-4 duration-500">
|
||||
{statItems.map((item, idx) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white dark:bg-zinc-900 p-4 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-sm flex flex-col gap-1 relative overflow-hidden group hover:border-amber-500/30 transition-all"
|
||||
>
|
||||
<div className={`p-2 rounded-xl w-fit ${item.bg} mb-1 flex items-center justify-center`}>
|
||||
<Icon size={16} className={item.color} />
|
||||
</div>
|
||||
<div className="text-[10px] font-black uppercase text-zinc-400 tracking-widest">{item.label}</div>
|
||||
<div className="text-lg font-black text-zinc-900 dark:text-white truncate">
|
||||
{item.value}
|
||||
</div>
|
||||
<TrendingUp size={12} className="absolute top-4 right-4 text-zinc-100 dark:text-zinc-800 group-hover:text-amber-500/20 transition-colors" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user