optimization api calls, update models
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"enableAllProjectMcpServers": true,
|
||||||
"enabledMcpjsonServers": [
|
"enabledMcpjsonServers": [
|
||||||
"supabase"
|
"supabase"
|
||||||
],
|
],
|
||||||
"enableAllProjectMcpServers": true
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__supabase__apply_migration"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AlertTriangle, Check, Edit } from 'lucide-react';
|
import { AlertTriangle, Check, Edit } from 'lucide-react';
|
||||||
import { Deck } from '../types';
|
import { Deck } from '../types';
|
||||||
import { validateDeck } from '../utils/deckValidation';
|
|
||||||
|
|
||||||
interface DeckCardProps {
|
interface DeckCardProps {
|
||||||
deck: Deck;
|
deck: Deck;
|
||||||
@@ -9,16 +8,12 @@ interface DeckCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DeckCard({ deck, onEdit }: 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"){
|
// Use cover card (already loaded)
|
||||||
console.log("deck", deck.name);
|
const coverImage = deck.coverCard?.image_uris?.normal;
|
||||||
console.log("cardEntities", deck.cards);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = validateDeck(deck);
|
|
||||||
const commander = deck.format === 'commander' ? deck.cards.find(card =>
|
|
||||||
card.is_commander
|
|
||||||
)?.card : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -27,11 +22,17 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {
|
|||||||
>
|
>
|
||||||
{/* Full Card Art */}
|
{/* Full Card Art */}
|
||||||
<div className="relative aspect-[5/7] overflow-hidden">
|
<div className="relative aspect-[5/7] overflow-hidden">
|
||||||
|
{coverImage ? (
|
||||||
<img
|
<img
|
||||||
src={commander?.image_uris?.normal || deck.cards[0]?.card.image_uris?.normal}
|
src={coverImage}
|
||||||
alt={commander?.name || deck.cards[0]?.card.name}
|
alt={deck.coverCard?.name || deck.name}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full bg-gray-700 flex items-center justify-center text-gray-500">
|
||||||
|
No Cover
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Overlay for text readability */}
|
{/* Overlay for text readability */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-900 via-gray-900/60 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-gray-900 via-gray-900/60 to-transparent" />
|
||||||
|
|
||||||
@@ -39,21 +40,21 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {
|
|||||||
<div className="absolute bottom-0 left-0 right-0 p-3">
|
<div className="absolute bottom-0 left-0 right-0 p-3">
|
||||||
<div className="flex items-start justify-between mb-1">
|
<div className="flex items-start justify-between mb-1">
|
||||||
<h3 className="text-base sm:text-lg font-bold text-white line-clamp-2 flex-1">{deck.name}</h3>
|
<h3 className="text-base sm:text-lg font-bold text-white line-clamp-2 flex-1">{deck.name}</h3>
|
||||||
{validation.isValid ? (
|
{isValid ? (
|
||||||
<Check size={16} className="text-green-400 ml-2 flex-shrink-0" />
|
<Check size={16} className="text-green-400 ml-2 flex-shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<AlertTriangle size={16} className="text-yellow-400 ml-2 flex-shrink-0" title={validation.errors.join(', ')} />
|
<AlertTriangle size={16} className="text-yellow-400 ml-2 flex-shrink-0" title={validationErrors.join(', ')} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-xs text-gray-300 mb-2">
|
<div className="flex items-center justify-between text-xs text-gray-300 mb-2">
|
||||||
<span className="capitalize">{deck.format}</span>
|
<span className="capitalize">{deck.format}</span>
|
||||||
<span>{deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards</span>
|
<span>{deck.cardCount || 0} cards</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{commander && (
|
{deck.format === 'commander' && deck.coverCard && (
|
||||||
<div className="text-xs text-blue-300 mb-2 truncate">
|
<div className="text-xs text-blue-300 mb-2 truncate">
|
||||||
<span className="font-semibold">Commander:</span> {commander.name}
|
<span className="font-semibold">Commander:</span> {deck.coverCard.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Deck } from '../types';
|
|||||||
import { supabase } from "../lib/supabase";
|
import { supabase } from "../lib/supabase";
|
||||||
import DeckCard from "./DeckCard";
|
import DeckCard from "./DeckCard";
|
||||||
import { PlusCircle } from 'lucide-react';
|
import { PlusCircle } from 'lucide-react';
|
||||||
|
import MigrateDeckButton from "./MigrateDeckButton.tsx";
|
||||||
|
|
||||||
interface DeckListProps {
|
interface DeckListProps {
|
||||||
onDeckEdit?: (deckId: string) => void;
|
onDeckEdit?: (deckId: string) => void;
|
||||||
@@ -23,58 +24,36 @@ const DeckList = ({ onDeckEdit, onCreateDeck }: DeckListProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decksWithCards = await Promise.all(decksData.map(async (deck) => {
|
// Get all unique cover card IDs
|
||||||
const { data: cardEntities, error: cardsError } = await supabase
|
const coverCardIds = decksData
|
||||||
.from('deck_cards')
|
.map(deck => deck.cover_card_id)
|
||||||
.select('*')
|
.filter(Boolean);
|
||||||
.eq('deck_id', deck.id);
|
|
||||||
|
|
||||||
|
// Fetch only cover cards (much lighter!)
|
||||||
|
const coverCards = coverCardIds.length > 0
|
||||||
|
? await getCardsByIds(coverCardIds)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Map decks with their cover cards
|
||||||
if (cardsError) {
|
const decksWithCoverCards = decksData.map(deck => {
|
||||||
console.error(`Error fetching cards for deck ${deck.id}:`, cardsError);
|
const coverCard = deck.cover_card_id
|
||||||
return { ...deck, cards: [] };
|
? coverCards.find(c => c.id === deck.cover_card_id)
|
||||||
}
|
: null;
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
...deck,
|
...deck,
|
||||||
cards,
|
cards: [], // Empty array, we don't load all cards here
|
||||||
|
coverCard: coverCard || null,
|
||||||
createdAt: new Date(deck.created_at),
|
createdAt: new Date(deck.created_at),
|
||||||
updatedAt: new Date(deck.updated_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,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
});
|
||||||
console.error("Error fetching cards from Scryfall:", error);
|
|
||||||
return { ...deck, cards: [] };
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
setDecks(decksWithCards);
|
setDecks(decksWithCoverCards);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,17 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
updatedAt: new Date(),
|
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 = {
|
const deckData = {
|
||||||
id: deckToSave.id,
|
id: deckToSave.id,
|
||||||
name: deckToSave.name,
|
name: deckToSave.name,
|
||||||
@@ -370,6 +381,10 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
user_id: deckToSave.userId,
|
user_id: deckToSave.userId,
|
||||||
created_at: deckToSave.createdAt,
|
created_at: deckToSave.createdAt,
|
||||||
updated_at: deckToSave.updatedAt,
|
updated_at: deckToSave.updatedAt,
|
||||||
|
cover_card_id: coverCardId,
|
||||||
|
validation_errors: validation.errors,
|
||||||
|
is_valid: validation.isValid,
|
||||||
|
card_count: totalCardCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save or update the deck
|
// Save or update the deck
|
||||||
|
|||||||
64
src/components/MigrateDeckButton.tsx
Normal file
64
src/components/MigrateDeckButton.tsx
Normal file
@@ -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<string | null>(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 (
|
||||||
|
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
|
||||||
|
<Database size={20} />
|
||||||
|
Deck Migration Tool
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-400 mb-4">
|
||||||
|
Update existing decks with optimization fields (cover image, validation cache, card count).
|
||||||
|
Run this once after the database migration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleMigrate}
|
||||||
|
disabled={isMigrating}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
{isMigrating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="animate-spin" size={20} />
|
||||||
|
Migrating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Database size={20} />
|
||||||
|
Migrate Decks
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<p className={`mt-3 text-sm ${result.includes('success') ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{result}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -55,6 +55,11 @@ export interface Deck {
|
|||||||
userId: string;
|
userId: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
coverCardId?: string;
|
||||||
|
coverCard?: Card | null;
|
||||||
|
validationErrors?: string[];
|
||||||
|
isValid?: boolean;
|
||||||
|
cardCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardEntity {
|
export interface CardEntity {
|
||||||
|
|||||||
116
src/utils/migrateDeckData.ts
Normal file
116
src/utils/migrateDeckData.ts
Normal file
@@ -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!');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user