diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ab32aad..dff52c0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,6 +1,11 @@ { + "enableAllProjectMcpServers": true, "enabledMcpjsonServers": [ "supabase" ], - "enableAllProjectMcpServers": true + "permissions": { + "allow": [ + "mcp__supabase__apply_migration" + ] + } } diff --git a/src/components/DeckCard.tsx b/src/components/DeckCard.tsx index cf8928e..81b4e2c 100644 --- a/src/components/DeckCard.tsx +++ b/src/components/DeckCard.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { AlertTriangle, Check, Edit } from 'lucide-react'; import { Deck } from '../types'; -import { validateDeck } from '../utils/deckValidation'; interface DeckCardProps { deck: Deck; @@ -9,16 +8,12 @@ interface DeckCardProps { } export default function DeckCard({ deck, onEdit }: DeckCardProps) { + // Use pre-calculated validation data + const isValid = deck.isValid ?? true; + const validationErrors = deck.validationErrors || []; - if(deck.id === "410ed539-a8f4-4bc4-91f1-6c113b9b7e25"){ - console.log("deck", deck.name); - console.log("cardEntities", deck.cards); - } - - const validation = validateDeck(deck); - const commander = deck.format === 'commander' ? deck.cards.find(card => - card.is_commander - )?.card : null; + // Use cover card (already loaded) + const coverImage = deck.coverCard?.image_uris?.normal; return (
{/* Full Card Art */}
- {commander?.name + {coverImage ? ( + {deck.coverCard?.name + ) : ( +
+ No Cover +
+ )} {/* Overlay for text readability */}
@@ -39,21 +40,21 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {

{deck.name}

- {validation.isValid ? ( + {isValid ? ( ) : ( - + )}
{deck.format} - {deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards + {deck.cardCount || 0} cards
- {commander && ( + {deck.format === 'commander' && deck.coverCard && (
- Commander: {commander.name} + Commander: {deck.coverCard.name}
)} diff --git a/src/components/DeckList.tsx b/src/components/DeckList.tsx index 5d3ccbb..abfbbee 100644 --- a/src/components/DeckList.tsx +++ b/src/components/DeckList.tsx @@ -4,6 +4,7 @@ import { Deck } from '../types'; import { supabase } from "../lib/supabase"; import DeckCard from "./DeckCard"; import { PlusCircle } from 'lucide-react'; +import MigrateDeckButton from "./MigrateDeckButton.tsx"; interface DeckListProps { onDeckEdit?: (deckId: string) => void; @@ -23,58 +24,36 @@ const DeckList = ({ onDeckEdit, onCreateDeck }: DeckListProps) => { return; } - const decksWithCards = await Promise.all(decksData.map(async (deck) => { - const { data: cardEntities, error: cardsError } = await supabase - .from('deck_cards') - .select('*') - .eq('deck_id', deck.id); + // Get all unique cover card IDs + const coverCardIds = decksData + .map(deck => deck.cover_card_id) + .filter(Boolean); + // Fetch only cover cards (much lighter!) + const coverCards = coverCardIds.length > 0 + ? await getCardsByIds(coverCardIds) + : []; + // Map decks with their cover cards + const decksWithCoverCards = decksData.map(deck => { + const coverCard = deck.cover_card_id + ? coverCards.find(c => c.id === deck.cover_card_id) + : null; - if (cardsError) { - console.error(`Error fetching cards for deck ${deck.id}:`, cardsError); - return { ...deck, cards: [] }; - } + return { + ...deck, + cards: [], // Empty array, we don't load all cards here + coverCard: coverCard || null, + createdAt: new Date(deck.created_at), + updatedAt: new Date(deck.updated_at), + validationErrors: deck.validation_errors || [], + isValid: deck.is_valid ?? true, + cardCount: deck.card_count || 0, + coverCardId: deck.cover_card_id, + }; + }); - const cardIds = cardEntities.map((entity) => entity.card_id); - const uniqueCardIds = [...new Set(cardIds)]; - - if(deck.id === "410ed539-a8f4-4bc4-91f1-6c113b9b7e25"){ - console.log("uniqueCardIds", 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, - is_commander: entity.is_commander, - }; - }); - - 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); + setDecks(decksWithCoverCards); setLoading(false); }; diff --git a/src/components/DeckManager.tsx b/src/components/DeckManager.tsx index 00238c0..98ad72f 100644 --- a/src/components/DeckManager.tsx +++ b/src/components/DeckManager.tsx @@ -363,6 +363,17 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { updatedAt: new Date(), }; + // Calculate validation for storage + const validation = validateDeck(deckToSave); + + // Determine cover card (commander or first card) + const commanderCard = deckFormat === 'commander' ? selectedCards.find(c => c.card.id === commander?.id) : null; + const coverCard = commanderCard?.card || selectedCards[0]?.card; + const coverCardId = coverCard?.id || null; + + // Calculate total card count + const totalCardCount = selectedCards.reduce((acc, curr) => acc + curr.quantity, 0); + const deckData = { id: deckToSave.id, name: deckToSave.name, @@ -370,6 +381,10 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { user_id: deckToSave.userId, created_at: deckToSave.createdAt, updated_at: deckToSave.updatedAt, + cover_card_id: coverCardId, + validation_errors: validation.errors, + is_valid: validation.isValid, + card_count: totalCardCount, }; // Save or update the deck diff --git a/src/components/MigrateDeckButton.tsx b/src/components/MigrateDeckButton.tsx new file mode 100644 index 0000000..8656957 --- /dev/null +++ b/src/components/MigrateDeckButton.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { Database, Loader2 } from 'lucide-react'; +import { migrateExistingDecks } from '../utils/migrateDeckData'; + +export default function MigrateDeckButton() { + const [isMigrating, setIsMigrating] = useState(false); + const [result, setResult] = useState(null); + + const handleMigrate = async () => { + if (!confirm('This will update all existing decks with optimization data. Continue?')) { + return; + } + + setIsMigrating(true); + setResult(null); + + try { + await migrateExistingDecks(); + setResult('Migration completed successfully!'); + } catch (error) { + console.error('Migration error:', error); + setResult('Migration failed. Check console for details.'); + } finally { + setIsMigrating(false); + } + }; + + return ( +
+

+ + Deck Migration Tool +

+

+ Update existing decks with optimization fields (cover image, validation cache, card count). + Run this once after the database migration. +

+ + + + {result && ( +

+ {result} +

+ )} +
+ ); +} diff --git a/src/types/index.ts b/src/types/index.ts index 2ef5b37..5c86ae7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -55,6 +55,11 @@ export interface Deck { userId: string; createdAt: Date; updatedAt: Date; + coverCardId?: string; + coverCard?: Card | null; + validationErrors?: string[]; + isValid?: boolean; + cardCount?: number; } export interface CardEntity { diff --git a/src/utils/migrateDeckData.ts b/src/utils/migrateDeckData.ts new file mode 100644 index 0000000..394f430 --- /dev/null +++ b/src/utils/migrateDeckData.ts @@ -0,0 +1,116 @@ +import { supabase } from '../lib/supabase'; +import { getCardsByIds } from '../services/api'; +import { validateDeck } from './deckValidation'; +import { Deck } from '../types'; + +/** + * Migrate existing decks to include optimization fields + * This should be run once to update all existing decks + */ +export async function migrateExistingDecks() { + console.log('Starting deck migration...'); + + // Get all decks + const { data: decksData, error: decksError } = await supabase + .from('decks') + .select('*'); + + if (decksError) { + console.error('Error fetching decks:', decksError); + return; + } + + console.log(`Found ${decksData.length} decks to migrate`); + + for (const deck of decksData) { + // Skip if already migrated + if (deck.cover_card_id && deck.card_count !== null) { + console.log(`Deck ${deck.name} already migrated, skipping`); + continue; + } + + console.log(`Migrating deck: ${deck.name}`); + + // Get deck cards + const { data: cardEntities, error: cardsError } = await supabase + .from('deck_cards') + .select('*') + .eq('deck_id', deck.id); + + if (cardsError || !cardEntities || cardEntities.length === 0) { + console.error(`Error fetching cards for deck ${deck.id}:`, cardsError); + continue; + } + + const cardIds = cardEntities.map(entity => entity.card_id); + const uniqueCardIds = [...new Set(cardIds)]; + + try { + // Fetch cards from Scryfall + const scryfallCards = await getCardsByIds(uniqueCardIds); + + if (!scryfallCards) { + console.error(`Failed to fetch cards for deck ${deck.id}`); + continue; + } + + const cards = cardEntities.map(entity => { + const card = scryfallCards.find(c => c.id === entity.card_id); + return { + card, + quantity: entity.quantity, + is_commander: entity.is_commander, + }; + }); + + // Create deck object for validation + const deckToValidate: Deck = { + id: deck.id, + name: deck.name, + format: deck.format, + cards, + userId: deck.user_id, + createdAt: new Date(deck.created_at), + updatedAt: new Date(deck.updated_at), + }; + + // Calculate validation + const validation = validateDeck(deckToValidate); + + // Determine cover card (commander or first card) + const commanderCard = deck.format === 'commander' + ? cardEntities.find(c => c.is_commander) + : null; + const coverCardId = commanderCard + ? commanderCard.card_id + : cardEntities[0]?.card_id || null; + + // Calculate total card count + const totalCardCount = cardEntities.reduce((acc, curr) => acc + curr.quantity, 0); + + // Update deck with optimization fields + const { error: updateError } = await supabase + .from('decks') + .update({ + cover_card_id: coverCardId, + validation_errors: validation.errors, + is_valid: validation.isValid, + card_count: totalCardCount, + }) + .eq('id', deck.id); + + if (updateError) { + console.error(`Error updating deck ${deck.id}:`, updateError); + } else { + console.log(`✓ Migrated deck: ${deck.name}`); + } + + // Small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + console.error(`Error processing deck ${deck.id}:`, error); + } + } + + console.log('Migration complete!'); +}