diff --git a/components/ScannerDestinationPicker.js b/components/ScannerDestinationPicker.js
new file mode 100644
index 0000000..617b8a4
--- /dev/null
+++ b/components/ScannerDestinationPicker.js
@@ -0,0 +1,214 @@
+const GAME_OPTIONS = [
+ { value: 'All', label: 'All games' },
+ { value: 'MTG', label: 'Magic' },
+ { value: 'Pokemon', label: 'Pokรฉmon' },
+ { value: 'Lorcana', label: 'Lorcana' },
+];
+
+function matchesGameFilter(itemGame, filter) {
+ if (filter === 'All') return true;
+ const normalized = (itemGame || '').toLowerCase();
+ if (filter === 'MTG') return normalized === 'mtg' || normalized.includes('magic');
+ if (filter === 'Pokemon') return normalized.includes('pokemon') || normalized.includes('pokรฉmon');
+ if (filter === 'Lorcana') return normalized.includes('lorcana');
+ return itemGame === filter;
+}
+
+export default function ScannerDestinationPicker({
+ gameFilter,
+ onGameFilterChange,
+ destination,
+ onDestinationChange,
+ collections = [],
+ decks = [],
+ disabled = false,
+}) {
+ const filteredCollections = collections.filter(
+ (collection) => matchesGameFilter(collection.tcg, gameFilter) || collection.tcg === 'All'
+ );
+ const filteredDecks = decks.filter((deck) => matchesGameFilter(deck.game, gameFilter));
+
+ const handleTypeChange = (type) => {
+ if (type === 'owned') {
+ onDestinationChange({ type: 'owned', id: null, label: 'My owned cards' });
+ return;
+ }
+ if (type === 'collection' && filteredCollections.length > 0) {
+ const first = filteredCollections[0];
+ onDestinationChange({
+ type: 'collection',
+ id: first.id,
+ label: first.name,
+ });
+ return;
+ }
+ if (type === 'deck' && filteredDecks.length > 0) {
+ const first = filteredDecks[0];
+ onDestinationChange({
+ type: 'deck',
+ id: first.id,
+ label: first.name,
+ });
+ }
+ };
+
+ const handleTargetChange = (event) => {
+ const value = event.target.value;
+ if (!value || !destination) return;
+
+ if (destination.type === 'collection') {
+ const collection = filteredCollections.find((item) => String(item.id) === value);
+ if (collection) {
+ onDestinationChange({
+ type: 'collection',
+ id: collection.id,
+ label: collection.name,
+ });
+ }
+ }
+
+ if (destination.type === 'deck') {
+ const deck = filteredDecks.find((item) => String(item.id) === value);
+ if (deck) {
+ onDestinationChange({
+ type: 'deck',
+ id: deck.id,
+ label: deck.name,
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+
+ Scan destination
+
+
+ {destination
+ ? `Every scan goes to: ${destination.label}`
+ : 'Choose where scanned cards should land'}
+
+
+
+
+
+
+
+
+
+
+ {destination?.type === 'collection' && filteredCollections.length > 0 && (
+
+ )}
+
+ {destination?.type === 'deck' && filteredDecks.length > 0 && (
+
+ )}
+
+ {destination?.type === 'collection' && filteredCollections.length === 0 && (
+
+ No collections match this game filter.
+
+ )}
+
+ {destination?.type === 'deck' && filteredDecks.length === 0 && (
+
+ No decks match this game filter.
+
+ )}
+
+ );
+}
diff --git a/pages/scanner.js b/pages/scanner.js
index 05146e7..f313cfe 100644
--- a/pages/scanner.js
+++ b/pages/scanner.js
@@ -2,11 +2,36 @@ import { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/router';
import Layout from '../components/Layout';
import CameraScanner from '../components/CameraScanner';
+import ScannerDestinationPicker from '../components/ScannerDestinationPicker';
import OCRSettings from '../components/OCRSettings';
import { ManaCost, ColorIdentity } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth';
+const SESSION_STORAGE_KEY = 'deckhearth:scanner-session';
+
+const DEFAULT_DESTINATION = { type: 'owned', id: null, label: 'My owned cards' };
+
+function loadSavedScannerSession() {
+ if (typeof window === 'undefined') {
+ return { destination: DEFAULT_DESTINATION, gameFilter: 'All' };
+ }
+ try {
+ const saved = JSON.parse(localStorage.getItem(SESSION_STORAGE_KEY));
+ return {
+ destination: saved?.destination || DEFAULT_DESTINATION,
+ gameFilter: saved?.gameFilter || 'All',
+ };
+ } catch {
+ return { destination: DEFAULT_DESTINATION, gameFilter: 'All' };
+ }
+}
+
+function destinationActionKey(destination) {
+ if (!destination) return 'pending';
+ return destination.type === 'owned' ? 'owned' : destination.type;
+}
+
export default function Scanner() {
const { user, loading: authLoading } = useAuth();
const router = useRouter();
@@ -23,6 +48,11 @@ export default function Scanner() {
const [bulkTarget, setBulkTarget] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const addingInFlightRef = useRef(new Set());
+ const [sessionDestination, setSessionDestination] = useState(
+ () => loadSavedScannerSession().destination
+ );
+ const [gameFilter, setGameFilter] = useState(() => loadSavedScannerSession().gameFilter);
+ const [autoRouteError, setAutoRouteError] = useState(null);
// Mana symbol settings
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
@@ -42,6 +72,38 @@ export default function Scanner() {
}
}, [user]);
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+ localStorage.setItem(
+ SESSION_STORAGE_KEY,
+ JSON.stringify({ destination: sessionDestination, gameFilter })
+ );
+ }, [sessionDestination, gameFilter]);
+
+ const handleGameFilterChange = (nextFilter) => {
+ setGameFilter(nextFilter);
+ setSessionDestination((current) => {
+ if (!current || current.type === 'owned') return current;
+ return DEFAULT_DESTINATION;
+ });
+ };
+
+ const routeCardToDestination = async (card, destination) => {
+ if (!destination || !card.databaseId) return false;
+
+ if (destination.type === 'owned') {
+ await addToOwnedCards(card);
+ } else if (destination.type === 'collection') {
+ await addToCollection(card, destination.id);
+ } else if (destination.type === 'deck') {
+ await addToDeck(card, destination.id);
+ } else {
+ return false;
+ }
+
+ return true;
+ };
+
const loadCollections = async () => {
try {
const response = await fetch('/api/collections', {
@@ -77,44 +139,55 @@ export default function Scanner() {
};
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];
- }
- });
+ setAutoRouteError(null);
+
+ const existingCardIndex = scannedCards.findIndex((existing) =>
+ existing.name === cardData.name &&
+ existing.set === cardData.set &&
+ !existing.processed
+ );
+
+ let cardEntry;
+
+ if (existingCardIndex !== -1) {
+ const existing = scannedCards[existingCardIndex];
+ cardEntry = {
+ ...existing,
+ quantity: (existing.quantity || 1) + 1,
+ timestamp: new Date().toISOString(),
+ };
+ setScannedCards((prev) => {
+ const updated = [...prev];
+ updated[existingCardIndex] = cardEntry;
+ return updated;
+ });
+ } else {
+ cardEntry = {
+ ...cardData,
+ id: Date.now() + Math.random(),
+ name: cardData.name,
+ set: cardData.set,
+ quantity: 1,
+ timestamp: new Date().toISOString(),
+ processed: false,
+ };
+ setScannedCards((prev) => [cardEntry, ...prev]);
+ }
+
+ if (!sessionDestination) return;
+
+ if (!cardEntry.databaseId) {
+ setAutoRouteError(`"${cardEntry.name}" was queued but is not in the catalog yet โ add it manually after review.`);
+ return;
+ }
+
+ try {
+ await routeCardToDestination(cardEntry, sessionDestination);
+ markCardAsProcessed(cardEntry.id, destinationActionKey(sessionDestination));
+ } catch (error) {
+ console.error('Auto-route failed:', error);
+ setAutoRouteError(`Could not add "${cardEntry.name}" to ${sessionDestination.label}. Use the card actions below.`);
+ }
};
// Quantity management functions
@@ -361,10 +434,34 @@ export default function Scanner() {
๐ Card Scanner
- Scan cards to identify them, then choose what to do with your collection
+ Pick a destination once โ every scan lands there until you change it
+
+
+ {autoRouteError && (
+
+ {autoRouteError}
+
+ )}
+
{/* Main Content - Full Height */}
{/* Camera Scanner */}