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:
149
.aiideas
149
.aiideas
@@ -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.
|
||||
@@ -7,6 +7,7 @@
|
||||
--background: #0F1014;
|
||||
--surface: #1A1B20;
|
||||
--primary: #C89D46;
|
||||
--text-secondary: #8F9096;
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
108
src/components/BottomNavigation.tsx
Normal file
108
src/components/BottomNavigation.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user