import React, { useState, useEffect } from 'react'; import { Plus, Search, Save, Trash2, Loader2, CheckCircle, XCircle, AlertCircle, PackagePlus, RefreshCw } from 'lucide-react'; import { Card, Deck } from '../types'; import { searchCards, getUserCollection, addCardToCollection, addMultipleCardsToCollection } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; import { supabase } from '../lib/supabase'; import { validateDeck } from '../utils/deckValidation'; import MagicCard from './MagicCard'; interface DeckManagerProps { initialDeck?: Deck; onSave?: () => void; } // const calculateManaCurve = (cards: { card; quantity: number }[]) => { // const manaValues = cards.map(({ card }) => { // if (!card.mana_cost) return 0; // // Basic heuristic: count mana symbols // return (card.mana_cost.match(/\{WUBRG0-9]\}/g) || []).length; // }); // const averageManaValue = manaValues.reduce((a, b) => a + b, 0) / manaValues.length; // return averageManaValue; // }; const suggestLandCountAndDistribution = ( cards: { card; quantity: number }[], format: string ) => { const formatRules = { standard: { minCards: 60 }, modern: { minCards: 60 }, commander: { minCards: 100 }, legacy: { minCards: 60 }, vintage: { minCards: 60 }, pauper: { minCards: 60 }, }; const { minCards } = formatRules[format as keyof typeof formatRules] || formatRules.standard; const deckSize = cards.reduce((acc, { quantity }) => acc + quantity, 0); const landsToAdd = Math.max(0, minCards - deckSize); const colorCounts = { W: 0, U: 0, B: 0, R: 0, G: 0 }; let totalColorSymbols = 0; cards.forEach(({ card, quantity }) => { if (card.mana_cost) { const wMatches = (card.mana_cost.match(/\{W\}/g) || []).length; const uMatches = (card.mana_cost.match(/\{U\}/g) || []).length; const bMatches = (card.mana_cost.match(/\{B\}/g) || []).length; const rMatches = (card.mana_cost.match(/\{R\}/g) || []).length; const gMatches = (card.mana_cost.match(/\{G\}/g) || []).length; colorCounts.W += wMatches * quantity; colorCounts.U += uMatches * quantity; colorCounts.B += bMatches * quantity; colorCounts.R += rMatches * quantity; colorCounts.G += gMatches * quantity; totalColorSymbols += (wMatches + uMatches + bMatches + rMatches + gMatches) * quantity; } }); const landDistribution: { [key: string]: number } = {}; for (const color in colorCounts) { const proportion = totalColorSymbols > 0 ? colorCounts[color as keyof typeof colorCounts] / totalColorSymbols : 0; landDistribution[color] = Math.round(landsToAdd * proportion); } const totalDistributed = Object.values(landDistribution).reduce( (acc, count) => acc + count, 0 ); if (totalDistributed > landsToAdd) { // Find the color with the most lands let maxColor = ''; let maxCount = 0; for (const color in landDistribution) { if (landDistribution[color] > maxCount) { maxColor = color; maxCount = landDistribution[color]; } } // Reduce the land count of that color landDistribution[maxColor] = maxCount - 1; } return { landCount: landsToAdd, landDistribution }; }; export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [selectedCards, setSelectedCards] = useState<{ card: Card; quantity: number; }[]>(initialDeck?.cards || []); const [deckName, setDeckName] = useState(initialDeck?.name || ''); const [deckFormat, setDeckFormat] = useState(initialDeck?.format || 'standard'); const [commander, setCommander] = useState( initialDeck?.cards.find(card => card.is_commander )?.card || null ); const { user } = useAuth(); const [isImporting, setIsImporting] = useState(false); const [isSaving, setIsSaving] = useState(false); const [snackbar, setSnackbar] = useState<{ message: string; type: 'success' | 'error' } | null>(null); // Collection management state const [userCollection, setUserCollection] = useState>(new Map()); const [isLoadingCollection, setIsLoadingCollection] = useState(true); const [addingCardId, setAddingCardId] = useState(null); const [isAddingAll, setIsAddingAll] = useState(false); const [cardFaceIndex, setCardFaceIndex] = useState>(new Map()); // Load user collection on component mount useEffect(() => { const loadUserCollection = async () => { if (!user) return; try { setIsLoadingCollection(true); const collection = await getUserCollection(user.id); setUserCollection(collection); } catch (error) { console.error('Error loading user collection:', error); setSnackbar({ message: 'Failed to load collection', type: 'error' }); } finally { setIsLoadingCollection(false); } }; loadUserCollection(); }, [user]); // Helper functions for double-faced cards const isDoubleFaced = (card: Card) => { const backFaceLayouts = ['transform', 'modal_dfc', 'double_faced_token', 'reversible_card']; return card.card_faces && card.card_faces.length > 1 && backFaceLayouts.includes(card.layout); }; const getCurrentFaceIndex = (cardId: string) => { return cardFaceIndex.get(cardId) || 0; }; const toggleCardFace = (cardId: string, totalFaces: number) => { setCardFaceIndex(prev => { const newMap = new Map(prev); const currentIndex = prev.get(cardId) || 0; const nextIndex = (currentIndex + 1) % totalFaces; newMap.set(cardId, nextIndex); return newMap; }); }; const getCardImageUri = (card: Card, faceIndex: number = 0) => { if (isDoubleFaced(card) && card.card_faces) { return card.card_faces[faceIndex]?.image_uris?.normal || card.card_faces[faceIndex]?.image_uris?.small; } return card.image_uris?.normal || card.image_uris?.small || card.card_faces?.[0]?.image_uris?.normal; }; // Helper function to check if a card is in the collection const isCardInCollection = (cardId: string, requiredQuantity: number = 1): boolean => { const ownedQuantity = userCollection.get(cardId) || 0; return ownedQuantity >= requiredQuantity; }; // Helper function to get missing cards const getMissingCards = () => { return selectedCards.filter(({ card, quantity }) => { return !isCardInCollection(card.id, quantity); }); }; // Add single card to collection const handleAddCardToCollection = async (cardId: string, quantity: number) => { if (!user) return; try { setAddingCardId(cardId); await addCardToCollection(user.id, cardId, quantity); // Update local collection state setUserCollection(prev => { const newMap = new Map(prev); const currentQty = newMap.get(cardId) || 0; newMap.set(cardId, currentQty + quantity); return newMap; }); setSnackbar({ message: 'Card added to collection!', type: 'success' }); } catch (error) { console.error('Error adding card to collection:', error); setSnackbar({ message: 'Failed to add card to collection', type: 'error' }); } finally { setAddingCardId(null); setTimeout(() => setSnackbar(null), 3000); } }; // Add all missing cards to collection const handleAddAllMissingCards = async () => { if (!user) return; const missingCards = getMissingCards(); if (missingCards.length === 0) { setSnackbar({ message: 'All cards are already in your collection!', type: 'success' }); setTimeout(() => setSnackbar(null), 3000); return; } try { setIsAddingAll(true); const cardsToAdd = missingCards.map(({ card, quantity }) => { const ownedQuantity = userCollection.get(card.id) || 0; const neededQuantity = Math.max(0, quantity - ownedQuantity); return { cardId: card.id, quantity: neededQuantity, }; }).filter(c => c.quantity > 0); await addMultipleCardsToCollection(user.id, cardsToAdd); // Update local collection state setUserCollection(prev => { const newMap = new Map(prev); cardsToAdd.forEach(({ cardId, quantity }) => { const currentQty = newMap.get(cardId) || 0; newMap.set(cardId, currentQty + quantity); }); return newMap; }); setSnackbar({ message: `Successfully added ${cardsToAdd.length} card(s) to collection!`, type: 'success' }); } catch (error) { console.error('Error adding cards to collection:', error); setSnackbar({ message: 'Failed to add cards to collection', type: 'error' }); } finally { setIsAddingAll(false); setTimeout(() => setSnackbar(null), 3000); } }; const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); if (!searchQuery.trim()) return; try { const cards = await searchCards(searchQuery); setSearchResults(cards); } catch (error) { console.error('Failed to search cards:', error); } }; const addCardToDeck = (card: Card) => { setSelectedCards(prev => { const isBasicLand = card.name === 'Plains' || card.name === 'Island' || card.name === 'Swamp' || card.name === 'Mountain' || card.name === 'Forest'; const existing = prev.find(c => c.card.id === card.id); if (existing) { return prev.map(c => c.card.id === card.id ? { ...c, quantity: isBasicLand ? c.quantity + 1 : Math.min(c.quantity + 1, 4), } : c ); } return [...prev, { card, quantity: 1 }]; }); }; const removeCardFromDeck = (cardId: string) => setSelectedCards(prev => prev.filter(c => c.card.id !== cardId)); const updateCardQuantity = (cardId: string, quantity: number) => { setSelectedCards(prev => { return prev.map(c => { if (c.card.id === cardId) { return { ...c, quantity: quantity }; } return c; }); }); }; const saveDeck = async () => { if (!deckName.trim() || selectedCards.length === 0 || !user) return; setIsSaving(true); try { const deckToSave: Deck = { id: initialDeck?.id || crypto.randomUUID(), name: deckName, format: deckFormat, cards: selectedCards, userId: user.id, createdAt: initialDeck?.createdAt || new Date(), updatedAt: new Date(), }; const deckData = { id: deckToSave.id, name: deckToSave.name, format: deckToSave.format, user_id: deckToSave.userId, created_at: deckToSave.createdAt, updated_at: deckToSave.updatedAt, }; // Save or update the deck const { error: deckError } = await supabase .from('decks') .upsert([deckData]) .select(); if (deckError) throw deckError; // Delete existing cards if updating if (initialDeck) { await supabase.from('deck_cards').delete().eq('deck_id', initialDeck.id); } // Save the deck cards const deckCards = selectedCards.map(card => ({ deck_id: deckToSave.id, card_id: card.card.id, quantity: card.quantity, is_commander: card.card.id === commander?.id, })); const { error: cardsError } = await supabase .from('deck_cards') .insert(deckCards); if (cardsError) throw cardsError; setSnackbar({ message: 'Deck saved successfully!', type: 'success' }); if (onSave) onSave(); } catch (error) { console.error('Error saving deck:', error); setSnackbar({ message: 'Failed to save deck.', type: 'error' }); } finally { setIsSaving(false); setTimeout(() => setSnackbar(null), 3000); // Clear snackbar after 3 seconds } }; const currentDeck: Deck = { id: initialDeck?.id || '', name: deckName, format: deckFormat, cards: selectedCards, userId: user?.id || '', createdAt: initialDeck?.createdAt || new Date(), updatedAt: new Date(), }; const validation = validateDeck(currentDeck); const deckSize = selectedCards.reduce((acc, curr) => acc + curr.quantity, 0); const { landCount: suggestedLandCountValue, landDistribution: suggestedLands, } = suggestLandCountAndDistribution(selectedCards, deckFormat); const totalPrice = selectedCards.reduce((acc, { card, quantity }) => { const isBasicLand = card.name === 'Plains' || card.name === 'Island' || card.name === 'Swamp' || card.name === 'Mountain' || card.name === 'Forest'; const price = isBasicLand ? 0 : card.prices?.usd ? parseFloat(card.prices.usd) : 0; return acc + price * quantity; }, 0); const addSuggestedLandsToDeck = async () => { const basicLandCards = { W: { name: 'Plains', set: 'unh' }, U: { name: 'Island', set: 'unh' }, B: { name: 'Swamp', set: 'unh' }, R: { name: 'Mountain', set: 'unh' }, G: { name: 'Forest', set: 'unh' }, }; for (const color in suggestedLands) { const landCount = suggestedLands[color]; if (landCount > 0) { const landName = basicLandCards[color]?.name; const landSet = basicLandCards[color]?.set; if (landName && landSet) { try { const cards = await searchCards(`${landName} set:${landSet}`); if (cards && cards.length > 0) { const landCard = cards[0]; // Take the first matching card for (let i = 0; i < landCount; i++) { addCardToDeck(landCard); } } } catch (error) { console.error(`Failed to add ${landName}:`, error); } } } } }; const handleFileUpload = async ( event: React.ChangeEvent ) => { const file = event.target.files?.[0]; if (!file) return; setIsImporting(true); try { const reader = new FileReader(); reader.onload = async e => { const text = e.target?.result as string; const lines = text.split('\n'); const cardsToAdd: { card: Card; quantity: number }[] = []; for (const line of lines) { const parts = line.trim().split(' '); const quantity = parseInt(parts[0]); const cardName = parts.slice(1).join(' '); if (isNaN(quantity) || quantity <= 0 || !cardName) continue; try { const searchResults = await searchCards(cardName); if (searchResults && searchResults.length > 0) { const card = searchResults[0]; cardsToAdd.push({ card, quantity }); } else { console.warn(`Card not found: ${cardName}`); setSnackbar({ message: `Card not found: ${cardName}`, type: 'error' }); } } catch (error) { console.error(`Failed to search card ${cardName}:`, error); setSnackbar({ message: `Failed to import card: ${cardName}`, type: 'error' }); } } setSelectedCards(prev => { const updatedCards = [...prev]; for (const { card, quantity } of cardsToAdd) { const existingCardIndex = updatedCards.findIndex( c => c.card.id === card.id ); if (existingCardIndex !== -1) { updatedCards[existingCardIndex].quantity = Math.min( updatedCards[existingCardIndex].quantity + quantity, 4 ); } else { updatedCards.push({ card, quantity }); } } return updatedCards; }); }; reader.readAsText(file); } finally { setIsImporting(false); } }; return (
{/* Card Search Section */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" placeholder="Search for cards..." />
{searchResults.map(card => { const currentFaceIndex = getCurrentFaceIndex(card.id); const isMultiFaced = isDoubleFaced(card); const inCollection = userCollection.get(card.id) || 0; const isAddingThisCard = addingCardId === card.id; const displayName = isMultiFaced && card.card_faces ? card.card_faces[currentFaceIndex]?.name || card.name : card.name; return (
{getCardImageUri(card, currentFaceIndex) ? ( {displayName} ) : ( )} {isMultiFaced && ( )}

{displayName}

{inCollection > 0 && ( x{inCollection} )}
{card.prices?.usd && (
${card.prices.usd}
)}
); })}
{/* Deck Builder Section */}
setDeckName(e.target.value)} className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" placeholder="Deck Name" /> {deckFormat === 'commander' && ( )}
{isImporting && (
)}
{!validation.isValid && (
    {validation.errors.map((error, index) => (
  • {error}
  • ))}
)}

Cards ({selectedCards.reduce((acc, curr) => acc + curr.quantity, 0)})

{!isLoadingCollection && getMissingCards().length > 0 && (
{getMissingCards().length} missing
)}
{!isLoadingCollection && getMissingCards().length > 0 && ( )} {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 (
{card.name}

{card.name} {isMissing && ( Missing {neededQuantity} )} {!isMissing && ownedQuantity > 0 && ( Owned ({ownedQuantity}) )}

{card.prices?.usd && (
${card.prices.usd}
)}
{isMissing && ( )} 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" />
); })}
Total Price: ${totalPrice.toFixed(2)}
{deckSize > 0 && (
Suggested Land Count: {suggestedLandCountValue} {Object.entries(suggestedLands).map(([landType, count]) => (
{landType}: {count}
))}
)} {deckSize > 0 && ( )}
{snackbar && (
{snackbar.type === 'success' ? ( ) : ( )} {snackbar.message}
)}
); }