diff --git a/components/CardEditorView.js b/components/CardEditorView.js
new file mode 100644
index 0000000..da35dc4
--- /dev/null
+++ b/components/CardEditorView.js
@@ -0,0 +1,100 @@
+import CardEditorForm from './CardEditorForm';
+import CardEditorPreview from './CardEditorPreview';
+import CardEditorSearchPanel from './CardEditorSearchPanel';
+
+export default function CardEditorView({
+ card,
+ formData,
+ handleColorChange,
+ handleInputChange,
+ handleSave,
+ message,
+ onSearchQueryChange,
+ router,
+ saving,
+ searchLoading,
+ searchQuery,
+ searchResults,
+ selectCard,
+ showEmptyState,
+}) {
+ return (
+
+
+
+
+ Admin Tools
+
+
+
+
+
+
+
+
+
+
+ Admin Card Editor
+
+
+ Search for a card to edit its details, or continue editing the selected card.
+
+
+
+
+
+ {card && (
+
+ )}
+
+ {showEmptyState && (
+
+
🔍
+
+ Search for a Card to Edit
+
+
+ Use the search box above to find a card you want to edit.
+
+
+ )}
+
+ );
+}
diff --git a/lib/use-card-editor.js b/lib/use-card-editor.js
new file mode 100644
index 0000000..f99cb02
--- /dev/null
+++ b/lib/use-card-editor.js
@@ -0,0 +1,185 @@
+import { useState, useEffect } from 'react';
+
+const EMPTY_FORM = {
+ 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: '',
+};
+
+/**
+ * Admin card editor state and handlers (god-component split).
+ */
+export function useCardEditor(router) {
+ 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);
+ const [formData, setFormData] = useState(EMPTY_FORM);
+
+ 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);
+ }
+ };
+
+ 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, 10) : 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!');
+ 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 onSearchQueryChange = (value) => {
+ setSearchQuery(value);
+ searchCards(value);
+ };
+
+ return {
+ card,
+ formData,
+ handleColorChange,
+ handleInputChange,
+ handleSave,
+ loading,
+ message,
+ onSearchQueryChange,
+ router,
+ saving,
+ searchLoading,
+ searchQuery,
+ searchResults,
+ selectCard,
+ showEmptyState: !card && !loading,
+ };
+}
diff --git a/pages/admin/card-editor.js b/pages/admin/card-editor.js
index 2b3ebb2..1ed61da 100644
--- a/pages/admin/card-editor.js
+++ b/pages/admin/card-editor.js
@@ -1,180 +1,24 @@
-/* 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';
+import CardEditorView from '../../components/CardEditorView';
+import { useCardEditor } from '../../lib/use-card-editor.js';
-// Make this component client-side only to avoid SSR issues
-const CardEditor = () => {
+function CardEditorPage() {
const router = useRouter();
- const { id } = router.query;
+ const editor = useCardEditor(router);
- 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) {
+ if (editor.loading) {
return (
{(user) => (
)}
@@ -186,91 +30,11 @@ const CardEditor = () => {
{(user) => (
-
- {/* Admin Navigation */}
-
-
-
- Admin Tools
-
-
-
-
-
-
-
-
-
-
- Admin Card Editor
-
-
- Search for a card to edit its details, or continue editing the selected card.
-
-
-
-
{
- setSearchQuery(value);
- searchCards(value);
- }}
- searchLoading={searchLoading}
- searchResults={searchResults}
- onSelectCard={selectCard}
- />
-
- {/* Card Editor Form */}
- {card && (
-
- )}
-
- {!card && !loading && (
-
-
🔍
-
- Search for a Card to Edit
-
-
- Use the search box above to find a card you want to edit.
-
-
- )}
-
-
+
+
)}
);
-};
+}
-// Export with dynamic import to disable SSR
-export default dynamic(() => Promise.resolve(CardEditor), { ssr: false });
\ No newline at end of file
+export default dynamic(() => Promise.resolve(CardEditorPage), { ssr: false });