- Redesigned card display with 2.5:3.5 aspect ratio and image-only view - Added infinite scroll to replace pagination - Implemented authentic card back placeholders for MTG, Pokemon, and Lorcana - Added rarity-based particle effects with tiered intensity (mythic/enchanted/rare/uncommon) - Enhanced hover details panel with structured card information - Fixed search functionality with debouncing and Enter key support - Improved filter system with working TCG, rarity, set, and price filters - Added favorite system for cards in both hover and detail views - Updated card detail page with comprehensive metadata and actions - Fixed API filtering with proper Vercel Postgres implementation - Added particle animations and rarity glow effects - Improved overall UX with better visual hierarchy and interactions
340 lines
No EOL
13 KiB
JavaScript
340 lines
No EOL
13 KiB
JavaScript
import fetch from 'node-fetch';
|
|
import fs from 'fs';
|
|
|
|
// Popular and recent sets for initial import
|
|
const POPULAR_SETS = {
|
|
mtg: [
|
|
{ code: 'neo', name: 'Kamigawa: Neon Dynasty' },
|
|
{ code: 'vow', name: 'Innistrad: Crimson Vow' },
|
|
{ code: 'mid', name: 'Innistrad: Midnight Hunt' },
|
|
{ code: 'afr', name: 'Adventures in the Forgotten Realms' },
|
|
{ code: 'stx', name: 'Strixhaven: School of Mages' },
|
|
{ code: 'khm', name: 'Kaldheim' },
|
|
{ code: 'znr', name: 'Zendikar Rising' },
|
|
{ code: 'iko', name: 'Ikoria: Lair of Behemoths' },
|
|
{ code: 'thb', name: 'Theros Beyond Death' },
|
|
{ code: 'eld', name: 'Throne of Eldraine' },
|
|
{ code: 'm20', name: 'Core Set 2020' },
|
|
{ code: 'war', name: 'War of the Spark' },
|
|
{ code: 'rna', name: 'Ravnica Allegiance' },
|
|
{ code: 'grn', name: 'Guilds of Ravnica' },
|
|
{ code: 'm19', name: 'Core Set 2019' },
|
|
{ code: 'dom', name: 'Dominaria' },
|
|
{ code: 'rix', name: 'Rivals of Ixalan' },
|
|
{ code: 'xln', name: 'Ixalan' },
|
|
{ code: 'hou', name: 'Hour of Devastation' },
|
|
{ code: 'akh', name: 'Amonkhet' },
|
|
{ code: 'aer', name: 'Aether Revolt' },
|
|
{ code: 'kld', name: 'Kaladesh' },
|
|
{ code: 'emn', name: 'Eldritch Moon' },
|
|
{ code: 'soi', name: 'Shadows over Innistrad' },
|
|
{ code: 'ogw', name: 'Oath of the Gatewatch' },
|
|
{ code: 'bfz', name: 'Battle for Zendikar' },
|
|
{ code: 'ori', name: 'Magic Origins' },
|
|
{ code: 'dtk', name: 'Dragons of Tarkir' },
|
|
{ code: 'frf', name: 'Fate Reforged' },
|
|
{ code: 'ktk', name: 'Khans of Tarkir' },
|
|
{ code: 'm15', name: 'Magic 2015' },
|
|
{ code: 'jou', name: 'Journey into Nyx' },
|
|
{ code: 'bng', name: 'Born of the Gods' },
|
|
{ code: 'ths', name: 'Theros' },
|
|
{ code: 'm14', name: 'Magic 2014' },
|
|
{ code: 'dgm', name: 'Dragon\'s Maze' },
|
|
{ code: 'gtc', name: 'Gatecrash' },
|
|
{ code: 'rtr', name: 'Return to Ravnica' },
|
|
{ code: 'm13', name: 'Magic 2013' },
|
|
{ code: 'avr', name: 'Avacyn Restored' },
|
|
{ code: 'dka', name: 'Dark Ascension' },
|
|
{ code: 'isd', name: 'Innistrad' },
|
|
{ code: 'm12', name: 'Magic 2012' },
|
|
{ code: 'nph', name: 'New Phyrexia' },
|
|
{ code: 'mbs', name: 'Mirrodin Besieged' },
|
|
{ code: 'som', name: 'Scars of Mirrodin' },
|
|
{ code: 'm11', name: 'Magic 2011' },
|
|
{ code: 'roe', name: 'Rise of the Eldrazi' },
|
|
{ code: 'wwk', name: 'Worldwake' },
|
|
{ code: 'zen', name: 'Zendikar' },
|
|
{ code: 'm10', name: 'Magic 2010' },
|
|
{ code: 'arb', name: 'Alara Reborn' },
|
|
{ code: 'con', name: 'Conflux' },
|
|
{ code: 'ala', name: 'Shards of Alara' },
|
|
{ code: 'eve', name: 'Eventide' },
|
|
{ code: 'shm', name: 'Shadowmoor' },
|
|
{ code: 'mor', name: 'Morningtide' },
|
|
{ code: 'lrw', name: 'Lorwyn' },
|
|
{ code: 'fut', name: 'Future Sight' },
|
|
{ code: 'plc', name: 'Planar Chaos' },
|
|
{ code: 'tsp', name: 'Time Spiral' },
|
|
{ code: 'csp', name: 'Coldsnap' },
|
|
{ code: 'dis', name: 'Dissension' },
|
|
{ code: 'gpt', name: 'Guildpact' },
|
|
{ code: 'rav', name: 'Ravnica: City of Guilds' },
|
|
{ code: '9ed', name: 'Ninth Edition' },
|
|
{ code: 'sok', name: 'Saviors of Kamigawa' },
|
|
{ code: 'bet', name: 'Betrayers of Kamigawa' },
|
|
{ code: 'chk', name: 'Champions of Kamigawa' },
|
|
{ code: '5dn', name: 'Fifth Dawn' },
|
|
{ code: 'dst', name: 'Darksteel' },
|
|
{ code: 'mrd', name: 'Mirrodin' },
|
|
{ code: '8ed', name: 'Eighth Edition' },
|
|
{ code: 'scg', name: 'Scourge' },
|
|
{ code: 'lgn', name: 'Legions' },
|
|
{ code: 'ons', name: 'Onslaught' },
|
|
{ code: '7ed', name: 'Seventh Edition' },
|
|
{ code: 'apc', name: 'Apocalypse' },
|
|
{ code: 'pls', name: 'Planeshift' },
|
|
{ code: 'inv', name: 'Invasion' },
|
|
{ code: '6ed', name: 'Classic Sixth Edition' },
|
|
{ code: 'pcy', name: 'Prophecy' },
|
|
{ code: 'nem', name: 'Nemesis' },
|
|
{ code: 'mmq', name: 'Mercadian Masques' },
|
|
{ code: 'uds', name: 'Urza\'s Destiny' },
|
|
{ code: 'ulg', name: 'Urza\'s Legacy' },
|
|
{ code: 'usg', name: 'Urza\'s Saga' },
|
|
{ code: '5ed', name: 'Fifth Edition' },
|
|
{ code: 'exo', name: 'Exodus' },
|
|
{ code: 'sth', name: 'Stronghold' },
|
|
{ code: 'tmp', name: 'Tempest' },
|
|
{ code: '4ed', name: 'Fourth Edition' },
|
|
{ code: 'vis', name: 'Visions' },
|
|
{ code: 'all', name: 'Alliances' },
|
|
{ code: 'ice', name: 'Ice Age' },
|
|
{ code: '3ed', name: 'Revised Edition' },
|
|
{ code: 'fem', name: 'Fallen Empires' },
|
|
{ code: 'drk', name: 'The Dark' },
|
|
{ code: 'leg', name: 'Legends' },
|
|
{ code: 'atq', name: 'Antiquities' },
|
|
{ code: 'arn', name: 'Arabian Nights' },
|
|
{ code: 'leb', name: 'Limited Edition Beta' },
|
|
{ code: 'lea', name: 'Limited Edition Alpha' }
|
|
],
|
|
pokemon: [
|
|
{ code: 'sv1', name: 'Scarlet & Violet' },
|
|
{ code: 'sv2', name: 'Paldea Evolved' },
|
|
{ code: 'sv3', name: 'Obsidian Flames' },
|
|
{ code: 'sv4', name: '151' },
|
|
{ code: 'sv5', name: 'Paradox Rift' },
|
|
{ code: 'sv6', name: 'Paldean Fates' },
|
|
{ code: 'sv7', name: 'Temporal Forces' },
|
|
{ code: 'swsh1', name: 'Sword & Shield' },
|
|
{ code: 'swsh2', name: 'Rebel Clash' },
|
|
{ code: 'swsh3', name: 'Darkness Ablaze' },
|
|
{ code: 'swsh4', name: 'Champions Path' },
|
|
{ code: 'swsh5', name: 'Vivid Voltage' },
|
|
{ code: 'swsh6', name: 'Shining Fates' },
|
|
{ code: 'swsh7', name: 'Battle Styles' },
|
|
{ code: 'swsh8', name: 'Chilling Reign' },
|
|
{ code: 'swsh9', name: 'Evolving Skies' },
|
|
{ code: 'swsh10', name: 'Celebrations' },
|
|
{ code: 'swsh11', name: 'Fusion Strike' },
|
|
{ code: 'swsh12', name: 'Brilliant Stars' },
|
|
{ code: 'swsh12', name: 'Astral Radiance' },
|
|
{ code: 'swsh12', name: 'Pokemon GO' },
|
|
{ code: 'swsh12', name: 'Lost Origin' },
|
|
{ code: 'swsh12', name: 'Silver Tempest' },
|
|
{ code: 'swsh12', name: 'Crown Zenith' },
|
|
{ code: 'sm1', name: 'Sun & Moon' },
|
|
{ code: 'sm2', name: 'Guardians Rising' },
|
|
{ code: 'sm3', name: 'Burning Shadows' },
|
|
{ code: 'sm4', name: 'Crimson Invasion' },
|
|
{ code: 'sm5', name: 'Ultra Prism' },
|
|
{ code: 'sm6', name: 'Forbidden Light' },
|
|
{ code: 'sm7', name: 'Celestial Storm' },
|
|
{ code: 'sm8', name: 'Dragon Majesty' },
|
|
{ code: 'sm9', name: 'Lost Thunder' },
|
|
{ code: 'sm10', name: 'Team Up' },
|
|
{ code: 'sm11', name: 'Detective Pikachu' },
|
|
{ code: 'sm12', name: 'Unbroken Bonds' },
|
|
{ code: 'sm115', name: 'Unified Minds' },
|
|
{ code: 'sm116', name: 'Hidden Fates' },
|
|
{ code: 'sm117', name: 'Cosmic Eclipse' },
|
|
{ code: 'xy1', name: 'XY' },
|
|
{ code: 'xy2', name: 'Flashfire' },
|
|
{ code: 'xy3', name: 'Furious Fists' },
|
|
{ code: 'xy4', name: 'Phantom Forces' },
|
|
{ code: 'xy5', name: 'Primal Clash' },
|
|
{ code: 'xy6', name: 'Double Crisis' },
|
|
{ code: 'xy7', name: 'Roaring Skies' },
|
|
{ code: 'xy8', name: 'Ancient Origins' },
|
|
{ code: 'xy9', name: 'BREAKthrough' },
|
|
{ code: 'xy10', name: 'BREAKpoint' },
|
|
{ code: 'xy11', name: 'Fates Collide' },
|
|
{ code: 'xy12', name: 'Steam Siege' },
|
|
{ code: 'xy12', name: 'Evolutions' },
|
|
{ code: 'bw1', name: 'Black & White' },
|
|
{ code: 'bw2', name: 'Emerging Powers' },
|
|
{ code: 'bw3', name: 'Noble Victories' },
|
|
{ code: 'bw4', name: 'Next Destinies' },
|
|
{ code: 'bw5', name: 'Dark Explorers' },
|
|
{ code: 'bw6', name: 'Dragons Exalted' },
|
|
{ code: 'bw7', name: 'Boundaries Crossed' },
|
|
{ code: 'bw8', name: 'Plasma Storm' },
|
|
{ code: 'bw9', name: 'Plasma Freeze' },
|
|
{ code: 'bw10', name: 'Plasma Blast' },
|
|
{ code: 'bw11', name: 'Legendary Treasures' },
|
|
{ code: 'hgss1', name: 'HeartGold & SoulSilver' },
|
|
{ code: 'hgss2', name: 'Unleashed' },
|
|
{ code: 'hgss3', name: 'Undaunted' },
|
|
{ code: 'hgss4', name: 'Triumphant' },
|
|
{ code: 'col1', name: 'Call of Legends' },
|
|
{ code: 'dpp1', name: 'Diamond & Pearl' },
|
|
{ code: 'dpp2', name: 'Mysterious Treasures' },
|
|
{ code: 'dpp3', name: 'Secret Wonders' },
|
|
{ code: 'dpp4', name: 'Great Encounters' },
|
|
{ code: 'dpp5', name: 'Majestic Dawn' },
|
|
{ code: 'dpp6', name: 'Legends Awakened' },
|
|
{ code: 'dpp7', name: 'Stormfront' },
|
|
{ code: 'dpp8', name: 'Platinum' },
|
|
{ code: 'dpp9', name: 'Rising Rivals' },
|
|
{ code: 'dpp10', name: 'Supreme Victors' },
|
|
{ code: 'dpp11', name: 'Arceus' },
|
|
{ code: 'ex1', name: 'Ruby & Sapphire' },
|
|
{ code: 'ex2', name: 'Sandstorm' },
|
|
{ code: 'ex3', name: 'Dragon' },
|
|
{ code: 'ex4', name: 'Team Magma vs Team Aqua' },
|
|
{ code: 'ex5', name: 'Hidden Legends' },
|
|
{ code: 'ex6', name: 'FireRed & LeafGreen' },
|
|
{ code: 'ex7', name: 'Team Rocket Returns' },
|
|
{ code: 'ex8', name: 'Deoxys' },
|
|
{ code: 'ex9', name: 'Emerald' },
|
|
{ code: 'ex10', name: 'Unseen Forces' },
|
|
{ code: 'ex11', name: 'Delta Species' },
|
|
{ code: 'ex12', name: 'Legend Maker' },
|
|
{ code: 'ex13', name: 'Holon Phantoms' },
|
|
{ code: 'ex14', name: 'Crystal Guardians' },
|
|
{ code: 'ex15', name: 'Dragon Frontiers' },
|
|
{ code: 'ex16', name: 'Power Keepers' },
|
|
{ code: 'neo1', name: 'Neo Genesis' },
|
|
{ code: 'neo2', name: 'Neo Discovery' },
|
|
{ code: 'neo3', name: 'Neo Revelation' },
|
|
{ code: 'neo4', name: 'Neo Destiny' },
|
|
{ code: 'gym1', name: 'Gym Heroes' },
|
|
{ code: 'gym2', name: 'Gym Challenge' },
|
|
{ code: 'base1', name: 'Base Set' },
|
|
{ code: 'base2', name: 'Base Set 2' },
|
|
{ code: 'base3', name: 'Base Set 3' }
|
|
],
|
|
lorcana: [
|
|
{ code: 'tfc', name: 'The First Chapter' },
|
|
{ code: 'rotf', name: 'Rise of the Floodborn' },
|
|
{ code: 'ink', name: 'Into the Inklands' }
|
|
]
|
|
};
|
|
|
|
async function importSet(game, setCode, setName) {
|
|
try {
|
|
console.log(`Importing ${game} set: ${setName} (${setCode})`);
|
|
|
|
const response = await fetch(`http://localhost:3000/api/cards/import-${game.toLowerCase()}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
setCode: setCode,
|
|
setName: setName
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
console.log(`✅ Successfully imported ${result.importedCount} cards from ${setName}`);
|
|
return { success: true, importedCount: result.importedCount };
|
|
} else {
|
|
console.log(`❌ Failed to import ${setName}: ${result.error}`);
|
|
return { success: false, error: result.error };
|
|
}
|
|
} catch (error) {
|
|
console.log(`❌ Error importing ${setName}: ${error.message}`);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
async function importPopularSets() {
|
|
console.log('🚀 Starting import of popular TCG sets...\n');
|
|
|
|
const results = {
|
|
mtg: { total: 0, successful: 0, failed: 0, sets: [] },
|
|
pokemon: { total: 0, successful: 0, failed: 0, sets: [] },
|
|
lorcana: { total: 0, successful: 0, failed: 0, sets: [] }
|
|
};
|
|
|
|
// Import Magic: The Gathering sets
|
|
console.log('🔮 Importing Magic: The Gathering sets...');
|
|
results.mtg.total = POPULAR_SETS.mtg.length;
|
|
|
|
for (const set of POPULAR_SETS.mtg) {
|
|
const result = await importSet('mtg', set.code, set.name);
|
|
results.mtg.sets.push({ ...set, ...result });
|
|
|
|
if (result.success) {
|
|
results.mtg.successful++;
|
|
} else {
|
|
results.mtg.failed++;
|
|
}
|
|
|
|
// Add a small delay to avoid overwhelming the API
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
// Import Pokemon sets
|
|
console.log('\n⚡ Importing Pokemon sets...');
|
|
results.pokemon.total = POPULAR_SETS.pokemon.length;
|
|
|
|
for (const set of POPULAR_SETS.pokemon) {
|
|
const result = await importSet('pokemon', set.code, set.name);
|
|
results.pokemon.sets.push({ ...set, ...result });
|
|
|
|
if (result.success) {
|
|
results.pokemon.successful++;
|
|
} else {
|
|
results.pokemon.failed++;
|
|
}
|
|
|
|
// Add a small delay to avoid overwhelming the API
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
// Import Lorcana sets
|
|
console.log('\n✨ Importing Disney Lorcana sets...');
|
|
results.lorcana.total = POPULAR_SETS.lorcana.length;
|
|
|
|
for (const set of POPULAR_SETS.lorcana) {
|
|
const result = await importSet('lorcana', set.code, set.name);
|
|
results.lorcana.sets.push({ ...set, ...result });
|
|
|
|
if (result.success) {
|
|
results.lorcana.successful++;
|
|
} else {
|
|
results.lorcana.failed++;
|
|
}
|
|
|
|
// Add a small delay to avoid overwhelming the API
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
// Print summary
|
|
console.log('\n📊 Popular Sets Import Summary:');
|
|
console.log('================================');
|
|
|
|
Object.entries(results).forEach(([game, stats]) => {
|
|
console.log(`\n${game.toUpperCase()}:`);
|
|
console.log(` Total sets: ${stats.total}`);
|
|
console.log(` Successful: ${stats.successful}`);
|
|
console.log(` Failed: ${stats.failed}`);
|
|
console.log(` Success rate: ${((stats.successful / stats.total) * 100).toFixed(1)}%`);
|
|
});
|
|
|
|
// Save detailed results to file
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const filename = `popular-sets-import-results-${timestamp}.json`;
|
|
|
|
fs.writeFileSync(filename, JSON.stringify(results, null, 2));
|
|
console.log(`\n📄 Detailed results saved to: ${filename}`);
|
|
|
|
console.log('\n🎉 Popular sets import completed!');
|
|
}
|
|
|
|
// Run the import
|
|
importPopularSets().catch(console.error);
|