deckhearth/components/ScanDisambiguationDialog.js
varutasu 071a3dca21
refactor(scanner): extract disambiguation dialog and upload helpers. (#67)
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 <cursoragent@cursor.com>
2026-06-02 11:01:32 -05:00

83 lines
3.4 KiB
JavaScript

/* 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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 p-4">
<div
ref={dialogRef}
className="max-w-lg w-full rounded-xl border p-6 max-h-[80vh] overflow-y-auto"
style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}
role="dialog"
aria-modal="true"
aria-labelledby="disambiguation-title"
>
<h3 id="disambiguation-title" className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Which card is this?
</h3>
<p className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}>
{disambiguation.message || 'Multiple matches found. Select the correct printing.'}
</p>
{disambiguation.visionHint && (
<p className="text-xs mb-3 px-2 py-1 rounded" style={{ color: 'var(--accent-ember)', backgroundColor: 'var(--bg-tertiary)' }}>
Vision detected set: {disambiguation.visionHint}
</p>
)}
<div className="space-y-2">
{disambiguation.candidates.map((candidate) => (
<button
type="button"
key={candidate.id}
onClick={() => onPick(candidate)}
className="w-full flex items-center gap-3 p-3 rounded-lg border text-left hover:opacity-90"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-tertiary)' }}
aria-label={`Select ${candidate.name}${candidate.set_name ? `, ${candidate.set_name}` : ''}`}
>
{candidate.image_url ? (
<img src={candidate.image_url} alt="" className="w-12 h-16 object-cover rounded" />
) : (
<div className="w-12 h-16 rounded flex items-center justify-center text-xs" style={{ backgroundColor: 'var(--bg-secondary)' }}>🃏</div>
)}
<div>
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>{candidate.name}</div>
<div className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{[candidate.set_name, candidate.set_code, candidate.card_number].filter(Boolean).join(' · ')}
</div>
</div>
</button>
))}
</div>
<button
type="button"
onClick={onNotInCatalog}
disabled={submittingReview}
className="mt-4 w-full py-2 rounded-lg text-sm font-medium disabled:opacity-50"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
>
{submittingReview ? 'Submitting…' : "My card isn't listed — send for review"}
</button>
<button
type="button"
onClick={onCancel}
className="mt-2 w-full py-2 rounded-lg border text-sm"
style={{ borderColor: 'var(--border)', color: 'var(--text-secondary)' }}
>
Cancel
</button>
</div>
</div>
);
}