feat: implement QOL features (Stats, Search, Dram of the Day)

This commit is contained in:
2025-12-18 12:34:51 +01:00
parent 7d395392d1
commit 35c2443473
6 changed files with 235 additions and 11 deletions

View 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>
);
}