diff --git a/.convoys/ship-readiness.md b/.convoys/ship-readiness.md
index 125c653..e5225ef 100644
--- a/.convoys/ship-readiness.md
+++ b/.convoys/ship-readiness.md
@@ -320,7 +320,7 @@ These MUST land before any anonymous traffic touches the production URL.
| `pages/deck-builder.js` | 823 | `DeckBuilder` — extract card-search, deck-list, mana-curve panels. |
| `components/CameraScanner.js` | ~45 | **RESOLVED 2026-06-02** — god-component-split slice shipped PRs #67–#72 + view extract. Pre-split ~1,050 lines; now composes `useCameraScanner` + `useScannerIdentification` + `CameraScannerView`. Logic lives in `lib/scanner-card-detection.js`, `lib/scanner-card-identify.js`, `lib/scan-capture-upload.js`, `components/ScanDisambiguationDialog.js`. |
| `pages/admin/card-editor.js` | 778 | Form heavy. Use a `useFormState` pattern + separate the search-results subview. |
-| `pages/scanner.js` | 776 | Mirror of CameraScanner concerns plus queue management. |
+| `pages/scanner.js` | ~75 | **RESOLVED 2026-06-02** — god-component-split slice (Briefs 1–3): session/route libs (#74), `useScannerQueue` (#75), `ScannerPageView`. Pre-split ~825 lines. |
| `pages/settings.js` | 669 | One screen per settings section is the usual fix. |
| `pages/profile.js` | 625 | Avatar generation logic alone is ~150 lines — extract `useGeneratedAvatar` hook. |
diff --git a/components/ScannerPageView.js b/components/ScannerPageView.js
new file mode 100644
index 0000000..9801355
--- /dev/null
+++ b/components/ScannerPageView.js
@@ -0,0 +1,353 @@
+import CameraScanner from './CameraScanner';
+import ScannerDestinationPicker from './ScannerDestinationPicker';
+import ScannedCardItem, { CONDITION_OPTIONS } from './ScannedCardItem';
+import OCRSettings from './OCRSettings';
+import { useFocusTrap } from '../lib/use-focus-trap.js';
+import { VOCAB } from '../lib/collection-vocabulary.js';
+
+export default function ScannerPageView({
+ gameFilter,
+ onGameFilterChange,
+ sessionDestination,
+ onDestinationChange,
+ scanDefaults,
+ onScanDefaultsChange,
+ queue,
+ showOCRSettings,
+ onOpenOCRSettings,
+ onCloseOCRSettings,
+ onScannerError,
+}) {
+ const createCollectionDialogRef = useFocusTrap(queue.showCreateCollection);
+
+ 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 &&
}
+
+ );
+}
diff --git a/pages/scanner.js b/pages/scanner.js
index b053dc3..a2a95fa 100644
--- a/pages/scanner.js
+++ b/pages/scanner.js
@@ -1,13 +1,8 @@
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 ScannerPageView from '../components/ScannerPageView';
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,
@@ -26,7 +21,6 @@ export default function Scanner() {
const [scanDefaults, setScanDefaults] = useState(() => loadSavedScannerSession().scanDefaults);
const queue = useScannerQueue({ user, sessionDestination, scanDefaults });
- const createCollectionDialogRef = useFocusTrap(queue.showCreateCollection);
useEffect(() => {
if (!authLoading && !user) {
@@ -46,7 +40,7 @@ export default function Scanner() {
});
};
- const handleError = (error) => {
+ const handleScannerError = (error) => {
console.error('Scanner error:', error);
};
@@ -66,328 +60,19 @@ export default function Scanner() {
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)} />}
-
+ setScanDefaults((current) => ({ ...current, ...patch }))}
+ queue={queue}
+ showOCRSettings={showOCRSettings}
+ onOpenOCRSettings={() => setShowOCRSettings(true)}
+ onCloseOCRSettings={() => setShowOCRSettings(false)}
+ onScannerError={handleScannerError}
+ />
);
}