add mana
This commit is contained in:
BIN
public/mana-color/forest.png
Normal file
BIN
public/mana-color/forest.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
public/mana-color/island.png
Normal file
BIN
public/mana-color/island.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
public/mana-color/moutain.png
Normal file
BIN
public/mana-color/moutain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
BIN
public/mana-color/plains.png
Normal file
BIN
public/mana-color/plains.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
BIN
public/mana-color/swamp.png
Normal file
BIN
public/mana-color/swamp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
@@ -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,11 +297,17 @@ 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]) => {
|
||||||
|
const iconPath = getManaIconPath(color);
|
||||||
|
return (
|
||||||
<div key={color} className="flex items-center space-x-2">
|
<div key={color} className="flex items-center space-x-2">
|
||||||
<span style={{ fontSize: '1.2em' }} className="md:text-[1.5em]">
|
{iconPath ? (
|
||||||
{color === 'W' ? '⚪' : color === 'U' ? '🔵' : color === 'B' ? '⚫' : color === 'R' ? '🔴' : color === 'G' ? '🟢' : '🟤'}
|
<img src={iconPath} alt={color} className="w-6 h-6 md:w-8 md:h-8" />
|
||||||
|
) : (
|
||||||
|
<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">
|
||||||
|
{color}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={count}
|
value={count}
|
||||||
@@ -309,7 +316,8 @@ const CardSearch = () => {
|
|||||||
min="0"
|
min="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
|
|||||||
@@ -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,91 +730,25 @@ 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
|
|
||||||
onClick={handleAddAllMissingCards}
|
|
||||||
disabled={isAddingAll}
|
|
||||||
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 ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="animate-spin" size={20} />
|
|
||||||
<span>Adding to collection...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PackagePlus size={20} />
|
|
||||||
<span>Add All Missing Cards to Collection</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</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
|
<div
|
||||||
key={card.id}
|
key={card.id}
|
||||||
className={`flex items-center gap-4 p-2 rounded-lg ${
|
className="flex items-center gap-3 p-2 rounded-lg bg-gray-700"
|
||||||
isMissing
|
|
||||||
? 'bg-yellow-900/20 border border-yellow-700/50'
|
|
||||||
: 'bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={card.image_uris?.art_crop}
|
src={card.image_uris?.art_crop}
|
||||||
alt={card.name}
|
alt={card.name}
|
||||||
className="w-12 h-12 rounded"
|
className="w-10 h-10 rounded"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<h4 className="font-medium flex items-center gap-2">
|
<h4 className="font-medium text-sm truncate">{card.name}</h4>
|
||||||
{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 && (
|
{card.prices?.usd && (
|
||||||
<div className="text-sm text-gray-400">${card.prices.usd}</div>
|
<div className="text-xs text-gray-400">${card.prices.usd}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={quantity}
|
value={quantity}
|
||||||
@@ -821,18 +756,17 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
updateCardQuantity(card.id, parseInt(e.target.value))
|
updateCardQuantity(card.id, parseInt(e.target.value))
|
||||||
}
|
}
|
||||||
min="1"
|
min="1"
|
||||||
className="w-16 px-2 py-1 bg-gray-600 border border-gray-500 rounded text-center"
|
className="w-14 px-2 py-1 bg-gray-600 border border-gray-500 rounded text-center text-sm"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeCardFromDeck(card.id)}
|
onClick={() => removeCardFromDeck(card.id)}
|
||||||
className="text-red-500 hover:text-red-400"
|
className="text-red-500 hover:text-red-400"
|
||||||
>
|
>
|
||||||
<Trash2 size={20} />
|
<Trash2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="font-bold text-xl">
|
<div className="font-bold text-xl">
|
||||||
@@ -860,12 +794,30 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!isLoadingCollection && getMissingCards().length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleAddAllMissingCards}
|
||||||
|
disabled={isAddingAll}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{isAddingAll ? (
|
||||||
|
<Loader2 className="animate-spin" size={20} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PackagePlus size={20} />
|
||||||
|
<span className="hidden sm:inline">Add Missing</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={saveDeck}
|
onClick={saveDeck}
|
||||||
disabled={
|
disabled={
|
||||||
!deckName.trim() || selectedCards.length === 0 || isSaving
|
!deckName.trim() || selectedCards.length === 0 || isSaving
|
||||||
}
|
}
|
||||||
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-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 ? (
|
{isSaving ? (
|
||||||
<>
|
<>
|
||||||
@@ -883,6 +835,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{snackbar && (
|
{snackbar && (
|
||||||
<div
|
<div
|
||||||
className={`fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg transition-all duration-300 ${
|
className={`fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg transition-all duration-300 ${
|
||||||
|
|||||||
71
src/components/ManaCost.tsx
Normal file
71
src/components/ManaCost.tsx
Normal 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;
|
||||||
Reference in New Issue
Block a user