import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Layout from '../components/Layout'; import CameraScanner from '../components/CameraScanner'; import OCRSettings from '../components/OCRSettings'; import { ManaCost, ColorIdentity } from '../components/ManaSymbols'; import ManaSymbolSettings from '../components/ManaSymbolSettings'; import { useAuth } from '../lib/use-auth'; export default function Scanner() { const { user } = useAuth(); const router = useRouter(); const [scannedCards, setScannedCards] = useState([]); const [collections, setCollections] = useState([]); const [decks, setDecks] = useState([]); const [showCreateCollection, setShowCreateCollection] = useState(false); const [newCollectionName, setNewCollectionName] = useState(''); const [showOCRSettings, setShowOCRSettings] = useState(false); // Bulk action states const [selectedCards, setSelectedCards] = useState(new Set()); const [bulkAction, setBulkAction] = useState(''); // 'owned', 'collection', 'deck' const [bulkTarget, setBulkTarget] = useState(''); const [isProcessing, setIsProcessing] = useState(false); // Mana symbol settings const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false }); // Redirect to login if not authenticated useEffect(() => { if (!user) { router.push('/login'); return; } }, [user, router]); // Load collections and decks useEffect(() => { if (user) { loadCollections(); loadDecks(); } }, [user]); const loadCollections = async () => { try { const response = await fetch('/api/collections', { headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` } }); if (response.ok) { const data = await response.json(); // Filter out system collections (like "All My Cards") const userCollections = data.filter(collection => !collection.is_system_collection); setCollections(userCollections); } } catch (error) { console.error('Error loading collections:', error); } }; const loadDecks = async () => { try { const response = await fetch('/api/decks', { headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` } }); if (response.ok) { const data = await response.json(); setDecks(data); } } catch (error) { console.error('Error loading decks:', error); } }; const handleCardScanned = async (cardData) => { console.log('Card scanned:', cardData); console.log('Looking for existing card with name:', cardData.name, 'set:', cardData.set); // Check if this card already exists in the queue setScannedCards(prev => { console.log('Current queue:', prev.map(c => ({ name: c.name, set: c.set, processed: c.processed }))); const existingCardIndex = prev.findIndex(existing => existing.name === cardData.name && existing.set === cardData.set && !existing.processed ); if (existingCardIndex !== -1) { console.log(`📈 Incrementing quantity for existing card: ${cardData.name}`); // Increment quantity of existing card const updatedCards = [...prev]; updatedCards[existingCardIndex] = { ...updatedCards[existingCardIndex], quantity: (updatedCards[existingCardIndex].quantity || 1) + 1, timestamp: new Date().toISOString() // Update timestamp }; return updatedCards; } else { console.log(`🆕 Adding new card to queue: ${cardData.name}`); // Add new card to queue const scannedCard = { ...cardData, id: Date.now() + Math.random(), // More unique ID for the queue name: cardData.name, set: cardData.set, quantity: 1, timestamp: new Date().toISOString(), processed: false }; return [scannedCard, ...prev]; } }); }; // Quantity management functions const incrementCardQuantity = (cardId) => { setScannedCards(prev => prev.map(card => card.id === cardId ? { ...card, quantity: (card.quantity || 1) + 1 } : card )); }; const decrementCardQuantity = (cardId) => { setScannedCards(prev => prev.map(card => card.id === cardId ? { ...card, quantity: Math.max(1, (card.quantity || 1) - 1) } : card )); }; const handleError = (error) => { console.error('Scanner error:', error); // You could show a toast notification here }; // Individual card actions const addSingleCardToOwned = async (card) => { try { await addToOwnedCards(card); markCardAsProcessed(card.id, 'owned'); } catch (error) { console.error('Error adding card to owned:', error); } }; const addSingleCardToCollection = async (card, collectionId) => { try { await addToCollection(card, collectionId); markCardAsProcessed(card.id, 'collection'); } catch (error) { console.error('Error adding card to collection:', error); } }; const addSingleCardToDeck = async (card, deckId) => { try { await addToDeck(card, deckId); markCardAsProcessed(card.id, 'deck'); } catch (error) { console.error('Error adding card to deck:', error); } }; // Bulk actions const handleBulkAction = async () => { if (!bulkAction || selectedCards.size === 0) return; setIsProcessing(true); try { const cardsToProcess = scannedCards.filter(card => selectedCards.has(card.id)); for (const card of cardsToProcess) { if (bulkAction === 'owned') { await addToOwnedCards(card); } else if (bulkAction === 'collection' && bulkTarget) { await addToCollection(card, bulkTarget); } else if (bulkAction === 'deck' && bulkTarget) { await addToDeck(card, bulkTarget); } markCardAsProcessed(card.id, bulkAction); } // Clear selections and reset bulk action state setSelectedCards(new Set()); setBulkAction(''); setBulkTarget(''); } catch (error) { console.error('Error processing bulk action:', error); } finally { setIsProcessing(false); } }; const markCardAsProcessed = (cardId, action) => { setScannedCards(prev => prev.map(card => card.id === cardId ? { ...card, processed: true, processedAction: action } : card )); }; // Helper functions for API calls const addToOwnedCards = async (cardData) => { const response = await fetch('/api/user-cards', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }, body: JSON.stringify({ cardId: cardData.databaseId, quantity: 1, condition: 'NM' }) }); if (!response.ok) { throw new Error('Failed to add to owned cards'); } }; const addToCollection = async (cardData, collectionId) => { const response = await fetch(`/api/collections/${collectionId}/cards`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }, body: JSON.stringify({ cardId: cardData.databaseId, quantity: 1 }) }); if (!response.ok) { throw new Error('Failed to add to collection'); } }; const addToDeck = async (cardData, deckId) => { const response = await fetch(`/api/decks/${deckId}/cards`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }, body: JSON.stringify({ cardId: cardData.databaseId, quantity: 1 }) }); if (!response.ok) { throw new Error('Failed to add to deck'); } }; const createCollection = async () => { if (!newCollectionName.trim()) return; try { const response = await fetch('/api/collections', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }, body: JSON.stringify({ name: newCollectionName, description: 'Created from card scanner', is_public: false }) }); if (response.ok) { const newCollection = await response.json(); setCollections(prev => [newCollection, ...prev]); setBulkTarget(newCollection.id.toString()); setNewCollectionName(''); setShowCreateCollection(false); } } catch (error) { console.error('Error creating collection:', error); } }; const clearScannedCards = () => { setScannedCards([]); setSelectedCards(new Set()); }; const removeScannedCard = (cardId) => { setScannedCards(prev => prev.filter(card => card.id !== cardId)); setSelectedCards(prev => { const newSet = new Set(prev); newSet.delete(cardId); return newSet; }); }; const toggleCardSelection = (cardId) => { setSelectedCards(prev => { const newSet = new Set(prev); if (newSet.has(cardId)) { newSet.delete(cardId); } else { newSet.add(cardId); } return newSet; }); }; const selectAllCards = () => { const unprocessedCards = scannedCards.filter(card => !card.processed); setSelectedCards(new Set(unprocessedCards.map(card => card.id))); }; const deselectAllCards = () => { setSelectedCards(new Set()); }; if (!user) { return
Redirecting to login...
; } return (
{/* Header */}

🃏 Card Scanner

Scan cards to identify them, then choose what to do with your collection

{/* Main Content - Full Height */}
{/* Camera Scanner */}

Camera Scanner

{/* Scanned Cards Queue */}

Scanned Cards

{scannedCards.length} cards
{scannedCards.length > 0 && ( )}
{/* Scanned Cards Queue - Scrollable */}
{scannedCards.length === 0 ? (
📱
No cards scanned yet
Start scanning to see cards here
) : ( scannedCards.map((card) => (
{/* Card Thumbnail with Checkbox Overlay */}
{card.image_url ? ( {card.name} ) : (
🃏
No Image
)}
{/* Checkbox Overlay */} {!card.processed && (
toggleCardSelection(card.id)} className="w-5 h-5 rounded border-2 border-white shadow-lg" style={{ accentColor: 'var(--accent-ember)' }} />
)}
{/* Card Content */}
{/* Confidence Badge */} {card.confidence && (
= 90 ? 'var(--accent-gold)' : card.confidence >= 70 ? 'var(--accent-ember)' : 'var(--text-secondary)', color: 'white' }}> {Math.round(card.confidence)}% confidence
)} {/* Title and Quantity Row */}

{card.name}

{/* Database Status */} {card.isExisting && (
✅ Found in database
)}
{/* Quantity Controls */} {!card.processed && (
{card.quantity || 1}
)}
{/* Card Details */}
{card.set && (
Set: {card.set} {card.setCode && ({card.setCode})}
)} {card.cardNumber && (
Number: {card.cardNumber}
)} {card.cardType && (
Type: {card.cardType}
)} {card.rarity && (
Rarity: {card.rarity}
)} {card.hp && (
HP: {card.hp}
)} {card.manaCost && (
Mana Cost: {card.manaCost}
)} {card.ocrText && (
Scanned Text:
{card.ocrText.substring(0, 150)}{card.ocrText.length > 150 ? '...' : ''}
)}
{/* Actions */} {!card.processed ? (
{/* Primary Actions Row */}
{/* Secondary Actions Row */}
{collections.length > 0 && ( )} {decks.length > 0 && ( )}
) : (
Added to {card.processedAction}
)}
)) )}
{/* Floating Bulk Actions Toolbar */} {selectedCards.size > 0 && (
{/* Selection Count */}
{selectedCards.size}
{selectedCards.size === 1 ? 'card selected' : 'cards selected'}
{/* Divider */}
{/* Quick Actions */}
{collections.length > 0 && ( )} {decks.length > 0 && ( )}
{/* Divider */}
{/* Clear Selection */}
)} {/* Bulk Actions Modal */} {/* This modal is no longer needed as bulk actions are in a floating toolbar */} {/* Create Collection Modal */} {showCreateCollection && (

Create New Collection

setNewCollectionName(e.target.value)} className="w-full px-4 py-2 rounded-lg border mb-4" style={{ backgroundColor: 'var(--bg-tertiary)', borderColor: 'var(--border)', color: 'var(--text-primary)' }} onKeyPress={(e) => { if (e.key === 'Enter') { createCollection(); } }} />
)} {/* OCR Settings Modal */} {showOCRSettings && ( setShowOCRSettings(false)} /> )}
); }