feat: scan flow page with candidates, confirm view and error guidance
This commit is contained in:
100
web/src/scan/ConfirmView.tsx
Normal file
100
web/src/scan/ConfirmView.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { ReleasePreview } from '../types.js'
|
||||
import Cover from '../components/Cover.js'
|
||||
|
||||
export default function ConfirmView({
|
||||
code,
|
||||
preview,
|
||||
matchAlbumId,
|
||||
onSetMatch,
|
||||
onClearMatch,
|
||||
onAdd,
|
||||
adding,
|
||||
addError,
|
||||
}: {
|
||||
code: string | null
|
||||
preview: ReleasePreview
|
||||
matchAlbumId: number | null
|
||||
onSetMatch: (albumId: number) => void
|
||||
onClearMatch: () => void
|
||||
onAdd: () => void
|
||||
adding: boolean
|
||||
addError: string | null
|
||||
}) {
|
||||
const { release, duplicate, ripMatch, matchCandidates } = preview
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<Cover src={release.coverUrl ?? release.thumbUrl} alt="" className="size-28 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-semibold leading-tight">{release.title}</h2>
|
||||
<p className="text-neutral-400">{release.artist}</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
{[release.year, release.formats.join(', '), release.labels[0], release.catno]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
{code && <p className="mt-1 text-xs text-neutral-500">Barcode {code}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ripMatch === 'ripped' && (
|
||||
<p className="rounded-xl bg-emerald-500/10 px-4 py-3 text-sm text-emerald-400">
|
||||
In your digital collection ✓
|
||||
</p>
|
||||
)}
|
||||
{ripMatch === 'not_ripped' && (
|
||||
<p className="rounded-xl bg-amber-500/10 px-4 py-3 text-sm text-amber-400">Not ripped yet</p>
|
||||
)}
|
||||
{ripMatch === 'ambiguous' && (
|
||||
<fieldset className="rounded-xl bg-amber-500/10 px-4 py-3 text-sm text-amber-400">
|
||||
<legend className="px-1">Possible matches in your library — which one is it?</legend>
|
||||
<div className="mt-1 space-y-2">
|
||||
{matchCandidates.map((m) => (
|
||||
<label key={m.id} className="flex items-center gap-2 text-neutral-200">
|
||||
<input
|
||||
type="radio"
|
||||
name="match"
|
||||
checked={matchAlbumId === m.id}
|
||||
onChange={() => onSetMatch(m.id)}
|
||||
/>
|
||||
{m.artist} — {m.title}
|
||||
</label>
|
||||
))}
|
||||
<label className="flex items-center gap-2 text-neutral-200">
|
||||
<input type="radio" name="match" checked={matchAlbumId === null} onChange={onClearMatch} />
|
||||
None of these — just add it
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{duplicate && (
|
||||
<p className="rounded-xl bg-red-500/10 px-4 py-3 text-sm text-red-400">
|
||||
Heads up: this release is already in your collection.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<details className="rounded-xl border border-neutral-800 bg-neutral-900 p-3">
|
||||
<summary className="cursor-pointer text-sm text-neutral-300">Tracklist</summary>
|
||||
<ol className="mt-2 space-y-1 text-sm text-neutral-400">
|
||||
{release.tracklist.map((t, i) => (
|
||||
<li key={i} className="flex gap-2">
|
||||
<span className="w-8 shrink-0 text-neutral-600">{t.position}</span>
|
||||
<span>{t.title}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
{addError && <p className="text-sm text-red-400">{addError}</p>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
disabled={adding}
|
||||
className="w-full rounded-xl bg-emerald-500 py-3 font-medium text-neutral-950 disabled:opacity-50"
|
||||
>
|
||||
{adding ? 'Adding…' : 'Add to collection'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user