2025-07-24 18:04:06 -04:00
|
|
|
|
import { useState, useEffect } from 'react';
|
2025-07-24 16:16:57 -04:00
|
|
|
|
import { useRouter } from 'next/router';
|
2025-07-24 18:04:06 -04:00
|
|
|
|
import dynamic from 'next/dynamic';
|
2025-07-23 22:26:54 -04:00
|
|
|
|
import Layout from '../../components/Layout';
|
2025-07-24 17:31:29 -04:00
|
|
|
|
import AdminProtected from '../../components/AdminProtected';
|
2025-07-23 22:26:54 -04:00
|
|
|
|
|
2025-07-24 18:04:06 -04:00
|
|
|
|
const CardImport = () => {
|
2025-07-24 16:16:57 -04:00
|
|
|
|
const router = useRouter();
|
2025-07-23 22:26:54 -04:00
|
|
|
|
const [importType, setImportType] = useState('mtg');
|
|
|
|
|
|
const [setCode, setSetCode] = useState('');
|
|
|
|
|
|
const [isImporting, setIsImporting] = useState(false);
|
|
|
|
|
|
const [importResult, setImportResult] = useState(null);
|
|
|
|
|
|
|
|
|
|
|
|
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',
|
2026-05-24 23:59:59 -04:00
|
|
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
2025-07-23 22:26:54 -04:00
|
|
|
|
},
|
|
|
|
|
|
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 (
|
2025-07-24 17:31:29 -04:00
|
|
|
|
<AdminProtected>
|
2025-07-24 18:04:06 -04:00
|
|
|
|
{(user) => (
|
|
|
|
|
|
<Layout user={user}>
|
2025-07-24 16:16:57 -04:00
|
|
|
|
<div className="container mx-auto px-6 py-8">
|
|
|
|
|
|
{/* Admin Navigation */}
|
|
|
|
|
|
<div className="mb-8 p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<h1 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Admin Tools
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => router.push('/admin/card-editor')}
|
|
|
|
|
|
className="px-4 py-2 rounded-lg font-medium transition-all duration-200 border"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
🖊️ Card Editor
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => router.push('/admin/card-import')}
|
|
|
|
|
|
className="px-4 py-2 rounded-lg font-medium gradient-bg-purple text-white"
|
|
|
|
|
|
>
|
|
|
|
|
|
📥 Card Import
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Card Import Section */}
|
2025-07-23 22:26:54 -04:00
|
|
|
|
<div className="mb-6">
|
2025-07-24 16:16:57 -04:00
|
|
|
|
<h2 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
2025-07-23 22:26:54 -04:00
|
|
|
|
Card Import Manager
|
2025-07-24 16:16:57 -04:00
|
|
|
|
</h2>
|
2025-07-23 22:26:54 -04:00
|
|
|
|
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
Import cards from external APIs into your database
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
|
|
|
|
{/* Import Form */}
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h2 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Import Settings
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
TCG Type
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={importType}
|
|
|
|
|
|
onChange={(e) => setImportType(e.target.value)}
|
|
|
|
|
|
className="input-field w-full"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="mtg">Magic: The Gathering</option>
|
|
|
|
|
|
<option value="pokemon">Pokemon TCG</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Set Code
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={setCode}
|
|
|
|
|
|
onChange={(e) => setSetCode(e.target.value)}
|
|
|
|
|
|
placeholder="e.g., neo, vow, swsh1"
|
|
|
|
|
|
className="input-field w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
Enter the set code (usually 3-4 characters)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleImport}
|
|
|
|
|
|
disabled={isImporting || !setCode.trim()}
|
|
|
|
|
|
className={`w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 ${
|
|
|
|
|
|
isImporting || !setCode.trim()
|
|
|
|
|
|
? 'opacity-50 cursor-not-allowed'
|
|
|
|
|
|
: 'gradient-bg-purple text-white hover:shadow-lg'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isImporting ? 'Importing...' : 'Import Cards'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Import Result */}
|
|
|
|
|
|
{importResult && (
|
|
|
|
|
|
<div className={`p-6 rounded-xl ${
|
|
|
|
|
|
importResult.success
|
|
|
|
|
|
? 'bg-green-100 border border-green-300'
|
|
|
|
|
|
: 'bg-red-100 border border-red-300'
|
|
|
|
|
|
}`}>
|
|
|
|
|
|
<h3 className={`font-semibold mb-2 ${
|
|
|
|
|
|
importResult.success ? 'text-green-800' : 'text-red-800'
|
|
|
|
|
|
}`}>
|
|
|
|
|
|
{importResult.success ? 'Import Successful' : 'Import Failed'}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="text-sm mb-2">
|
|
|
|
|
|
{importResult.message}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{importResult.success && (
|
|
|
|
|
|
<div className="text-sm space-y-1">
|
|
|
|
|
|
<p>Imported: {importResult.imported}</p>
|
|
|
|
|
|
<p>Skipped: {importResult.skipped}</p>
|
|
|
|
|
|
<p>Total: {importResult.total}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Popular Sets */}
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h2 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Popular Sets
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Magic: The Gathering
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
{popularSets.mtg.map((set) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={set.code}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setImportType('mtg');
|
|
|
|
|
|
setSetCode(set.code);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="text-left p-2 rounded-lg text-sm hover:bg-opacity-20 transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
|
color: 'var(--text-secondary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="font-medium">{set.code.toUpperCase()}</div>
|
|
|
|
|
|
<div className="text-xs opacity-75">{set.name}</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Pokemon TCG
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
{popularSets.pokemon.map((set) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={set.code}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setImportType('pokemon');
|
|
|
|
|
|
setSetCode(set.code);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="text-left p-2 rounded-lg text-sm hover:bg-opacity-20 transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
|
color: 'var(--text-secondary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="font-medium">{set.code.toUpperCase()}</div>
|
|
|
|
|
|
<div className="text-xs opacity-75">{set.name}</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Import Tips */}
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h2 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Import Tips
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<ul className="space-y-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
<li>• Set codes are case-insensitive</li>
|
|
|
|
|
|
<li>• Duplicate cards will be skipped automatically</li>
|
|
|
|
|
|
<li>• Import may take several minutes for large sets</li>
|
|
|
|
|
|
<li>• Prices are fetched from TCGPlayer when available</li>
|
|
|
|
|
|
<li>• Images are stored as URLs to external sources</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-07-24 17:31:29 -04:00
|
|
|
|
</Layout>
|
2025-07-24 18:04:06 -04:00
|
|
|
|
)}
|
2025-07-24 17:31:29 -04:00
|
|
|
|
</AdminProtected>
|
2025-07-23 22:26:54 -04:00
|
|
|
|
);
|
2025-07-24 18:04:06 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Export with dynamic import to disable SSR
|
|
|
|
|
|
export default dynamic(() => Promise.resolve(CardImport), { ssr: false });
|