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} + />
); }