feat: Buddy System & Bulk Scanner

- Add Buddy linking via QR code/handshake (buddy_invites table)
- Add Bulk Scanner for rapid-fire bottle scanning in sessions
- Add processing_status to bottles for background AI analysis
- Fix offline OCR with proper tessdata caching in Service Worker
- Fix Supabase GoTrueClient singleton warning
- Add collection refresh after offline sync completes

New components:
- BuddyHandshake.tsx - QR code display and code entry
- BulkScanSheet.tsx - Camera UI with capture queue
- BottleSkeletonCard.tsx - Pending bottle display
- useBulkScanner.ts - Queue management hook
- buddy-link.ts - Server actions for buddy linking
- bulk-scan.ts - Server actions for batch processing
This commit is contained in:
2025-12-25 22:11:50 +01:00
parent afe9197776
commit 75461d7c30
22 changed files with 2050 additions and 146 deletions

View File

@@ -0,0 +1,94 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { Sparkles, AlertCircle, Loader2 } from 'lucide-react';
interface BottleSkeletonCardProps {
name?: string;
imageUrl?: string;
processingStatus: 'pending' | 'analyzing' | 'error';
onClick?: () => void;
}
export default function BottleSkeletonCard({
name,
imageUrl,
processingStatus,
onClick
}: BottleSkeletonCardProps) {
const isError = processingStatus === 'error';
const isPending = processingStatus === 'pending';
const isAnalyzing = processingStatus === 'analyzing';
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
onClick={onClick}
className={`relative bg-zinc-900 rounded-2xl border overflow-hidden cursor-pointer transition-all ${isError
? 'border-red-500/50 hover:border-red-500'
: 'border-zinc-800 hover:border-orange-600/50'
}`}
>
{/* Image */}
<div className="aspect-[3/4] bg-zinc-950 relative overflow-hidden">
{imageUrl ? (
<img
src={imageUrl}
alt="Bottle preview"
className={`w-full h-full object-cover ${isError ? 'opacity-50 grayscale' : 'opacity-60'}`}
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<div className="w-16 h-24 bg-zinc-800 rounded-lg animate-pulse" />
</div>
)}
{/* Processing Overlay */}
<div className={`absolute inset-0 flex flex-col items-center justify-center ${isError ? 'bg-red-950/50' : 'bg-black/40'
}`}>
{isError ? (
<>
<AlertCircle size={32} className="text-red-500 mb-2" />
<span className="text-xs font-bold text-red-400">Fehler</span>
</>
) : (
<>
{isPending ? (
<Loader2 size={28} className="text-orange-500 animate-spin mb-2" />
) : (
<Sparkles size={28} className="text-orange-500 animate-pulse mb-2" />
)}
<span className="text-xs font-bold text-zinc-300">
{isPending ? 'Warten...' : 'KI analysiert...'}
</span>
</>
)}
</div>
{/* Status Badge */}
<div className={`absolute top-2 right-2 px-2 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest ${isError
? 'bg-red-500/20 text-red-400'
: isAnalyzing
? 'bg-orange-500/20 text-orange-400'
: 'bg-zinc-800/80 text-zinc-500'
}`}>
{isError ? 'Fehler' : isAnalyzing ? '✨ Analyse' : 'Warte'}
</div>
</div>
{/* Info */}
<div className="p-3">
{/* Skeleton Name */}
<div className="h-4 bg-zinc-800 rounded animate-pulse mb-2 w-3/4" />
{/* Skeleton Details */}
<div className="flex gap-2">
<div className="h-3 bg-zinc-800/50 rounded animate-pulse w-12" />
<div className="h-3 bg-zinc-800/50 rounded animate-pulse w-8" />
</div>
</div>
</motion.div>
);
}