Wire scanner feedback toasts and restore rescan uploads #157

Merged
rstillwell merged 1 commit from fix/scanner-feedback-rescan into main 2026-09-08 20:08:02 -04:00
3 changed files with 37 additions and 14 deletions

View file

@ -63,6 +63,7 @@ function readFileToImageData(file) {
export function useScannerIdentification({ export function useScannerIdentification({
onCardScanned, onCardScanned,
onScanImageUploaded,
onError, onError,
onTrackerComplete, onTrackerComplete,
onTrackerReset, onTrackerReset,
@ -102,15 +103,20 @@ export function useScannerIdentification({
}, 3000); }, 3000);
// Show result immediately — don't block on upload // Show result immediately — don't block on upload
onCardScanned(buildScannedCardPayload(finalCard, { imageData, scanImageUrl: null, ocrMeta })); const queueCardId = onCardScanned(
buildScannedCardPayload(finalCard, { imageData, scanImageUrl: null, ocrMeta })
);
finishTrackedCard(cardTracker); finishTrackedCard(cardTracker);
// Upload capture in background (fire-and-forget) // Upload capture in background (fire-and-forget)
if (imageData) { if (imageData && queueCardId) {
uploadScanCapture(imageData) uploadScanCapture(imageData)
.then((url) => { .then((url) => {
if (debugLog && isDebugMode) { if (url) {
debugLog('📤', `Background upload complete: ${url ? 'success' : '429'}`); onScanImageUploaded?.(queueCardId, url);
}
if (isDebugMode()) {
debugLog('📤', `Background upload complete: ${url ? 'success' : 'skipped'}`);
} }
}) })
.catch((uploadError) => { .catch((uploadError) => {

View file

@ -176,6 +176,7 @@ export function useScannerQueue({ user, sessionDestination, scanDefaults, deckMo
persistCart(nextQueue, next); persistCart(nextQueue, next);
return next; return next;
}); });
return cardEntry.id;
}; };
const incrementCardQuantity = (cardId) => { const incrementCardQuantity = (cardId) => {

View file

@ -22,6 +22,8 @@ import { clearScannerCartStorage } from '../lib/scanner-session.js';
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js'; import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
const TOAST_DURATION_MS = 2500; const TOAST_DURATION_MS = 2500;
const NOTICE_TOAST_DURATION_MS = 8000;
const ERROR_TOAST_DURATION_MS = 5000;
export default function Scanner() { export default function Scanner() {
const { user, loading: authLoading } = useAuth(); const { user, loading: authLoading } = useAuth();
@ -53,13 +55,24 @@ export default function Scanner() {
const batchInputRef = useRef(null); const batchInputRef = useRef(null);
const disambiguationActiveRef = useRef(false); const disambiguationActiveRef = useRef(false);
const showPageToast = useCallback((message, type = 'success', durationMs = TOAST_DURATION_MS) => {
clearTimeout(toastTimerRef.current);
setPageToast({ message, visible: true, type });
toastTimerRef.current = setTimeout(() => {
setPageToast((current) => ({ ...current, visible: false }));
}, durationMs);
}, []);
const { scanDefaults } = useScannerSession(); const { scanDefaults } = useScannerSession();
const queue = useScannerQueue({ user, scanDefaults, deckMode: null }); const queue = useScannerQueue({ user, scanDefaults, deckMode: null });
const onVerifyCardRef = useRef(null); const onVerifyCardRef = useRef(null);
const camera = useCameraScanner({ const camera = useCameraScanner({
onError: (msg) => console.error('Camera error:', msg), onError: (msg) => {
console.error('Camera error:', msg);
showPageToast(msg, 'error', ERROR_TOAST_DURATION_MS);
},
onVerifyCard: (cardTracker) => onVerifyCardRef.current?.(cardTracker), onVerifyCard: (cardTracker) => onVerifyCardRef.current?.(cardTracker),
verificationPausedRef, verificationPausedRef,
autoDetectPausedRef, autoDetectPausedRef,
@ -67,7 +80,13 @@ export default function Scanner() {
const identification = useScannerIdentification({ const identification = useScannerIdentification({
onCardScanned: queue.handleCardScanned, onCardScanned: queue.handleCardScanned,
onError: (msg) => console.error('Identification error:', msg), onScanImageUploaded: (cardId, url) => {
queue.updateCardMetadata(cardId, { scanImageUrl: url });
},
onError: (msg) => {
console.error('Identification error:', msg);
showPageToast(msg, 'error', ERROR_TOAST_DURATION_MS);
},
onTrackerComplete: camera.removeTrackedCard, onTrackerComplete: camera.removeTrackedCard,
onTrackerReset: camera.resetTrackedCard, onTrackerReset: camera.resetTrackedCard,
videoRef: camera.videoRef, videoRef: camera.videoRef,
@ -102,6 +121,11 @@ export default function Scanner() {
disambiguationActiveRef.current = Boolean(identification.disambiguation); disambiguationActiveRef.current = Boolean(identification.disambiguation);
}, [identification.disambiguation]); }, [identification.disambiguation]);
useEffect(() => {
if (!identification.scanNotice) return;
showPageToast(identification.scanNotice, 'info', NOTICE_TOAST_DURATION_MS);
}, [identification.scanNotice, showPageToast]);
useEffect(() => { useEffect(() => {
const mobileCheckoutPauses = const mobileCheckoutPauses =
typeof window !== 'undefined' && typeof window !== 'undefined' &&
@ -137,14 +161,6 @@ export default function Scanner() {
return () => clearTimeout(toastTimerRef.current); return () => clearTimeout(toastTimerRef.current);
}, []); }, []);
const showPageToast = useCallback((message, type = 'success') => {
clearTimeout(toastTimerRef.current);
setPageToast({ message, visible: true, type });
toastTimerRef.current = setTimeout(() => {
setPageToast((current) => ({ ...current, visible: false }));
}, TOAST_DURATION_MS);
}, []);
const handleLeaveConfirm = () => { const handleLeaveConfirm = () => {
clearScannerCartStorage(); clearScannerCartStorage();
queue.clearScannedCards(); queue.clearScannedCards();