@@ -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!');
+}