deckhearth/components/scanner/ReviewCardItem.js

247 lines
10 KiB
JavaScript
Raw Permalink Normal View History

/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
import { formatProcessedDestination } from '../../lib/collection-vocabulary.js';
const CONDITION_OPTIONS = ['NM', 'LP', 'MP', 'HP', 'DMG'];
export default function ReviewCardItem({
card,
collections,
decks,
onIncrement,
onDecrement,
onUpdateMetadata,
onRemove,
onUndo,
isAdding = false,
sessionDestination,
}) {
const destinationLabel = card.processedAction
? formatProcessedDestination(card.processedAction)
: sessionDestination?.label ?? 'My Collection';
return (
<div
className={`glass-panel rounded-xl p-3 mb-3 transition-opacity duration-200 ${card.processed ? 'opacity-60' : ''}`}
style={{
borderLeft: !card.processed ? '3px solid var(--accent-ember)' : '3px solid transparent',
}}
>
<div className="flex gap-3">
{/* Thumbnail */}
<div className="flex-shrink-0 w-10 h-14 rounded-lg overflow-hidden">
{card.image_url ? (
<img
src={card.image_url}
alt={card.name}
className="w-full h-full object-cover"
loading="lazy"
/>
) : (
<div
className="w-full h-full flex items-center justify-center rounded-lg text-xs"
style={{ backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-secondary)' }}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
)}
</div>
{/* Card info + controls */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2">
{card.processed ? (
<svg className="w-4 h-4 flex-shrink-0" style={{ color: 'var(--accent-gold)' }} fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
) : (
<span
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: 'var(--accent-flame)' }}
aria-hidden="true"
/>
)}
<h3
className="font-semibold text-sm truncate"
style={{ color: 'var(--text-primary)' }}
>
{card.name}
</h3>
</div>
<p className="text-xs mt-0.5 truncate" style={{ color: 'var(--text-secondary)' }}>
{[card.set, card.cardNumber].filter(Boolean).join(' · ')}
</p>
</div>
{/* Remove button (unprocessed only) */}
{!card.processed && (
<button
type="button"
onClick={onRemove}
className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg transition-colors duration-150 hover:opacity-80"
style={{ color: 'var(--text-secondary)' }}
aria-label={`Remove ${card.name} from scan queue`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
</div>
{card.processed ? (
<div className="flex items-center gap-2 mt-2">
<span className="text-xs" style={{ color: 'var(--text-secondary)' }}>
Added to {destinationLabel}
</span>
{onUndo && (
<button
type="button"
onClick={onUndo}
className="text-xs font-medium underline transition-opacity duration-150 hover:opacity-70"
style={{ color: 'var(--accent-ember)', minHeight: '44px', display: 'inline-flex', alignItems: 'center' }}
aria-label={`Undo adding ${card.name}`}
>
Undo
</button>
)}
</div>
) : (
<>
{/* Metadata summary line */}
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
{card.condition || 'NM'} · {card.isFoil ? 'Foil' : 'Not Foil'} · Qty: {card.quantity || 1}
</p>
{/* Inline edit controls */}
<div className="flex flex-col sm:flex-row gap-2 mt-2">
{/* Condition */}
<label className="inline-flex items-center gap-1.5 text-xs" style={{ color: 'var(--text-secondary)' }}>
<span className="sr-only">Condition for {card.name}</span>
<select
value={card.condition || 'NM'}
onChange={(e) => onUpdateMetadata({ condition: e.target.value })}
className="px-2 py-1.5 rounded-lg text-xs"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
minHeight: '44px',
}}
aria-label={`Condition for ${card.name}`}
>
{CONDITION_OPTIONS.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</label>
{/* Foil toggle */}
<label
className="inline-flex items-center gap-1.5 text-xs cursor-pointer"
style={{ color: 'var(--text-secondary)', minHeight: '44px' }}
>
<input
type="checkbox"
checked={Boolean(card.isFoil)}
onChange={(e) => onUpdateMetadata({ isFoil: e.target.checked })}
className="w-4 h-4 rounded"
style={{ accentColor: 'var(--accent-ember)' }}
/>
<span>Foil</span>
</label>
{/* Quantity ± */}
<div className="inline-flex items-center gap-1">
<button
type="button"
onClick={onDecrement}
disabled={isAdding || (card.quantity || 1) <= 1}
className="w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold transition-opacity duration-150 hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed"
style={{
backgroundColor: 'var(--bg-secondary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
minWidth: '44px',
minHeight: '44px',
}}
aria-label={`Decrease quantity of ${card.name}`}
>
</button>
<span
className="min-w-[1.5rem] text-center text-sm font-medium"
style={{ color: 'var(--text-primary)' }}
aria-live="polite"
aria-label={`Quantity: ${card.quantity || 1}`}
>
{card.quantity || 1}
</span>
<button
type="button"
onClick={onIncrement}
disabled={isAdding}
className="w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold transition-opacity duration-150 hover:opacity-80 disabled:opacity-30 disabled:cursor-not-allowed"
style={{
backgroundColor: 'var(--accent-ember)',
color: '#ffffff',
minWidth: '44px',
minHeight: '44px',
}}
aria-label={`Increase quantity of ${card.name}`}
>
+
</button>
</div>
</div>
{/* Destination override */}
{(collections.length > 0 || decks.length > 0) && (
<div className="mt-2">
<select
value={card.destinationOverride || ''}
onChange={(e) => {
const val = e.target.value;
if (!val) {
onUpdateMetadata({ destinationOverride: null });
} else {
onUpdateMetadata({ destinationOverride: val });
}
}}
className="w-full px-2 py-1.5 rounded-lg text-xs"
style={{
backgroundColor: 'var(--bg-secondary)',
border: '1px solid var(--border)',
color: 'var(--text-secondary)',
minHeight: '44px',
}}
aria-label={`Change destination for ${card.name}`}
>
<option value="">
{sessionDestination ? `Default (${sessionDestination.label})` : 'Change destination'}
</option>
{collections.map((c) => (
<option key={`col-${c.id}`} value={`collection:${c.id}`}>
{c.name}
</option>
))}
{decks.map((d) => (
<option key={`deck-${d.id}`} value={`deck:${d.id}`}>
{d.name}
</option>
))}
</select>
</div>
)}
</>
)}
</div>
</div>
</div>
);
}