From b8bdacefda04ab43b1ffe01043e698d122110a0f Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 3 Jun 2026 17:30:29 -0500 Subject: [PATCH] refactor(card-editor): useCardEditor + CardEditorView (Brief 3) Extract admin editor state into useCardEditor and UI into CardEditorView; keep dynamic(ssr: false) on the page export. Page is ~40 lines. Co-authored-by: Cursor --- components/CardEditorView.js | 100 +++++++++++++ lib/use-card-editor.js | 185 +++++++++++++++++++++++++ pages/admin/card-editor.js | 262 ++--------------------------------- 3 files changed, 298 insertions(+), 249 deletions(-) create mode 100644 components/CardEditorView.js create mode 100644 lib/use-card-editor.js 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 }); -- 2.45.2