2026-06-02 02:03:39 -04:00
|
|
|
|
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
2025-07-24 16:16:57 -04:00
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
import { useRouter } from 'next/router';
|
2025-07-24 18:04:06 -04:00
|
|
|
|
import dynamic from 'next/dynamic';
|
2025-07-24 16:16:57 -04:00
|
|
|
|
import Layout from '../../components/Layout';
|
2025-07-24 17:31:29 -04:00
|
|
|
|
import AdminProtected from '../../components/AdminProtected';
|
2026-06-03 18:24:43 -04:00
|
|
|
|
import CardEditorSearchPanel from '../../components/CardEditorSearchPanel';
|
2025-07-24 16:16:57 -04:00
|
|
|
|
|
2025-07-24 18:04:06 -04:00
|
|
|
|
// Make this component client-side only to avoid SSR issues
|
|
|
|
|
|
const CardEditor = () => {
|
2025-07-24 16:16:57 -04:00
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const { id } = router.query;
|
|
|
|
|
|
|
|
|
|
|
|
const [card, setCard] = useState(null);
|
2026-06-02 02:03:39 -04:00
|
|
|
|
const [fetchLoading, setFetchLoading] = useState(false);
|
|
|
|
|
|
const loading = id ? fetchLoading : false;
|
2025-07-24 16:16:57 -04:00
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
const [message, setMessage] = useState('');
|
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
|
const [searchResults, setSearchResults] = useState([]);
|
|
|
|
|
|
const [searchLoading, setSearchLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// Form state
|
|
|
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
set_name: '',
|
|
|
|
|
|
set_code: '',
|
|
|
|
|
|
card_number: '',
|
|
|
|
|
|
rarity: '',
|
|
|
|
|
|
game: '',
|
|
|
|
|
|
mana_cost: '',
|
|
|
|
|
|
cmc: '',
|
|
|
|
|
|
card_type: '',
|
|
|
|
|
|
colors: [],
|
|
|
|
|
|
oracle_text: '',
|
|
|
|
|
|
power: '',
|
|
|
|
|
|
toughness: '',
|
|
|
|
|
|
image_url: '',
|
|
|
|
|
|
stock_image_url: '',
|
|
|
|
|
|
current_price: '',
|
|
|
|
|
|
market_price: ''
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const fetchCard = async (cardId) => {
|
2026-06-02 02:03:39 -04:00
|
|
|
|
setFetchLoading(true);
|
2025-07-24 16:16:57 -04:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/cards/${cardId}`);
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const cardData = await response.json();
|
|
|
|
|
|
setCard(cardData);
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
name: cardData.name || '',
|
|
|
|
|
|
set_name: cardData.set_name || '',
|
|
|
|
|
|
set_code: cardData.set_code || '',
|
|
|
|
|
|
card_number: cardData.card_number || '',
|
|
|
|
|
|
rarity: cardData.rarity || '',
|
|
|
|
|
|
game: cardData.game || '',
|
|
|
|
|
|
mana_cost: cardData.mana_cost || '',
|
|
|
|
|
|
cmc: cardData.cmc || '',
|
|
|
|
|
|
card_type: cardData.card_type || '',
|
|
|
|
|
|
colors: Array.isArray(cardData.colors) ? cardData.colors : [],
|
|
|
|
|
|
oracle_text: cardData.oracle_text || '',
|
|
|
|
|
|
power: cardData.power || '',
|
|
|
|
|
|
toughness: cardData.toughness || '',
|
|
|
|
|
|
image_url: cardData.image_url || '',
|
|
|
|
|
|
stock_image_url: cardData.stock_image_url || '',
|
|
|
|
|
|
current_price: cardData.current_price || '',
|
|
|
|
|
|
market_price: cardData.market_price || ''
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setMessage('Card not found');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setMessage('Error loading card: ' + error.message);
|
|
|
|
|
|
} finally {
|
2026-06-02 02:03:39 -04:00
|
|
|
|
setFetchLoading(false);
|
2025-07-24 16:16:57 -04:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-02 02:03:39 -04:00
|
|
|
|
// Load card data when editing an existing card
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (id) {
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- load card when route id is set
|
|
|
|
|
|
fetchCard(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
2025-07-24 16:16:57 -04:00
|
|
|
|
const searchCards = async (query) => {
|
|
|
|
|
|
if (!query.trim()) {
|
|
|
|
|
|
setSearchResults([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSearchLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/cards/search?query=${encodeURIComponent(query)}&limit=20`);
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
setSearchResults(data.cards || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Search error:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSearchLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleInputChange = (field, value) => {
|
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[field]: value
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleColorChange = (color, checked) => {
|
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
colors: checked
|
|
|
|
|
|
? [...prev.colors, color]
|
|
|
|
|
|
: prev.colors.filter(c => c !== color)
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
|
if (!card?.id) {
|
|
|
|
|
|
setMessage('No card selected to edit');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSaving(true);
|
|
|
|
|
|
setMessage('');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/cards/${card.id}`, {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
cmc: formData.cmc ? parseInt(formData.cmc) : null,
|
|
|
|
|
|
current_price: formData.current_price ? parseFloat(formData.current_price) : null,
|
|
|
|
|
|
market_price: formData.market_price ? parseFloat(formData.market_price) : null
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const updatedCard = await response.json();
|
|
|
|
|
|
setCard(updatedCard);
|
|
|
|
|
|
setMessage('Card updated successfully!');
|
|
|
|
|
|
|
|
|
|
|
|
// Clear message after 3 seconds
|
|
|
|
|
|
setTimeout(() => setMessage(''), 3000);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
|
setMessage('Error updating card: ' + (errorData.error || 'Unknown error'));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setMessage('Error updating card: ' + error.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectCard = (selectedCard) => {
|
|
|
|
|
|
router.push(`/admin/card-editor?id=${selectedCard.id}`);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const rarityOptions = [
|
|
|
|
|
|
'common', 'uncommon', 'rare', 'mythic', 'legendary',
|
|
|
|
|
|
'holographic', 'enchanted', 'super rare', 'ultra rare'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const gameOptions = ['MTG', 'Pokemon', 'Lorcana'];
|
|
|
|
|
|
|
|
|
|
|
|
const colorOptions = ['White', 'Blue', 'Black', 'Red', 'Green', 'Colorless'];
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
2025-07-24 17:31:29 -04:00
|
|
|
|
<AdminProtected>
|
2025-07-24 18:04:06 -04:00
|
|
|
|
{(user) => (
|
|
|
|
|
|
<Layout user={user}>
|
|
|
|
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
|
|
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2" style={{ borderColor: 'var(--text-accent)' }}></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Layout>
|
|
|
|
|
|
)}
|
2025-07-24 17:31:29 -04:00
|
|
|
|
</AdminProtected>
|
2025-07-24 16:16:57 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 gradient-bg-purple text-white"
|
|
|
|
|
|
>
|
|
|
|
|
|
🖊️ Card Editor
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => router.push('/admin/card-import')}
|
|
|
|
|
|
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 Import
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
|
<h2 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Admin Card Editor
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
Search for a card to edit its details, or continue editing the selected card.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-03 18:24:43 -04:00
|
|
|
|
<CardEditorSearchPanel
|
|
|
|
|
|
searchQuery={searchQuery}
|
|
|
|
|
|
onSearchQueryChange={(value) => {
|
|
|
|
|
|
setSearchQuery(value);
|
|
|
|
|
|
searchCards(value);
|
|
|
|
|
|
}}
|
|
|
|
|
|
searchLoading={searchLoading}
|
|
|
|
|
|
searchResults={searchResults}
|
|
|
|
|
|
onSelectCard={selectCard}
|
|
|
|
|
|
/>
|
2025-07-24 16:16:57 -04:00
|
|
|
|
|
|
|
|
|
|
{/* Card Editor Form */}
|
|
|
|
|
|
{card && (
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
|
|
|
|
{/* Card Preview */}
|
|
|
|
|
|
<div className="lg:col-span-1">
|
|
|
|
|
|
<div className="sticky top-8">
|
|
|
|
|
|
<h2 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Card Preview
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<div className="w-full max-w-xs mx-auto">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="w-full rounded-lg overflow-hidden shadow-lg"
|
|
|
|
|
|
style={{ aspectRatio: '5/7' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{formData.image_url ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={formData.image_url}
|
|
|
|
|
|
alt={formData.name}
|
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
|
onError={(e) => {
|
|
|
|
|
|
e.target.style.display = 'none';
|
|
|
|
|
|
e.target.nextSibling.style.display = 'flex';
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`w-full h-full flex items-center justify-center ${formData.image_url ? 'hidden' : 'flex'}`}
|
|
|
|
|
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="text-center p-4">
|
|
|
|
|
|
<div className="text-4xl mb-2">🃏</div>
|
|
|
|
|
|
<div className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
{formData.name || 'Card Name'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
{formData.set_name || 'Set Name'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="mt-4 text-center">
|
|
|
|
|
|
<h3 className="font-bold" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
{formData.name || 'Card Name'}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
{formData.set_name} • {formData.game}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
{formData.rarity} • {formData.card_type}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{formData.current_price && (
|
|
|
|
|
|
<p className="text-lg font-bold mt-2 gradient-text-gold">
|
|
|
|
|
|
${parseFloat(formData.current_price).toFixed(2)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Editor Form */}
|
|
|
|
|
|
<div className="lg:col-span-2">
|
|
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
|
|
|
|
<h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Edit Card Details
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
|
disabled={saving}
|
|
|
|
|
|
className={`px-6 py-3 rounded-xl font-medium transition-all duration-200 ${
|
|
|
|
|
|
saving
|
|
|
|
|
|
? 'opacity-50 cursor-not-allowed'
|
|
|
|
|
|
: 'gradient-bg-purple text-white hover:shadow-lg'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{message && (
|
|
|
|
|
|
<div className={`p-4 rounded-lg mb-6 ${
|
|
|
|
|
|
message.includes('Error')
|
|
|
|
|
|
? 'bg-red-100 text-red-700 border border-red-200'
|
|
|
|
|
|
: 'bg-green-100 text-green-700 border border-green-200'
|
|
|
|
|
|
}`}>
|
|
|
|
|
|
{message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
{/* Basic Information */}
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Basic Information
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Card Name *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.name}
|
|
|
|
|
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Game *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={formData.game}
|
|
|
|
|
|
onChange={(e) => handleInputChange('game', e.target.value)}
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">Select Game</option>
|
|
|
|
|
|
{gameOptions.map(game => (
|
|
|
|
|
|
<option key={game} value={game}>{game}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Set Name
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.set_name}
|
|
|
|
|
|
onChange={(e) => handleInputChange('set_name', e.target.value)}
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Set Code
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.set_code}
|
|
|
|
|
|
onChange={(e) => handleInputChange('set_code', e.target.value)}
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Card Number
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.card_number}
|
|
|
|
|
|
onChange={(e) => handleInputChange('card_number', e.target.value)}
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Rarity
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={formData.rarity}
|
|
|
|
|
|
onChange={(e) => handleInputChange('rarity', e.target.value)}
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">Select Rarity</option>
|
|
|
|
|
|
{rarityOptions.map(rarity => (
|
|
|
|
|
|
<option key={rarity} value={rarity}>{rarity}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Game Mechanics */}
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Game Mechanics
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Card Type
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.card_type}
|
|
|
|
|
|
onChange={(e) => handleInputChange('card_type', e.target.value)}
|
|
|
|
|
|
placeholder="e.g., Creature, Instant, Pokemon, Character"
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Mana Cost
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.mana_cost}
|
|
|
|
|
|
onChange={(e) => handleInputChange('mana_cost', e.target.value)}
|
|
|
|
|
|
placeholder="e.g., {2}{U}{U}, 3, etc."
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
CMC (Converted Mana Cost)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={formData.cmc}
|
|
|
|
|
|
onChange={(e) => handleInputChange('cmc', e.target.value)}
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Power
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.power}
|
|
|
|
|
|
onChange={(e) => handleInputChange('power', e.target.value)}
|
|
|
|
|
|
placeholder="e.g., 2, *, X"
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Toughness
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.toughness}
|
|
|
|
|
|
onChange={(e) => handleInputChange('toughness', e.target.value)}
|
|
|
|
|
|
placeholder="e.g., 2, *, X"
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Colors */}
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Colors
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="flex flex-wrap gap-3">
|
|
|
|
|
|
{colorOptions.map(color => (
|
|
|
|
|
|
<label key={color} className="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={formData.colors.includes(color)}
|
|
|
|
|
|
onChange={(e) => handleColorChange(color, e.target.checked)}
|
|
|
|
|
|
className="rounded"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span style={{ color: 'var(--text-primary)' }}>{color}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Card Text */}
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Card Text
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Oracle Text / Abilities
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={formData.oracle_text}
|
|
|
|
|
|
onChange={(e) => handleInputChange('oracle_text', e.target.value)}
|
|
|
|
|
|
rows={4}
|
|
|
|
|
|
placeholder="Enter the card's rules text, abilities, or flavor text..."
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Images */}
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Card Images
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Primary Image URL
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
value={formData.image_url}
|
|
|
|
|
|
onChange={(e) => handleInputChange('image_url', e.target.value)}
|
|
|
|
|
|
placeholder="https://example.com/card-image.jpg"
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Stock Image URL
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
value={formData.stock_image_url}
|
|
|
|
|
|
onChange={(e) => handleInputChange('stock_image_url', e.target.value)}
|
|
|
|
|
|
placeholder="https://example.com/stock-image.jpg"
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Pricing */}
|
|
|
|
|
|
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Pricing Information
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Current Price ($)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
value={formData.current_price}
|
|
|
|
|
|
onChange={(e) => handleInputChange('current_price', e.target.value)}
|
|
|
|
|
|
placeholder="0.00"
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Market Price ($)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
value={formData.market_price}
|
|
|
|
|
|
onChange={(e) => handleInputChange('market_price', e.target.value)}
|
|
|
|
|
|
placeholder="0.00"
|
|
|
|
|
|
className="w-full p-3 rounded-lg border transition-all duration-200"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
|
borderColor: 'var(--border)',
|
|
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!card && !loading && (
|
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
|
<div className="text-6xl mb-4">🔍</div>
|
|
|
|
|
|
<h2 className="text-2xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Search for a Card to Edit
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
Use the search box above to find a card you want to edit.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</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-24 16:16:57 -04:00
|
|
|
|
);
|
2025-07-24 18:04:06 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Export with dynamic import to disable SSR
|
|
|
|
|
|
export default dynamic(() => Promise.resolve(CardEditor), { ssr: false });
|