feat: Add Spotify-style backdrop, Cascade OCR, Smart Scan Flow & OCR Dashboard
- BottleGrid: Implement blurred backdrop effect for bottle cards - Cascade OCR: TextDetector → RegEx → Fuzzy Match → window.ai pipeline - Smart Scan: Native OCR for Android, Live Text fallback for iOS - OCR Dashboard: Admin page at /admin/ocr-logs with stats and scan history - Features: Add feature flags in src/config/features.ts - SQL: Add ocr_logs table migration - Services: Update analyze-bottle to use OpenRouter, add save-ocr-log
This commit is contained in:
@@ -32,77 +32,91 @@ interface BottleCardProps {
|
||||
|
||||
function BottleCard({ bottle, sessionId }: BottleCardProps) {
|
||||
const { t, locale } = useI18n();
|
||||
const imageUrl = getStorageUrl(bottle.image_url);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/bottles/${bottle.id}${sessionId ? `?session_id=${sessionId}` : ''}`}
|
||||
className="block h-full group relative overflow-hidden rounded-2xl bg-zinc-800/20 backdrop-blur-sm border border-white/[0.05] transition-all duration-500 hover:border-orange-500/30 hover:shadow-2xl hover:shadow-orange-950/20 active:scale-[0.98] flex flex-col"
|
||||
className="block h-full group relative overflow-hidden rounded-2xl bg-zinc-900 border border-white/[0.05] transition-all duration-500 hover:border-orange-500/30 hover:shadow-2xl hover:shadow-orange-950/20 active:scale-[0.98]"
|
||||
>
|
||||
{/* Image Layer - Clean Split Top */}
|
||||
<div className="aspect-[4/3] overflow-hidden shrink-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 ease-out"
|
||||
/>
|
||||
</div>
|
||||
{/* === SPOTIFY-STYLE IMAGE SECTION === */}
|
||||
<div className="relative aspect-[3/4] overflow-hidden">
|
||||
|
||||
{/* Info Layer - Clean Split Bottom */}
|
||||
<div className="p-4 flex-1 flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-black text-orange-600 uppercase tracking-[0.2em] leading-none mb-1">
|
||||
{bottle.distillery}
|
||||
</p>
|
||||
<h3 className="font-bold text-lg text-zinc-50 leading-tight tracking-tight">
|
||||
{bottle.name || t('grid.unknownBottle')}
|
||||
</h3>
|
||||
{/* Layer 1: Blurred Backdrop */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
className="w-full h-full object-cover scale-125 blur-[20px] saturate-150 brightness-[0.6]"
|
||||
/>
|
||||
{/* Vignette Overlay */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(0,0,0,0) 20%, rgba(0,0,0,0.5) 80%)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 bg-zinc-800 text-zinc-400 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
{/* Layer 2: Sharp Foreground Image */}
|
||||
<div className="absolute inset-[10px] z-10 flex items-center justify-center">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={bottle.name}
|
||||
loading="lazy"
|
||||
className="max-w-full max-h-full object-contain drop-shadow-[0_10px_20px_rgba(0,0,0,0.5)] group-hover:scale-105 transition-transform duration-500 ease-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top Overlays */}
|
||||
{(bottle.is_whisky === false || (bottle.confidence && bottle.confidence < 70)) && (
|
||||
<div className="absolute top-3 right-3 z-20">
|
||||
<div className="bg-red-500 text-white p-1.5 rounded-full shadow-lg">
|
||||
<AlertCircle size={12} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sessionId && (
|
||||
<div className="absolute top-3 left-3 z-20 bg-orange-600 text-white text-[9px] font-bold px-2 py-1 rounded-md flex items-center gap-1.5 shadow-xl">
|
||||
<PlusCircle size={12} />
|
||||
ADD
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Gradient Overlay for Text */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 z-10 h-32"
|
||||
style={{
|
||||
background: 'linear-gradient(to top, rgba(0,0,0,0.9) 0%, transparent 100%)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Info Overlay at Bottom */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-20 p-4 text-white">
|
||||
<p className="text-[10px] font-black text-orange-500 uppercase tracking-[0.2em] leading-none mb-1">
|
||||
{bottle.distillery}
|
||||
</p>
|
||||
<h3 className="font-bold text-lg text-zinc-50 leading-tight tracking-tight line-clamp-2">
|
||||
{bottle.name || t('grid.unknownBottle')}
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<span className="px-2 py-1 bg-white/10 backdrop-blur-sm text-zinc-300 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
{shortenCategory(bottle.category)}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-zinc-800 text-zinc-400 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
<span className="px-2 py-1 bg-white/10 backdrop-blur-sm text-zinc-300 text-[9px] font-bold uppercase tracking-widest rounded-md">
|
||||
{bottle.abv}% VOL
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Metadata items */}
|
||||
<div className="flex items-center gap-4 pt-3 border-t border-zinc-800/50 mt-auto">
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium text-zinc-500">
|
||||
<Calendar size={12} className="text-zinc-500" />
|
||||
{new Date(bottle.created_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
|
||||
</div>
|
||||
{bottle.last_tasted && (
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium text-zinc-500">
|
||||
<Clock size={12} className="text-zinc-500" />
|
||||
{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-3 right-3 z-10">
|
||||
<div className="bg-red-500 text-white p-1.5 rounded-full shadow-lg">
|
||||
<AlertCircle size={12} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sessionId && (
|
||||
<div className="absolute top-3 left-3 z-10 bg-orange-600 text-white text-[9px] font-bold px-2 py-1 rounded-md flex items-center gap-1.5 shadow-xl">
|
||||
<PlusCircle size={12} />
|
||||
ADD
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface BottleGridProps {
|
||||
bottles: any[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user