diff --git a/CARD_DATA_SOURCES.md b/CARD_DATA_SOURCES.md index f8a117a..0948ec1 100644 --- a/CARD_DATA_SOURCES.md +++ b/CARD_DATA_SOURCES.md @@ -49,11 +49,21 @@ const response = await fetch('https://db.ygoprodeck.com/api/v7/cardinfo.php?fnam const cards = await response.json(); ``` -### **4. Disney Lorcana** -**For Lorcana cards** -- **Status**: No official API available yet -- **Alternative**: Manual data entry or community data scraping -- **Future**: May have official API when game matures +### **4. Disney Lorcana (Lorcast API)** +**Best for Lorcana cards** +- **URL**: https://api.lorcast.com/v0 +- **Free**: No API key required +- **Rate Limit**: 10 requests/second (50-100ms delay recommended) +- **Coverage**: All Lorcana cards with images and pricing +- **Status**: Beta API (v0) - production safe + +**Example Usage**: +```javascript +// Search for a card +const response = await fetch('https://api.lorcast.com/v0/cards?q=name:Mickey%20Mouse'); +const data = await response.json(); +const cards = data.cards; +``` ## 🚀 **Implementation Strategy** @@ -115,6 +125,14 @@ const cards = await response.json(); - `oracle_text`: Effect text - `power`/`toughness`: ATK/DEF +#### **Disney Lorcana** +- `mana_cost`: Ink cost +- `cmc`: Ink cost value +- `card_type`: Card type +- `colors`: Ink colors +- `oracle_text`: Card text +- `power`/`toughness`: Strength/Willpower + ## 🛠️ **Usage Examples** ### **Search Cards from External APIs** @@ -149,6 +167,7 @@ node scripts/populate-card-database.js - **Scryfall**: 10 requests/second - **Pokémon API**: 5 requests/second - **Yu-Gi-Oh! API**: No strict limits +- **Lorcast (Lorcana)**: 10 requests/second (50-100ms delay) ### **Caching Strategy** - **Search results**: Cache for 5 minutes @@ -218,6 +237,7 @@ if (card) { - **Scryfall API Docs**: https://scryfall.com/docs/api - **Pokémon TCG API**: https://dev.pokemontcg.io/ - **Yu-Gi-Oh! API**: https://ygoprodeck.com/api-guide/ +- **Lorcast API**: https://lorcast.com/docs/api - **TCGPlayer API**: https://docs.tcgplayer.com/ ## 🚨 **Important Notes** diff --git a/scripts/populate-card-database.js b/scripts/populate-card-database.js index 4a4ccfa..233405f 100644 --- a/scripts/populate-card-database.js +++ b/scripts/populate-card-database.js @@ -50,6 +50,18 @@ const POPULAR_CARDS = { 'Elemental HERO Neos', 'Stardust Dragon', 'Number 39: Utopia' + ], + LORCANA: [ + 'Mickey Mouse - Brave Little Tailor', + 'Elsa - Snow Queen', + 'Be Prepared', + 'Mickey Mouse - Steamboat Pilot', + 'Maleficent - Dragon Form', + 'Aurora - Briar Rose', + 'Stitch - Experiment 626', + 'Aladdin - Street Rat', + 'Cinderella - Ballroom Sensation', + 'Belle - Strange but Special' ] }; @@ -77,6 +89,7 @@ class RateLimiter { const scryfallLimiter = new RateLimiter(10, 1000); // 10 requests per second const pokemonLimiter = new RateLimiter(5, 1000); // 5 requests per second +const lorcastLimiter = new RateLimiter(10, 1000); // 10 requests per second (50-100ms delay) // API Services const mtgService = { @@ -217,6 +230,55 @@ const yugiohService = { } }; +const lorcanaService = { + async getCardByName(name) { + await lorcastLimiter.waitForSlot(); + + try { + const response = await fetch( + `https://api.lorcast.com/v0/cards?q=name:${encodeURIComponent(name)}` + ); + + if (!response.ok) { + console.log(`❌ Lorcana card not found: ${name}`); + return null; + } + + const data = await response.json(); + + if (data.cards.length === 0) { + console.log(`❌ Lorcana card not found: ${name}`); + return null; + } + + const card = data.cards[0]; + + return { + name: card.name, + game: 'LORCANA', + set_name: card.set?.name || '', + set_code: card.set?.code || '', + card_number: card.number, + rarity: card.rarity, + mana_cost: card.cost?.toString(), + cmc: card.cost, + card_type: card.type, + colors: card.colors || [], + oracle_text: card.text || '', + power: card.strength?.toString(), + toughness: card.willpower?.toString(), + image_url: card.image_url, + stock_image_url: card.image_url, + current_price: card.price?.market || null, + market_price: card.price?.low || null, + }; + } catch (error) { + console.error(`❌ Error fetching Lorcana card ${name}:`, error.message); + return null; + } + } +}; + // Database API const databaseAPI = { async addCard(cardData) { @@ -280,6 +342,9 @@ async function populateDatabase() { case 'YUGIOH': cardData = await yugiohService.getCardByName(cardName); break; + case 'LORCANA': + cardData = await lorcanaService.getCardByName(cardName); + break; } if (cardData) { diff --git a/src/components/cards/CardDatabaseBrowser.tsx b/src/components/cards/CardDatabaseBrowser.tsx index 042a384..da7fc6e 100644 --- a/src/components/cards/CardDatabaseBrowser.tsx +++ b/src/components/cards/CardDatabaseBrowser.tsx @@ -222,6 +222,16 @@ const CardDatabaseBrowser: React.FC = ({ > Yu-Gi-Oh! + {/* Content */} diff --git a/src/services/cardDataSources.ts b/src/services/cardDataSources.ts index b165a83..59e9fd4 100644 --- a/src/services/cardDataSources.ts +++ b/src/services/cardDataSources.ts @@ -4,6 +4,8 @@ import type { Card } from '../types'; const SCRYFALL_BASE_URL = 'https://api.scryfall.com'; const POKEMON_API_BASE_URL = 'https://api.pokemontcg.io/v2'; const YUGIOH_API_BASE_URL = 'https://db.ygoprodeck.com/api/v7'; +const LORCAST_API_BASE_URL = 'https://api.lorcast.com/v0'; + // Rate limiting utilities class RateLimiter { @@ -33,6 +35,7 @@ class RateLimiter { // Rate limiters for each API const scryfallLimiter = new RateLimiter(10, 1000); // 10 requests per second const pokemonLimiter = new RateLimiter(5, 1000); // 5 requests per second +const lorcastLimiter = new RateLimiter(10, 1000); // 10 requests per second (50-100ms delay) // ============================================================================ // MAGIC: THE GATHERING (Scryfall API) @@ -274,6 +277,151 @@ export const yugiohService = { } }; +// ============================================================================ +// DISNEY LORCANA API (Lorcast) +// ============================================================================ + +export const lorcanaService = { + async searchCards(query: string): Promise { + await lorcastLimiter.waitForSlot(); + + try { + const response = await fetch( + `${LORCAST_API_BASE_URL}/cards?q=${encodeURIComponent(query)}` + ); + + if (!response.ok) { + throw new Error(`Lorcast API error: ${response.status}`); + } + + const data = await response.json(); + + return data.cards.map((card: any) => ({ + id: card.id, + name: card.name, + set_name: card.set?.name || '', + set_code: card.set?.code || '', + card_number: card.number, + rarity: card.rarity, + game: 'LORCANA', + mana_cost: card.cost?.toString(), + cmc: card.cost, + card_type: card.type, + colors: card.colors || [], + oracle_text: card.text || '', + power: card.strength?.toString(), + toughness: card.willpower?.toString(), + image_url: card.image_url, + stock_image_url: card.image_url, + current_price: card.price?.market || undefined, + market_price: card.price?.low || undefined, + verified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + })); + } catch (error) { + console.error('Error fetching Lorcana cards:', error); + return []; + } + }, + + async getCardByName(name: string): Promise { + await lorcastLimiter.waitForSlot(); + + try { + const response = await fetch( + `${LORCAST_API_BASE_URL}/cards?q=name:${encodeURIComponent(name)}` + ); + + if (!response.ok) { + return null; + } + + const data = await response.json(); + + if (data.cards.length === 0) { + return null; + } + + const card = data.cards[0]; + + return { + id: card.id, + name: card.name, + set_name: card.set?.name || '', + set_code: card.set?.code || '', + card_number: card.number, + rarity: card.rarity, + game: 'LORCANA', + mana_cost: card.cost?.toString(), + cmc: card.cost, + card_type: card.type, + colors: card.colors || [], + oracle_text: card.text || '', + power: card.strength?.toString(), + toughness: card.willpower?.toString(), + image_url: card.image_url, + stock_image_url: card.image_url, + current_price: card.price?.market || undefined, + market_price: card.price?.low || undefined, + verified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + } catch (error) { + console.error('Error fetching Lorcana card:', error); + return null; + } + }, + + async getRandomCards(count = 20): Promise { + await lorcastLimiter.waitForSlot(); + + try { + // Get all cards and randomly select + const response = await fetch(`${LORCAST_API_BASE_URL}/cards`); + + if (!response.ok) { + throw new Error(`Lorcast API error: ${response.status}`); + } + + const data = await response.json(); + const allCards = data.cards || []; + + // Randomly select cards + const shuffled = allCards.sort(() => 0.5 - Math.random()); + const selectedCards = shuffled.slice(0, count); + + return selectedCards.map((card: any) => ({ + id: card.id, + name: card.name, + set_name: card.set?.name || '', + set_code: card.set?.code || '', + card_number: card.number, + rarity: card.rarity, + game: 'LORCANA', + mana_cost: card.cost?.toString(), + cmc: card.cost, + card_type: card.type, + colors: card.colors || [], + oracle_text: card.text || '', + power: card.strength?.toString(), + toughness: card.willpower?.toString(), + image_url: card.image_url, + stock_image_url: card.image_url, + current_price: card.price?.market || undefined, + market_price: card.price?.low || undefined, + verified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + })); + } catch (error) { + console.error('Error fetching random Lorcana cards:', error); + return []; + } + } +}; + // ============================================================================ // UNIFIED CARD SEARCH // ============================================================================ @@ -298,6 +446,11 @@ export const cardDataService = { results.push(...yugiohCards); } + if (!game || game === 'LORCANA') { + const lorcanaCards = await lorcanaService.searchCards(query); + 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) @@ -324,6 +477,10 @@ export const cardDataService = { return await yugiohService.getCardByName(name); } + if (game === 'LORCANA') { + return await lorcanaService.getCardByName(name); + } + // Try all games if no specific game is specified const mtgCard = await mtgService.getCardByName(name); if (mtgCard) return mtgCard; @@ -334,6 +491,9 @@ export const cardDataService = { const yugiohCard = await yugiohService.getCardByName(name); if (yugiohCard) return yugiohCard; + const lorcanaCard = await lorcanaService.getCardByName(name); + if (lorcanaCard) return lorcanaCard; + return null; } catch (error) { console.error('Error in unified card search by name:', error); @@ -346,10 +506,15 @@ export const cardDataService = { try { if (!game || game === 'MTG') { - const mtgCards = await mtgService.getRandomCards(Math.ceil(count / 3)); + const mtgCards = await mtgService.getRandomCards(Math.ceil(count / 4)); results.push(...mtgCards); } + if (!game || game === 'LORCANA') { + const lorcanaCards = await lorcanaService.getRandomCards(Math.ceil(count / 4)); + results.push(...lorcanaCards); + } + // Note: Pokémon and Yu-Gi-Oh! APIs don't have random card endpoints // You could implement random selection from popular cards lists