From 151cacfee0879065a8cfa871e3b3ea78f75db822 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 2 Jun 2026 08:50:55 -0500 Subject: [PATCH] refactor(scanner): extract disambiguation dialog and upload helpers. Brief 1 of god-component-split: move ScanDisambiguationDialog and scan-capture-upload lib out of CameraScanner (~90 lines) without behavior changes. Co-authored-by: Cursor --- components/CameraScanner.js | 111 +++---------------------- components/ScanDisambiguationDialog.js | 83 ++++++++++++++++++ lib/scan-capture-upload.js | 29 +++++++ 3 files changed, 124 insertions(+), 99 deletions(-) create mode 100644 components/ScanDisambiguationDialog.js create mode 100644 lib/scan-capture-upload.js diff --git a/components/CameraScanner.js b/components/CameraScanner.js index 77229ea..1498de6 100644 --- a/components/CameraScanner.js +++ b/components/CameraScanner.js @@ -1,34 +1,6 @@ -/* eslint-disable @next/next/no-img-element -- External card image URLs in disambiguation UI; next/image migration is out of scope. */ import { useState, useEffect, useRef } from 'react'; -import { useFocusTrap } from '../lib/use-focus-trap.js'; - -function rateLimitCooldownUntil(ms) { - return Date.now() + ms; -} - -async function uploadScanCapture(imageData) { - const response = await fetch('/api/scan/upload-image', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${localStorage.getItem('auth_token')}`, - }, - body: JSON.stringify({ imageData }), - }); - - if (response.status === 429) { - console.warn('Scan image upload rate-limited'); - return null; - } - - if (!response.ok) { - const errBody = await response.json().catch(() => ({})); - throw new Error(errBody.error || 'Failed to upload scan image'); - } - - const result = await response.json(); - return result.url || null; -} +import { rateLimitCooldownUntil, uploadScanCapture } from '../lib/scan-capture-upload.js'; +import ScanDisambiguationDialog from './ScanDisambiguationDialog.js'; export default function CameraScanner({ onCardScanned, onError }) { const [isStreaming, setIsStreaming] = useState(false); @@ -52,7 +24,6 @@ export default function CameraScanner({ onCardScanned, onError }) { const activeVerificationRef = useRef(0); const lastErrorAtRef = useRef(0); const disambiguationRefineRef = useRef(null); - const disambiguationDialogRef = useFocusTrap(Boolean(disambiguation)); // Mana symbol settings const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false }); @@ -1059,74 +1030,16 @@ export default function CameraScanner({ onCardScanned, onError }) { )} - {disambiguation && ( -
-
-

- Which card is this? -

-

- {disambiguation.message || 'Multiple matches found. Select the correct printing.'} -

- {disambiguation.visionHint && ( -

- Vision detected set: {disambiguation.visionHint} -

- )} -
- {disambiguation.candidates.map((candidate) => ( - - ))} -
- - -
-
- )} + { + setDisambiguation(null); + disambiguationRefineRef.current = null; + }} + /> ); } \ No newline at end of file diff --git a/components/ScanDisambiguationDialog.js b/components/ScanDisambiguationDialog.js new file mode 100644 index 0000000..4a499c5 --- /dev/null +++ b/components/ScanDisambiguationDialog.js @@ -0,0 +1,83 @@ +/* eslint-disable @next/next/no-img-element -- external Scryfall/card CDN URLs */ +import { useRef } from 'react'; + +/** + * Modal for picking among multiple catalog matches after a scan. + */ +export default function ScanDisambiguationDialog({ + disambiguation, + submittingReview, + onPick, + onNotInCatalog, + onCancel, +}) { + const dialogRef = useFocusTrap(Boolean(disambiguation)); + + if (!disambiguation) return null; + + return ( +
+
+

+ Which card is this? +

+

+ {disambiguation.message || 'Multiple matches found. Select the correct printing.'} +

+ {disambiguation.visionHint && ( +

+ Vision detected set: {disambiguation.visionHint} +

+ )} +
+ {disambiguation.candidates.map((candidate) => ( + + ))} +
+ + +
+
+ ); +} diff --git a/lib/scan-capture-upload.js b/lib/scan-capture-upload.js new file mode 100644 index 0000000..fd265fe --- /dev/null +++ b/lib/scan-capture-upload.js @@ -0,0 +1,29 @@ +/** Cooldown timestamp for rate-limit backoff (ms from now). */ +export function rateLimitCooldownUntil(ms) { + return Date.now() + ms; +} + +/** Upload a base64 scan capture to Blob storage; returns public URL or null on 429. */ +export async function uploadScanCapture(imageData) { + const response = await fetch('/api/scan/upload-image', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + }, + body: JSON.stringify({ imageData }), + }); + + if (response.status === 429) { + console.warn('Scan image upload rate-limited'); + return null; + } + + if (!response.ok) { + const errBody = await response.json().catch(() => ({})); + throw new Error(errBody.error || 'Failed to upload scan image'); + } + + const result = await response.json(); + return result.url || null; +} -- 2.45.2