Add Disney Lorcana support via Lorcast API - Complete integration with search, browse, and database population
This commit is contained in:
parent
2f215da3ca
commit
e29728e2be
4 changed files with 266 additions and 6 deletions
|
|
@ -49,11 +49,21 @@ const response = await fetch('https://db.ygoprodeck.com/api/v7/cardinfo.php?fnam
|
||||||
const cards = await response.json();
|
const cards = await response.json();
|
||||||
```
|
```
|
||||||
|
|
||||||
### **4. Disney Lorcana**
|
### **4. Disney Lorcana (Lorcast API)**
|
||||||
**For Lorcana cards**
|
**Best for Lorcana cards**
|
||||||
- **Status**: No official API available yet
|
- **URL**: https://api.lorcast.com/v0
|
||||||
- **Alternative**: Manual data entry or community data scraping
|
- **Free**: No API key required
|
||||||
- **Future**: May have official API when game matures
|
- **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**
|
## 🚀 **Implementation Strategy**
|
||||||
|
|
||||||
|
|
@ -115,6 +125,14 @@ const cards = await response.json();
|
||||||
- `oracle_text`: Effect text
|
- `oracle_text`: Effect text
|
||||||
- `power`/`toughness`: ATK/DEF
|
- `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**
|
## 🛠️ **Usage Examples**
|
||||||
|
|
||||||
### **Search Cards from External APIs**
|
### **Search Cards from External APIs**
|
||||||
|
|
@ -149,6 +167,7 @@ node scripts/populate-card-database.js
|
||||||
- **Scryfall**: 10 requests/second
|
- **Scryfall**: 10 requests/second
|
||||||
- **Pokémon API**: 5 requests/second
|
- **Pokémon API**: 5 requests/second
|
||||||
- **Yu-Gi-Oh! API**: No strict limits
|
- **Yu-Gi-Oh! API**: No strict limits
|
||||||
|
- **Lorcast (Lorcana)**: 10 requests/second (50-100ms delay)
|
||||||
|
|
||||||
### **Caching Strategy**
|
### **Caching Strategy**
|
||||||
- **Search results**: Cache for 5 minutes
|
- **Search results**: Cache for 5 minutes
|
||||||
|
|
@ -218,6 +237,7 @@ if (card) {
|
||||||
- **Scryfall API Docs**: https://scryfall.com/docs/api
|
- **Scryfall API Docs**: https://scryfall.com/docs/api
|
||||||
- **Pokémon TCG API**: https://dev.pokemontcg.io/
|
- **Pokémon TCG API**: https://dev.pokemontcg.io/
|
||||||
- **Yu-Gi-Oh! API**: https://ygoprodeck.com/api-guide/
|
- **Yu-Gi-Oh! API**: https://ygoprodeck.com/api-guide/
|
||||||
|
- **Lorcast API**: https://lorcast.com/docs/api
|
||||||
- **TCGPlayer API**: https://docs.tcgplayer.com/
|
- **TCGPlayer API**: https://docs.tcgplayer.com/
|
||||||
|
|
||||||
## 🚨 **Important Notes**
|
## 🚨 **Important Notes**
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,18 @@ const POPULAR_CARDS = {
|
||||||
'Elemental HERO Neos',
|
'Elemental HERO Neos',
|
||||||
'Stardust Dragon',
|
'Stardust Dragon',
|
||||||
'Number 39: Utopia'
|
'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 scryfallLimiter = new RateLimiter(10, 1000); // 10 requests per second
|
||||||
const pokemonLimiter = new RateLimiter(5, 1000); // 5 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
|
// API Services
|
||||||
const mtgService = {
|
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
|
// Database API
|
||||||
const databaseAPI = {
|
const databaseAPI = {
|
||||||
async addCard(cardData) {
|
async addCard(cardData) {
|
||||||
|
|
@ -280,6 +342,9 @@ async function populateDatabase() {
|
||||||
case 'YUGIOH':
|
case 'YUGIOH':
|
||||||
cardData = await yugiohService.getCardByName(cardName);
|
cardData = await yugiohService.getCardByName(cardName);
|
||||||
break;
|
break;
|
||||||
|
case 'LORCANA':
|
||||||
|
cardData = await lorcanaService.getCardByName(cardName);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardData) {
|
if (cardData) {
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,16 @@ const CardDatabaseBrowser: React.FC<CardDatabaseBrowserProps> = ({
|
||||||
>
|
>
|
||||||
Yu-Gi-Oh!
|
Yu-Gi-Oh!
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedGame('LORCANA')}
|
||||||
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
|
||||||
|
selectedGame === 'LORCANA'
|
||||||
|
? 'bg-purple-500 text-white'
|
||||||
|
: 'bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Disney Lorcana
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import type { Card } from '../types';
|
||||||
const SCRYFALL_BASE_URL = 'https://api.scryfall.com';
|
const SCRYFALL_BASE_URL = 'https://api.scryfall.com';
|
||||||
const POKEMON_API_BASE_URL = 'https://api.pokemontcg.io/v2';
|
const POKEMON_API_BASE_URL = 'https://api.pokemontcg.io/v2';
|
||||||
const YUGIOH_API_BASE_URL = 'https://db.ygoprodeck.com/api/v7';
|
const YUGIOH_API_BASE_URL = 'https://db.ygoprodeck.com/api/v7';
|
||||||
|
const LORCAST_API_BASE_URL = 'https://api.lorcast.com/v0';
|
||||||
|
|
||||||
|
|
||||||
// Rate limiting utilities
|
// Rate limiting utilities
|
||||||
class RateLimiter {
|
class RateLimiter {
|
||||||
|
|
@ -33,6 +35,7 @@ class RateLimiter {
|
||||||
// Rate limiters for each API
|
// Rate limiters for each API
|
||||||
const scryfallLimiter = new RateLimiter(10, 1000); // 10 requests per second
|
const scryfallLimiter = new RateLimiter(10, 1000); // 10 requests per second
|
||||||
const pokemonLimiter = new RateLimiter(5, 1000); // 5 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)
|
// MAGIC: THE GATHERING (Scryfall API)
|
||||||
|
|
@ -274,6 +277,151 @@ export const yugiohService = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DISNEY LORCANA API (Lorcast)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const lorcanaService = {
|
||||||
|
async searchCards(query: string): Promise<Card[]> {
|
||||||
|
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<Card | null> {
|
||||||
|
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<Card[]> {
|
||||||
|
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
|
// UNIFIED CARD SEARCH
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -298,6 +446,11 @@ export const cardDataService = {
|
||||||
results.push(...yugiohCards);
|
results.push(...yugiohCards);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!game || game === 'LORCANA') {
|
||||||
|
const lorcanaCards = await lorcanaService.searchCards(query);
|
||||||
|
results.push(...lorcanaCards);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove duplicates and sort by relevance
|
// Remove duplicates and sort by relevance
|
||||||
const uniqueCards = results.filter((card, index, self) =>
|
const uniqueCards = results.filter((card, index, self) =>
|
||||||
index === self.findIndex(c => c.name === card.name && c.game === card.game)
|
index === self.findIndex(c => c.name === card.name && c.game === card.game)
|
||||||
|
|
@ -324,6 +477,10 @@ export const cardDataService = {
|
||||||
return await yugiohService.getCardByName(name);
|
return await yugiohService.getCardByName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (game === 'LORCANA') {
|
||||||
|
return await lorcanaService.getCardByName(name);
|
||||||
|
}
|
||||||
|
|
||||||
// Try all games if no specific game is specified
|
// Try all games if no specific game is specified
|
||||||
const mtgCard = await mtgService.getCardByName(name);
|
const mtgCard = await mtgService.getCardByName(name);
|
||||||
if (mtgCard) return mtgCard;
|
if (mtgCard) return mtgCard;
|
||||||
|
|
@ -334,6 +491,9 @@ export const cardDataService = {
|
||||||
const yugiohCard = await yugiohService.getCardByName(name);
|
const yugiohCard = await yugiohService.getCardByName(name);
|
||||||
if (yugiohCard) return yugiohCard;
|
if (yugiohCard) return yugiohCard;
|
||||||
|
|
||||||
|
const lorcanaCard = await lorcanaService.getCardByName(name);
|
||||||
|
if (lorcanaCard) return lorcanaCard;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in unified card search by name:', error);
|
console.error('Error in unified card search by name:', error);
|
||||||
|
|
@ -346,10 +506,15 @@ export const cardDataService = {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!game || game === 'MTG') {
|
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);
|
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
|
// Note: Pokémon and Yu-Gi-Oh! APIs don't have random card endpoints
|
||||||
// You could implement random selection from popular cards lists
|
// You could implement random selection from popular cards lists
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue