From 904403508f40713354f36a9144c94074d35c4665 Mon Sep 17 00:00:00 2001 From: Reynier Matthieu Date: Tue, 4 Mar 2025 17:04:05 +0100 Subject: [PATCH] wip import decks from txt file magic online format --- src/components/DeckList.tsx | 36 ++-- src/components/DeckManager.tsx | 297 +++++++++++++++++++++++++++------ src/utils/deckValidation.ts | 10 -- 3 files changed, 265 insertions(+), 78 deletions(-) diff --git a/src/components/DeckList.tsx b/src/components/DeckList.tsx index 5dc5d3e..49016e5 100644 --- a/src/components/DeckList.tsx +++ b/src/components/DeckList.tsx @@ -35,22 +35,32 @@ const DeckList = ({ onDeckEdit }: DeckListProps) => { const cardIds = cardEntities.map((entity) => entity.card_id); const uniqueCardIds = [...new Set(cardIds)]; - const scryfallCards = await getCardsByIds(uniqueCardIds); + try { + const scryfallCards = await getCardsByIds(uniqueCardIds); + + if (!scryfallCards) { + console.error("scryfallCards is undefined after getCardsByIds"); + return { ...deck, cards: [] }; + } + + const cards = cardEntities.map((entity) => { + const card = scryfallCards.find((c) => c.id === entity.card_id); + return { + card, + quantity: entity.quantity, + }; + }); - const cards = cardEntities.map((entity) => { - const card = scryfallCards.find((c) => c.id === entity.card_id); return { - card, - quantity: entity.quantity, + ...deck, + cards, + createdAt: new Date(deck.created_at), + updatedAt: new Date(deck.updated_at), }; - }); - - return { - ...deck, - cards, - createdAt: new Date(deck.created_at), - updatedAt: new Date(deck.updated_at), - }; + } catch (error) { + console.error("Error fetching cards from Scryfall:", error); + return { ...deck, cards: [] }; + } })); setDecks(decksWithCards); diff --git a/src/components/DeckManager.tsx b/src/components/DeckManager.tsx index 1ada387..1585144 100644 --- a/src/components/DeckManager.tsx +++ b/src/components/DeckManager.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Plus, Search, Save, Trash2 } from 'lucide-react'; +import { Plus, Search, Save, Trash2, Upload, Loader2, CheckCircle, XCircle } from 'lucide-react'; import { Card, Deck } from '../types'; import { searchCards } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; @@ -12,7 +12,7 @@ interface DeckManagerProps { onSave?: () => void; } -const calculateManaCurve = (cards: { card: Card; quantity: number }[]) => { +const calculateManaCurve = (cards: { card; quantity: number }[]) => { const manaValues = cards.map(({ card }) => { if (!card.mana_cost) return 0; // Basic heuristic: count mana symbols @@ -23,7 +23,10 @@ const calculateManaCurve = (cards: { card: Card; quantity: number }[]) => { return averageManaValue; }; -const suggestLandCountAndDistribution = (cards: { card: Card; quantity: number }[], format: string) => { +const suggestLandCountAndDistribution = ( + cards: { card; quantity: number }[], + format: string +) => { const formatRules = { standard: { minCards: 60, targetLands: 24.5 }, modern: { minCards: 60, targetLands: 24.5 }, @@ -33,9 +36,14 @@ const suggestLandCountAndDistribution = (cards: { card: Card; quantity: number } pauper: { minCards: 60, targetLands: 24.5 }, }; - const { minCards, targetLands } = formatRules[format as keyof typeof formatRules] || formatRules.standard; + const { minCards, targetLands } = + formatRules[format as keyof typeof formatRules] || formatRules.standard; const deckSize = cards.reduce((acc, { quantity }) => acc + quantity, 0); - const nonLandCards = cards.reduce((acc, { card, quantity }) => card.type_line?.toLowerCase().includes('land') ? acc : acc + quantity, 0); + const nonLandCards = cards.reduce( + (acc, { card, quantity }) => + card.type_line?.toLowerCase().includes('land') ? acc : acc + quantity, + 0 + ); const landsToAdd = Math.max(0, minCards - deckSize); const colorCounts = { W: 0, U: 0, B: 0, R: 0, G: 0 }; @@ -55,17 +63,24 @@ const suggestLandCountAndDistribution = (cards: { card: Card; quantity: number } colorCounts.R += rMatches * quantity; colorCounts.G += gMatches * quantity; - totalColorSymbols += (wMatches + uMatches + bMatches + rMatches + 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; + const proportion = + totalColorSymbols > 0 + ? colorCounts[color as keyof typeof colorCounts] / totalColorSymbols + : 0; landDistribution[color] = Math.round(landsToAdd * proportion); } - let totalDistributed = Object.values(landDistribution).reduce((acc, count) => acc + count, 0); + let totalDistributed = Object.values(landDistribution).reduce( + (acc, count) => acc + count, + 0 + ); if (totalDistributed > landsToAdd) { // Find the color with the most lands @@ -88,12 +103,21 @@ const suggestLandCountAndDistribution = (cards: { card: Card; quantity: number } 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 [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(c => + c.card.type_line?.toLowerCase().includes('legendary creature') + )?.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); const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); @@ -109,12 +133,20 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { const addCardToDeck = (card: Card) => { setSelectedCards(prev => { - const isBasicLand = card.name === 'Plains' || card.name === 'Island' || card.name === 'Swamp' || card.name === 'Mountain' || card.name === 'Forest'; + 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, + quantity: isBasicLand ? c.quantity + 1 : Math.min(c.quantity + 1, 4), + } : c ); } @@ -129,7 +161,12 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { setSelectedCards(prev => { return prev.map(c => { if (c.card.id === cardId) { - const isBasicLand = c.card.name === 'Plains' || c.card.name === 'Island' || c.card.name === 'Swamp' || c.card.name === 'Mountain' || c.card.name === 'Forest'; + const isBasicLand = + c.card.name === 'Plains' || + c.card.name === 'Island' || + c.card.name === 'Swamp' || + c.card.name === 'Mountain' || + c.card.name === 'Forest'; return { ...c, quantity: quantity }; } return c; @@ -140,30 +177,27 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { const saveDeck = async () => { if (!deckName.trim() || selectedCards.length === 0 || !user) return; - 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 validation = validateDeck(deckToSave); - if (!validation.isValid) { - alert(`Deck validation failed: ${validation.errors.join(', ')}`); - 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 validation = validateDeck(deckToSave); + const deckData = { id: deckToSave.id, name: deckToSave.name, format: deckToSave.format, user_id: deckToSave.userId, created_at: deckToSave.createdAt, - updated_at: deckToSave.updatedAt + updated_at: deckToSave.updatedAt, }; // Save or update the deck @@ -176,10 +210,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { // Delete existing cards if updating if (initialDeck) { - await supabase - .from('deck_cards') - .delete() - .eq('deck_id', initialDeck.id); + await supabase.from('deck_cards').delete().eq('deck_id', initialDeck.id); } // Save the deck cards @@ -187,7 +218,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { deck_id: deckToSave.id, card_id: card.card.id, quantity: card.quantity, - is_commander: card.card.type_line?.toLowerCase().includes('legendary creature') || false + is_commander: card.card.id === commander?.id, })); const { error: cardsError } = await supabase @@ -196,10 +227,14 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { if (cardsError) throw cardsError; + setSnackbar({ message: 'Deck saved successfully!', type: 'success' }); if (onSave) onSave(); } catch (error) { console.error('Error saving deck:', error); - alert('Failed to save deck'); + setSnackbar({ message: 'Failed to save deck.', type: 'error' }); + } finally { + setIsSaving(false); + setTimeout(() => setSnackbar(null), 3000); // Clear snackbar after 3 seconds } }; @@ -210,17 +245,25 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { cards: selectedCards, userId: user?.id || '', createdAt: initialDeck?.createdAt || new Date(), - updatedAt: 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 { + 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); + 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); @@ -256,6 +299,67 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { } }; + 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}`); + alert(`Card not found: ${cardName}`); + } + } catch (error) { + console.error(`Failed to search card ${cardName}:`, error); + alert(`Failed to search card ${cardName}: ${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 (
@@ -264,11 +368,14 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
- + setSearchQuery(e.target.value)} + onChange={e => 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..." /> @@ -310,14 +417,14 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { setDeckName(e.target.value)} + onChange={e => 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 && (
    @@ -343,7 +495,10 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { Cards ({selectedCards.reduce((acc, curr) => acc + curr.quantity, 0)}) {selectedCards.map(({ card, quantity }) => ( -
    +
    {card.name}

    {card.name}

    {card.prices?.usd && ( -
    - ${card.prices.usd} -
    +
    ${card.prices.usd}
    )}
    updateCardQuantity(card.id, parseInt(e.target.value))} + 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" /> @@ -368,7 +523,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { onClick={() => removeCardFromDeck(card.id)} className="text-red-500 hover:text-red-400" > - +
    ))} @@ -401,16 +556,48 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
+ {snackbar && ( +
+
+
+ {snackbar.type === 'success' ? ( + + ) : ( + + )} + {snackbar.message} +
+ +
+
+ )}
); } diff --git a/src/utils/deckValidation.ts b/src/utils/deckValidation.ts index d468882..2384ab6 100644 --- a/src/utils/deckValidation.ts +++ b/src/utils/deckValidation.ts @@ -75,16 +75,6 @@ export function validateDeck(deck: Deck): DeckValidation { } }); - // Check commander requirement - if (rules.requiresCommander) { - const hasCommander = deck.cards.some(({ card }) => - card.type_line?.toLowerCase().includes('legendary creature') - ); - if (!hasCommander) { - errors.push('Deck must have a legendary creature as commander'); - } - } - return { isValid: errors.length === 0, errors,