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>
761 lines
22 KiB
JavaScript
761 lines
22 KiB
JavaScript
/* 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>
|
||
);
|
||
}
|