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

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

View File

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