deckhearth/pages/admin/card-editor.js

762 lines
31 KiB
JavaScript
Raw Normal View History

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';
// Make this component client-side only to avoid SSR issues
const CardEditor = () => {
const router = useRouter();
const { id } = router.query;
const [card, setCard] = useState(null);
const [loading, setLoading] = useState(true);
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: ''
});
// Load card data if ID is provided
useEffect(() => {
if (id) {
fetchCard(id);
} else {
setLoading(false);
}
}, [id]);
const fetchCard = async (cardId) => {
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 {
setLoading(false);
}
};
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 (
<AdminProtected>
{(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>
)}
</AdminProtected>
);
}
return (
<AdminProtected>
{(user) => (
<Layout user={user}>
<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>
{/* Card Search */}
<div className="mb-8 p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h2 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Find Card to Edit
</h2>
<div className="flex gap-4 mb-4">
<div className="flex-1">
<input
type="text"
placeholder="Search for cards by name..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
searchCards(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>
{/* Search Results */}
{searchLoading && (
<div className="text-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 mx-auto" style={{ borderColor: 'var(--text-accent)' }}></div>
</div>
)}
{searchResults.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{searchResults.map((searchCard) => (
<div
key={searchCard.id}
onClick={() => selectCard(searchCard)}
className="p-4 rounded-lg border cursor-pointer transition-all duration-200 hover:shadow-lg hover:scale-105"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)'
}}
>
<div className="flex items-center gap-3">
<div className="w-12 h-16 rounded-lg overflow-hidden flex-shrink-0" style={{ backgroundColor: 'var(--bg-secondary)' }}>
{searchCard.image_url ? (
<img
src={searchCard.image_url}
alt={searchCard.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-xs" style={{ color: 'var(--text-secondary)' }}>
{searchCard.game}
</div>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold" style={{ color: 'var(--text-primary)' }}>
{searchCard.name}
</h3>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{searchCard.set_name} {searchCard.game}
</p>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{searchCard.rarity}
</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 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>
</Layout>
)}
</AdminProtected>
);
};
// Export with dynamic import to disable SSR
export default dynamic(() => Promise.resolve(CardEditor), { ssr: false });