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();
|
||||
```
|
||||
|
||||
### **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**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -222,6 +222,16 @@ const CardDatabaseBrowser: React.FC<CardDatabaseBrowserProps> = ({
|
|||
>
|
||||
Yu-Gi-Oh!
|
||||
</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>
|
||||
|
||||
{/* Content */}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
// ============================================================================
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue