deckhearth/pages/admin/card-editor.js
varutasu 4c5271ffbf
refactor(card-editor): extract preview and form components (Brief 2) (#92)
Move the sticky preview column and edit form into CardEditorPreview and
CardEditorForm; page composer drops to ~275 lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:29:11 -05:00

276 lines
No EOL
8.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
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 CardEditorForm from '../../components/CardEditorForm';
import CardEditorPreview from '../../components/CardEditorPreview';
import CardEditorSearchPanel from '../../components/CardEditorSearchPanel';
// 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 [fetchLoading, setFetchLoading] = useState(false);
const loading = id ? fetchLoading : false;
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) => {
setFetchLoading(true);
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 {
setFetchLoading(false);
}
};
// 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]);
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}`);
};
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>
<CardEditorSearchPanel
searchQuery={searchQuery}
onSearchQueryChange={(value) => {
setSearchQuery(value);
searchCards(value);
}}
searchLoading={searchLoading}
searchResults={searchResults}
onSelectCard={selectCard}
/>
{/* Card Editor Form */}
{card && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-1">
<CardEditorPreview formData={formData} />
</div>
<div className="lg:col-span-2">
<CardEditorForm
formData={formData}
onInputChange={handleInputChange}
onColorChange={handleColorChange}
onSave={handleSave}
saving={saving}
message={message}
/>
</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 });