import { useCallback, useEffect, useReducer, useState } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { api, ApiError } from '../api.js' import CandidateCard from '../components/CandidateCard.js' import ConfirmView from '../scan/ConfirmView.js' import Cover from '../components/Cover.js' import { scanReducer, INITIAL_SCAN_STATE, type ScanErrorKind } from '../scan/reducer.js' function toErrorKind(err: unknown): ScanErrorKind { if (err instanceof ApiError) { if (err.code === 'not_found') return 'not_found' if (err.code === 'no_discogs_token') return 'no_discogs_token' if (err.code === 'discogs_rate_limited' || err.status === 429) return 'rate_limited' } return 'server' } const FORMATS = ['Vinyl', 'CD', 'Cassette'] as const export default function AddPage() { const [params] = useSearchParams() const [q, setQ] = useState(params.get('q') ?? '') const [format, setFormat] = useState<(typeof FORMATS)[number] | ''>('') const [state, dispatch] = useReducer(scanReducer, INITIAL_SCAN_STATE) const search = useCallback( (query: string) => { if (!query.trim()) return dispatch({ type: 'DETECT', code: query.trim() }) void (async () => { try { const { candidates } = await api.lookupSearch(query.trim(), format || undefined) if (candidates.length === 0) { dispatch({ type: 'NOT_FOUND', code: query.trim() }) } else { dispatch({ type: 'CANDIDATES', code: query.trim(), candidates }) } } catch (err) { dispatch({ type: 'ERROR', kind: toErrorKind(err), code: query.trim(), source: 'lookup' }) } })() }, [format] ) // Load the release preview once a candidate is selected. useEffect(() => { if (state.phase !== 'confirm' || state.preview) return const candidateId = state.candidate.id void (async () => { try { const preview = await api.getReleasePreview(candidateId) dispatch({ type: 'PREVIEW', preview, candidateId }) } catch (err) { dispatch({ type: 'ERROR', kind: toErrorKind(err), code: null, source: 'preview', candidateId }) } })() }, [state]) const add = useCallback(() => { if (state.phase !== 'confirm' || !state.preview || state.adding) return const { candidate, matchAlbumId } = state dispatch({ type: 'ADD_START' }) void (async () => { try { const item = await api.addToCollection({ releaseId: candidate.id, ...(matchAlbumId !== null ? { matchAlbumId } : {}), }) dispatch({ type: 'ADDED', item }) } catch (err) { const message = err instanceof ApiError ? err.code === 'duplicate' ? 'Already in your collection.' : err.detail ?? err.code : 'Something went wrong' dispatch({ type: 'ADD_ERROR', message }) } })() }, [state]) return (
Searching…
} {state.phase === 'candidates' && (Which release is it?
{state.candidates.map((c) => (Checking release…
} {state.preview && (Added to collection ✓
View item{state.kind === 'not_found' ? 'Nothing found' : 'Search failed'}
{state.kind === 'not_found' && 'Try different spelling, or add the year.'} {state.kind === 'no_discogs_token' && 'Add your Discogs token in Settings first.'} {state.kind === 'rate_limited' && 'Discogs is rate limiting us. Try again shortly.'}