import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import Layout from '../components/Layout'; import CameraScanner from '../components/CameraScanner'; import ScannerDestinationPicker from '../components/ScannerDestinationPicker'; import ScannedCardItem, { CONDITION_OPTIONS } from '../components/ScannedCardItem'; import OCRSettings from '../components/OCRSettings'; import { useAuth } from '../lib/use-auth'; import { useFocusTrap } from '../lib/use-focus-trap.js'; import { VOCAB } from '../lib/collection-vocabulary.js'; import { DEFAULT_SCANNER_DESTINATION, loadSavedScannerSession, saveScannerSession, } from '../lib/scanner-session.js'; import { useScannerQueue } from '../lib/use-scanner-queue.js'; export default function Scanner() { const { user, loading: authLoading } = useAuth(); const router = useRouter(); const [showOCRSettings, setShowOCRSettings] = useState(false); const [sessionDestination, setSessionDestination] = useState( () => loadSavedScannerSession().destination ); const [gameFilter, setGameFilter] = useState(() => loadSavedScannerSession().gameFilter); const [scanDefaults, setScanDefaults] = useState(() => loadSavedScannerSession().scanDefaults); const queue = useScannerQueue({ user, sessionDestination, scanDefaults }); const createCollectionDialogRef = useFocusTrap(queue.showCreateCollection); useEffect(() => { if (!authLoading && !user) { router.push('/login'); } }, [authLoading, user, router]); useEffect(() => { saveScannerSession({ destination: sessionDestination, gameFilter, scanDefaults }); }, [sessionDestination, gameFilter, scanDefaults]); const handleGameFilterChange = (nextFilter) => { setGameFilter(nextFilter); setSessionDestination((current) => { if (!current || current.type === 'owned') return current; return DEFAULT_SCANNER_DESTINATION; }); }; const handleError = (error) => { console.error('Scanner error:', error); }; if (authLoading) { return (
); } if (!user) { return
Redirecting to login...
; } return (

🃏 Card Scanner

Pick a destination once — every scan lands there until you change it

{queue.autoRouteError && (
{queue.autoRouteError}
)}

Defaults for new scans

Applied to each card when it enters the queue

Camera Scanner

Scanned Cards

{queue.scannedCards.length} cards
{queue.scannedCards.length > 0 && ( )}
{queue.scannedCards.length === 0 ? (
No cards scanned yet
Start scanning to see cards here
) : (
    {queue.scannedCards.map((card) => (
  • queue.toggleCardSelection(card.id)} onIncrement={() => queue.incrementCardQuantity(card.id)} onDecrement={() => queue.decrementCardQuantity(card.id)} onUpdateMetadata={(patch) => queue.updateCardMetadata(card.id, patch)} onMarkOwned={() => queue.addSingleCardToOwned(card)} onAddToCollection={(collectionId) => queue.addSingleCardToCollection(card, collectionId)} onAddToDeck={(deckId) => queue.addSingleCardToDeck(card, deckId)} onRemove={() => queue.removeScannedCard(card.id)} isAdding={queue.addingCardIds.has(card.id)} />
  • ))}
)}
{queue.selectedCards.size > 0 && (
{queue.selectedCards.size}
{queue.selectedCards.size === 1 ? 'card selected' : 'cards selected'}
{queue.collections.length > 0 && ( )} {queue.decks.length > 0 && ( )}
)} {queue.showCreateCollection && (

Create New List

queue.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') { queue.createCollection(); } }} />
)} {showOCRSettings && setShowOCRSettings(false)} />}
); }