deckhearth/components/DeckBuilderCardBrowser.js
Randall Stillwell 9f620b119f refactor(deck-builder): extract deck list and card browser (Brief 2)
Split the main deck panel list and right-hand card browser sidebar into
DeckBuilderDeckList and DeckBuilderCardBrowser; page is ~408 lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:26:23 -05:00

761 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
import { ManaCost, ColorFilterSymbol } from './ManaSymbols';
import ManaSymbolSettings from './ManaSymbolSettings';
export default function DeckBuilderCardBrowser({
selectedCard,
onSelectCard,
onClearSelectedCard,
viewMode,
onViewModeChange,
showSettings,
onToggleSettings,
onCollapseSidebar,
searchQuery,
onSearchQueryChange,
showFilters,
onToggleFilters,
filters,
onFiltersChange,
onClearFilters,
onToggleColorFilter,
searchLoading,
searchResults,
manaSymbolSettings,
onManaSymbolSettingsChange,
onAddCardToDeck,
}) {
return (
<div className="bg-bg-tertiary rounded-lg h-full flex flex-col relative border border-border">
{!selectedCard ? (
<>
{/* Header */}
<div className="flex justify-between items-center p-4 border-b border-border bg-bg-secondary rounded-t-lg">
<h3 className="text-lg font-semibold text-text-primary">Card Browser</h3>
<div className="flex items-center space-x-2">
{/* View Mode Toggle */}
<div className="flex bg-bg-primary rounded-lg p-1">
<button
onClick={() => onViewModeChange('list')}
className={`px-2 py-1 rounded text-xs transition-colors ${
viewMode === 'list'
? 'bg-accent-ember text-white'
: 'text-text-secondary hover:text-text-primary'
}`}
title="List View"
>
</button>
<button
onClick={() => onViewModeChange('thumbnail')}
className={`px-2 py-1 rounded text-xs transition-colors ${
viewMode === 'thumbnail'
? 'bg-accent-ember text-white'
: 'text-text-secondary hover:text-text-primary'
}`}
title="Thumbnail View"
>
</button>
</div>
<button
onClick={() => onToggleSettings()}
className={`text-text-secondary hover:text-text-primary transition-colors p-1 ${
showSettings ? 'text-accent-ember' : ''
}`}
title="Settings"
>
</button>
<button
onClick={() => onCollapseSidebar()}
className="text-text-secondary hover:text-text-primary transition-colors p-1"
title="Collapse"
>
</button>
</div>
</div>
{/* Search */}
<div className="p-4 border-b border-border">
<div className="flex space-x-2">
<input
type="text"
value={searchQuery}
onChange={(e) => onSearchQueryChange(e.target.value)}
placeholder="Search for cards..."
className="flex-1 px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-primary text-text-primary text-sm"
/>
<button
onClick={() => onToggleFilters()}
className={`p-2 rounded-lg transition-colors ${
showFilters || filters.colors.length > 0 || filters.cmc || filters.rarity
? 'bg-accent-ember text-white'
: 'bg-bg-primary text-text-secondary hover:bg-bg-tertiary'
}`}
title="Filters"
>
🔍
</button>
</div>
</div>
{/* Settings Panel */}
{showSettings && (
<div className="p-4 border-b border-border bg-bg-primary">
<ManaSymbolSettings onSettingsChange={onManaSymbolSettingsChange} />
</div>
)}
{/* Quick Filters */}
{showFilters && (
<div className="p-4 border-b border-border bg-bg-primary space-y-3">
{/* Color Filters */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-text-secondary text-xs font-medium">Colors</label>
{(filters.colors.length > 0 || filters.cmc || filters.rarity) && (
<button
onClick={onClearFilters}
className="text-xs text-accent-ember hover:underline"
>
Clear All
</button>
)}
</div>
<div className="flex space-x-1">
{['W', 'U', 'B', 'R', 'G'].map(color => (
<ColorFilterSymbol
key={color}
color={color}
isActive={filters.colors.includes(color)}
onClick={onToggleColorFilter}
size="sm"
useSVG={manaSymbolSettings.useSVG}
/>
))}
</div>
</div>
{/* CMC and Rarity */}
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-text-secondary text-xs font-medium mb-1">CMC</label>
<select
value={filters.cmc}
onChange={(e) => onFiltersChange({ ...filters, cmc: e.target.value })}
className="w-full px-2 py-1 border border-border rounded text-xs focus:outline-none focus:ring-1 focus:ring-accent-ember bg-bg-secondary text-text-primary"
>
<option value="">Any</option>
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6+">6+</option>
</select>
</div>
<div>
<label className="block text-text-secondary text-xs font-medium mb-1">Rarity</label>
<select
value={filters.rarity}
onChange={(e) => onFiltersChange({ ...filters, rarity: e.target.value })}
className="w-full px-2 py-1 border border-border rounded text-xs focus:outline-none focus:ring-1 focus:ring-accent-ember bg-bg-secondary text-text-primary"
>
<option value="">Any</option>
<option value="common">Common</option>
<option value="uncommon">Uncommon</option>
<option value="rare">Rare</option>
<option value="mythic">Mythic</option>
</select>
</div>
</div>
</div>
)}
{/* Card List */}
<div className="flex-1 overflow-y-auto p-4">
{viewMode === 'list' ? (
/* List View */
<div className="space-y-1">
{searchLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accent-ember mx-auto"></div>
<p className="text-text-secondary mt-2 text-xs">Searching...</p>
</div>
) : searchResults.length === 0 ? (
<div className="text-center py-8">
<div className="text-2xl mb-2">🔍</div>
<p className="text-text-secondary text-xs">
{searchQuery ? 'No cards found' : 'No cards available'}
</p>
</div>
) : (
searchResults.map((card) => (
<div
key={card.id}
className="flex items-center p-2 bg-bg-secondary rounded hover:bg-bg-primary transition-colors cursor-pointer"
onClick={() => onSelectCard(card)}
>
<div className="flex items-center space-x-2 flex-1 min-w-0">
{card.image_url && (
<img
src={card.image_url}
alt={card.name}
className="w-8 h-11 object-cover rounded flex-shrink-0"
/>
)}
<div className="min-w-0 flex-1">
<h4 className="font-medium text-text-primary text-xs truncate">{card.name}</h4>
<p className="text-text-secondary text-xs truncate">{card.set_name}</p>
<div className="flex items-center space-x-1">
{card.mana_cost && (
<ManaCost cost={card.mana_cost} size="xs" useSVG={manaSymbolSettings.useSVG} />
)}
{card.rarity && (
<span className={`text-xs px-1 rounded capitalize ${
card.rarity === 'mythic' ? 'bg-orange-100 text-orange-800' :
card.rarity === 'rare' ? 'bg-yellow-100 text-yellow-800' :
card.rarity === 'uncommon' ? 'bg-gray-100 text-gray-800' :
'bg-green-100 text-green-800'
}`}>
{card.rarity[0].toUpperCase()}
</span>
)}
</div>
</div>
</div>
</div>
))
)}
</div>
) : (
/* Thumbnail View */
<div>
{searchLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accent-ember mx-auto"></div>
<p className="text-text-secondary mt-2 text-xs">Searching...</p>
</div>
) : searchResults.length === 0 ? (
<div className="text-center py-8">
<div className="text-2xl mb-2">🔍</div>
<p className="text-text-secondary text-xs">
{searchQuery ? 'No cards found' : 'No cards available'}
</p>
</div>
) : (
<div className="grid grid-cols-3 gap-2">
{searchResults.map((card) => (
<div
key={card.id}
className="relative group cursor-pointer"
onClick={() => onSelectCard(card)}
>
{card.image_url ? (
<div className="relative">
<img
src={card.image_url}
alt={card.name}
className="w-full aspect-[2.5/3.5] object-cover rounded-lg shadow-sm group-hover:shadow-md transition-shadow"
/>
{/* Hover overlay with card name */}
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-60 transition-all duration-200 rounded-lg flex items-end">
<div className="p-2 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<p className="text-xs font-medium truncate">{card.name}</p>
<p className="text-xs opacity-75 truncate">{card.set_name}</p>
</div>
</div>
{/* Rarity indicator */}
{card.rarity && (
<div className={`absolute top-1 right-1 w-2 h-2 rounded-full ${
card.rarity === 'mythic' ? 'bg-orange-500' :
card.rarity === 'rare' ? 'bg-yellow-500' :
card.rarity === 'uncommon' ? 'bg-gray-400' :
'bg-green-500'
}`}></div>
)}
</div>
) : (
<div className="w-full aspect-[2.5/3.5] bg-bg-secondary rounded-lg flex items-center justify-center group-hover:bg-bg-primary transition-colors">
<div className="text-center p-2">
<p className="text-xs font-medium text-text-primary truncate">{card.name}</p>
<p className="text-xs text-text-secondary truncate">{card.set_name}</p>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
</>
) : (
/* Card Detail View */
<>
{/* Detail Header */}
<div className="flex items-center p-4 border-b border-border bg-bg-secondary rounded-t-lg">
<button
onClick={() => onClearSelectedCard()}
className="text-text-secondary hover:text-text-primary transition-colors mr-3"
>
Back
</button>
<h3 className="text-lg font-semibold text-text-primary truncate">{selectedCard.name}</h3>
</div>
{/* Card Detail Content */}
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4">
{/* Card Image */}
{selectedCard.image_url && (
<div className="text-center">
<img
src={selectedCard.image_url}
alt={selectedCard.name}
className="w-full max-w-64 mx-auto rounded-lg shadow-lg"
/>
</div>
)}
{/* Card Info */}
<div className="space-y-3">
<div>
<h4 className="font-semibold text-text-primary text-lg">{selectedCard.name}</h4>
<p className="text-text-secondary text-sm">{selectedCard.set_name}</p>
</div>
{selectedCard.mana_cost && (
<div>
<label className="block text-text-secondary text-xs font-medium mb-1">Mana Cost</label>
<div className="bg-bg-primary px-3 py-2 rounded">
<ManaCost cost={selectedCard.mana_cost} size="md" useSVG={manaSymbolSettings.useSVG} />
</div>
</div>
)}
{selectedCard.card_type && (
<div>
<label className="block text-text-secondary text-xs font-medium mb-1">Type</label>
<div className="bg-bg-primary px-3 py-2 rounded text-sm">{selectedCard.card_type}</div>
</div>
)}
{selectedCard.oracle_text && (
<div>
<label className="block text-text-secondary text-xs font-medium mb-1">Oracle Text</label>
<div className="bg-bg-primary px-3 py-2 rounded text-sm whitespace-pre-wrap">{selectedCard.oracle_text}</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
{selectedCard.rarity && (
<div>
<label className="block text-text-secondary text-xs font-medium mb-1">Rarity</label>
<div className={`px-3 py-2 rounded text-sm capitalize ${
selectedCard.rarity === 'mythic' ? 'bg-orange-100 text-orange-800' :
selectedCard.rarity === 'rare' ? 'bg-yellow-100 text-yellow-800' :
selectedCard.rarity === 'uncommon' ? 'bg-gray-100 text-gray-800' :
'bg-green-100 text-green-800'
}`}>
{selectedCard.rarity}
</div>
</div>
)}
{selectedCard.cmc !== undefined && (
<div>
<label className="block text-text-secondary text-xs font-medium mb-1">CMC</label>
<div className="bg-bg-primary px-3 py-2 rounded text-sm">{selectedCard.cmc}</div>
</div>
)}
</div>
</div>
</div>
</div>
</>
)}
{/* Floating Action Button */}
{selectedCard && (
<div className="absolute bottom-4 left-4 right-4">
<button
onClick={() => onAddCardToDeck(selectedCard)}
className="w-full bg-accent-ember text-white py-3 rounded-lg hover:bg-accent-ember-dark transition-colors font-semibold shadow-lg"
>
Add to Deck
</button>
</div>
)}
</div>
);
}