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