- Created type-safe i18n system with TranslationKeys interface - Added German (de) and English (en) translations with 160+ keys - Implemented I18nContext provider and useI18n hook - Added LanguageSwitcher component for language selection - Refactored all major components to use translations: * Home page, StatsDashboard, DramOfTheDay * BottleGrid, EditBottleForm, CameraCapture * BuddyList, SessionList, TastingNoteForm * StatusSwitcher and bottle management features - Implemented locale-aware currency formatting (EUR) - Implemented locale-aware date formatting - Added localStorage persistence for language preference - Added automatic browser language detection - Organized translations into 8 main categories - System is extensible for additional languages
102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import React, { useMemo } from 'react';
|
|
import { TrendingUp, CreditCard, Star, Home, BarChart3 } from 'lucide-react';
|
|
import { useI18n } from '@/i18n/I18nContext';
|
|
|
|
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 { t, locale } = useI18n();
|
|
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: t('home.stats.totalValue'),
|
|
value: stats.totalValue.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }),
|
|
icon: CreditCard,
|
|
color: 'text-green-600',
|
|
bg: 'bg-green-50 dark:bg-green-900/20'
|
|
},
|
|
{
|
|
label: t('home.stats.activeBottles'),
|
|
value: stats.activeCount,
|
|
icon: Home,
|
|
color: 'text-blue-600',
|
|
bg: 'bg-blue-50 dark:bg-blue-900/20'
|
|
},
|
|
{
|
|
label: t('home.stats.avgRating'),
|
|
value: `${stats.avgRating}/100`,
|
|
icon: Star,
|
|
color: 'text-amber-600',
|
|
bg: 'bg-amber-50 dark:bg-amber-900/20'
|
|
},
|
|
{
|
|
label: t('home.stats.topDistillery'),
|
|
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>
|
|
);
|
|
}
|