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:
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight, Play, Square } from 'lucide-react';
|
||||
import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight, Play, Square, Zap } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import AvatarStack from '@/components/AvatarStack';
|
||||
import { deleteSession } from '@/services/delete-session';
|
||||
@@ -13,6 +13,8 @@ import { useI18n } from '@/i18n/I18nContext';
|
||||
import SessionTimeline from '@/components/SessionTimeline';
|
||||
import SessionABVCurve from '@/components/SessionABVCurve';
|
||||
import OfflineIndicator from '@/components/OfflineIndicator';
|
||||
import BulkScanSheet from '@/components/BulkScanSheet';
|
||||
import BottleSkeletonCard from '@/components/BottleSkeletonCard';
|
||||
|
||||
interface Buddy {
|
||||
id: string;
|
||||
@@ -44,6 +46,7 @@ interface SessionTasting {
|
||||
image_url?: string | null;
|
||||
abv: number;
|
||||
category?: string;
|
||||
processing_status?: string;
|
||||
};
|
||||
tasting_tags: {
|
||||
tags: {
|
||||
@@ -66,9 +69,31 @@ export default function SessionDetailPage() {
|
||||
const [isAddingParticipant, setIsAddingParticipant] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [isBulkScanOpen, setIsBulkScanOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessionData();
|
||||
|
||||
// Subscribe to bottle updates for realtime processing status
|
||||
const channel = supabase
|
||||
.channel('bottle-updates')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'UPDATE', schema: 'public', table: 'bottles' },
|
||||
(payload) => {
|
||||
// Refresh if a bottle's processing_status changed
|
||||
if (payload.new && payload.old) {
|
||||
if (payload.new.processing_status !== payload.old.processing_status) {
|
||||
fetchSessionData();
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
const fetchSessionData = async () => {
|
||||
@@ -102,7 +127,7 @@ export default function SessionDetailPage() {
|
||||
id,
|
||||
rating,
|
||||
tasted_at,
|
||||
bottles(id, name, distillery, image_url, abv, category),
|
||||
bottles(id, name, distillery, image_url, abv, category, processing_status),
|
||||
tasting_tags(tags(name))
|
||||
`)
|
||||
.eq('session_id', id)
|
||||
@@ -388,13 +413,24 @@ export default function SessionDetailPage() {
|
||||
<GlassWater size={16} className="text-orange-600" />
|
||||
Verkostete Flaschen
|
||||
</h3>
|
||||
<Link
|
||||
href={`/?session_id=${id}`} // Redirect to home with context
|
||||
className="bg-orange-600 hover:bg-orange-700 text-white px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 transition-all shadow-lg shadow-orange-600/20"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Flasche hinzufügen
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
{!session.ended_at && (
|
||||
<button
|
||||
onClick={() => setIsBulkScanOpen(true)}
|
||||
className="bg-zinc-800 hover:bg-zinc-700 text-orange-500 px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 transition-all border border-zinc-700"
|
||||
>
|
||||
<Zap size={16} />
|
||||
Bulk Scan
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
href={`/?session_id=${id}`}
|
||||
className="bg-orange-600 hover:bg-orange-700 text-white px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 transition-all shadow-lg shadow-orange-600/20"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Flasche
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SessionTimeline
|
||||
@@ -412,8 +448,20 @@ export default function SessionDetailPage() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div >
|
||||
</main >
|
||||
</div>
|
||||
|
||||
{/* Bulk Scan Sheet */}
|
||||
<BulkScanSheet
|
||||
isOpen={isBulkScanOpen}
|
||||
onClose={() => setIsBulkScanOpen(false)}
|
||||
sessionId={id as string}
|
||||
sessionName={session.name}
|
||||
onSuccess={(bottleIds) => {
|
||||
setIsBulkScanOpen(false);
|
||||
fetchSessionData();
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user