refactor(deck-builder): extract deck list and card browser (Brief 2) (#91)
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>
This commit is contained in:
parent
5a3f799926
commit
55f1643fbb
3 changed files with 882 additions and 444 deletions
761
components/DeckBuilderCardBrowser.js
Normal file
761
components/DeckBuilderCardBrowser.js
Normal file
|
|
@ -0,0 +1,761 @@
|
|||
/* 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>
|
||||
);
|
||||
}
|
||||
85
components/DeckBuilderDeckList.js
Normal file
85
components/DeckBuilderDeckList.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
|
||||
import { ManaCost } from './ManaSymbols';
|
||||
|
||||
export default function DeckBuilderDeckList({
|
||||
deckCards,
|
||||
sidebarOpen,
|
||||
onOpenSidebar,
|
||||
manaSymbolSettings,
|
||||
onAddCard,
|
||||
onRemoveCard,
|
||||
}) {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto space-y-2">
|
||||
{deckCards.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🃏</div>
|
||||
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3>
|
||||
<p className="text-text-secondary mb-4">Start building your deck by searching for cards</p>
|
||||
{!sidebarOpen && (
|
||||
<button
|
||||
onClick={onOpenSidebar}
|
||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
>
|
||||
Open Card Browser
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
deckCards
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((card) => (
|
||||
<div key={`${card.card_id}-${card.id}`} className="flex items-center justify-between p-4 bg-bg-primary rounded-lg hover:bg-bg-tertiary transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
{card.image_url && (
|
||||
<img
|
||||
src={card.image_url}
|
||||
alt={card.name}
|
||||
className="w-14 h-20 object-cover rounded shadow-md"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-primary text-lg">{card.name}</h4>
|
||||
<p className="text-text-secondary text-sm">{card.set_name}</p>
|
||||
<div className="flex items-center space-x-3 mt-1">
|
||||
{card.mana_cost && (
|
||||
<div className="bg-bg-secondary px-2 py-1 rounded">
|
||||
<ManaCost cost={card.mana_cost} size="sm" useSVG={manaSymbolSettings.useSVG} />
|
||||
</div>
|
||||
)}
|
||||
{card.rarity && (
|
||||
<span className={`text-xs px-2 py-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}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-text-primary font-bold text-lg">{card.quantity}x</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button
|
||||
onClick={() => onRemoveCard(card.card_id, 1)}
|
||||
className="w-8 h-8 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors flex items-center justify-center font-bold"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAddCard(card, 1)}
|
||||
className="w-8 h-8 bg-accent-ember text-white rounded-full hover:bg-accent-ember-dark transition-colors flex items-center justify-center font-bold"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,12 +3,11 @@ import { useState, useEffect, useRef } from 'react';
|
|||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import Layout from '../components/Layout';
|
||||
import DeckBuilderCardBrowser from '../components/DeckBuilderCardBrowser';
|
||||
import DeckBuilderDeckList from '../components/DeckBuilderDeckList';
|
||||
import DeckBuilderStatsBar from '../components/DeckBuilderStatsBar';
|
||||
import { ManaCost, ColorIdentity, ColorFilterSymbol } from '../components/ManaSymbols';
|
||||
import ManaSymbolSettings from '../components/ManaSymbolSettings';
|
||||
import { useAuth } from '../lib/use-auth';
|
||||
import { computeDeckStats, isBasicLand } from '../lib/deck-builder-stats';
|
||||
import { getColorIdentity, getColorSymbol } from '../lib/mana-symbols';
|
||||
|
||||
export default function DeckBuilder() {
|
||||
const { user } = useAuth();
|
||||
|
|
@ -317,452 +316,45 @@ export default function DeckBuilder() {
|
|||
|
||||
<DeckBuilderStatsBar stats={stats} />
|
||||
|
||||
{/* Deck Cards List */}
|
||||
<div className="flex-1 overflow-y-auto space-y-2">
|
||||
{deckCards.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🃏</div>
|
||||
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3>
|
||||
<p className="text-text-secondary mb-4">Start building your deck by searching for cards</p>
|
||||
{!sidebarOpen && (
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
>
|
||||
Open Card Browser
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
deckCards
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((card) => (
|
||||
<div key={`${card.card_id}-${card.id}`} className="flex items-center justify-between p-4 bg-bg-primary rounded-lg hover:bg-bg-tertiary transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
{card.image_url && (
|
||||
<img
|
||||
src={card.image_url}
|
||||
alt={card.name}
|
||||
className="w-14 h-20 object-cover rounded shadow-md"
|
||||
<DeckBuilderDeckList
|
||||
deckCards={deckCards}
|
||||
sidebarOpen={sidebarOpen}
|
||||
onOpenSidebar={() => setSidebarOpen(true)}
|
||||
manaSymbolSettings={manaSymbolSettings}
|
||||
onAddCard={(card, quantity) => addCardToDeck(card, quantity)}
|
||||
onRemoveCard={(cardId, quantity) => removeCardFromDeck(cardId, quantity)}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-primary text-lg">{card.name}</h4>
|
||||
<p className="text-text-secondary text-sm">{card.set_name}</p>
|
||||
<div className="flex items-center space-x-3 mt-1">
|
||||
{card.mana_cost && (
|
||||
<div className="bg-bg-secondary px-2 py-1 rounded">
|
||||
<ManaCost cost={card.mana_cost} size="sm" useSVG={manaSymbolSettings.useSVG} />
|
||||
</div>
|
||||
)}
|
||||
{card.rarity && (
|
||||
<span className={`text-xs px-2 py-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}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-text-primary font-bold text-lg">{card.quantity}x</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button
|
||||
onClick={() => removeCardFromDeck(card.card_id, 1)}
|
||||
className="w-8 h-8 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors flex items-center justify-center font-bold"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
onClick={() => addCardToDeck(card, 1)}
|
||||
className="w-8 h-8 bg-accent-ember text-white rounded-full hover:bg-accent-ember-dark transition-colors flex items-center justify-center font-bold"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full Height Right Sidebar - Card Browser */}
|
||||
<div className={`transition-all duration-300 ${sidebarOpen ? 'w-96' : 'w-0 overflow-hidden'}`}>
|
||||
<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={() => setViewMode('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={() => setViewMode('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={() => setShowSettings(!showSettings)}
|
||||
className={`text-text-secondary hover:text-text-primary transition-colors p-1 ${
|
||||
showSettings ? 'text-accent-ember' : ''
|
||||
}`}
|
||||
title="Settings"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
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) => setSearchQuery(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={() => setShowFilters(!showFilters)}
|
||||
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={setManaSymbolSettings} />
|
||||
</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={clearFilters}
|
||||
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={toggleColorFilter}
|
||||
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) => setFilters({...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) => setFilters({...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={() => setSelectedCard(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={() => setSelectedCard(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={() => setSelectedCard(null)}
|
||||
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={() => {
|
||||
addCardToDeck(selectedCard);
|
||||
<DeckBuilderCardBrowser
|
||||
selectedCard={selectedCard}
|
||||
onSelectCard={setSelectedCard}
|
||||
onClearSelectedCard={() => setSelectedCard(null)}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showSettings={showSettings}
|
||||
onToggleSettings={() => setShowSettings(!showSettings)}
|
||||
onCollapseSidebar={() => setSidebarOpen(false)}
|
||||
searchQuery={searchQuery}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
showFilters={showFilters}
|
||||
onToggleFilters={() => setShowFilters(!showFilters)}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
onClearFilters={clearFilters}
|
||||
onToggleColorFilter={toggleColorFilter}
|
||||
searchLoading={searchLoading}
|
||||
searchResults={searchResults}
|
||||
manaSymbolSettings={manaSymbolSettings}
|
||||
onManaSymbolSettingsChange={setManaSymbolSettings}
|
||||
onAddCardToDeck={(card) => {
|
||||
addCardToDeck(card);
|
||||
setSelectedCard(null);
|
||||
}}
|
||||
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>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue