2026-05-28 15:55:01 -04:00
|
|
|
import { VOCAB } from '../lib/collection-vocabulary.js';
|
|
|
|
|
|
2026-05-27 14:50:54 -04:00
|
|
|
const GAME_OPTIONS = [
|
|
|
|
|
{ value: 'All', label: 'All games' },
|
|
|
|
|
{ value: 'MTG', label: 'Magic' },
|
|
|
|
|
{ value: 'Pokemon', label: 'Pokémon' },
|
|
|
|
|
{ value: 'Lorcana', label: 'Lorcana' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function matchesGameFilter(itemGame, filter) {
|
|
|
|
|
if (filter === 'All') return true;
|
|
|
|
|
const normalized = (itemGame || '').toLowerCase();
|
|
|
|
|
if (filter === 'MTG') return normalized === 'mtg' || normalized.includes('magic');
|
|
|
|
|
if (filter === 'Pokemon') return normalized.includes('pokemon') || normalized.includes('pokémon');
|
|
|
|
|
if (filter === 'Lorcana') return normalized.includes('lorcana');
|
|
|
|
|
return itemGame === filter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ScannerDestinationPicker({
|
|
|
|
|
gameFilter,
|
|
|
|
|
onGameFilterChange,
|
|
|
|
|
destination,
|
|
|
|
|
onDestinationChange,
|
|
|
|
|
collections = [],
|
|
|
|
|
decks = [],
|
|
|
|
|
disabled = false,
|
|
|
|
|
}) {
|
|
|
|
|
const filteredCollections = collections.filter(
|
|
|
|
|
(collection) => matchesGameFilter(collection.tcg, gameFilter) || collection.tcg === 'All'
|
|
|
|
|
);
|
|
|
|
|
const filteredDecks = decks.filter((deck) => matchesGameFilter(deck.game, gameFilter));
|
|
|
|
|
|
|
|
|
|
const handleTypeChange = (type) => {
|
|
|
|
|
if (type === 'owned') {
|
2026-05-28 15:55:01 -04:00
|
|
|
onDestinationChange({ type: 'owned', id: null, label: VOCAB.MY_COLLECTION });
|
2026-05-27 14:50:54 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (type === 'collection' && filteredCollections.length > 0) {
|
|
|
|
|
const first = filteredCollections[0];
|
|
|
|
|
onDestinationChange({
|
|
|
|
|
type: 'collection',
|
|
|
|
|
id: first.id,
|
|
|
|
|
label: first.name,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (type === 'deck' && filteredDecks.length > 0) {
|
|
|
|
|
const first = filteredDecks[0];
|
|
|
|
|
onDestinationChange({
|
|
|
|
|
type: 'deck',
|
|
|
|
|
id: first.id,
|
|
|
|
|
label: first.name,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTargetChange = (event) => {
|
|
|
|
|
const value = event.target.value;
|
|
|
|
|
if (!value || !destination) return;
|
|
|
|
|
|
|
|
|
|
if (destination.type === 'collection') {
|
|
|
|
|
const collection = filteredCollections.find((item) => String(item.id) === value);
|
|
|
|
|
if (collection) {
|
|
|
|
|
onDestinationChange({
|
|
|
|
|
type: 'collection',
|
|
|
|
|
id: collection.id,
|
|
|
|
|
label: collection.name,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (destination.type === 'deck') {
|
|
|
|
|
const deck = filteredDecks.find((item) => String(item.id) === value);
|
|
|
|
|
if (deck) {
|
|
|
|
|
onDestinationChange({
|
|
|
|
|
type: 'deck',
|
|
|
|
|
id: deck.id,
|
|
|
|
|
label: deck.name,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
className="mx-6 mb-4 rounded-xl border p-4"
|
|
|
|
|
style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}
|
|
|
|
|
aria-label="Scan destination"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-sm font-semibold uppercase tracking-wide mb-1" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
Scan destination
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
{destination
|
|
|
|
|
? `Every scan goes to: ${destination.label}`
|
|
|
|
|
: 'Choose where scanned cards should land'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-3 sm:items-end">
|
|
|
|
|
<label className="flex flex-col gap-1 text-sm">
|
|
|
|
|
<span style={{ color: 'var(--text-secondary)' }}>Game filter</span>
|
|
|
|
|
<select
|
|
|
|
|
value={gameFilter}
|
|
|
|
|
onChange={(e) => onGameFilterChange(e.target.value)}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className="px-3 py-2 rounded-lg border min-w-[10rem]"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{GAME_OPTIONS.map((option) => (
|
|
|
|
|
<option key={option.value} value={option.value}>
|
|
|
|
|
{option.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<fieldset className="flex flex-wrap gap-2" disabled={disabled}>
|
|
|
|
|
<legend className="sr-only">Destination type</legend>
|
|
|
|
|
{[
|
2026-05-28 15:55:01 -04:00
|
|
|
{ type: 'owned', label: `💎 ${VOCAB.MY_COLLECTION}` },
|
|
|
|
|
{ type: 'collection', label: `📚 ${VOCAB.LIST}` },
|
2026-05-27 14:50:54 -04:00
|
|
|
{ type: 'deck', label: '🃏 Deck' },
|
|
|
|
|
].map((option) => {
|
|
|
|
|
const isActive = destination?.type === option.type;
|
|
|
|
|
const isDisabled =
|
|
|
|
|
(option.type === 'collection' && filteredCollections.length === 0) ||
|
|
|
|
|
(option.type === 'deck' && filteredDecks.length === 0);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={option.type}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => handleTypeChange(option.type)}
|
|
|
|
|
disabled={isDisabled}
|
|
|
|
|
aria-pressed={isActive}
|
|
|
|
|
className="px-3 py-2 rounded-lg text-sm font-medium border disabled:opacity-40"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: isActive ? 'var(--accent-ember)' : 'var(--bg-tertiary)',
|
|
|
|
|
borderColor: isActive ? 'var(--accent-ember)' : 'var(--border)',
|
|
|
|
|
color: isActive ? 'white' : 'var(--text-primary)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{option.label}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</fieldset>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{destination?.type === 'collection' && filteredCollections.length > 0 && (
|
|
|
|
|
<label className="flex flex-col gap-1 mt-4 text-sm">
|
2026-05-28 15:55:01 -04:00
|
|
|
<span style={{ color: 'var(--text-secondary)' }}>{VOCAB.LIST}</span>
|
2026-05-27 14:50:54 -04:00
|
|
|
<select
|
|
|
|
|
value={String(destination.id)}
|
|
|
|
|
onChange={handleTargetChange}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className="px-3 py-2 rounded-lg border"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{filteredCollections.map((collection) => (
|
|
|
|
|
<option key={collection.id} value={collection.id}>
|
|
|
|
|
{collection.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{destination?.type === 'deck' && filteredDecks.length > 0 && (
|
|
|
|
|
<label className="flex flex-col gap-1 mt-4 text-sm">
|
|
|
|
|
<span style={{ color: 'var(--text-secondary)' }}>Deck</span>
|
|
|
|
|
<select
|
|
|
|
|
value={String(destination.id)}
|
|
|
|
|
onChange={handleTargetChange}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className="px-3 py-2 rounded-lg border"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{filteredDecks.map((deck) => (
|
|
|
|
|
<option key={deck.id} value={deck.id}>
|
|
|
|
|
{deck.name} ({deck.game})
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{destination?.type === 'collection' && filteredCollections.length === 0 && (
|
|
|
|
|
<p className="mt-3 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
2026-05-29 10:51:57 -04:00
|
|
|
No lists match this game filter.
|
2026-05-27 14:50:54 -04:00
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{destination?.type === 'deck' && filteredDecks.length === 0 && (
|
|
|
|
|
<p className="mt-3 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
No decks match this game filter.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|