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>
276 lines
No EOL
8.5 KiB
JavaScript
276 lines
No EOL
8.5 KiB
JavaScript
/* 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 }); |