deckhearth/components/ScannerDestinationPicker.js
Randall Stillwell 0b670c1a11 Fix remaining list/collection copy gaps from review.
Sweep community, settings, share modal, scanner create-list modal,
and invite flows for vocabulary consistency before merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 09:51:57 -05:00

216 lines
7.4 KiB
JavaScript

import { VOCAB } from '../lib/collection-vocabulary.js';
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') {
onDestinationChange({ type: 'owned', id: null, label: VOCAB.MY_COLLECTION });
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>
{[
{ type: 'owned', label: `💎 ${VOCAB.MY_COLLECTION}` },
{ type: 'collection', label: `📚 ${VOCAB.LIST}` },
{ 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">
<span style={{ color: 'var(--text-secondary)' }}>{VOCAB.LIST}</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)',
}}
>
{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)' }}>
No lists match this game filter.
</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>
);
}