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:
94
src/components/BottleSkeletonCard.tsx
Normal file
94
src/components/BottleSkeletonCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user