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:
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check, Trash2, ChevronDown, ChevronUp, Play, Sparkles } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import AvatarStack from './AvatarStack';
|
||||
import { deleteSession } from '@/services/delete-session';
|
||||
@@ -182,45 +182,52 @@ export default function SessionList() {
|
||||
</form>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8 text-zinc-500">
|
||||
<Loader2 size={24} className="animate-spin" />
|
||||
<div className="flex justify-center py-12 text-zinc-700">
|
||||
<Loader2 size={32} className="animate-spin" />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-14 h-14 mx-auto rounded-2xl bg-zinc-800/50 flex items-center justify-center mb-4">
|
||||
<Calendar size={24} className="text-zinc-500" />
|
||||
<div className="text-center py-12 bg-zinc-950/50 rounded-[32px] border border-dashed border-zinc-800">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-zinc-900 flex items-center justify-center mb-6 border border-white/5 shadow-inner">
|
||||
<Calendar size={28} className="text-zinc-700" />
|
||||
</div>
|
||||
<p className="text-sm font-bold text-zinc-400 mb-1">Keine Sessions</p>
|
||||
<p className="text-xs text-zinc-600 max-w-[200px] mx-auto">
|
||||
Erstelle eine Tasting-Session um mehrere Whiskys zu vergleichen
|
||||
<p className="text-sm font-black text-zinc-400 mb-2 uppercase tracking-widest">{t('session.noSessions') || 'Keine Sessions'}</p>
|
||||
<p className="text-[10px] text-zinc-600 font-bold uppercase tracking-tight max-w-[200px] mx-auto leading-relaxed">
|
||||
Erstelle eine Tasting-Session um deine Drams zeitlich zu ordnen.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`flex items-center justify-between p-4 rounded-2xl border transition-all ${activeSession?.id === session.id
|
||||
? 'bg-orange-600 border-orange-600 shadow-lg shadow-orange-950/20'
|
||||
: 'bg-zinc-950 border-zinc-800 hover:border-zinc-700'
|
||||
className={`group relative flex items-center justify-between p-5 rounded-[28px] border transition-all duration-500 overflow-hidden ${activeSession?.id === session.id
|
||||
? 'bg-orange-500/[0.03] border-orange-500/40 shadow-[0_0_40px_rgba(234,88,12,0.1)]'
|
||||
: 'bg-zinc-950/50 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 text-lg truncate flex items-center gap-2 ${activeSession?.id === session.id ? 'text-white' : 'text-zinc-50'}`}>
|
||||
{session.name}
|
||||
{/* Active Glow Decor */}
|
||||
{activeSession?.id === session.id && (
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-600/10 blur-[60px] -mr-16 -mt-16 pointer-events-none" />
|
||||
)}
|
||||
|
||||
<Link href={`/sessions/${session.id}`} className="flex-1 space-y-2 min-w-0 z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`font-black text-xl tracking-tight truncate ${activeSession?.id === session.id ? 'text-white' : 'text-zinc-200 group-hover:text-white transition-colors'}`}>
|
||||
{session.name}
|
||||
</div>
|
||||
{session.ended_at && (
|
||||
<span className={`text-[8px] font-bold uppercase px-1.5 py-0.5 rounded border ${activeSession?.id === session.id ? 'bg-black/10 border-black/20 text-white' : 'bg-zinc-800 border-zinc-700 text-zinc-500'}`}>Closed</span>
|
||||
<span className="text-[8px] font-black uppercase px-2 py-0.5 rounded-full bg-zinc-800/50 border border-zinc-700/50 text-zinc-500 tracking-widest">Archiv</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex items-center gap-4 text-[10px] font-bold uppercase tracking-widest ${activeSession?.id === session.id ? 'text-white/60' : 'text-zinc-500'}`}>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar size={12} />
|
||||
<div className={`flex items-center gap-5 text-[10px] font-black uppercase tracking-[0.15em] ${activeSession?.id === session.id ? 'text-orange-500/80' : 'text-zinc-500'}`}>
|
||||
<span className="flex items-center gap-2">
|
||||
<Calendar size={13} strokeWidth={2.5} />
|
||||
{new Date(session.scheduled_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
|
||||
</span>
|
||||
{session.whisky_count! > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<GlassWater size={12} />
|
||||
{session.whisky_count} Whiskys
|
||||
<span className="flex items-center gap-2">
|
||||
<GlassWater size={13} strokeWidth={2.5} />
|
||||
{session.whisky_count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -230,34 +237,37 @@ export default function SessionList() {
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<div className="flex items-center gap-1 z-10">
|
||||
{activeSession?.id !== session.id ? (
|
||||
!session.ended_at ? (
|
||||
<button
|
||||
onClick={() => setActiveSession({ id: session.id, name: session.name })}
|
||||
className="p-2 bg-zinc-800 text-zinc-50 rounded-xl hover:bg-orange-600 hover:text-white transition-all"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setActiveSession({ id: session.id, name: session.name });
|
||||
}}
|
||||
className="p-3 text-zinc-600 hover:text-orange-500 transition-all hover:scale-110 active:scale-95"
|
||||
title="Start Session"
|
||||
>
|
||||
<GlassWater size={18} />
|
||||
<Play size={22} fill="currentColor" className="opacity-40" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="p-2 bg-zinc-900 text-zinc-500 rounded-xl border border-zinc-800 opacity-50">
|
||||
<Check size={18} />
|
||||
<div className="p-3 text-zinc-800">
|
||||
<Check size={20} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="p-2 bg-black/10 text-white rounded-xl">
|
||||
<Check size={18} />
|
||||
<div className="p-3 text-orange-500 animate-pulse">
|
||||
<Sparkles size={20} />
|
||||
</div>
|
||||
)}
|
||||
<ChevronRight size={20} className={activeSession?.id === session.id ? 'text-white/40' : 'text-zinc-700'} />
|
||||
|
||||
<div className="w-px h-8 bg-white/5 mx-1" />
|
||||
|
||||
<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'
|
||||
: 'text-zinc-600 hover:text-red-500'
|
||||
}`}
|
||||
className="p-3 text-zinc-700 hover:text-red-500 transition-all opacity-0 group-hover:opacity-100"
|
||||
title="Session löschen"
|
||||
>
|
||||
{isDeleting === session.id ? (
|
||||
@@ -266,6 +276,7 @@ export default function SessionList() {
|
||||
<Trash2 size={18} />
|
||||
)}
|
||||
</button>
|
||||
<ChevronRight size={20} className="text-zinc-800 group-hover:text-zinc-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user