optimization api calls, update models
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": [
|
||||
"supabase"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__supabase__apply_migration"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
@@ -27,11 +22,17 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {
|
||||
>
|
||||
{/* Full Card Art */}
|
||||
<div className="relative aspect-[5/7] overflow-hidden">
|
||||
<img
|
||||
src={commander?.image_uris?.normal || deck.cards[0]?.card.image_uris?.normal}
|
||||
alt={commander?.name || deck.cards[0]?.card.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
{coverImage ? (
|
||||
<img
|
||||
src={coverImage}
|
||||
alt={deck.coverCard?.name || deck.name}
|
||||
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 */}
|
||||
<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="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>
|
||||
{validation.isValid ? (
|
||||
{isValid ? (
|
||||
<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 className="flex items-center justify-between text-xs text-gray-300 mb-2">
|
||||
<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>
|
||||
|
||||
{commander && (
|
||||
{deck.format === 'commander' && deck.coverCard && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
coverCardId?: string;
|
||||
coverCard?: Card | null;
|
||||
validationErrors?: string[];
|
||||
isValid?: boolean;
|
||||
cardCount?: number;
|
||||
}
|
||||
|
||||
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