This commit is contained in:
2025-11-21 21:23:04 +01:00
parent ebae5a82db
commit e83874162f
8 changed files with 165 additions and 133 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
public/mana-color/swamp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -4,6 +4,7 @@ import { searchCards, getUserCollection, addCardToCollection } from '../services
import { Card } from '../types'; import { Card } from '../types';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import MagicCard from './MagicCard'; import MagicCard from './MagicCard';
import { getManaIconPath } from './ManaCost';
const CardSearch = () => { const CardSearch = () => {
const { user } = useAuth(); const { user } = useAuth();
@@ -296,20 +297,27 @@ const CardSearch = () => {
{/* Mana Cost */} {/* Mana Cost */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
{Object.entries(manaCost).map(([color, count]) => ( {Object.entries(manaCost).map(([color, count]) => {
<div key={color} className="flex items-center space-x-2"> const iconPath = getManaIconPath(color);
<span style={{ fontSize: '1.2em' }} className="md:text-[1.5em]"> return (
{color === 'W' ? '⚪' : color === 'U' ? '🔵' : color === 'B' ? '⚫' : color === 'R' ? '🔴' : color === 'G' ? '🟢' : '🟤'} <div key={color} className="flex items-center space-x-2">
</span> {iconPath ? (
<input <img src={iconPath} alt={color} className="w-6 h-6 md:w-8 md:h-8" />
type="number" ) : (
value={count} <span className="w-6 h-6 md:w-8 md:h-8 flex items-center justify-center bg-gray-500 text-white font-bold rounded-full text-sm">
onChange={(e) => setManaCost({ ...manaCost, [color]: parseInt(e.target.value) })} {color}
className="w-14 sm:w-16 px-2 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" </span>
min="0" )}
/> <input
</div> type="number"
))} value={count}
onChange={(e) => setManaCost({ ...manaCost, [color]: parseInt(e.target.value) })}
className="w-14 sm:w-16 px-2 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white"
min="0"
/>
</div>
);
})}
</div> </div>
{/* Stats */} {/* Stats */}

View File

@@ -6,6 +6,7 @@ import { useAuth } from '../contexts/AuthContext';
import { supabase } from '../lib/supabase'; import { supabase } from '../lib/supabase';
import { validateDeck } from '../utils/deckValidation'; import { validateDeck } from '../utils/deckValidation';
import MagicCard from './MagicCard'; import MagicCard from './MagicCard';
import { ManaCost } from './ManaCost';
interface DeckManagerProps { interface DeckManagerProps {
initialDeck?: Deck; initialDeck?: Deck;
@@ -580,7 +581,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
<h3 className="font-medium text-sm truncate">{displayName}</h3> <h3 className="font-medium text-sm truncate">{displayName}</h3>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
{card.mana_cost && ( {card.mana_cost && (
<div className="text-xs text-gray-400">{card.mana_cost}</div> <ManaCost cost={card.mana_cost} size={14} />
)} )}
{card.prices?.usd && ( {card.prices?.usd && (
<div className="text-xs text-gray-400">${card.prices.usd}</div> <div className="text-xs text-gray-400">${card.prices.usd}</div>
@@ -729,110 +730,43 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
<h3 className="font-bold text-xl"> <h3 className="font-bold text-xl">
Cards ({selectedCards.reduce((acc, curr) => acc + curr.quantity, 0)}) Cards ({selectedCards.reduce((acc, curr) => acc + curr.quantity, 0)})
</h3> </h3>
{!isLoadingCollection && getMissingCards().length > 0 && (
<div className="flex items-center gap-2 text-sm text-yellow-500">
<AlertCircle size={16} />
<span>{getMissingCards().length} missing</span>
</div>
)}
</div> </div>
{!isLoadingCollection && getMissingCards().length > 0 && ( {selectedCards.map(({ card, quantity }) => (
<button <div
onClick={handleAddAllMissingCards} key={card.id}
disabled={isAddingAll} className="flex items-center gap-3 p-2 rounded-lg bg-gray-700"
className="w-full px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg flex items-center justify-center gap-2 mb-3 relative"
> >
{isAddingAll ? ( <img
<> src={card.image_uris?.art_crop}
<Loader2 className="animate-spin" size={20} /> alt={card.name}
<span>Adding to collection...</span> className="w-10 h-10 rounded"
</> />
) : ( <div className="flex-1 min-w-0">
<> <h4 className="font-medium text-sm truncate">{card.name}</h4>
<PackagePlus size={20} /> {card.prices?.usd && (
<span>Add All Missing Cards to Collection</span> <div className="text-xs text-gray-400">${card.prices.usd}</div>
</> )}
)}
</button>
)}
{selectedCards.map(({ card, quantity }) => {
const ownedQuantity = userCollection.get(card.id) || 0;
const isMissing = !isCardInCollection(card.id, quantity);
const neededQuantity = Math.max(0, quantity - ownedQuantity);
return (
<div
key={card.id}
className={`flex items-center gap-4 p-2 rounded-lg ${
isMissing
? 'bg-yellow-900/20 border border-yellow-700/50'
: 'bg-gray-700'
}`}
>
<img
src={card.image_uris?.art_crop}
alt={card.name}
className="w-12 h-12 rounded"
/>
<div className="flex-1">
<h4 className="font-medium flex items-center gap-2">
{card.name}
{isMissing && (
<span className="text-xs bg-yellow-600 px-2 py-0.5 rounded-full flex items-center gap-1">
<AlertCircle size={12} />
Missing {neededQuantity}
</span>
)}
{!isMissing && ownedQuantity > 0 && (
<span className="text-xs bg-green-600 px-2 py-0.5 rounded-full flex items-center gap-1">
<CheckCircle size={12} />
Owned ({ownedQuantity})
</span>
)}
</h4>
{card.prices?.usd && (
<div className="text-sm text-gray-400">${card.prices.usd}</div>
)}
</div>
<div className="flex items-center gap-2">
{isMissing && (
<button
onClick={() => handleAddCardToCollection(card.id, neededQuantity)}
disabled={addingCardId === card.id}
className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded text-sm flex items-center gap-1"
title={`Add ${neededQuantity} to collection`}
>
{addingCardId === card.id ? (
<Loader2 className="animate-spin" size={16} />
) : (
<>
<Plus size={16} />
<span className="hidden sm:inline">Add</span>
</>
)}
</button>
)}
<input
type="number"
value={quantity}
onChange={e =>
updateCardQuantity(card.id, parseInt(e.target.value))
}
min="1"
className="w-16 px-2 py-1 bg-gray-600 border border-gray-500 rounded text-center"
/>
<button
onClick={() => removeCardFromDeck(card.id)}
className="text-red-500 hover:text-red-400"
>
<Trash2 size={20} />
</button>
</div>
</div> </div>
); <div className="flex items-center gap-2">
})} <input
type="number"
value={quantity}
onChange={e =>
updateCardQuantity(card.id, parseInt(e.target.value))
}
min="1"
className="w-14 px-2 py-1 bg-gray-600 border border-gray-500 rounded text-center text-sm"
/>
<button
onClick={() => removeCardFromDeck(card.id)}
className="text-red-500 hover:text-red-400"
>
<Trash2 size={18} />
</button>
</div>
</div>
))}
</div> </div>
<div className="font-bold text-xl"> <div className="font-bold text-xl">
@@ -860,25 +794,44 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
</button> </button>
)} )}
<button <div className="flex gap-2">
onClick={saveDeck} {!isLoadingCollection && getMissingCards().length > 0 && (
disabled={ <button
!deckName.trim() || selectedCards.length === 0 || isSaving onClick={handleAddAllMissingCards}
} disabled={isAddingAll}
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg flex items-center justify-center gap-2 relative" className="flex-1 px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg flex items-center justify-center gap-2"
> title="Add missing cards to collection"
{isSaving ? ( >
<> {isAddingAll ? (
<Loader2 className="animate-spin text-white absolute left-2 top-1/2 -translate-y-1/2" size={20} /> <Loader2 className="animate-spin" size={20} />
<span className="opacity-0">Save Deck</span> ) : (
</> <>
) : ( <PackagePlus size={20} />
<> <span className="hidden sm:inline">Add Missing</span>
<Save size={20} /> </>
<span>{initialDeck ? 'Update Deck' : 'Save Deck'}</span> )}
</> </button>
)} )}
</button> <button
onClick={saveDeck}
disabled={
!deckName.trim() || selectedCards.length === 0 || isSaving
}
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg flex items-center justify-center gap-2 relative"
>
{isSaving ? (
<>
<Loader2 className="animate-spin text-white absolute left-2 top-1/2 -translate-y-1/2" size={20} />
<span className="opacity-0">Save Deck</span>
</>
) : (
<>
<Save size={20} />
<span>{initialDeck ? 'Update Deck' : 'Save Deck'}</span>
</>
)}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,71 @@
import React from 'react';
// Map mana symbols to their icon paths
const MANA_ICONS: Record<string, string> = {
W: '/mana-color/plains.png',
U: '/mana-color/island.png',
B: '/mana-color/swamp.png',
R: '/mana-color/moutain.png', // Note: filename has typo "moutain"
G: '/mana-color/forest.png',
};
interface ManaSymbolProps {
symbol: string;
size?: number;
}
// Renders a single mana symbol (either as an icon or as text for numbers/other)
export function ManaSymbol({ symbol, size = 16 }: ManaSymbolProps) {
const iconPath = MANA_ICONS[symbol];
if (iconPath) {
return (
<img
src={iconPath}
alt={symbol}
className="inline-block"
style={{ width: size, height: size }}
/>
);
}
// For numbers and other symbols, show as a circle with the symbol
return (
<span
className="inline-flex items-center justify-center bg-gray-500 text-white font-bold rounded-full"
style={{ width: size, height: size, fontSize: size * 0.6 }}
>
{symbol}
</span>
);
}
interface ManaCostProps {
cost: string;
size?: number;
}
// Parses and renders a full mana cost string like "{2}{W}{U}"
export function ManaCost({ cost, size = 16 }: ManaCostProps) {
if (!cost) return null;
// Parse mana cost string: {2}{W}{U} -> ['2', 'W', 'U']
const symbols = cost.match(/\{([^}]+)\}/g)?.map(s => s.slice(1, -1)) || [];
if (symbols.length === 0) return null;
return (
<span className="inline-flex items-center gap-0.5">
{symbols.map((symbol, index) => (
<ManaSymbol key={index} symbol={symbol} size={size} />
))}
</span>
);
}
// Helper to get icon path for a color (for use in filters, etc.)
export function getManaIconPath(color: string): string | null {
return MANA_ICONS[color] || null;
}
export default ManaCost;