import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import dynamic from 'next/dynamic'; import Layout from '../../components/Layout'; import AdminProtected from '../../components/AdminProtected'; import { Button } from '../../components/ui'; const CardImport = () => { const router = useRouter(); const [importType, setImportType] = useState('mtg'); const [setCode, setSetCode] = useState(''); const [isImporting, setIsImporting] = useState(false); const [importResult, setImportResult] = useState(null); const [isSyncing, setIsSyncing] = useState(false); const [syncResult, setSyncResult] = useState(null); const handleCatalogSync = async () => { setIsSyncing(true); setSyncResult(null); try { const response = await fetch('/api/admin/sync-catalog', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('auth_token')}`, }, }); const result = await response.json(); if (response.ok) { setSyncResult({ success: true, ...result }); } else { setSyncResult({ success: false, message: result.error || 'Catalog sync failed', }); } } catch (error) { setSyncResult({ success: false, message: `Network error: ${error.message}`, }); } finally { setIsSyncing(false); } }; const handleImport = async () => { if (!setCode.trim()) { alert('Please enter a set code'); return; } setIsImporting(true); setImportResult(null); try { const endpoint = importType === 'mtg' ? '/api/cards/import-mtg' : '/api/cards/import-pokemon'; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, }, body: JSON.stringify({ setCode: setCode.trim() }), }); const result = await response.json(); if (response.ok) { setImportResult({ success: true, message: result.message, imported: result.imported, skipped: result.skipped, total: result.total }); } else { setImportResult({ success: false, message: result.error || 'Import failed' }); } } catch (error) { setImportResult({ success: false, message: 'Network error: ' + error.message }); } finally { setIsImporting(false); } }; const popularSets = { 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' } ], pokemon: [ { code: 'swsh1', name: 'Sword & Shield' }, { code: 'swsh2', name: 'Rebel Clash' }, { code: 'swsh3', name: 'Darkness Ablaze' }, { code: 'swsh4', name: 'Vivid Voltage' }, { code: 'swsh5', name: 'Battle Styles' }, { code: 'swsh6', name: 'Chilling Reign' }, { code: 'swsh7', name: 'Evolving Skies' }, { code: 'swsh8', name: 'Fusion Strike' }, { code: 'swsh9', name: 'Brilliant Stars' }, { code: 'swsh10', name: 'Astral Radiance' } ] }; return ( {(user) => (
{/* Admin Navigation */}

Admin Tools

{/* Card Import Section */}

Card Import Manager

Import cards from external APIs into your database

{/* Catalog sync */}

Catalog sync

Import up to three newest missing MTG and Pokémon sets — the same job the weekly Vercel cron runs. Pending scan submissions for those sets are auto-linked to the catalog when a unique match exists.

{syncResult && (

{syncResult.success ? 'Catalog sync complete' : 'Catalog sync failed'}

{syncResult.success ? (

Imported {syncResult.imported} cards ({syncResult.skipped} skipped).

{syncResult.submissionsReconciled > 0 && (

Linked {syncResult.submissionsReconciled} pending scan submission {syncResult.submissionsReconciled === 1 ? '' : 's'} to catalog cards.

)} {(syncResult.pendingMtgSets != null || syncResult.pendingPokemonSets != null) && (

Still pending: {syncResult.pendingMtgSets} MTG sets,{' '} {syncResult.pendingPokemonSets} Pokémon sets.

)} {syncResult.lorcana &&

{syncResult.lorcana}

} {Array.isArray(syncResult.setsProcessed) && syncResult.setsProcessed.length > 0 && (
    {syncResult.setsProcessed.map((set) => (
  • {set.name} ({set.game}/{set.setCode}) — {set.imported} imported
  • ))}
)} {Array.isArray(syncResult.reconciliation) && syncResult.reconciliation.length > 0 && (
    {syncResult.reconciliation.flatMap((entry) => entry.details.map((detail) => (
  • Submission #{detail.submissionId}: {detail.name} {detail.cardNumber ? ` (${detail.cardNumber})` : ''} → card #{detail.cardId}
  • )) )}
)} {Array.isArray(syncResult.errors) && syncResult.errors.length > 0 && (
    {syncResult.errors.map((entry) => (
  • {entry.game}/{entry.setCode}: {entry.message}
  • ))}
)}
) : (

{syncResult.message}

)}
)}
{/* Import Form */}

Import Settings

setSetCode(e.target.value)} placeholder="e.g., neo, vow, swsh1" className="input-field w-full" />

Enter the set code (usually 3-4 characters)

{/* Import Result */} {importResult && (

{importResult.success ? 'Import Successful' : 'Import Failed'}

{importResult.message}

{importResult.success && (

Imported: {importResult.imported}

Skipped: {importResult.skipped}

Total: {importResult.total}

)}
)}
{/* Popular Sets */}

Popular Sets

Magic: The Gathering

{popularSets.mtg.map((set) => ( ))}

Pokemon TCG

{popularSets.pokemon.map((set) => ( ))}
{/* Import Tips */}

Import Tips

  • • Set codes are case-insensitive
  • • Duplicate cards will be skipped automatically
  • • Import may take several minutes for large sets
  • • Prices are fetched from TCGPlayer when available
  • • Images are stored as URLs to external sources
)}
); }; // Export with dynamic import to disable SSR export default dynamic(() => Promise.resolve(CardImport), { ssr: false });