/** * Component: Bulk Import - Match Review Step * Documentation: documentation/features/bulk-import.md * * Scrollable list of discovered audiobooks with Audible matches, * skip toggles, library status badges, and import controls. */ 'use client'; import React from 'react'; import { ArrowLeftIcon, CheckCircleIcon, ExclamationTriangleIcon, MusicalNoteIcon, XCircleIcon, } from '@heroicons/react/24/outline'; import { CheckCircleIcon as CheckCircleSolid } from '@heroicons/react/24/solid'; import { ScannedBook, formatBytes } from './types'; interface MatchReviewStepProps { books: ScannedBook[]; onToggleSkip: (index: number) => void; onStartImport: () => void; isImporting: boolean; importResults: any; onClose: () => void; onBack: () => void; } function BookRow({ book, onToggleSkip, }: { book: ScannedBook; onToggleSkip: () => void; }) { const isDisabled = book.inLibrary || book.hasActiveRequest; const isSkipped = book.skipped; const hasMatch = book.match !== null; // Low confidence when search term came from a filename or folder name fallback, // BUT not when an ASIN was extracted directly from the folder name (that's a // direct lookup and is as reliable as embedded metadata tags). const isLowConfidence = (book.metadataSource === 'file_name' || book.metadataSource === 'folder_name') && !book.extractedAsin; return (
{/* Cover Art */}
{hasMatch && book.match!.coverArtUrl ? ( /* eslint-disable-next-line @next/next/no-img-element */ {book.match!.title} { (e.target as HTMLImageElement).src = '/placeholder_cover.svg'; }} /> ) : (
)}
{/* Book Info */}
{hasMatch ? ( <>

{book.match!.title}

{isLowConfidence && ( Low Confidence )}

{book.match!.author} {book.match!.narrator && ( {' '}· {book.match!.narrator} )}

) : ( <>

{book.folderName}

No Match

Could not find this title on Audible

)}

{book.relativePath}

{/* Badges */}
{/* Audio file count */} {book.audioFileCount} {/* Status badges */} {book.inLibrary && ( In Library )} {book.hasActiveRequest && !book.inLibrary && ( Requested )}
{/* Skip Toggle */}
); } export function MatchReviewStep({ books, onToggleSkip, onStartImport, isImporting, importResults, onClose, onBack, }: MatchReviewStepProps) { const toImport = books.filter((b) => !b.skipped && b.match !== null); const skippedCount = books.filter((b) => b.skipped).length; const inLibraryCount = books.filter((b) => b.inLibrary).length; const noMatchCount = books.filter((b) => b.match === null).length; const matchedCount = books.filter((b) => b.match !== null).length; // Import completed state if (importResults) { const succeeded = importResults.summary?.succeeded || 0; const failed = importResults.summary?.failed || 0; return (
{importResults.success !== false ? ( <>

Import Started

{succeeded} audiobook{succeeded !== 1 ? 's' : ''} queued for import.

{failed > 0 && (

{failed} book{failed !== 1 ? 's' : ''} could not be queued.

)}

Files will be organized, tagged, and imported into your library. Check the admin dashboard for progress.

) : ( <>

Import Failed

{importResults.error || 'An unexpected error occurred'}

)}
); } // Empty state (no audiobooks found) if (books.length === 0) { return (

No Audiobooks Found

The selected folder does not contain any folders with audio files. Try selecting a different folder.

); } return (
{/* Summary header */}
{books.length} discovered · {matchedCount} matched {noMatchCount > 0 && ( <> · {noMatchCount} unmatched )} {inLibraryCount > 0 && ( <> · {inLibraryCount} in library )}
{/* Scrollable book list */}
{books.map((book) => ( onToggleSkip(book.index)} /> ))}
{/* Import footer */}
{toImport.length} {' '} book{toImport.length !== 1 ? 's' : ''} to import {skippedCount > 0 && ( {' '}({skippedCount} skipped) )}
); }