Implement database-based card loading system - add card loader API, database search, and admin interface
This commit is contained in:
parent
ee67b05e28
commit
7057417a89
5 changed files with 756 additions and 34 deletions
370
api/admin/load-cards.js
Normal file
370
api/admin/load-cards.js
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { verifyToken } from '../setup-auth.js';
|
||||
|
||||
// Rate limiting for external APIs
|
||||
const rateLimiters = {
|
||||
mtg: { lastCall: 0, minInterval: 50 }, // 50ms between calls
|
||||
pokemon: { lastCall: 0, minInterval: 100 }, // 100ms between calls
|
||||
lorcana: { lastCall: 0, minInterval: 100 } // 100ms between calls
|
||||
};
|
||||
|
||||
async function waitForRateLimit(api) {
|
||||
const now = Date.now();
|
||||
const limiter = rateLimiters[api];
|
||||
const timeSinceLastCall = now - limiter.lastCall;
|
||||
|
||||
if (timeSinceLastCall < limiter.minInterval) {
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, limiter.minInterval - timeSinceLastCall)
|
||||
);
|
||||
}
|
||||
|
||||
limiter.lastCall = Date.now();
|
||||
}
|
||||
|
||||
// Load MTG cards from Scryfall
|
||||
async function loadMTGCards() {
|
||||
console.log('🃏 Loading MTG cards from Scryfall...');
|
||||
|
||||
try {
|
||||
// Get total count first
|
||||
const countResponse = await fetch('https://api.scryfall.com/cards/search?q=game:paper');
|
||||
const countData = await countResponse.json();
|
||||
const totalCards = countData.total_cards;
|
||||
|
||||
console.log(`📊 Found ${totalCards} MTG cards to load`);
|
||||
|
||||
let loadedCount = 0;
|
||||
let page = 1;
|
||||
|
||||
while (loadedCount < Math.min(totalCards, 1000)) { // Limit to 1000 for now
|
||||
await waitForRateLimit('mtg');
|
||||
|
||||
const response = await fetch(`https://api.scryfall.com/cards/search?q=game:paper&page=${page}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.data || data.data.length === 0) break;
|
||||
|
||||
for (const card of data.data) {
|
||||
try {
|
||||
await sql.query(`
|
||||
INSERT INTO cards (
|
||||
name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
|
||||
card_type, colors, oracle_text, power, toughness, image_url,
|
||||
stock_image_url, current_price, market_price, scryfall_id, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
ON CONFLICT (scryfall_id) DO NOTHING
|
||||
`, [
|
||||
card.name,
|
||||
card.set_name,
|
||||
card.set,
|
||||
card.collector_number,
|
||||
card.rarity,
|
||||
'MTG',
|
||||
card.mana_cost,
|
||||
card.cmc,
|
||||
card.type_line,
|
||||
JSON.stringify(card.colors),
|
||||
card.oracle_text,
|
||||
card.power,
|
||||
card.toughness,
|
||||
card.image_uris?.normal || card.image_uris?.small,
|
||||
card.image_uris?.small,
|
||||
card.prices?.usd ? parseFloat(card.prices.usd) : null,
|
||||
card.prices?.usd_foil ? parseFloat(card.prices.usd_foil) : null,
|
||||
card.id,
|
||||
true
|
||||
]);
|
||||
|
||||
loadedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading MTG card ${card.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
page++;
|
||||
console.log(`✅ Loaded ${loadedCount} MTG cards so far...`);
|
||||
}
|
||||
|
||||
console.log(`🎉 Successfully loaded ${loadedCount} MTG cards`);
|
||||
return loadedCount;
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading MTG cards:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Load Pokémon cards from Pokémon TCG API
|
||||
async function loadPokemonCards() {
|
||||
console.log('⚡ Loading Pokémon cards from Pokémon TCG API...');
|
||||
|
||||
try {
|
||||
let loadedCount = 0;
|
||||
let page = 1;
|
||||
const pageSize = 250; // Max allowed by API
|
||||
|
||||
while (loadedCount < 1000) { // Limit to 1000 for now
|
||||
await waitForRateLimit('pokemon');
|
||||
|
||||
const response = await fetch(`https://api.pokemontcg.io/v2/cards?page=${page}&pageSize=${pageSize}`, {
|
||||
headers: {
|
||||
'X-Api-Key': process.env.POKEMON_API_KEY || ''
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.data || data.data.length === 0) break;
|
||||
|
||||
for (const card of data.data) {
|
||||
try {
|
||||
await sql.query(`
|
||||
INSERT INTO cards (
|
||||
name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
|
||||
card_type, colors, oracle_text, power, toughness, image_url,
|
||||
stock_image_url, current_price, market_price, tcg_player_id, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
ON CONFLICT (tcg_player_id) DO NOTHING
|
||||
`, [
|
||||
card.name,
|
||||
card.set.name,
|
||||
card.set.id,
|
||||
card.number,
|
||||
card.rarity,
|
||||
'POKEMON',
|
||||
card.convertedRetreatCost?.toString() || null,
|
||||
card.convertedRetreatCost,
|
||||
card.supertype + (card.subtypes ? ' - ' + card.subtypes.join(', ') : ''),
|
||||
JSON.stringify(card.types || []),
|
||||
card.flavorText || card.rules?.join(' ') || '',
|
||||
card.attacks?.[0]?.damage || null,
|
||||
card.hp || null,
|
||||
card.images?.large,
|
||||
card.images?.small,
|
||||
card.cardmarket?.prices?.averageSellPrice ? parseFloat(card.cardmarket.prices.averageSellPrice) : null,
|
||||
card.cardmarket?.prices?.lowPrice ? parseFloat(card.cardmarket.prices.lowPrice) : null,
|
||||
card.id,
|
||||
true
|
||||
]);
|
||||
|
||||
loadedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading Pokémon card ${card.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
page++;
|
||||
console.log(`✅ Loaded ${loadedCount} Pokémon cards so far...`);
|
||||
}
|
||||
|
||||
console.log(`🎉 Successfully loaded ${loadedCount} Pokémon cards`);
|
||||
return loadedCount;
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading Pokémon cards:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Load Lorcana cards from multiple sources
|
||||
async function loadLorcanaCards() {
|
||||
console.log('🏰 Loading Lorcana cards from multiple sources...');
|
||||
|
||||
try {
|
||||
let loadedCount = 0;
|
||||
|
||||
// Try Lorcana API first
|
||||
try {
|
||||
await waitForRateLimit('lorcana');
|
||||
const response = await fetch('https://api.lorcana-api.com/cards/fetch?pagesize=1000');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
console.log(`📊 Found ${data.cards.length} cards from Lorcana API`);
|
||||
|
||||
for (const card of data.cards) {
|
||||
try {
|
||||
await sql.query(`
|
||||
INSERT INTO cards (
|
||||
name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
|
||||
card_type, colors, oracle_text, power, toughness, image_url,
|
||||
stock_image_url, current_price, market_price, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
ON CONFLICT (name, set_code, card_number) DO NOTHING
|
||||
`, [
|
||||
card.name || card.card_name || card.title || '',
|
||||
card.set?.name || card.set_name || '',
|
||||
card.set?.code || card.set_code || '',
|
||||
card.number || card.card_number || card.card_num || '',
|
||||
card.rarity || card.rarity_name || '',
|
||||
'LORCANA',
|
||||
card.cost?.toString() || card.mana_cost || card.ink_cost?.toString() || '',
|
||||
card.cost || card.cmc || card.ink_cost || 0,
|
||||
card.type || card.card_type || card.type_name || '',
|
||||
JSON.stringify(card.colors || card.ink || []),
|
||||
card.text || card.oracle_text || card.description || card.effect || '',
|
||||
card.strength?.toString() || card.power || card.attack?.toString() || '',
|
||||
card.willpower?.toString() || card.toughness || card.defense?.toString() || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.price?.market || card.current_price || null,
|
||||
card.price?.low || card.market_price || null,
|
||||
true
|
||||
]);
|
||||
|
||||
loadedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading Lorcana card ${card.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Lorcana API failed, trying Lorcast...');
|
||||
}
|
||||
|
||||
// Try Lorcast API as fallback
|
||||
if (loadedCount === 0) {
|
||||
try {
|
||||
await waitForRateLimit('lorcana');
|
||||
const response = await fetch('https://api.lorcast.com/v0/cards');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
console.log(`📊 Found ${data.cards.length} cards from Lorcast API`);
|
||||
|
||||
for (const card of data.cards) {
|
||||
try {
|
||||
await sql.query(`
|
||||
INSERT INTO cards (
|
||||
name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
|
||||
card_type, colors, oracle_text, power, toughness, image_url,
|
||||
stock_image_url, current_price, market_price, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
ON CONFLICT (name, set_code, card_number) DO NOTHING
|
||||
`, [
|
||||
card.name || card.card_name || card.title || '',
|
||||
card.set?.name || card.set_name || '',
|
||||
card.set?.code || card.set_code || '',
|
||||
card.number || card.card_number || card.card_num || '',
|
||||
card.rarity || card.rarity_name || '',
|
||||
'LORCANA',
|
||||
card.cost?.toString() || card.mana_cost || card.ink_cost?.toString() || '',
|
||||
card.cost || card.cmc || card.ink_cost || 0,
|
||||
card.type || card.card_type || card.type_name || '',
|
||||
JSON.stringify(card.colors || card.ink || []),
|
||||
card.text || card.oracle_text || card.description || card.effect || '',
|
||||
card.strength?.toString() || card.power || card.attack?.toString() || '',
|
||||
card.willpower?.toString() || card.toughness || card.defense?.toString() || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.price?.market || card.current_price || null,
|
||||
card.price?.low || card.market_price || null,
|
||||
true
|
||||
]);
|
||||
|
||||
loadedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading Lorcana card ${card.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Lorcast API also failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🎉 Successfully loaded ${loadedCount} Lorcana cards`);
|
||||
return loadedCount;
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading Lorcana cards:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/admin/load-cards - Load cards from external APIs
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user is admin (you can implement your own admin check)
|
||||
const { searchParams } = new URL(request.url);
|
||||
const game = searchParams.get('game'); // 'MTG', 'POKEMON', 'LORCANA', or 'ALL'
|
||||
|
||||
console.log(`🚀 Starting card loading process for game: ${game}`);
|
||||
|
||||
let results = {};
|
||||
|
||||
if (game === 'MTG' || game === 'ALL') {
|
||||
results.mtg = await loadMTGCards();
|
||||
}
|
||||
|
||||
if (game === 'POKEMON' || game === 'ALL') {
|
||||
results.pokemon = await loadPokemonCards();
|
||||
}
|
||||
|
||||
if (game === 'LORCANA' || game === 'ALL') {
|
||||
results.lorcana = await loadLorcanaCards();
|
||||
}
|
||||
|
||||
const totalLoaded = Object.values(results).reduce((sum, count) => sum + count, 0);
|
||||
|
||||
console.log(`🎉 Card loading completed! Total loaded: ${totalLoaded}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully loaded ${totalLoaded} cards`,
|
||||
results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in card loading API:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load cards', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/admin/load-cards - Get loading status
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get card counts from database
|
||||
const result = await sql.query(`
|
||||
SELECT
|
||||
game,
|
||||
COUNT(*) as count
|
||||
FROM cards
|
||||
GROUP BY game
|
||||
`);
|
||||
|
||||
const counts = {};
|
||||
result.rows.forEach(row => {
|
||||
counts[row.game] = parseInt(row.count);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
counts,
|
||||
total: Object.values(counts).reduce((sum, count) => sum + count, 0)
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting card counts:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get card counts', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
158
api/cards/search.js
Normal file
158
api/cards/search.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { sql } from '@vercel/postgres';
|
||||
|
||||
// GET /api/cards/search - Search cards from database
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get('q') || searchParams.get('search') || '';
|
||||
const game = searchParams.get('game');
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const limit = parseInt(searchParams.get('limit') || '20');
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
console.log(`🔍 Searching cards: "${query}" game: "${game}" page: ${page}`);
|
||||
|
||||
// Build the SQL query
|
||||
let sqlQuery = `
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
set_name,
|
||||
set_code,
|
||||
card_number,
|
||||
rarity,
|
||||
game,
|
||||
mana_cost,
|
||||
cmc,
|
||||
card_type,
|
||||
colors,
|
||||
oracle_text,
|
||||
power,
|
||||
toughness,
|
||||
image_url,
|
||||
stock_image_url,
|
||||
current_price,
|
||||
market_price,
|
||||
verified,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM cards
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Add search filter
|
||||
if (query.trim()) {
|
||||
sqlQuery += ` AND (
|
||||
name ILIKE $${paramIndex} OR
|
||||
oracle_text ILIKE $${paramIndex} OR
|
||||
card_type ILIKE $${paramIndex} OR
|
||||
set_name ILIKE $${paramIndex}
|
||||
)`;
|
||||
params.push(`%${query}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Add game filter
|
||||
if (game && game !== 'ALL') {
|
||||
sqlQuery += ` AND game = $${paramIndex}`;
|
||||
params.push(game);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Add ordering and pagination
|
||||
sqlQuery += ` ORDER BY name ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
params.push(limit, offset);
|
||||
|
||||
console.log(`📝 SQL Query: ${sqlQuery}`);
|
||||
console.log(`📝 Parameters:`, params);
|
||||
|
||||
// Execute the query
|
||||
const result = await sql.query(sqlQuery, params);
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM cards
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const countParams = [];
|
||||
let countParamIndex = 1;
|
||||
|
||||
if (query.trim()) {
|
||||
countQuery += ` AND (
|
||||
name ILIKE $${countParamIndex} OR
|
||||
oracle_text ILIKE $${countParamIndex} OR
|
||||
card_type ILIKE $${countParamIndex} OR
|
||||
set_name ILIKE $${countParamIndex}
|
||||
)`;
|
||||
countParams.push(`%${query}%`);
|
||||
countParamIndex++;
|
||||
}
|
||||
|
||||
if (game && game !== 'ALL') {
|
||||
countQuery += ` AND game = $${countParamIndex}`;
|
||||
countParams.push(game);
|
||||
countParamIndex++;
|
||||
}
|
||||
|
||||
const countResult = await sql.query(countQuery, countParams);
|
||||
const total = parseInt(countResult.rows[0].total);
|
||||
|
||||
// Transform the results
|
||||
const cards = result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
set_name: row.set_name,
|
||||
set_code: row.set_code,
|
||||
card_number: row.card_number,
|
||||
rarity: row.rarity,
|
||||
game: row.game,
|
||||
mana_cost: row.mana_cost,
|
||||
cmc: row.cmc,
|
||||
card_type: row.card_type,
|
||||
colors: row.colors ? JSON.parse(row.colors) : [],
|
||||
oracle_text: row.oracle_text,
|
||||
power: row.power,
|
||||
toughness: row.toughness,
|
||||
image_url: row.image_url,
|
||||
stock_image_url: row.stock_image_url,
|
||||
current_price: row.current_price,
|
||||
market_price: row.market_price,
|
||||
verified: row.verified,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
|
||||
console.log(`✅ Found ${cards.length} cards (total: ${total})`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: cards,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
hasNext: page * limit < total,
|
||||
hasPrev: page > 1
|
||||
},
|
||||
search: {
|
||||
query,
|
||||
game,
|
||||
results: cards.length
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error searching cards:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to search cards', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { Navigate } from 'react-router-dom';
|
|||
import UserManagement from './UserManagement';
|
||||
import CardManagement from './CardManagement';
|
||||
import SystemStats from './SystemStats';
|
||||
import CardLoader from './CardLoader';
|
||||
|
||||
type AdminTab = 'dashboard' | 'users' | 'cards' | 'decks' | 'settings';
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ const AdminPanel: React.FC = () => {
|
|||
case 'users':
|
||||
return <UserManagement />;
|
||||
case 'cards':
|
||||
return <CardManagement />;
|
||||
return <CardLoader />;
|
||||
case 'decks':
|
||||
return <div className="p-6">Deck Management - Coming Soon</div>;
|
||||
case 'settings':
|
||||
|
|
|
|||
185
src/components/admin/CardLoader.tsx
Normal file
185
src/components/admin/CardLoader.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
interface CardCounts {
|
||||
MTG?: number;
|
||||
POKEMON?: number;
|
||||
LORCANA?: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
interface LoadingResults {
|
||||
mtg?: number;
|
||||
pokemon?: number;
|
||||
lorcana?: number;
|
||||
}
|
||||
|
||||
const CardLoader: React.FC = () => {
|
||||
const { user, token } = useAuth();
|
||||
const [cardCounts, setCardCounts] = useState<CardCounts>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingResults, setLoadingResults] = useState<LoadingResults>({});
|
||||
const [selectedGame, setSelectedGame] = useState<string>('ALL');
|
||||
const [message, setMessage] = useState<string>('');
|
||||
|
||||
// Fetch current card counts
|
||||
const fetchCardCounts = async () => {
|
||||
try {
|
||||
const response = await fetch('https://tcg-vault.vercel.app/api/admin/load-cards', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCardCounts(data.counts || {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching card counts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCardCounts();
|
||||
}, []);
|
||||
|
||||
// Load cards from external APIs
|
||||
const loadCards = async (game: string) => {
|
||||
setLoading(true);
|
||||
setMessage(`Loading ${game} cards...`);
|
||||
setLoadingResults({});
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://tcg-vault.vercel.app/api/admin/load-cards?game=${game}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setLoadingResults(data.results || {});
|
||||
setMessage(`Successfully loaded cards! ${data.message}`);
|
||||
|
||||
// Refresh card counts
|
||||
setTimeout(() => {
|
||||
fetchCardCounts();
|
||||
}, 1000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setMessage(`Error loading cards: ${errorData.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading cards:', error);
|
||||
setMessage('Error loading cards. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadCards = () => {
|
||||
loadCards(selectedGame);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-6">Card Database Loader</h2>
|
||||
|
||||
{/* Current Card Counts */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-700 mb-3">Current Database Status</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">{cardCounts.MTG || 0}</div>
|
||||
<div className="text-sm text-blue-500">MTG Cards</div>
|
||||
</div>
|
||||
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-yellow-600">{cardCounts.POKEMON || 0}</div>
|
||||
<div className="text-sm text-yellow-500">Pokémon Cards</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600">{cardCounts.LORCANA || 0}</div>
|
||||
<div className="text-sm text-purple-500">Lorcana Cards</div>
|
||||
</div>
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">{cardCounts.total || 0}</div>
|
||||
<div className="text-sm text-green-500">Total Cards</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Load Cards Section */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-700 mb-3">Load Cards from External APIs</h3>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-4">
|
||||
<select
|
||||
value={selectedGame}
|
||||
onChange={(e) => setSelectedGame(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="ALL">All Games (MTG, Pokémon, Lorcana)</option>
|
||||
<option value="MTG">Magic: The Gathering</option>
|
||||
<option value="POKEMON">Pokémon</option>
|
||||
<option value="LORCANA">Disney Lorcana</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={handleLoadCards}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Load Cards'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Loading Results */}
|
||||
{Object.keys(loadingResults).length > 0 && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 className="font-semibold text-gray-700 mb-2">Loading Results:</h4>
|
||||
<div className="space-y-2">
|
||||
{loadingResults.mtg !== undefined && (
|
||||
<div className="text-blue-600">MTG: {loadingResults.mtg} cards loaded</div>
|
||||
)}
|
||||
{loadingResults.pokemon !== undefined && (
|
||||
<div className="text-yellow-600">Pokémon: {loadingResults.pokemon} cards loaded</div>
|
||||
)}
|
||||
{loadingResults.lorcana !== undefined && (
|
||||
<div className="text-purple-600">Lorcana: {loadingResults.lorcana} cards loaded</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message */}
|
||||
{message && (
|
||||
<div className={`mt-4 p-3 rounded-lg ${
|
||||
message.includes('Error')
|
||||
? 'bg-red-50 text-red-700 border border-red-200'
|
||||
: 'bg-green-50 text-green-700 border border-green-200'
|
||||
}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="text-lg font-semibold text-gray-700 mb-2">Instructions</h3>
|
||||
<ul className="text-sm text-gray-600 space-y-1">
|
||||
<li>• This will load cards from external APIs into your database</li>
|
||||
<li>• MTG cards come from Scryfall API</li>
|
||||
<li>• Pokémon cards come from Pokémon TCG API</li>
|
||||
<li>• Lorcana cards come from Lorcana API and Lorcast API</li>
|
||||
<li>• Loading may take several minutes for large datasets</li>
|
||||
<li>• Cards are deduplicated automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardLoader;
|
||||
|
|
@ -744,48 +744,56 @@ export const lorcanaService = {
|
|||
|
||||
export const cardDataService = {
|
||||
async searchCards(query: string, game?: string): Promise<Card[]> {
|
||||
const results: Card[] = [];
|
||||
|
||||
console.log(`🔍 Unified search: "${query}" for game: "${game}"`);
|
||||
console.log(`🔍 Database search: "${query}" for game: "${game}"`);
|
||||
|
||||
try {
|
||||
if (!game || game === 'MTG') {
|
||||
console.log('🔍 Searching MTG...');
|
||||
const mtgCards = await mtgService.searchCards(query);
|
||||
console.log(`✅ Found ${mtgCards.length} MTG cards`);
|
||||
results.push(...mtgCards);
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
limit: '50'
|
||||
});
|
||||
|
||||
if (game && game !== 'ALL') {
|
||||
params.append('game', game);
|
||||
}
|
||||
|
||||
if (!game || game === 'POKEMON') {
|
||||
console.log('🔍 Searching Pokémon...');
|
||||
const pokemonCards = await pokemonService.searchCards(query);
|
||||
console.log(`✅ Found ${pokemonCards.length} Pokémon cards`);
|
||||
results.push(...pokemonCards);
|
||||
const response = await fetch(`https://tcg-vault.vercel.app/api/cards/search?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Database search failed: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!game || game === 'YUGIOH') {
|
||||
console.log('🔍 Searching Yu-Gi-Oh!...');
|
||||
const yugiohCards = await yugiohService.searchCards(query);
|
||||
console.log(`✅ Found ${yugiohCards.length} Yu-Gi-Oh! cards`);
|
||||
results.push(...yugiohCards);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
console.log(`✅ Found ${data.data.length} cards from database`);
|
||||
return data.data.map((card: any) => ({
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
set_name: card.set_name,
|
||||
set_code: card.set_code,
|
||||
card_number: card.card_number,
|
||||
rarity: card.rarity,
|
||||
game: card.game,
|
||||
mana_cost: card.mana_cost,
|
||||
cmc: card.cmc,
|
||||
card_type: card.card_type,
|
||||
colors: card.colors || [],
|
||||
oracle_text: card.oracle_text,
|
||||
power: card.power,
|
||||
toughness: card.toughness,
|
||||
image_url: card.image_url,
|
||||
stock_image_url: card.stock_image_url,
|
||||
current_price: card.current_price,
|
||||
market_price: card.market_price,
|
||||
verified: card.verified,
|
||||
createdAt: card.createdAt,
|
||||
updatedAt: card.updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!game || game === 'LORCANA') {
|
||||
console.log('🔍 Searching Lorcana...');
|
||||
const lorcanaCards = await lorcanaService.searchCards(query);
|
||||
console.log(`✅ Found ${lorcanaCards.length} Lorcana cards`);
|
||||
results.push(...lorcanaCards);
|
||||
}
|
||||
|
||||
// Remove duplicates and sort by relevance
|
||||
const uniqueCards = results.filter((card, index, self) =>
|
||||
index === self.findIndex(c => c.name === card.name && c.game === card.game)
|
||||
);
|
||||
|
||||
console.log(`🎯 Total results: ${results.length}, Unique: ${uniqueCards.length}`);
|
||||
return uniqueCards;
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error in unified card search:', error);
|
||||
console.error('Error in database card search:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue