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

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