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

149
.aiideas
View File

@@ -1,110 +1,117 @@
Rolle: Du bist ein Senior Frontend Engineer(React / Tailwind) und UI / UX Designer.Ziel: Wir machen ein umfassendes UI - Overhaul einer bestehenden Whisky - App("WhiskyVault") und benennen sie um in "DramLog".Das Ziel ist eine Premium - Mobile - Experience(Dark Mode, Gold Akzente, Serif Fonts).
Rolle: Du bist ein Senior Frontend Engineer und UX - Experte mit Spezialisierung auf "Mobile-First" - Webanwendungen.Dein Fokus liegt auf High - End Ästhetik(Dark Mode), flüssigen Animationen und reibungsloser User Experience.
Tech Stack: React, Tailwind CSS, Lucide - React Icons.
1. Globales Design - System(Anwenden auf alle Views)
Aufgabe: Wir bauen den Core - Flow einer bestehenden Whisky - Tasting - App um.Implementiere den neuen "Scan & Taste" Flow.Ziel ist eine Single - Page - App - Experience(SPA) ohne Reloads, die sich nativ anfühlt.
Branding: App Name ist "DramLog".
Tech Stack(Anpassen falls nötig):
Farben:
Framework: Next.js(Bitte nutze den bestehenden Stack der App)
Background: #0F1014(Deep Rich Black)
Styling: Tailwind CSS
Surface: #1A1B20(Card Backgrounds)
Icons: Lucide - React oder HeroIcons
Primary Accent: #C89D46(Whisky Amber / Gold)
Charts: Recharts oder Chart.js(für das Radar Chart)
Text - Secondary: #8F9096
Animation: Framer Motion(für Transitions)
Typografie:
1. Design System & Vibe
Importiere und nutze Playfair Display(Serif) für: Überschriften(h1 - h3), Whisky - Namen auf Cards.
Theme: Strict Dark Mode.
Nutze Inter(Sans) für: UI - Elemente, Labels, Fließtext, Daten.
Background: #0F1014(Deep Anthracite / Black)
Shape: rounded - 2xl für Cards, rounded - full für Buttons.
Surface / Cards: #1A1B20(Lighter Anthracite)
2. Spezifische Component Refactorings
Primary Accent: #C89D46(Whisky Gold / Amber)
A.Dashboard(Home View):
Text: Sans - Serif(Inter) für UI, Serif(Playfair Display) für Überschriften / Namen.
Entferne die grauen Hintergründe der 4 oberen Statistik - Boxen.Zeige die Werte(Zahlen) groß in Playfair Display(Weiß) und die Labels klein darunter(Grau).Ordne sie in einem Grid oder einer flex - row mit justify - between an.
Stil: "Premium & Warm".Runde Ecken(rounded - 2xl), Glassmorphism für Overlays(backdrop - blur - md, bg - white / 5), feine Borders(border - white / 10).
Benenne "Dein Bestand" um in "Collection".
2. Der Flow(Schritt für Schritt Implementierung)
Mache die Listen - Einträge unter "Tasting Sessions" interaktiv.Entferne die sichtbaren Trash / Edit Buttons und nutze ein sauberes Listen - Layout.
Bitte implementiere folgende Views / Components als zusammenhängenden Flow:
A.Der Entry Point(Floating Action Button)
B.Whisky Card(Collection Grid):
Erstelle einen prominenten, schwebenden Button(unten mittig, fixed), der über dem Dashboard liegt.
Redesign der Card - Komponente.
Icon: Kamera - Symbol.
Bild: Muss bis an den Rand gehen(w - full, kein Padding).
Interaction: Beim Klick simulieren wir einen Kamera - Scan(nutze vorerst ein Mock - Timeout von 2s mit einer Lade - Animation "Analysiere Etikett..."), danach Transition zu View B.
Overlay: Lege einen bg - gradient - to - t from - black via - black / 80 to - transparent über das untere Drittel des Bildes.
B.Der Tasting Editor(Main Component)
Text: Platziere Name(Serif) und Destillerie(Sans, Uppercase, Gold) weiß auf dem Bild im unteren Bereich(über dem Gradient).
Dies ist der wichtigste Screen.Layout - Struktur:
Tags: Mache Tags minimalistisch(bg - white / 10 backdrop - blur - sm text - xs border border - white / 10).
Top Bar(Sticky):
3. Die "Floating" Navigation(Core Feature)
Zeige einen "Context Indicator".
Erstelle eine neue Komponente BottomNavigation.Sie ersetzt alle bisherigen Menüs.Sie muss am unteren Bildschirmrand fixiert sein.WICHTIG: Implementiere exakt dieses Layout - Pattern für den schwebenden Button:
JavaScript
Logik: Zeige Text "Trinkst du in Gesellschaft? + Session wählen".
// Reference Implementation for BottomNavigation.jsx
import { Home, Grid, Scan, User, Search } from 'lucide-react';
Interaction: Klick öffnet ein Bottom Sheet(siehe C).Wenn eine Session gewählt wurde, zeige: "Session: [Name]".
export const BottomNavigation = () => {
return (
<div className= "fixed bottom-0 left-0 w-full z-50" >
{/* Background Container mit Glassmorphism */ }
< div className = "relative bg-[#0F1014]/90 backdrop-blur-xl border-t border-white/10 pb-safe pt-2" >
Hero Section:
<div className="flex justify-between items-end px-6 h-16" >
Zeige das(gemockte) Foto der Flasche links.
{/* Left Actions */ }
< button className = "flex flex-col items-center gap-1 text-[#C89D46] w-12" >
<Home size={ 24 } />
< span className = "text-[10px] font-medium" > Home </span>
</button>
Rechts daneben: Name(Serif, Gold), Alter, ABV. (Mock Data: "Lagavulin 16, Islay, 43%").
< button className = "flex flex-col items-center gap-1 text-gray-500 hover:text-white w-12 transition-colors" >
<Grid size={ 24 } />
< span className = "text-[10px] font-medium" > Shelf </span>
</button>
Form Section(Scrollable):
{/* Spacer für den Center Button */ }
<div className="w-16" />
Slider: Erstelle eine Custom - Komponente für "Nose", "Taste", "Finish".Nutze keine Zahlen - Inputs, sondern Range - Slider(0 - 100).
{/* Right Actions */ }
< button className = "flex flex-col items-center gap-1 text-gray-500 hover:text-white w-12 transition-colors" >
<Search size={ 24 } />
< span className = "text-[10px] font-medium" > Search </span>
</button>
Smart Tags(Wichtig!): Implementiere eine Chip - Auswahl.
< button className = "flex flex-col items-center gap-1 text-gray-500 hover:text-white w-12 transition-colors" >
<User size={ 24 } />
< span className = "text-[10px] font-medium" > Profile </span>
</button>
</div>
Design: "Ghost Button" Style(transparenter BG, feiner Border).
{/* THE FLOATING MAGIC BUTTON */ }
<div className="absolute -top-8 left-1/2 -translate-x-1/2" >
<button
className="flex items-center justify-center w-16 h-16 rounded-full
bg - gradient - to - tr from - [#C89D46] to - [#E0B456]
shadow - [0_0_20px_rgba(200, 157, 70, 0.4)]
border - 4 border - [#0F1014]
active: scale - 95 transition - transform duration - 200"
aria - label="Scan Bottle"
>
<Scan color="#0F1014" size = { 32} strokeWidth = { 2} />
</button>
</div>
Active State: Füllt sich mit #C89D46(Gold), Text wird dunkel.
</div>
</div>
);
};
Data: Mocke AI - Vorschläge wie["Rauch", "Torf", "Jod", "Vanille"].
Aufgabe:
Sticky Footer:
Implementiere das globale Styling(Fonts / Colors).
Ein Button "Save Tasting"(Full Width), der immer sichtbar unten schwebt(z - index: 50).
Baue die BottomNavigation ein(ersetze alte Navs).
C.Das Session Bottom Sheet(Overlay)
Wenn man in View B auf die Top Bar klickt, fährt von unten ein Sheet hoch(Höhe: 50vh).
Inhalt: Input Feld für "Neue Session" und Liste "Aktuelle Sessions".
Beim Auswählen schließt sich das Sheet und aktualisiert den State in View B(Context Bar).
D.Die Result Card(The Reward)
Nach dem Speichern(Transition: Fade out Editor -> Fade in Card):
Zeige eine "Trading Card" im 9: 16 Verhältnis, zentriert.
Inhalt:
Großes Foto der Flasche mit Vignette.
Ein Radar Chart(Spider Web) für die 5 Geschmacksprofile(Nose, Taste, Finish, Balance, Complexity).
Ein "Badge" oben rechts mit dem Score(z.B. 8.5).
Action: Ein Button "Share Image" unter der Karte. (Logik: Bereite navigator.share vor).
3. Technische Anforderungen & State
Nutze einen lokalen State(oder Context), um die Daten zwischen Editor und Result zu halten.
Mocke die "AI Response"(Flaschenerkennung) mit einem festen Datensatz(JSON), damit wir das UI testen können.
Achte auf Mobile - Viewport - Height(dvh), damit Safari - Bars nichts verdecken.
Wenn du etwas schon hast pass es an und integriere es in den neuen Flow
Passe die Dashboard View an den neuen "Clean Look" an.
Passe die WhiskyCard an den neuen "Editorial Look" an.

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}
</span>
<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>
</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">
{/* 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-105 transition-transform duration-500"
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-1000 ease-out"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
{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>
)}
{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>
)}
{/* 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>
<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="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-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'
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').toUpperCase()}
{t('common.all')}
</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'
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]'
}`}
>
{dist.toUpperCase()}
{dist}
</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,10 +126,9 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
}
};
if (!isOpen) return null;
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -139,7 +144,12 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
</button>
<div className="flex-1 w-full h-full flex flex-col relative min-h-0">
{state === 'SCANNING' && (
{/*
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
initial={{ scale: 0.9, opacity: 0 }}
@@ -150,15 +160,15 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
<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"
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-amber-500" />
<Loader2 size={48} className="animate-spin text-[#C89D46]" />
</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">
<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>
@@ -177,12 +187,12 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
<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>
<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/5 border border-white/10 rounded-2xl text-white font-black uppercase tracking-widest text-[10px] hover:bg-white/10 transition-all"
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>
@@ -215,8 +225,8 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
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>
<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>
)}
@@ -250,6 +260,7 @@ export default function ScanAndTasteFlow({ isOpen, onClose, base64Image }: ScanA
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,19 +125,22 @@ 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 */}
<div className="w-full bg-black/40 backdrop-blur-md border-b border-white/10 shrink-0">
<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"
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-amber-500">Kontext</p>
<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-amber-500 group-hover:translate-y-1 transition-transform" />
<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">
<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
@@ -383,16 +386,19 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
{/* 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-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"
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-amber-200">{rating}</div>
<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>
);