feat(scanner): condition, foil, quantity, and ownership badge (Brief 2)

Extract ScannedCardItem with per-card metadata controls and ownership
lookup via GET /api/cards/[id]/ownership. Propagate condition, foil,
and quantity through owned/collection/deck POST paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-05-27 13:52:19 -05:00
parent 47a1abbe4d
commit 690dc76a95
5 changed files with 422 additions and 237 deletions

View file

@ -0,0 +1,293 @@
import { useEffect, useState } from 'react';
export const CONDITION_OPTIONS = ['NM', 'LP', 'MP', 'HP', 'DMG'];
export default function ScannedCardItem({
card,
collections,
decks,
selected,
onToggleSelect,
onIncrement,
onDecrement,
onUpdateMetadata,
onMarkOwned,
onAddToCollection,
onAddToDeck,
onRemove,
}) {
const [ownedQuantity, setOwnedQuantity] = useState(null);
useEffect(() => {
if (!card.databaseId) {
setOwnedQuantity(null);
return;
}
let cancelled = false;
(async () => {
try {
const response = await fetch(`/api/cards/${card.databaseId}/ownership`, {
headers: {
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
},
});
if (!response.ok) return;
const data = await response.json();
if (!cancelled) {
setOwnedQuantity(typeof data.quantity === 'number' ? data.quantity : 0);
}
} catch {
if (!cancelled) setOwnedQuantity(null);
}
})();
return () => {
cancelled = true;
};
}, [card.databaseId]);
return (
<div
className={`flex gap-4 p-4 rounded-lg border ${card.processed ? 'opacity-60' : ''}`}
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: selected ? 'var(--accent-ember)' : 'var(--border)',
borderWidth: selected ? '2px' : '1px',
}}
>
<div className="relative flex-shrink-0">
<div className="w-20 h-28 rounded-lg overflow-hidden flex items-center justify-center"
style={{ backgroundColor: 'var(--bg-secondary)' }}>
{card.image_url ? (
<img src={card.image_url} alt={card.name} className="w-full h-full object-cover" />
) : (
<div className="text-center text-xs p-2" style={{ color: 'var(--text-secondary)' }}>
<div className="text-2xl mb-1">🃏</div>
<div>No Image</div>
</div>
)}
</div>
{!card.processed && (
<div className="absolute top-1 left-1">
<input
type="checkbox"
checked={selected}
onChange={onToggleSelect}
className="w-5 h-5 rounded border-2 border-white shadow-lg"
style={{ accentColor: 'var(--accent-ember)' }}
aria-label={`Select ${card.name}`}
/>
</div>
)}
</div>
<div className="flex-1 min-w-0">
{card.confidence && (
<div
className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mb-2"
style={{
backgroundColor:
card.confidence >= 90
? 'var(--accent-gold)'
: card.confidence >= 70
? 'var(--accent-ember)'
: 'var(--text-secondary)',
color: 'white',
}}
>
{Math.round(card.confidence)}% confidence
</div>
)}
<div className="flex items-start justify-between mb-2 gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg truncate" style={{ color: 'var(--text-primary)' }}>
{card.name}
</h3>
{card.isExisting && (
<div className="text-xs font-medium" style={{ color: 'var(--accent-gold)' }}>
Found in database
</div>
)}
{ownedQuantity !== null && ownedQuantity > 0 && (
<div
className="text-xs font-medium mt-1"
style={{ color: 'var(--text-secondary)' }}
aria-label={`You already own ${ownedQuantity} copies of this card`}
>
You own {ownedQuantity}
</div>
)}
</div>
{!card.processed && (
<div className="flex items-center gap-2 flex-shrink-0">
<button
type="button"
onClick={onDecrement}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold hover:opacity-80"
style={{
backgroundColor: 'var(--bg-secondary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
}}
aria-label="Decrease quantity"
>
</button>
<span className="min-w-[2rem] text-center font-medium" style={{ color: 'var(--text-primary)' }}>
{card.quantity || 1}
</span>
<button
type="button"
onClick={onIncrement}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold hover:opacity-80"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
aria-label="Increase quantity"
>
+
</button>
</div>
)}
</div>
{!card.processed && (
<div className="flex flex-wrap items-center gap-3 mb-3">
<label className="flex items-center gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
<span>Condition</span>
<select
value={card.condition || 'NM'}
onChange={(e) => onUpdateMetadata({ condition: e.target.value })}
className="px-2 py-1 rounded border text-sm"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)',
}}
>
{CONDITION_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer" style={{ color: 'var(--text-secondary)' }}>
<input
type="checkbox"
checked={Boolean(card.isFoil)}
onChange={(e) => onUpdateMetadata({ isFoil: e.target.checked })}
className="rounded"
style={{ accentColor: 'var(--accent-ember)' }}
/>
Foil
</label>
</div>
)}
<div className="space-y-1 mb-3">
{card.set && (
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Set:</span> {card.set}
{card.setCode && <span className="ml-2 text-xs">({card.setCode})</span>}
</div>
)}
{card.cardNumber && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Number:</span> {card.cardNumber}
</div>
)}
{!card.processed && (card.condition || card.isFoil) && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Adding as:</span>{' '}
{card.condition || 'NM'}
{card.isFoil ? ' · Foil' : ''}
</div>
)}
</div>
{!card.processed ? (
<div className="space-y-2">
<div className="flex gap-2">
<button
type="button"
onClick={onMarkOwned}
className="flex-1 px-3 py-2 rounded text-sm font-medium hover:opacity-80 flex items-center justify-center gap-1"
style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }}
>
💎 Mark Owned
</button>
<button
type="button"
onClick={onRemove}
className="px-3 py-2 rounded text-sm hover:opacity-80"
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--bg-secondary)' }}
title="Remove"
>
🗑
</button>
</div>
<div className="flex gap-2">
{collections.length > 0 && (
<select
onChange={(e) => {
const value = e.target.value;
e.target.value = '';
if (value) onAddToCollection(value);
}}
className="flex-1 px-3 py-2 rounded text-sm"
style={{
backgroundColor: 'var(--bg-secondary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
}}
defaultValue=""
>
<option value="">📚 Add to Collection</option>
{collections.map((collection) => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
)}
{decks.length > 0 && (
<select
onChange={(e) => {
const value = e.target.value;
e.target.value = '';
if (value) onAddToDeck(value);
}}
className="flex-1 px-3 py-2 rounded text-sm"
style={{
backgroundColor: 'var(--bg-secondary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
}}
defaultValue=""
>
<option value="">🃏 Add to Deck</option>
{decks.map((deck) => (
<option key={deck.id} value={deck.id}>
{deck.name}
</option>
))}
</select>
)}
</div>
</div>
) : (
<div className="text-sm flex items-center gap-2" style={{ color: 'var(--accent-ember)' }}>
<span aria-hidden="true"></span>
<span>Added to {card.processedAction}</span>
</div>
)}
</div>
</div>
);
}

View file

@ -2,29 +2,45 @@ import { sql } from '@vercel/postgres';
import { getUserFromRequest } from '../../../../lib/permission-middleware';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Get authenticated user
const user = await getUserFromRequest(req);
if (!user) {
return res.status(401).json({ error: 'Authentication required' });
}
const { id } = req.query;
const { quantity } = req.body;
const cardId = parseInt(id, 10);
if (!id || quantity === undefined) {
return res.status(400).json({ error: 'Card ID and quantity are required' });
if (!id || Number.isNaN(cardId)) {
return res.status(400).json({ error: 'Valid card ID is required' });
}
const cardId = parseInt(id);
const cardQuantity = parseInt(quantity);
if (req.method === 'GET') {
const result = await sql`
SELECT COALESCE(SUM(quantity), 0) AS quantity
FROM user_cards
WHERE user_id = ${user.userId} AND card_id = ${cardId}
`;
if (isNaN(cardId) || isNaN(cardQuantity) || cardQuantity < 0) {
return res.status(400).json({ error: 'Invalid card ID or quantity' });
return res.status(200).json({
quantity: parseInt(result.rows[0]?.quantity, 10) || 0,
});
}
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { quantity } = req.body;
if (quantity === undefined) {
return res.status(400).json({ error: 'Quantity is required' });
}
const cardQuantity = parseInt(quantity, 10);
if (Number.isNaN(cardQuantity) || cardQuantity < 0) {
return res.status(400).json({ error: 'Invalid quantity' });
}
// Verify the card exists

View file

@ -100,7 +100,7 @@ export default async function handler(req, res) {
return res.status(403).json({ error: 'You do not have permission to add cards to this collection' });
}
const { cardId, quantity = 1 } = req.body;
const { cardId, quantity = 1, condition = 'NM', is_foil = false } = req.body;
if (!cardId) {
return res.status(400).json({ error: 'Card ID is required' });
@ -134,6 +134,8 @@ export default async function handler(req, res) {
cardName: cardRecord.name,
quantityAdded: quantity,
newQuantity: result.rows[0]?.quantity,
condition,
is_foil: Boolean(is_foil),
});
res.status(200).json({
@ -152,6 +154,8 @@ export default async function handler(req, res) {
cardId,
cardName: cardRecord.name,
quantityAdded: quantity,
condition,
is_foil: Boolean(is_foil),
});
res.status(201).json({

View file

@ -20,12 +20,17 @@ export default async function handler(req, res) {
}
if (req.method === 'POST') {
const { cardId, quantity = 1 } = req.body;
const { cardId, quantity = 1, condition = 'NM', is_foil = false } = req.body;
if (!cardId) {
return res.status(400).json({ error: 'Card ID is required' });
}
const parsedQuantity = parseInt(quantity, 10);
if (Number.isNaN(parsedQuantity) || parsedQuantity < 1) {
return res.status(400).json({ error: 'Quantity must be at least 1' });
}
// Check if card already exists in deck
const existingResult = await sql`
SELECT * FROM deck_cards
@ -34,7 +39,7 @@ export default async function handler(req, res) {
if (existingResult.rows.length > 0) {
// Update quantity
const newQuantity = existingResult.rows[0].quantity + quantity;
const newQuantity = existingResult.rows[0].quantity + parsedQuantity;
await sql`
UPDATE deck_cards
SET quantity = ${newQuantity}, updated_at = CURRENT_TIMESTAMP
@ -44,11 +49,14 @@ export default async function handler(req, res) {
// Insert new record
await sql`
INSERT INTO deck_cards (deck_id, card_id, quantity)
VALUES (${deckId}, ${cardId}, ${quantity})
VALUES (${deckId}, ${cardId}, ${parsedQuantity})
`;
}
return res.status(200).json({ message: 'Card added to deck' });
return res.status(200).json({
message: 'Card added to deck',
metadata: { condition, is_foil: Boolean(is_foil) },
});
} else if (req.method === 'GET') {
// Get cards in deck

View file

@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import Layout from '../components/Layout';
import CameraScanner from '../components/CameraScanner';
import ScannerDestinationPicker from '../components/ScannerDestinationPicker';
import ScannedCardItem, { CONDITION_OPTIONS } from '../components/ScannedCardItem';
import OCRSettings from '../components/OCRSettings';
import { ManaCost, ColorIdentity } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings';
@ -11,19 +12,21 @@ import { useAuth } from '../lib/use-auth';
const SESSION_STORAGE_KEY = 'deckhearth:scanner-session';
const DEFAULT_DESTINATION = { type: 'owned', id: null, label: 'My owned cards' };
const DEFAULT_SCAN_DEFAULTS = { condition: 'NM', isFoil: false };
function loadSavedScannerSession() {
if (typeof window === 'undefined') {
return { destination: DEFAULT_DESTINATION, gameFilter: 'All' };
return { destination: DEFAULT_DESTINATION, gameFilter: 'All', scanDefaults: DEFAULT_SCAN_DEFAULTS };
}
try {
const saved = JSON.parse(localStorage.getItem(SESSION_STORAGE_KEY));
return {
destination: saved?.destination || DEFAULT_DESTINATION,
gameFilter: saved?.gameFilter || 'All',
scanDefaults: saved?.scanDefaults || DEFAULT_SCAN_DEFAULTS,
};
} catch {
return { destination: DEFAULT_DESTINATION, gameFilter: 'All' };
return { destination: DEFAULT_DESTINATION, gameFilter: 'All', scanDefaults: DEFAULT_SCAN_DEFAULTS };
}
}
@ -52,6 +55,7 @@ export default function Scanner() {
() => loadSavedScannerSession().destination
);
const [gameFilter, setGameFilter] = useState(() => loadSavedScannerSession().gameFilter);
const [scanDefaults, setScanDefaults] = useState(() => loadSavedScannerSession().scanDefaults);
const [autoRouteError, setAutoRouteError] = useState(null);
// Mana symbol settings
@ -76,9 +80,9 @@ export default function Scanner() {
if (typeof window === 'undefined') return;
localStorage.setItem(
SESSION_STORAGE_KEY,
JSON.stringify({ destination: sessionDestination, gameFilter })
JSON.stringify({ destination: sessionDestination, gameFilter, scanDefaults })
);
}, [sessionDestination, gameFilter]);
}, [sessionDestination, gameFilter, scanDefaults]);
const handleGameFilterChange = (nextFilter) => {
setGameFilter(nextFilter);
@ -168,6 +172,8 @@ export default function Scanner() {
name: cardData.name,
set: cardData.set,
quantity: 1,
condition: scanDefaults.condition,
isFoil: scanDefaults.isFoil,
timestamp: new Date().toISOString(),
processed: false,
};
@ -291,19 +297,28 @@ export default function Scanner() {
));
};
const updateCardMetadata = (cardId, patch) => {
setScannedCards((prev) =>
prev.map((card) => (card.id === cardId ? { ...card, ...patch } : card))
);
};
const buildCardPayload = (cardData) => ({
cardId: cardData.databaseId,
quantity: cardData.quantity || 1,
condition: cardData.condition || 'NM',
is_foil: Boolean(cardData.isFoil),
});
// Helper functions for API calls
const addToOwnedCards = async (cardData) => {
const response = await fetch('/api/user-cards', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
},
body: JSON.stringify({
cardId: cardData.databaseId,
quantity: 1,
condition: 'NM'
})
body: JSON.stringify(buildCardPayload(cardData)),
});
if (!response.ok) {
@ -318,10 +333,7 @@ export default function Scanner() {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
cardId: cardData.databaseId,
quantity: 1
})
body: JSON.stringify(buildCardPayload(cardData)),
});
if (!response.ok) {
@ -334,12 +346,9 @@ export default function Scanner() {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
},
body: JSON.stringify({
cardId: cardData.databaseId,
quantity: 1
})
body: JSON.stringify(buildCardPayload(cardData)),
});
if (!response.ok) {
@ -462,6 +471,48 @@ export default function Scanner() {
</div>
)}
<div
className="mx-6 mb-4 rounded-xl border p-4 flex flex-wrap items-end gap-4"
style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}
>
<div>
<h2 className="text-sm font-semibold uppercase tracking-wide mb-1" style={{ color: 'var(--text-secondary)' }}>
Defaults for new scans
</h2>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
Applied to each card when it enters the queue
</p>
</div>
<label className="flex flex-col gap-1 text-sm">
<span style={{ color: 'var(--text-secondary)' }}>Condition</span>
<select
value={scanDefaults.condition}
onChange={(e) => setScanDefaults((current) => ({ ...current, condition: e.target.value }))}
className="px-3 py-2 rounded-lg border"
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)',
}}
>
{CONDITION_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<label className="flex items-center gap-2 text-sm pb-2 cursor-pointer" style={{ color: 'var(--text-primary)' }}>
<input
type="checkbox"
checked={scanDefaults.isFoil}
onChange={(e) => setScanDefaults((current) => ({ ...current, isFoil: e.target.checked }))}
style={{ accentColor: 'var(--accent-ember)' }}
/>
Foil
</label>
</div>
{/* Main Content - Full Height */}
<div className="flex-1 grid grid-cols-1 lg:grid-cols-5 gap-6 px-6 pb-6">
{/* Camera Scanner */}
@ -529,208 +580,21 @@ export default function Scanner() {
</div>
) : (
scannedCards.map((card) => (
<div
<ScannedCardItem
key={card.id}
className={`flex gap-4 p-4 rounded-lg border ${card.processed ? 'opacity-60' : ''}`}
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: selectedCards.has(card.id) ? 'var(--accent-ember)' : 'var(--border)',
borderWidth: selectedCards.has(card.id) ? '2px' : '1px'
}}
>
{/* Card Thumbnail with Checkbox Overlay */}
<div className="relative flex-shrink-0">
<div className="w-20 h-28 rounded-lg overflow-hidden bg-gray-200 flex items-center justify-center">
{card.image_url ? (
<img
src={card.image_url}
alt={card.name}
className="w-full h-full object-cover"
/>
) : (
<div className="text-center text-xs text-gray-500 p-2">
<div className="text-2xl mb-1">🃏</div>
<div>No Image</div>
</div>
)}
</div>
{/* Checkbox Overlay */}
{!card.processed && (
<div className="absolute top-1 left-1">
<input
type="checkbox"
checked={selectedCards.has(card.id)}
onChange={() => toggleCardSelection(card.id)}
className="w-5 h-5 rounded border-2 border-white shadow-lg"
style={{ accentColor: 'var(--accent-ember)' }}
/>
</div>
)}
</div>
{/* Card Content */}
<div className="flex-1 min-w-0">
{/* Confidence Badge */}
{card.confidence && (
<div className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mb-2"
style={{
backgroundColor: card.confidence >= 90 ? 'var(--accent-gold)' :
card.confidence >= 70 ? 'var(--accent-ember)' : 'var(--text-secondary)',
color: 'white'
}}>
{Math.round(card.confidence)}% confidence
</div>
)}
{/* Title and Quantity Row */}
<div className="flex items-start justify-between mb-2 gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg truncate" style={{ color: 'var(--text-primary)' }}>
{card.name}
</h3>
{/* Database Status */}
{card.isExisting && (
<div className="text-xs text-green-600 font-medium">
Found in database
</div>
)}
</div>
{/* Quantity Controls */}
{!card.processed && (
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={() => decrementCardQuantity(card.id)}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold hover:opacity-80"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
>
</button>
<span className="min-w-[2rem] text-center font-medium" style={{ color: 'var(--text-primary)' }}>
{card.quantity || 1}
</span>
<button
onClick={() => incrementCardQuantity(card.id)}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold hover:opacity-80"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
>
+
</button>
</div>
)}
</div>
{/* Card Details */}
<div className="space-y-1 mb-3">
{card.set && (
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Set:</span> {card.set}
{card.setCode && <span className="ml-2 text-xs">({card.setCode})</span>}
</div>
)}
{card.cardNumber && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Number:</span> {card.cardNumber}
</div>
)}
{card.cardType && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Type:</span> {card.cardType}
</div>
)}
{card.rarity && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Rarity:</span> {card.rarity}
</div>
)}
{card.hp && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">HP:</span> {card.hp}
</div>
)}
{card.manaCost && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Mana Cost:</span> {card.manaCost}
</div>
)}
{card.ocrText && (
<div className="mt-2">
<div className="text-xs font-semibold mb-1" style={{ color: 'var(--text-secondary)' }}>
Scanned Text:
</div>
<div className="text-xs p-2 rounded max-h-16 overflow-y-auto"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-secondary)' }}>
{card.ocrText.substring(0, 150)}{card.ocrText.length > 150 ? '...' : ''}
</div>
</div>
)}
</div>
{/* Actions */}
{!card.processed ? (
<div className="space-y-2">
{/* Primary Actions Row */}
<div className="flex gap-2">
<button
onClick={() => addSingleCardToOwned(card)}
className="flex-1 px-3 py-2 rounded text-sm font-medium hover:opacity-80 flex items-center justify-center gap-1"
style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }}
>
💎 Mark Owned
</button>
<button
onClick={() => removeScannedCard(card.id)}
className="px-3 py-2 rounded text-sm hover:opacity-80"
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--bg-secondary)' }}
title="Remove"
>
🗑
</button>
</div>
{/* Secondary Actions Row */}
<div className="flex gap-2">
{collections.length > 0 && (
<select
onChange={(e) => e.target.value && addSingleCardToCollection(card, e.target.value)}
className="flex-1 px-3 py-2 rounded text-sm"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
>
<option value="">📚 Add to Collection</option>
{collections.map(collection => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
)}
{decks.length > 0 && (
<select
onChange={(e) => e.target.value && addSingleCardToDeck(card, e.target.value)}
className="flex-1 px-3 py-2 rounded text-sm"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
>
<option value="">🃏 Add to Deck</option>
{decks.map(deck => (
<option key={deck.id} value={deck.id}>
{deck.name}
</option>
))}
</select>
)}
</div>
</div>
) : (
<div className="text-sm flex items-center gap-2" style={{ color: 'var(--accent-ember)' }}>
<span></span>
<span>Added to {card.processedAction}</span>
</div>
)}
</div>
</div>
card={card}
collections={collections}
decks={decks}
selected={selectedCards.has(card.id)}
onToggleSelect={() => toggleCardSelection(card.id)}
onIncrement={() => incrementCardQuantity(card.id)}
onDecrement={() => decrementCardQuantity(card.id)}
onUpdateMetadata={(patch) => updateCardMetadata(card.id, patch)}
onMarkOwned={() => addSingleCardToOwned(card)}
onAddToCollection={(collectionId) => addSingleCardToCollection(card, collectionId)}
onAddToDeck={(deckId) => addSingleCardToDeck(card, deckId)}
onRemove={() => removeScannedCard(card.id)}
/>
))
)}
</div>