DramLog UI Overhaul: Rebranding, Navigation Improvements, and Scan Workflow Fixes

- Renamed app to DramLog and updated branding to Gold (#C89D46)
- Implemented new BottomNavigation with Floating Scan Button
- Fixed 'black screen' race condition in ScanAndTasteFlow
- Refactored TastingEditor and StatsDashboard for a cleaner editorial look
- Standardized colors and typography across the application
This commit is contained in:
2025-12-21 23:41:33 +01:00
parent d83d2a8873
commit cf491d83b6
11 changed files with 769 additions and 628 deletions

View File

@@ -7,6 +7,7 @@
--background: #0F1014;
--surface: #1A1B20;
--primary: #C89D46;
--text-secondary: #8F9096;
--border: rgba(255, 255, 255, 0.1);
}
}

View File

@@ -18,15 +18,15 @@ import { Playfair_Display } from "next/font/google";
export const metadata: Metadata = {
title: {
default: "Whisky Vault",
template: "%s | Whisky Vault"
default: "DramLog",
template: "%s | DramLog"
},
description: "Dein persönlicher Whisky-Begleiter zum Scannen und Verkosten.",
description: "Premium Digitaler Tasting Begleiter für Genießer.",
manifest: "/manifest.webmanifest",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "Whisky Vault",
title: "DramLog",
},
formatDetection: {
telephone: false,

View File

@@ -12,8 +12,8 @@ import LanguageSwitcher from "@/components/LanguageSwitcher";
import OfflineIndicator from "@/components/OfflineIndicator";
import { useI18n } from "@/i18n/I18nContext";
import { useSession } from "@/context/SessionContext";
import { Sparkles, Camera } from "lucide-react";
import FloatingScannerButton from '@/components/FloatingScannerButton';
import { Sparkles, X } from "lucide-react";
import { BottomNavigation } from '@/components/BottomNavigation';
import ScanAndTasteFlow from '@/components/ScanAndTasteFlow';
export default function Home() {
@@ -151,13 +151,13 @@ export default function Home() {
if (!user) {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-6 bg-zinc-50 dark:bg-black">
<div className="mb-12 text-center">
<h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter mb-4">
WHISKY<span className="text-amber-600">VAULT</span>
<main className="flex min-h-screen flex-col items-center justify-center p-6 bg-[#0F1014]">
<div className="mb-12 text-center animate-in fade-in zoom-in duration-1000">
<h1 className="text-6xl font-display font-bold text-white tracking-tighter mb-4">
DRAM<span className="text-[#C89D46]">LOG</span>
</h1>
<p className="text-zinc-500 max-w-sm mx-auto">
{t('home.searchPlaceholder').replace('...', '')}
<p className="text-[#8F9096] max-w-sm mx-auto font-sans tracking-wide">
Premium Digitaler Tasting Begleiter für Genießer.
</p>
<div className="mt-8">
<LanguageSwitcher />
@@ -169,20 +169,20 @@ export default function Home() {
}
return (
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-zinc-50 dark:bg-black">
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-8">
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-[#0F1014] pb-32">
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-12">
<header className="w-full flex flex-col sm:flex-row justify-between items-center gap-4 sm:gap-0">
<div className="flex flex-col items-center sm:items-start group">
<h1 className="text-4xl font-black text-zinc-900 dark:text-white tracking-tighter">
WHISKY<span className="text-amber-600">VAULT</span>
<h1 className="text-4xl font-display font-bold text-white tracking-tighter">
DRAM<span className="text-[#C89D46]">LOG</span>
</h1>
{activeSession && (
<div className="flex items-center gap-2 mt-1 animate-in fade-in slide-in-from-left-2 duration-700">
<div className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#C89D46] opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-[#C89D46]"></span>
</div>
<span className="text-[9px] font-black uppercase tracking-widest text-red-500 flex items-center gap-1">
<span className="text-[9px] font-sans font-bold uppercase tracking-widest text-[#C89D46] flex items-center gap-1">
<Sparkles size={10} className="animate-pulse" />
Live: {activeSession.name}
</span>
@@ -195,7 +195,7 @@ export default function Home() {
<DramOfTheDay bottles={bottles} />
<button
onClick={handleLogout}
className="text-sm font-medium text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors"
className="text-xs font-sans font-bold uppercase tracking-widest text-[#8F9096] hover:text-white transition-colors"
>
{t('home.logout')}
</button>
@@ -206,7 +206,7 @@ export default function Home() {
<StatsDashboard bottles={bottles} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-5xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 w-full max-w-5xl">
<div className="flex flex-col gap-8">
<SessionList />
</div>
@@ -215,25 +215,27 @@ export default function Home() {
</div>
</div>
<div className="w-full mt-12">
<h2 className="text-2xl font-bold mb-6 text-zinc-800 dark:text-zinc-100 flex items-center gap-3">
{t('home.collection')}
<span className="text-sm font-normal text-zinc-500 bg-zinc-100 dark:bg-zinc-800 px-3 py-1 rounded-full">
{bottles.length}
<div className="w-full mt-4">
<div className="flex items-end justify-between mb-8">
<h2 className="text-3xl font-display font-bold text-white">
Collection
</h2>
<span className="text-xs font-sans font-bold text-[#8F9096] uppercase tracking-widest pb-1">
{bottles.length} Bottles
</span>
</h2>
</div>
{isLoading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-600"></div>
<div className="flex justify-center py-20">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-[#C89D46]"></div>
</div>
) : fetchError ? (
<div className="p-8 bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-3xl text-center">
<p className="text-zinc-800 dark:text-zinc-200 font-bold mb-2">{t('common.error')}</p>
<p className="text-zinc-500 text-sm italic mb-4">{fetchError}</p>
<div className="p-12 bg-[#1A1B20] border border-white/10 rounded-3xl text-center">
<p className="text-white font-display text-xl mb-4">{t('common.error')}</p>
<p className="text-[#8F9096] text-sm italic mb-8 mx-auto max-w-xs">{fetchError}</p>
<button
onClick={fetchCollection}
className="px-6 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl text-xs font-bold uppercase tracking-widest transition-all"
className="px-10 py-4 bg-[#C89D46] hover:bg-[#E0B456] text-[#0F1014] rounded-full text-xs font-sans font-bold uppercase tracking-widest transition-all"
>
{t('home.reTry')}
</button>
@@ -244,7 +246,14 @@ export default function Home() {
</div>
</div>
<FloatingScannerButton onImageSelected={handleImageSelected} />
<BottomNavigation
onScan={handleImageSelected}
onHome={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
onShelf={() => document.getElementById('collection')?.scrollIntoView({ behavior: 'smooth' })}
onSearch={() => document.getElementById('search-filter')?.scrollIntoView({ behavior: 'smooth' })}
onProfile={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
/>
<ScanAndTasteFlow
isOpen={isFlowOpen}
onClose={() => setIsFlowOpen(false)}

View File

@@ -36,68 +36,73 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
return (
<Link
href={`/bottles/${bottle.id}${sessionId ? `?session_id=${sessionId}` : ''}`}
className="block h-full group"
className="block h-[420px] group relative overflow-hidden rounded-2xl border border-white/5 transition-all duration-700 hover:shadow-[0_20px_50px_rgba(0,0,0,0.5)] active:scale-[0.98]"
>
<div className="h-full bg-white dark:bg-zinc-900 rounded-[2rem] overflow-hidden border border-zinc-200 dark:border-zinc-800 shadow-sm transition-all duration-300 hover:shadow-2xl hover:shadow-amber-900/10 hover:-translate-y-1 group-hover:border-amber-500/30">
<div className="aspect-[4/3] overflow-hidden bg-zinc-100 dark:bg-zinc-800 relative">
<img
src={getStorageUrl(bottle.image_url)}
alt={bottle.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
{/* Image Layer - Edge to Edge */}
<div className="absolute inset-0">
<img
src={getStorageUrl(bottle.image_url)}
alt={bottle.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-1000 ease-out"
/>
{sessionId && (
<div className="absolute top-3 left-3 bg-amber-600 text-white text-[9px] font-black px-2 py-1.5 rounded-xl flex items-center gap-1.5 border border-amber-400 shadow-xl animate-in slide-in-from-left-4 duration-500">
<PlusCircle size={12} strokeWidth={3} />
{t('grid.addSession')}
</div>
)}
{/* Gradient Overlay as requested: bottom third, black to transparent */}
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/60 to-transparent" />
</div>
{bottle.last_tasted && (
<div className="absolute top-3 right-3 bg-zinc-900/80 backdrop-blur-md text-white text-[9px] font-black px-2 py-1 rounded-lg flex items-center gap-1 border border-white/10 ring-1 ring-black/5">
<Clock size={10} />
{new Date(bottle.last_tasted).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
</div>
)}
</div>
<div className="p-3 md:p-5 space-y-3 md:space-y-4">
<div>
<div className="flex justify-between items-start mb-1">
<p className="text-[9px] md:text-[10px] font-black text-amber-600 uppercase tracking-[0.2em] leading-none">{bottle.distillery}</p>
{(bottle.is_whisky === false || (bottle.confidence && bottle.confidence < 70)) && (
<div className="flex items-center gap-1 text-[8px] font-black bg-red-500 text-white px-1.5 py-0.5 rounded-full animate-pulse">
<AlertCircle size={8} />
{t('grid.reviewRequired')}
</div>
)}
</div>
<h3 className={`font-black text-lg md:text-xl leading-tight group-hover:text-amber-600 transition-colors line-clamp-2 min-h-[3rem] md:min-h-[3.5rem] flex items-center ${bottle.is_whisky === false ? 'text-red-600 dark:text-red-400' : 'text-zinc-900 dark:text-zinc-100'
}`}>
{bottle.name || t('grid.unknownBottle')}
</h3>
</div>
<div className="flex flex-wrap gap-1.5 md:gap-2">
<span className="px-2 py-0.5 md:px-2.5 md:py-1 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 text-[9px] md:text-[10px] font-black uppercase tracking-widest rounded-lg border border-zinc-200/50 dark:border-zinc-700/50">
{/* Content Layer */}
<div className="absolute inset-0 flex flex-col justify-end p-6">
<div className="space-y-3">
{/* Tags Layer - Minimalist Glassmorphism */}
<div className="flex flex-wrap gap-2 opacity-0 group-hover:opacity-100 translate-y-4 group-hover:translate-y-0 transition-all duration-500">
<span className="px-3 py-1 bg-white/10 backdrop-blur-md border border-white/10 text-[9px] font-sans font-bold uppercase tracking-widest text-[#C89D46] rounded-full">
{shortenCategory(bottle.category)}
</span>
<span className="px-2 py-0.5 md:px-2.5 md:py-1 bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 text-[9px] md:text-[10px] font-black uppercase tracking-widest rounded-lg border border-amber-200/50 dark:border-amber-800/20">
<span className="px-3 py-1 bg-white/10 backdrop-blur-md border border-white/10 text-[9px] font-sans font-bold uppercase tracking-widest text-white/60 rounded-full">
{bottle.abv}% VOL
</span>
</div>
<div className="pt-1 md:pt-2 flex items-center gap-2 text-[9px] md:text-[10px] font-bold text-zinc-400 uppercase tracking-wider border-t border-zinc-100 dark:border-zinc-800">
<Calendar size={10} className="text-zinc-300" />
<span className="opacity-70 text-[8px] md:text-[9px]">{t('grid.addedOn')}</span>
<span className="text-zinc-500 dark:text-zinc-300">
<div>
<p className="text-[10px] font-sans font-bold text-[#C89D46] uppercase tracking-[0.2em] mb-1">
{bottle.distillery}
</p>
<h3 className="font-display font-bold text-2xl text-white leading-tight drop-shadow-lg">
{bottle.name || t('grid.unknownBottle')}
</h3>
</div>
{/* Metadata items */}
<div className="flex items-center gap-4 pt-2 border-t border-white/10 opacity-60 group-hover:opacity-100 transition-opacity">
<div className="flex items-center gap-1.5 text-[10px] font-sans font-medium text-white/70">
<Calendar size={12} className="text-[#C89D46]" />
{new Date(bottle.created_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
</span>
</div>
{bottle.last_tasted && (
<div className="flex items-center gap-1.5 text-[10px] font-sans font-medium text-white/70">
<Clock size={12} className="text-[#C89D46]" />
{new Date(bottle.last_tasted).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
</div>
)}
</div>
</div>
</div>
{/* Top Overlays */}
{(bottle.is_whisky === false || (bottle.confidence && bottle.confidence < 70)) && (
<div className="absolute top-4 right-4 z-10">
<div className="bg-red-500/90 backdrop-blur-sm text-white p-2 rounded-full animate-pulse shadow-lg">
<AlertCircle size={14} />
</div>
</div>
)}
{sessionId && (
<div className="absolute top-4 left-4 z-10 bg-[#C89D46] text-[#0F1014] text-[9px] font-black px-3 py-1.5 rounded-full flex items-center gap-2 border border-white/20 shadow-xl">
<PlusCircle size={14} />
ADD TO SESSION
</div>
)}
</Link>
);
}
@@ -190,51 +195,51 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
const activeFiltersCount = (selectedCategory ? 1 : 0) + (selectedDistillery ? 1 : 0);
return (
<div className="w-full space-y-8">
{/* Search and Filters */}
<div className="w-full max-w-6xl mx-auto px-4 space-y-4">
<div className="flex flex-col md:flex-row gap-3">
<div id="collection" className="w-full space-y-8 scroll-mt-32">
{/* Search and Filters - Minimalist Look */}
<div id="search-filter" className="w-full max-w-6xl mx-auto px-4 space-y-6 scroll-mt-32">
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1 group">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-400 group-focus-within:text-amber-600 transition-colors" size={18} />
<Search className="absolute left-0 top-1/2 -translate-y-1/2 text-[#8F9096] group-focus-within:text-[#C89D46] transition-colors" size={20} />
<input
type="text"
placeholder={t('grid.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-12 py-3.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-[1.25rem] focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500/50 outline-none transition-all shadow-sm"
className="w-full pl-8 pr-8 py-4 bg-transparent border-b border-white/10 focus:border-[#C89D46] outline-none transition-all text-white placeholder:text-[#8F9096] font-sans"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-amber-600 transition-colors"
className="absolute right-0 top-1/2 -translate-y-1/2 text-[#8F9096] hover:text-white transition-colors"
>
<X size={18} />
<X size={20} />
</button>
)}
</div>
<div className="flex gap-3">
<div className="flex gap-4 items-center">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="flex-1 md:flex-none px-4 py-3.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-[1.25rem] text-sm font-bold focus:ring-2 focus:ring-amber-500/20 outline-none cursor-pointer appearance-none text-zinc-700 dark:text-zinc-300 shadow-sm"
className="bg-transparent border-none text-[#8F9096] text-xs font-sans font-bold uppercase tracking-widest outline-none cursor-pointer hover:text-white transition-colors appearance-none"
>
<option value="created_at">{t('grid.sortBy.createdAt')}</option>
<option value="last_tasted">{t('grid.sortBy.lastTasted')}</option>
<option value="name">{t('grid.sortBy.name')}</option>
<option value="created_at" className="bg-[#0F1014]">{t('grid.sortBy.createdAt')}</option>
<option value="last_tasted" className="bg-[#0F1014]">{t('grid.sortBy.lastTasted')}</option>
<option value="name" className="bg-[#0F1014]">{t('grid.sortBy.name')}</option>
</select>
<button
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
className={`px-5 py-3.5 rounded-[1.25rem] text-sm font-bold flex items-center gap-2 transition-all border shadow-sm ${isFiltersOpen || activeFiltersCount > 0
? 'bg-amber-600 border-amber-600 text-white'
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-700 dark:text-zinc-300'
className={`flex items-center gap-2 text-xs font-sans font-bold uppercase tracking-widest transition-all ${isFiltersOpen || activeFiltersCount > 0
? 'text-[#C89D46]'
: 'text-[#8F9096] hover:text-white'
}`}
>
<Filter size={18} />
<span className="hidden sm:inline">{t('grid.filters')}</span>
{activeFiltersCount > 0 && (
<span className="bg-white text-amber-600 w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-black">
<span className="bg-[#C89D46] text-[#0F1014] w-4 h-4 rounded-full flex items-center justify-center text-[8px] font-black">
{activeFiltersCount}
</span>
)}
@@ -242,78 +247,75 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
</div>
</div>
{/* Category Quick Filter (Always visible row) */}
<div className="flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide touch-pan-x">
{/* Category Quick Filter - Glass Chips */}
<div className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide touch-pan-x">
<button
onClick={() => setSelectedCategory(null)}
className={`px-4 py-2 rounded-xl text-xs font-black whitespace-nowrap transition-all border ${selectedCategory === null
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-500 hover:border-zinc-300'
className={`px-4 py-2 rounded-full text-[10px] font-sans font-bold uppercase tracking-widest whitespace-nowrap transition-all border ${selectedCategory === null
? 'bg-[#C89D46] border-[#C89D46] text-[#0F1014]'
: 'bg-white/5 border-white/10 text-[#8F9096] hover:border-white/20'
}`}
>
{t('common.all').toUpperCase()}
{t('common.all')}
</button>
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(selectedCategory === cat ? null : cat)}
className={`px-4 py-2 rounded-xl text-xs font-black whitespace-nowrap transition-all border ${selectedCategory === cat
? 'bg-amber-600 border-amber-600 text-white shadow-lg shadow-amber-600/20'
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-500 hover:border-zinc-300'
className={`px-4 py-2 rounded-full text-[10px] font-sans font-bold uppercase tracking-widest whitespace-nowrap transition-all border ${selectedCategory === cat
? 'bg-[#C89D46] border-[#C89D46] text-[#0F1014]'
: 'bg-white/5 border-white/10 text-[#8F9096] hover:border-white/20'
}`}
>
{shortenCategory(cat).toUpperCase()}
{shortenCategory(cat)}
</button>
))}
</div>
{/* Collapsible Advanced Filters */}
{/* Collapsible Advanced Filters - Minimalist Overlay */}
{isFiltersOpen && (
<div className="p-6 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-[2rem] space-y-6 shadow-xl animate-in fade-in slide-in-from-top-4 duration-300">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 px-1">{t('grid.filter.distillery')}</label>
<div className="flex flex-wrap gap-2">
<div className="p-8 bg-[#1A1B20] border border-white/10 rounded-3xl space-y-8 animate-in fade-in slide-in-from-top-4 duration-500">
<div className="space-y-4">
<label className="text-[10px] font-sans font-bold uppercase tracking-[0.2em] text-[#8F9096]">{t('grid.filter.distillery')}</label>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedDistillery(null)}
className={`px-4 py-2 rounded-full text-[10px] font-sans font-bold uppercase tracking-widest transition-all border ${selectedDistillery === null
? 'bg-[#C89D46] border-[#C89D46] text-[#0F1014]'
: 'bg-white/5 border-white/10 text-[#8F9096]'
}`}
>
{t('common.all')}
</button>
{distilleries.map((dist) => (
<button
onClick={() => setSelectedDistillery(null)}
className={`px-3 py-1.5 rounded-xl text-[10px] font-black transition-all border ${selectedDistillery === null
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900'
: 'bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700 text-zinc-500'
key={dist}
onClick={() => setSelectedDistillery(selectedDistillery === dist ? null : dist)}
className={`px-4 py-2 rounded-full text-[10px] font-sans font-bold uppercase tracking-widest transition-all border ${selectedDistillery === dist
? 'bg-[#C89D46] border-[#C89D46] text-[#0F1014]'
: 'bg-white/5 border-white/10 text-[#8F9096]'
}`}
>
{t('common.all').toUpperCase()}
{dist}
</button>
{distilleries.map((dist) => (
<button
key={dist}
onClick={() => setSelectedDistillery(selectedDistillery === dist ? null : dist)}
className={`px-3 py-1.5 rounded-xl text-[10px] font-black transition-all border ${selectedDistillery === dist
? 'bg-amber-600 border-amber-600 text-white'
: 'bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700 text-zinc-500'
}`}
>
{dist.toUpperCase()}
</button>
))}
</div>
))}
</div>
</div>
<div className="pt-4 border-t border-zinc-100 dark:border-zinc-800 flex justify-between items-center">
<div className="pt-6 border-t border-white/5 flex justify-between items-center">
<button
onClick={() => {
setSelectedCategory(null);
setSelectedDistillery(null);
setSearchQuery('');
}}
className="text-[10px] font-black text-red-500 uppercase tracking-widest hover:underline"
className="text-[10px] font-sans font-bold text-red-500 uppercase tracking-widest hover:text-red-400"
>
{t('grid.resetAll')}
</button>
<button
onClick={() => setIsFiltersOpen(false)}
className="px-6 py-2 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[10px] font-black rounded-xl uppercase tracking-widest transition-transform active:scale-95"
className="px-8 py-3 bg-white text-[#0F1014] text-[10px] font-sans font-bold rounded-full uppercase tracking-widest transition-transform active:scale-95"
>
{t('grid.close')}
</button>

View File

@@ -0,0 +1,108 @@
'use client';
import React from 'react';
import { Home, Grid, Scan, User, Search } from 'lucide-react';
import { usePathname } from 'next/navigation';
interface BottomNavigationProps {
onHome?: () => void;
onShelf?: () => void;
onSearch?: () => void;
onProfile?: () => void;
onScan: (base64: string) => void;
}
export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan }: BottomNavigationProps) => {
const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleScanClick = () => {
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
onScan(reader.result as string);
};
reader.readAsDataURL(file);
}
};
return (
<div className="fixed bottom-0 left-0 w-full z-50 pointer-events-none">
{/* Hidden Input for Scanning */}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
className="hidden"
/>
{/* Background Container mit Glassmorphism */}
<div className="relative bg-[#0F1014]/90 backdrop-blur-xl border-t border-white/10 pb-safe pt-2 pointer-events-auto shadow-[0_-10px_40px_rgba(0,0,0,0.5)]">
<div className="flex justify-between items-end px-6 h-16">
{/* Left Actions */}
<button
onClick={onHome}
className="flex flex-col items-center gap-1 text-[#C89D46] w-12 transition-all active:scale-90"
>
<Home size={24} />
<span className="text-[10px] font-medium font-sans uppercase tracking-widest opacity-80">Home</span>
</button>
<button
onClick={onShelf}
className="flex flex-col items-center gap-1 text-[#8F9096] hover:text-white w-12 transition-all active:scale-90"
>
<Grid size={24} />
<span className="text-[10px] font-medium font-sans uppercase tracking-widest opacity-80">Shelf</span>
</button>
{/* Spacer für den Center Button */}
<div className="w-16" />
{/* Right Actions */}
<button
onClick={onSearch}
className="flex flex-col items-center gap-1 text-[#8F9096] hover:text-white w-12 transition-all active:scale-90"
>
<Search size={24} />
<span className="text-[10px] font-medium font-sans uppercase tracking-widest opacity-80">Search</span>
</button>
<button
onClick={onProfile}
className="flex flex-col items-center gap-1 text-[#8F9096] hover:text-white w-12 transition-all active:scale-90"
>
<User size={24} />
<span className="text-[10px] font-medium font-sans uppercase tracking-widest opacity-80">Admin</span>
</button>
</div>
{/* THE FLOATING MAGIC BUTTON */}
<div className="absolute -top-10 left-1/2 -translate-x-1/2 pointer-events-auto">
<button
onClick={handleScanClick}
className="flex items-center justify-center w-20 h-20 rounded-full
bg-gradient-to-tr from-[#C89D46] to-[#E0B456]
shadow-[0_0_30px_rgba(200,157,70,0.4)]
border-[6px] border-[#0F1014]
active:scale-95 transition-transform duration-200"
aria-label="Scan Bottle"
>
<div className="bg-[#0F1014] p-3 rounded-full">
<Scan color="#C89D46" size={32} strokeWidth={2.5} />
</div>
</button>
{/* Visual Gold Glow */}
<div className="absolute inset-0 rounded-full bg-[#C89D46]/20 blur-2xl -z-10 animate-pulse" />
</div>
</div>
</div>
);
};

View File

@@ -80,18 +80,18 @@ export default function BuddyList() {
};
return (
<div className="bg-white dark:bg-zinc-900 rounded-3xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xl transition-all duration-300">
<div className="bg-[#1A1B21] rounded-3xl p-6 border border-white/5 shadow-xl transition-all duration-300">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold flex items-center gap-2 text-zinc-800 dark:text-zinc-100 italic">
<Users size={24} className="text-amber-600" />
<h3 className="text-sm font-sans font-bold uppercase tracking-[0.2em] flex items-center gap-2 text-[#8F9096]">
<Users size={18} className="text-[#C89D46]" />
{t('buddy.title')}
{!isCollapsed && buddies.length > 0 && (
<span className="text-sm font-normal text-zinc-400 not-italic ml-2">({buddies.length})</span>
<span className="text-[10px] font-sans font-bold opacity-50 ml-2">({buddies.length})</span>
)}
</h3>
<button
onClick={handleToggleCollapse}
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-xl transition-colors text-zinc-400 hover:text-amber-600"
className="p-2 hover:bg-white/5 rounded-xl transition-colors text-[#8F9096] hover:text-[#C89D46]"
title={isCollapsed ? 'Aufklappen' : 'Einklappen'}
>
{isCollapsed ? <ChevronDown size={20} /> : <ChevronUp size={20} />}
@@ -106,46 +106,46 @@ export default function BuddyList() {
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder={t('buddy.placeholder')}
className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-2 text-sm text-white placeholder:text-[#8F9096] focus:outline-none focus:border-[#C89D46] transition-colors"
/>
<button
type="submit"
disabled={isAdding || !newName.trim()}
className="bg-amber-600 hover:bg-amber-700 text-white p-2 rounded-xl transition-all disabled:opacity-50"
className="bg-[#C89D46] hover:bg-[#A67D2E] text-[#0F1014] p-2 rounded-xl transition-all disabled:opacity-50"
>
{isAdding ? <Loader2 size={20} className="animate-spin" /> : <UserPlus size={20} />}
</button>
</form>
{isLoading ? (
<div className="flex justify-center py-8 text-zinc-400">
<div className="flex justify-center py-8 text-[#8F9096]">
<Loader2 size={24} className="animate-spin" />
</div>
) : buddies.length === 0 ? (
<div className="text-center py-8 text-zinc-500 text-sm">
<div className="text-center py-8 text-[#8F9096] text-xs font-sans">
{t('buddy.noBuddies')}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[400px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-zinc-200 dark:scrollbar-thumb-zinc-800">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[400px] overflow-y-auto pr-2 scrollbar-none">
{buddies.map((buddy) => (
<div
key={buddy.id}
className="flex items-center justify-between p-4 bg-white dark:bg-zinc-800/40 rounded-3xl border border-zinc-100 dark:border-zinc-800 group hover:border-amber-500/30 hover:shadow-md transition-all duration-300"
className="flex items-center justify-between p-4 bg-white/5 rounded-[2rem] border border-white/5 group hover:border-white/10 transition-all duration-300"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-2xl bg-amber-50 dark:bg-amber-900/20 flex items-center justify-center text-amber-600 dark:text-amber-500 font-bold shadow-inner">
<div className="w-10 h-10 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center text-[#C89D46] font-display font-bold shadow-inner">
{buddy.name[0].toUpperCase()}
</div>
<div className="flex flex-col">
<span className="font-bold text-zinc-800 dark:text-zinc-100 text-sm tracking-tight">{buddy.name}</span>
<span className="font-bold text-white text-sm tracking-tight">{buddy.name}</span>
{buddy.buddy_profile_id && (
<span className="text-[9px] font-black uppercase text-green-600 dark:text-green-500 tracking-widest">{t('common.link')}</span>
<span className="text-[9px] font-sans font-black uppercase text-[#C89D46]/80 tracking-widest">{t('common.link')}</span>
)}
</div>
</div>
<button
onClick={() => handleDeleteBuddy(buddy.id)}
className="text-zinc-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all p-2 hover:bg-red-50 dark:hover:bg-red-900/10 rounded-xl"
className="text-[#8F9096] hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all p-2 hover:bg-white/5 rounded-xl"
>
<Trash2 size={16} />
</button>
@@ -160,17 +160,17 @@ export default function BuddyList() {
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<div className="flex -space-x-1.5 overflow-hidden">
{buddies.slice(0, 5).map((b, i) => (
<div key={b.id} className="w-7 h-7 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200/50 dark:border-amber-800/50 flex items-center justify-center text-[10px] font-black text-amber-600 dark:text-amber-500 shadow-sm">
<div key={b.id} className="w-7 h-7 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-[10px] font-display font-bold text-[#C89D46] shadow-sm">
{b.name[0].toUpperCase()}
</div>
))}
{buddies.length > 5 && (
<div className="w-7 h-7 rounded-lg bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 flex items-center justify-center text-[8px] font-black text-zinc-500 shadow-sm">
<div className="w-7 h-7 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-[8px] font-sans font-bold text-[#8F9096] shadow-sm">
+{buddies.length - 5}
</div>
)}
</div>
<span className="text-[10px] text-zinc-400 font-black uppercase tracking-widest ml-1">{buddies.length} Buddies</span>
<span className="text-[10px] text-[#8F9096] font-sans font-bold uppercase tracking-widest ml-1">{buddies.length} Buddies</span>
</div>
)}
</div>

View File

@@ -33,8 +33,10 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
const { locale } = useI18n();
const supabase = createClient();
// Trigger scan when open and image provided
useEffect(() => {
if (isOpen && base64Image) {
console.log('[ScanFlow] Starting handleScan...');
handleScan(base64Image);
} else if (!isOpen) {
setState('IDLE');
@@ -51,15 +53,19 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
try {
const cleanBase64 = image.split(',')[1] || image;
console.log('[ScanFlow] Calling magicScan service...');
const result = await magicScan(cleanBase64, 'gemini', locale);
if (result.success && result.data) {
console.log('[ScanFlow] magicScan success');
setBottleMetadata(result.data);
setState('EDITOR');
} else {
console.error('[ScanFlow] magicScan failure:', result.error);
throw new Error(result.error || 'Flasche konnte nicht erkannt werden.');
}
} catch (err: any) {
console.error('[ScanFlow] handleScan error:', err);
setError(err.message);
setState('ERROR');
}
@@ -120,136 +126,141 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
}
};
if (!isOpen) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-[100dvh] w-screen overflow-hidden overscroll-none"
>
{/* Close Button */}
<button
onClick={onClose}
className="absolute top-6 right-6 z-[70] p-2 rounded-full bg-white/5 border border-white/10 text-white/60 hover:text-white transition-colors"
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-[100dvh] w-screen overflow-hidden overscroll-none"
>
<X size={24} />
</button>
{/* Close Button */}
<button
onClick={onClose}
className="absolute top-6 right-6 z-[70] p-2 rounded-full bg-white/5 border border-white/10 text-white/60 hover:text-white transition-colors"
>
<X size={24} />
</button>
<div className="flex-1 w-full h-full flex flex-col relative min-h-0">
{state === 'SCANNING' && (
<div className="flex-1 flex flex-col items-center justify-center">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex flex-col items-center gap-6"
>
<div className="relative">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
className="w-32 h-32 rounded-full border-2 border-dashed border-amber-500/30"
/>
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 size={48} className="animate-spin text-amber-500" />
</div>
</div>
<div className="text-center space-y-2">
<h2 className="text-2xl font-black text-white uppercase tracking-tight">Analysiere Etikett...</h2>
<p className="text-amber-500 font-black uppercase tracking-widest text-[10px] flex items-center justify-center gap-2">
<Sparkles size={12} /> KI-gestütztes Scanning
</p>
</div>
</motion.div>
</div>
)}
{state === 'ERROR' && (
<div className="flex-1 flex flex-col items-center justify-center">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex flex-col items-center gap-6 p-8 text-center"
>
<div className="w-20 h-20 rounded-full bg-red-500/10 flex items-center justify-center text-red-500">
<AlertCircle size={40} />
</div>
<div className="space-y-2">
<h2 className="text-2xl font-black text-white uppercase tracking-tight">Ups! Da lief was schief.</h2>
<p className="text-white/60 text-sm max-w-xs mx-auto">{error || 'Wir konnten die Flasche leider nicht erkennen. Bitte versuch es mit einem anderen Foto.'}</p>
</div>
<button
onClick={onClose}
className="px-8 py-4 bg-white/5 border border-white/10 rounded-2xl text-white font-black uppercase tracking-widest text-[10px] hover:bg-white/10 transition-all"
>
Schließen
</button>
</motion.div>
</div>
)}
{state === 'EDITOR' && bottleMetadata && (
<motion.div
key="editor"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -50, opacity: 0 }}
className="flex-1 w-full h-full flex flex-col min-h-0"
>
<TastingEditor
bottleMetadata={bottleMetadata}
image={base64Image}
onSave={handleSaveTasting}
onOpenSessions={() => setIsSessionsOpen(true)}
activeSessionName={activeSession?.name}
activeSessionId={activeSession?.id}
/>
</motion.div>
)}
{(isSaving) && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute inset-0 z-[80] bg-[#0F1014]/80 backdrop-blur-sm flex flex-col items-center justify-center gap-6"
>
<Loader2 size={48} className="animate-spin text-amber-500" />
<h2 className="text-xl font-black text-white uppercase tracking-tight">Speichere Tasting...</h2>
</motion.div>
)}
{state === 'RESULT' && tastingData && bottleMetadata && (
<div className="flex-1 overflow-y-auto">
<div className="min-h-full flex flex-col items-center justify-center py-20 px-6">
<div className="flex-1 w-full h-full flex flex-col relative min-h-0">
{/*
Robust state check:
If we are IDLE but have an image, we are essentially SCANNING (or about to be).
If we have no image, we shouldn't really be here, but show error just in case.
*/}
{(state === 'SCANNING' || (state === 'IDLE' && base64Image)) && (
<div className="flex-1 flex flex-col items-center justify-center">
<motion.div
key="result"
initial={{ scale: 0.8, opacity: 0 }}
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="w-full max-w-sm"
className="flex flex-col items-center gap-6"
>
<ResultCard
data={{
...tastingData,
complexity: tastingData.complexity || 75,
balance: tastingData.balance || 85,
}}
bottleName={bottleMetadata.name || 'Unknown Whisky'}
image={base64Image}
onShare={handleShare}
/>
<div className="relative">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
className="w-32 h-32 rounded-full border-2 border-dashed border-[#C89D46]/30"
/>
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 size={48} className="animate-spin text-[#C89D46]" />
</div>
</div>
<div className="text-center space-y-2">
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-tight">Analysiere Etikett...</h2>
<p className="text-[#C89D46] font-sans font-bold uppercase tracking-widest text-[10px] flex items-center justify-center gap-2">
<Sparkles size={12} /> KI-gestütztes Scanning
</p>
</div>
</motion.div>
</div>
</div>
)}
</div>
)}
<SessionBottomSheet
isOpen={isSessionsOpen}
onClose={() => setIsSessionsOpen(false)}
/>
</motion.div>
{state === 'ERROR' && (
<div className="flex-1 flex flex-col items-center justify-center">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex flex-col items-center gap-6 p-8 text-center"
>
<div className="w-20 h-20 rounded-full bg-red-500/10 flex items-center justify-center text-red-500">
<AlertCircle size={40} />
</div>
<div className="space-y-2">
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-tight">Ups! Da lief was schief.</h2>
<p className="text-white/60 text-sm max-w-xs mx-auto font-sans">{error || 'Wir konnten die Flasche leider nicht erkennen. Bitte versuch es mit einem anderen Foto.'}</p>
</div>
<button
onClick={onClose}
className="px-8 py-4 bg-white text-[#0F1014] rounded-2xl font-sans font-bold uppercase tracking-widest text-[10px] hover:bg-white/90 transition-all"
>
Schließen
</button>
</motion.div>
</div>
)}
{state === 'EDITOR' && bottleMetadata && (
<motion.div
key="editor"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -50, opacity: 0 }}
className="flex-1 w-full h-full flex flex-col min-h-0"
>
<TastingEditor
bottleMetadata={bottleMetadata}
image={base64Image}
onSave={handleSaveTasting}
onOpenSessions={() => setIsSessionsOpen(true)}
activeSessionName={activeSession?.name}
activeSessionId={activeSession?.id}
/>
</motion.div>
)}
{(isSaving) && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute inset-0 z-[80] bg-[#0F1014]/80 backdrop-blur-sm flex flex-col items-center justify-center gap-6"
>
<Loader2 size={48} className="animate-spin text-[#C89D46]" />
<h2 className="text-xl font-display font-bold text-white uppercase tracking-tight">Speichere Tasting...</h2>
</motion.div>
)}
{state === 'RESULT' && tastingData && bottleMetadata && (
<div className="flex-1 overflow-y-auto">
<div className="min-h-full flex flex-col items-center justify-center py-20 px-6">
<motion.div
key="result"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="w-full max-w-sm"
>
<ResultCard
data={{
...tastingData,
complexity: tastingData.complexity || 75,
balance: tastingData.balance || 85,
}}
bottleName={bottleMetadata.name || 'Unknown Whisky'}
image={base64Image}
onShare={handleShare}
/>
</motion.div>
</div>
</div>
)}
</div>
<SessionBottomSheet
isOpen={isSessionsOpen}
onClose={() => setIsSessionsOpen(false)}
/>
</motion.div>
)}
</AnimatePresence>
);
}

View File

@@ -140,18 +140,18 @@ export default function SessionList() {
};
return (
<div className="bg-white dark:bg-zinc-900 rounded-3xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-xl transition-all duration-300">
<div className="bg-[#1A1B21] rounded-3xl p-6 border border-white/5 shadow-xl transition-all duration-300">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold flex items-center gap-2 text-zinc-800 dark:text-zinc-100 italic">
<Calendar size={24} className="text-amber-600" />
<h3 className="text-sm font-sans font-bold uppercase tracking-[0.2em] flex items-center gap-2 text-[#8F9096]">
<Calendar size={18} className="text-[#C89D46]" />
{t('session.title')}
{!isCollapsed && sessions.length > 0 && (
<span className="text-sm font-normal text-zinc-400 not-italic ml-2">({sessions.length})</span>
<span className="text-[10px] font-sans font-bold opacity-50 ml-2">({sessions.length})</span>
)}
</h3>
<button
onClick={handleToggleCollapse}
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-xl transition-colors text-zinc-400 hover:text-amber-600"
className="p-2 hover:bg-white/5 rounded-xl transition-colors text-[#8F9096] hover:text-[#C89D46]"
title={isCollapsed ? 'Aufklappen' : 'Einklappen'}
>
{isCollapsed ? <ChevronDown size={20} /> : <ChevronUp size={20} />}
@@ -166,23 +166,23 @@ export default function SessionList() {
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder={t('session.sessionName')}
className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50"
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-2 text-sm text-white placeholder:text-[#8F9096] focus:outline-none focus:border-[#C89D46] transition-colors"
/>
<button
type="submit"
disabled={isCreating || !newName.trim()}
className="bg-amber-600 hover:bg-amber-700 text-white p-2 rounded-xl transition-all disabled:opacity-50"
className="bg-[#C89D46] hover:bg-[#A67D2E] text-[#0F1014] p-2 rounded-xl transition-all disabled:opacity-50"
>
{isCreating ? <Loader2 size={20} className="animate-spin" /> : <Plus size={20} />}
</button>
</form>
{isLoading ? (
<div className="flex justify-center py-8 text-zinc-400">
<div className="flex justify-center py-8 text-[#8F9096]">
<Loader2 size={24} className="animate-spin" />
</div>
) : sessions.length === 0 ? (
<div className="text-center py-8 text-zinc-500 text-sm">
<div className="text-center py-8 text-[#8F9096] text-xs font-sans">
{t('session.noSessions')}
</div>
) : (
@@ -190,19 +190,19 @@ export default function SessionList() {
{sessions.map((session) => (
<div
key={session.id}
className={`flex items-center justify-between p-4 rounded-2xl border group transition-all ${activeSession?.id === session.id
? 'bg-amber-600 border-amber-600 shadow-lg shadow-amber-600/20'
: 'bg-zinc-50 dark:bg-zinc-800/50 border-zinc-100 dark:border-zinc-800 hover:border-amber-500/30'
className={`flex items-center justify-between p-4 rounded-2xl border transition-all ${activeSession?.id === session.id
? 'bg-[#C89D46] border-[#C89D46] shadow-lg shadow-[#C89D46]/20'
: 'bg-white/5 border-white/5 hover:border-white/10'
}`}
>
<Link href={`/sessions/${session.id}`} className="flex-1 space-y-1 min-w-0">
<div className={`font-bold truncate flex items-center gap-2 ${activeSession?.id === session.id ? 'text-white' : 'text-zinc-800 dark:text-zinc-100'}`}>
<div className={`font-display font-bold text-lg truncate flex items-center gap-2 ${activeSession?.id === session.id ? 'text-[#0F1014]' : 'text-white'}`}>
{session.name}
{session.ended_at && (
<span className={`text-[8px] font-black uppercase px-1.5 py-0.5 rounded border ${activeSession?.id === session.id ? 'bg-white/20 border-white/30 text-white' : 'bg-zinc-100 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700 text-zinc-400'}`}>Closed</span>
<span className={`text-[8px] font-sans font-black uppercase px-1.5 py-0.5 rounded border ${activeSession?.id === session.id ? 'bg-black/10 border-black/20 text-[#0F1014]' : 'bg-white/10 border-white/20 text-[#8F9096]'}`}>Closed</span>
)}
</div>
<div className={`flex items-center gap-4 text-[10px] font-black uppercase tracking-widest ${activeSession?.id === session.id ? 'text-white/80' : 'text-zinc-400'}`}>
<div className={`flex items-center gap-4 text-[10px] font-sans font-bold uppercase tracking-widest ${activeSession?.id === session.id ? 'text-[#0F1014]/60' : 'text-[#8F9096]'}`}>
<span className="flex items-center gap-1">
<Calendar size={12} />
{new Date(session.scheduled_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
@@ -215,7 +215,7 @@ export default function SessionList() {
)}
</div>
{session.participants && session.participants.length > 0 && (
<div className="pt-1">
<div className="pt-2">
<AvatarStack names={session.participants} limit={5} />
</div>
)}
@@ -225,28 +225,28 @@ export default function SessionList() {
!session.ended_at ? (
<button
onClick={() => setActiveSession({ id: session.id, name: session.name })}
className="p-2 bg-white dark:bg-zinc-700 text-amber-600 rounded-xl shadow-sm border border-zinc-200 dark:border-zinc-600 hover:scale-110 transition-transform"
className="p-2 bg-white/10 text-white rounded-xl hover:bg-[#C89D46] hover:text-[#0F1014] transition-all"
title="Start Session"
>
<GlassWater size={18} />
</button>
) : (
<div className="p-2 bg-zinc-100 dark:bg-zinc-800/50 text-zinc-400 rounded-xl border border-zinc-200 dark:border-zinc-700 opacity-50">
<div className="p-2 bg-white/5 text-[#8F9096] rounded-xl border border-white/5 opacity-50">
<Check size={18} />
</div>
)
) : (
<div className="p-2 bg-white/20 text-white rounded-xl">
<div className="p-2 bg-black/10 text-[#0F1014] rounded-xl">
<Check size={18} />
</div>
)}
<ChevronRight size={20} className={activeSession?.id === session.id ? 'text-white/50' : 'text-zinc-300'} />
<ChevronRight size={20} className={activeSession?.id === session.id ? 'text-[#0F1014]/40' : 'text-white/20'} />
<button
onClick={(e) => handleDeleteSession(e, session.id)}
disabled={!!isDeleting}
className={`p-2 rounded-xl transition-all ${activeSession?.id === session.id
? 'text-white/40 hover:text-white hover:bg-white/10'
: 'text-zinc-300 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/10'
? 'text-[#0F1014]/40 hover:text-[#0F1014]'
: 'text-[#8F9096] hover:text-red-400'
}`}
title="Session löschen"
>
@@ -268,17 +268,17 @@ export default function SessionList() {
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<div className="flex -space-x-1.5 overflow-hidden">
{sessions.slice(0, 3).map((s, i) => (
<div key={s.id} className="w-7 h-7 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200/50 dark:border-amber-800/50 flex items-center justify-center text-[10px] font-black text-amber-600 dark:text-amber-500 shadow-sm">
<div key={s.id} className="w-7 h-7 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-[10px] font-display font-bold text-[#C89D46] shadow-sm">
{s.name[0].toUpperCase()}
</div>
))}
{sessions.length > 3 && (
<div className="w-7 h-7 rounded-lg bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 flex items-center justify-center text-[8px] font-black text-zinc-500 shadow-sm">
<div className="w-7 h-7 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-[8px] font-sans font-bold text-[#8F9096] shadow-sm">
+{sessions.length - 3}
</div>
)}
</div>
<span className="text-[10px] text-zinc-400 font-black uppercase tracking-widest ml-1">{sessions.length} Sessions</span>
<span className="text-[10px] text-[#8F9096] font-sans font-bold uppercase tracking-widest ml-1">{sessions.length} Sessions</span>
</div>
)}
</div>

View File

@@ -74,22 +74,19 @@ export default function StatsDashboard({ bottles }: StatsDashboardProps) {
];
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">
<div className="w-full grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-4 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"
className="flex flex-col gap-1 text-center md:text-left"
>
<div className={`p-2 rounded-xl w-fit ${item.bg} mb-1 flex items-center justify-center`}>
<Icon size={16} className={item.color} />
<div className="text-4xl md:text-5xl font-display font-bold text-white tracking-tighter">
{idx === 1 ? stats.totalCount : item.value}
</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 className="text-[10px] font-sans font-bold uppercase text-[#8F9096] tracking-widest px-1">
{item.label}
</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>
);
})}

View File

@@ -125,272 +125,278 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
return (
<div className="flex-1 flex flex-col w-full bg-[#0F1014] h-full overflow-hidden">
{/* Top Context Bar - Flex Child 1 */}
<button
onClick={onOpenSessions}
className="w-full p-6 bg-black/40 backdrop-blur-md border-b border-white/10 flex items-center justify-between group shrink-0"
>
<div className="text-left">
<p className="text-[10px] font-black uppercase tracking-widest text-amber-500">Kontext</p>
<p className="font-bold text-white leading-none mt-1">{activeSessionName || 'Trinkst du in Gesellschaft?'}</p>
</div>
<ChevronDown size={20} className="text-amber-500 group-hover:translate-y-1 transition-transform" />
</button>
<div className="w-full bg-black/40 backdrop-blur-md border-b border-white/10 shrink-0">
<button
onClick={onOpenSessions}
className="max-w-2xl mx-auto w-full p-6 flex items-center justify-between group"
>
<div className="text-left">
<p className="text-[10px] font-black uppercase tracking-widest text-[#C89D46]">Kontext</p>
<p className="font-bold text-white leading-none mt-1">{activeSessionName || 'Trinkst du in Gesellschaft?'}</p>
</div>
<ChevronDown size={20} className="text-[#C89D46] group-hover:translate-y-1 transition-transform" />
</button>
</div>
{/* Main Scrollable Content - Flex Child 2 */}
<div className="flex-1 overflow-y-auto overflow-x-hidden px-6 py-8 space-y-12">
{/* Palette Warning */}
{showPaletteWarning && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="p-5 bg-amber-500/10 border border-amber-500/20 rounded-3xl flex items-start gap-3"
>
<AlertTriangle size={24} className="text-amber-500 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-[10px] font-black uppercase tracking-wider text-amber-500">Palette-Checker</p>
<p className="text-xs font-bold text-white leading-relaxed">
Dein letzter Dram "{lastDramInSession?.name}" war torfig. Trink etwas Wasser!
</p>
<button onClick={() => setShowPaletteWarning(false)} className="text-[10px] font-black uppercase text-amber-500 underline mt-2 block">Verstanden</button>
<div className="flex-1 overflow-y-auto overflow-x-hidden">
<div className="max-w-2xl mx-auto px-6 py-12 space-y-12">
{/* Palette Warning */}
{showPaletteWarning && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="p-5 bg-amber-500/10 border border-amber-500/20 rounded-3xl flex items-start gap-3"
>
<AlertTriangle size={24} className="text-amber-500 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-[10px] font-black uppercase tracking-wider text-amber-500">Palette-Checker</p>
<p className="text-xs font-bold text-white leading-relaxed">
Dein letzter Dram "{lastDramInSession?.name}" war torfig. Trink etwas Wasser!
</p>
<button onClick={() => setShowPaletteWarning(false)} className="text-[10px] font-black uppercase text-amber-500 underline mt-2 block">Verstanden</button>
</div>
</motion.div>
)}
{/* Hero Section */}
<div className="flex items-center gap-6">
<div className="w-24 h-32 bg-white/5 rounded-2xl border border-white/10 flex items-center justify-center overflow-hidden shrink-0 shadow-2xl relative">
{image ? (
<img src={image} alt="Bottle Preview" className="w-full h-full object-cover" />
) : (
<div className="text-[10px] text-white/20 uppercase font-black rotate-[-15deg]">No Photo</div>
)}
</div>
<div className="flex-1 min-w-0">
<h1 className="text-3xl font-black text-amber-600 mb-1 truncate leading-none uppercase tracking-tight">
{bottleMetadata.distillery || 'Destillerie'}
</h1>
<p className="text-white text-xl font-bold truncate mb-2">{bottleMetadata.name || 'Unbekannter Malt'}</p>
<p className="text-white/40 text-[10px] font-black uppercase tracking-widest leading-none">
{bottleMetadata.category || 'Whisky'} {bottleMetadata.abv ? `${bottleMetadata.abv}%` : ''} {bottleMetadata.age ? `${bottleMetadata.age}y` : ''}
</p>
</div>
</motion.div>
)}
{/* Hero Section */}
<div className="flex items-center gap-6">
<div className="w-24 h-32 bg-white/5 rounded-2xl border border-white/10 flex items-center justify-center overflow-hidden shrink-0 shadow-2xl relative">
{image ? (
<img src={image} alt="Bottle Preview" className="w-full h-full object-cover" />
) : (
<div className="text-[10px] text-white/20 uppercase font-black rotate-[-15deg]">No Photo</div>
)}
</div>
<div className="flex-1 min-w-0">
<h1 className="text-3xl font-black text-amber-600 mb-1 truncate leading-none uppercase tracking-tight">
{bottleMetadata.distillery || 'Destillerie'}
</h1>
<p className="text-white text-xl font-bold truncate mb-2">{bottleMetadata.name || 'Unbekannter Malt'}</p>
<p className="text-white/40 text-[10px] font-black uppercase tracking-widest leading-none">
{bottleMetadata.category || 'Whisky'} {bottleMetadata.abv ? `${bottleMetadata.abv}%` : ''} {bottleMetadata.age ? `${bottleMetadata.age}y` : ''}
</p>
</div>
</div>
{/* Rating Slider */}
<div className="space-y-6 bg-white/5 p-8 rounded-[40px] border border-white/10 shadow-inner relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 pointer-events-none">
<Zap size={120} className="text-amber-500" />
</div>
<div className="flex items-center justify-between relative z-10">
<label className="text-xs font-black text-white/40 uppercase tracking-[0.2em] flex items-center gap-2">
<Star size={14} className="text-amber-500 fill-amber-500" />
{t('tasting.rating')}
</label>
<span className="text-4xl font-black text-amber-600 tracking-tighter">{rating}<span className="text-white/20 text-sm ml-1">/100</span></span>
</div>
<input
type="range"
min="0"
max="100"
value={rating}
onChange={(e) => setRating(parseInt(e.target.value))}
className="w-full h-2 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-amber-600 transition-all"
/>
<div className="flex justify-between text-[10px] text-zinc-500 font-black uppercase tracking-widest px-1 relative z-10">
<span>Swill</span>
<span>Dram</span>
<span>Legendary</span>
</div>
<div className="flex gap-3 pt-2 relative z-10">
{['Bottle', 'Sample'].map(type => (
<button
key={type}
onClick={() => setIsSample(type === 'Sample')}
className={`flex-1 py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest border transition-all ${(type === 'Sample' ? isSample : !isSample)
{/* Rating Slider */}
<div className="space-y-6 bg-white/5 p-8 rounded-[40px] border border-white/10 shadow-inner relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 pointer-events-none">
<Zap size={120} className="text-amber-500" />
</div>
<div className="flex items-center justify-between relative z-10">
<label className="text-xs font-black text-white/40 uppercase tracking-[0.2em] flex items-center gap-2">
<Star size={14} className="text-amber-500 fill-amber-500" />
{t('tasting.rating')}
</label>
<span className="text-4xl font-black text-amber-600 tracking-tighter">{rating}<span className="text-white/20 text-sm ml-1">/100</span></span>
</div>
<input
type="range"
min="0"
max="100"
value={rating}
onChange={(e) => setRating(parseInt(e.target.value))}
className="w-full h-2 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-amber-600 transition-all"
/>
<div className="flex justify-between text-[10px] text-zinc-500 font-black uppercase tracking-widest px-1 relative z-10">
<span>Swill</span>
<span>Dram</span>
<span>Legendary</span>
</div>
<div className="flex gap-3 pt-2 relative z-10">
{['Bottle', 'Sample'].map(type => (
<button
key={type}
onClick={() => setIsSample(type === 'Sample')}
className={`flex-1 py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest border transition-all ${(type === 'Sample' ? isSample : !isSample)
? 'bg-zinc-100 border-zinc-100 text-zinc-900 shadow-lg'
: 'bg-transparent border-white/10 text-white/40 hover:border-white/30'
}`}
>
{type}
</button>
))}
</div>
</div>
{/* Evaluation Sliders Area */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<CustomSlider label="Complexity" value={complexityScore} onChange={setComplexityScore} icon={<Sparkles size={18} />} />
<CustomSlider label="Balance" value={balanceScore} onChange={setBalanceScore} icon={<Check size={18} />} />
</div>
{/* Sections */}
<div className="space-y-12 pb-12">
{/* Nose Section */}
<div className="space-y-8 bg-zinc-900/50 p-8 rounded-[40px] border border-white/5">
<div className="flex items-center gap-5">
<div className="w-14 h-14 rounded-3xl bg-amber-500/10 flex items-center justify-center text-amber-500 shrink-0 border border-amber-500/20 shadow-xl">
<Wind size={28} />
</div>
<div>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">{t('tasting.nose')}</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Aroma & Bouquet</p>
</div>
</div>
<div className="space-y-6">
<CustomSlider label="Nose Intensity" value={noseScore} onChange={setNoseScore} icon={<Sparkles size={16} />} />
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Tags</p>
<TagSelector
category="nose"
selectedTagIds={noseTagIds}
onToggleTag={(id) => setNoseTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Eigene Notizen</p>
<textarea
value={noseNotes}
onChange={(e) => setNoseNotes(e.target.value)}
placeholder={t('tasting.notesPlaceholder') || "Wie riecht er?..."}
className="w-full p-6 bg-zinc-900 border-none rounded-3xl text-sm text-zinc-200 focus:ring-2 focus:ring-amber-500 outline-none min-h-[120px] resize-none transition-all placeholder:text-zinc-600 shadow-inner"
/>
</div>
}`}
>
{type}
</button>
))}
</div>
</div>
{/* Palate Section */}
<div className="space-y-8 bg-zinc-900/50 p-8 rounded-[40px] border border-white/5">
<div className="flex items-center gap-5">
<div className="w-14 h-14 rounded-3xl bg-amber-500/10 flex items-center justify-center text-amber-500 shrink-0 border border-amber-500/20 shadow-xl">
<Utensils size={28} />
</div>
<div>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">{t('tasting.palate')}</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Geschmack & Textur</p>
</div>
</div>
<div className="space-y-6">
<CustomSlider label="Taste Impact" value={tasteScore} onChange={setTasteScore} icon={<Sparkles size={16} />} />
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Tags</p>
<TagSelector
category="taste"
selectedTagIds={palateTagIds}
onToggleTag={(id) => setPalateTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Eigene Notizen</p>
<textarea
value={palateNotes}
onChange={(e) => setPalateNotes(e.target.value)}
placeholder={t('tasting.notesPlaceholder') || "Wie schmeckt er?..."}
className="w-full p-6 bg-zinc-900 border-none rounded-3xl text-sm text-zinc-200 focus:ring-2 focus:ring-amber-500 outline-none min-h-[120px] resize-none transition-all placeholder:text-zinc-600 shadow-inner"
/>
</div>
</div>
{/* Evaluation Sliders Area */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<CustomSlider label="Complexity" value={complexityScore} onChange={setComplexityScore} icon={<Sparkles size={18} />} />
<CustomSlider label="Balance" value={balanceScore} onChange={setBalanceScore} icon={<Check size={18} />} />
</div>
{/* Finish Section */}
<div className="space-y-8 bg-zinc-900/50 p-8 rounded-[40px] border border-white/5">
<div className="flex items-center gap-5">
<div className="w-14 h-14 rounded-3xl bg-amber-500/10 flex items-center justify-center text-amber-500 shrink-0 border border-amber-500/20 shadow-xl">
<Droplets size={28} />
</div>
<div>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">{t('tasting.finish')}</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Abgang & Nachklang</p>
</div>
</div>
<div className="space-y-6">
<CustomSlider label="Finish Duration" value={finishScore} onChange={setFinishScore} icon={<Sparkles size={16} />} />
<div className="space-y-6 pt-4 border-t border-white/5">
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Aroma Tags</p>
<TagSelector
category="finish"
selectedTagIds={finishTagIds}
onToggleTag={(id) => setFinishTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Gefühl & Textur</p>
<TagSelector
category="texture"
selectedTagIds={textureTagIds}
onToggleTag={(id) => setTextureTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Eigene Notizen</p>
<textarea
value={finishNotes}
onChange={(e) => setFinishNotes(e.target.value)}
placeholder={t('tasting.notesPlaceholder') || "Der bleibende Eindruck..."}
className="w-full p-6 bg-zinc-900 border-none rounded-3xl text-sm text-zinc-200 focus:ring-2 focus:ring-amber-500 outline-none min-h-[120px] resize-none transition-all placeholder:text-zinc-600 shadow-inner"
/>
</div>
</div>
</div>
{/* Buddy Selection */}
{buddies && buddies.length > 0 && (
{/* Sections */}
<div className="space-y-12 pb-12">
{/* Nose Section */}
<div className="space-y-8 bg-zinc-900/50 p-8 rounded-[40px] border border-white/5">
<div className="flex items-center gap-5">
<div className="w-14 h-14 rounded-3xl bg-amber-500/10 flex items-center justify-center text-amber-500 shrink-0 border border-amber-500/20 shadow-xl">
<Users size={28} />
<Wind size={28} />
</div>
<div>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">Mit wem trinkst du?</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Gesellschaft & Buddies</p>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">{t('tasting.nose')}</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Aroma & Bouquet</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{buddies.map(buddy => (
<button
key={buddy.id}
onClick={() => toggleBuddy(buddy.id)}
className={`px-5 py-3 rounded-2xl text-[10px] font-black uppercase transition-all border flex items-center gap-2 ${selectedBuddyIds.includes(buddy.id)
? 'bg-amber-600 border-amber-600 text-white shadow-lg'
: 'bg-transparent border-white/10 text-white/40 hover:border-white/30'
}`}
>
{selectedBuddyIds.includes(buddy.id) && <Check size={14} />}
{buddy.name}
</button>
))}
<div className="space-y-6">
<CustomSlider label="Nose Intensity" value={noseScore} onChange={setNoseScore} icon={<Sparkles size={16} />} />
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Tags</p>
<TagSelector
category="nose"
selectedTagIds={noseTagIds}
onToggleTag={(id) => setNoseTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Eigene Notizen</p>
<textarea
value={noseNotes}
onChange={(e) => setNoseNotes(e.target.value)}
placeholder={t('tasting.notesPlaceholder') || "Wie riecht er?..."}
className="w-full p-6 bg-zinc-900 border-none rounded-3xl text-sm text-zinc-200 focus:ring-2 focus:ring-amber-500 outline-none min-h-[120px] resize-none transition-all placeholder:text-zinc-600 shadow-inner"
/>
</div>
</div>
</div>
)}
</div>
</div>
{/* Sticky Footer - Flex Child 3 */}
<div className="w-full p-8 bg-black/60 backdrop-blur-xl border-t border-white/10 shrink-0">
<button
onClick={handleInternalSave}
className="w-full py-5 bg-amber-600 text-white rounded-3xl font-black uppercase tracking-widest text-xs flex items-center justify-center gap-4 shadow-xl active:scale-[0.98] transition-all"
>
<Send size={20} />
{t('tasting.saveTasting')}
<div className="ml-auto bg-black/20 px-3 py-1 rounded-full text-[10px] font-black text-amber-200">{rating}</div>
</button>
{/* Palate Section */}
<div className="space-y-8 bg-zinc-900/50 p-8 rounded-[40px] border border-white/5">
<div className="flex items-center gap-5">
<div className="w-14 h-14 rounded-3xl bg-amber-500/10 flex items-center justify-center text-amber-500 shrink-0 border border-amber-500/20 shadow-xl">
<Utensils size={28} />
</div>
<div>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">{t('tasting.palate')}</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Geschmack & Textur</p>
</div>
</div>
<div className="space-y-6">
<CustomSlider label="Taste Impact" value={tasteScore} onChange={setTasteScore} icon={<Sparkles size={16} />} />
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Tags</p>
<TagSelector
category="taste"
selectedTagIds={palateTagIds}
onToggleTag={(id) => setPalateTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Eigene Notizen</p>
<textarea
value={palateNotes}
onChange={(e) => setPalateNotes(e.target.value)}
placeholder={t('tasting.notesPlaceholder') || "Wie schmeckt er?..."}
className="w-full p-6 bg-zinc-900 border-none rounded-3xl text-sm text-zinc-200 focus:ring-2 focus:ring-amber-500 outline-none min-h-[120px] resize-none transition-all placeholder:text-zinc-600 shadow-inner"
/>
</div>
</div>
</div>
{/* Finish Section */}
<div className="space-y-8 bg-zinc-900/50 p-8 rounded-[40px] border border-white/5">
<div className="flex items-center gap-5">
<div className="w-14 h-14 rounded-3xl bg-amber-500/10 flex items-center justify-center text-amber-500 shrink-0 border border-amber-500/20 shadow-xl">
<Droplets size={28} />
</div>
<div>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">{t('tasting.finish')}</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Abgang & Nachklang</p>
</div>
</div>
<div className="space-y-6">
<CustomSlider label="Finish Duration" value={finishScore} onChange={setFinishScore} icon={<Sparkles size={16} />} />
<div className="space-y-6 pt-4 border-t border-white/5">
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Aroma Tags</p>
<TagSelector
category="finish"
selectedTagIds={finishTagIds}
onToggleTag={(id) => setFinishTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Gefühl & Textur</p>
<TagSelector
category="texture"
selectedTagIds={textureTagIds}
onToggleTag={(id) => setTextureTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])}
suggestedTagNames={suggestedTags}
suggestedCustomTagNames={suggestedCustomTags}
/>
</div>
</div>
<div className="space-y-3">
<p className="text-[10px] font-black text-white/20 uppercase tracking-widest px-1">Eigene Notizen</p>
<textarea
value={finishNotes}
onChange={(e) => setFinishNotes(e.target.value)}
placeholder={t('tasting.notesPlaceholder') || "Der bleibende Eindruck..."}
className="w-full p-6 bg-zinc-900 border-none rounded-3xl text-sm text-zinc-200 focus:ring-2 focus:ring-amber-500 outline-none min-h-[120px] resize-none transition-all placeholder:text-zinc-600 shadow-inner"
/>
</div>
</div>
</div>
{/* Buddy Selection */}
{buddies && buddies.length > 0 && (
<div className="space-y-8 bg-zinc-900/50 p-8 rounded-[40px] border border-white/5">
<div className="flex items-center gap-5">
<div className="w-14 h-14 rounded-3xl bg-amber-500/10 flex items-center justify-center text-amber-500 shrink-0 border border-amber-500/20 shadow-xl">
<Users size={28} />
</div>
<div>
<h3 className="text-xl font-black text-white uppercase tracking-widest leading-none">Mit wem trinkst du?</h3>
<p className="text-[10px] text-white/30 font-black uppercase tracking-widest mt-2 px-0.5">Gesellschaft & Buddies</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{buddies.map(buddy => (
<button
key={buddy.id}
onClick={() => toggleBuddy(buddy.id)}
className={`px-5 py-3 rounded-2xl text-[10px] font-black uppercase transition-all border flex items-center gap-2 ${selectedBuddyIds.includes(buddy.id)
? 'bg-amber-600 border-amber-600 text-white shadow-lg'
: 'bg-transparent border-white/10 text-white/40 hover:border-white/30'
}`}
>
{selectedBuddyIds.includes(buddy.id) && <Check size={14} />}
{buddy.name}
</button>
))}
</div>
</div>
)}
</div>
</div>
{/* Sticky Footer - Flex Child 3 */}
<div className="w-full p-8 bg-black/60 backdrop-blur-xl border-t border-white/10 shrink-0">
<div className="max-w-2xl mx-auto">
<button
onClick={handleInternalSave}
className="w-full py-5 bg-[#C89D46] text-[#0F1014] rounded-3xl font-black uppercase tracking-widest text-xs flex items-center justify-center gap-4 shadow-xl active:scale-[0.98] transition-all"
>
<Send size={20} />
{t('tasting.saveTasting')}
<div className="ml-auto bg-black/20 px-3 py-1 rounded-full text-[10px] font-black text-[#0F1014]/60">{rating}</div>
</button>
</div>
</div>
</div>
</div>
);
@@ -401,12 +407,12 @@ function CustomSlider({ label, value, onChange, icon }: any) {
<div className="space-y-4 bg-zinc-900/50 p-6 rounded-3xl border border-white/5 shadow-inner">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-white/40">
<div className="p-2 rounded-xl bg-amber-500/10 text-amber-500">
<div className="p-2 rounded-xl bg-[#C89D46]/10 text-[#C89D46]">
{icon}
</div>
<span className="text-[10px] font-black uppercase tracking-[0.2em]">{label}</span>
</div>
<span className="text-2xl font-black text-amber-600 tracking-tighter">{value}</span>
<span className="text-2xl font-black text-[#C89D46] tracking-tighter">{value}</span>
</div>
<input
type="range"
@@ -414,7 +420,7 @@ function CustomSlider({ label, value, onChange, icon }: any) {
max="100"
value={value}
onChange={(e) => onChange(parseInt(e.target.value))}
className="w-full h-1.5 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-amber-600 transition-all"
className="w-full h-1.5 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-[#C89D46] transition-all"
/>
</div>
);