From a641a22371c155139768e497889aa96d04f935d3 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Sat, 13 Jun 2026 01:13:05 -0500 Subject: [PATCH] refactor(decks): useDecksPage + DecksPageView (Brief 3) Extract list page state into useDecksPage and layout into DecksPageView; pages/decks.js is a thin auth-gated composer. Co-authored-by: Cursor --- components/DecksPageView.js | 84 +++++++++++++++ lib/use-decks-page.js | 143 +++++++++++++++++++++++++ pages/decks.js | 206 ++---------------------------------- 3 files changed, 234 insertions(+), 199 deletions(-) create mode 100644 components/DecksPageView.js create mode 100644 lib/use-decks-page.js diff --git a/components/DecksPageView.js b/components/DecksPageView.js new file mode 100644 index 0000000..ac3d889 --- /dev/null +++ b/components/DecksPageView.js @@ -0,0 +1,84 @@ +import DecksCreateModal from './DecksCreateModal'; +import DecksEditModal from './DecksEditModal'; +import DecksGrid from './DecksGrid'; +import { Button } from './ui'; + +export default function DecksPageView({ + decks, + editingDeck, + handleCreateDeck, + handleDeleteDeck, + handleEditDeck, + newDeck, + setEditingDeck, + setNewDeck, + setShowCreateModal, + showCreateModal, +}) { + return ( +
+
+
+

+ My Decks +

+

+ Build and manage your MTG decks +

+
+ +
+ +
+
+
+ {decks.length} +
+
Total Decks
+
+
+
+ {decks.filter((d) => d.format === 'Commander').length} +
+
Commander
+
+
+
+ {decks.filter((d) => d.is_public).length} +
+
Public
+
+
+
+ {decks.reduce((sum, deck) => sum + (deck.card_count || 0), 0)} +
+
Total Cards
+
+
+ + setShowCreateModal(true)} + /> + + setShowCreateModal(false)} + newDeck={newDeck} + setNewDeck={setNewDeck} + onCreate={handleCreateDeck} + /> + + setEditingDeck(null)} + onEdit={handleEditDeck} + /> +
+ ); +} diff --git a/lib/use-decks-page.js b/lib/use-decks-page.js new file mode 100644 index 0000000..05ef3de --- /dev/null +++ b/lib/use-decks-page.js @@ -0,0 +1,143 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +const EMPTY_NEW_DECK = { + name: '', + description: '', + format: 'Commander', + is_public: false, +}; + +/** + * Decks list page state and handlers (god-component split). + */ +export function useDecksPage({ user = null, authLoading = true } = {}) { + const router = useRouter(); + const [decks, setDecks] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreateModal, setShowCreateModal] = useState(false); + const [editingDeck, setEditingDeck] = useState(null); + const [newDeck, setNewDeck] = useState(EMPTY_NEW_DECK); + + const fetchDecks = async () => { + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch('/api/decks', { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + setDecks(data); + } else { + console.error('Failed to fetch decks'); + } + } catch (error) { + console.error('Error fetching decks:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (user) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- load decks when user is available + fetchDecks(); + } + }, [user]); + + const handleCreateDeck = async (e) => { + e.preventDefault(); + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch('/api/decks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(newDeck), + }); + + if (response.ok) { + const createdDeck = await response.json(); + setDecks([createdDeck, ...decks]); + setShowCreateModal(false); + setNewDeck(EMPTY_NEW_DECK); + router.push(`/deck-builder?deck=${createdDeck.id}`); + } else { + console.error('Failed to create deck'); + } + } catch (error) { + console.error('Error creating deck:', error); + } + }; + + const handleEditDeck = async (e) => { + e.preventDefault(); + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch(`/api/decks/${editingDeck.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(editingDeck), + }); + + if (response.ok) { + const updatedDeck = await response.json(); + setDecks(decks.map((deck) => (deck.id === updatedDeck.id ? updatedDeck : deck))); + setEditingDeck(null); + } else { + console.error('Failed to update deck'); + } + } catch (error) { + console.error('Error updating deck:', error); + } + }; + + const handleDeleteDeck = async (deckId) => { + if (!confirm('Are you sure you want to delete this deck? This action cannot be undone.')) { + return; + } + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch(`/api/decks/${deckId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }); + + if (response.ok) { + setDecks(decks.filter((deck) => deck.id !== deckId)); + } else { + console.error('Failed to delete deck'); + } + } catch (error) { + console.error('Error deleting deck:', error); + } + }; + + const showLoggedOut = !authLoading && !user; + const showInitialLoading = Boolean(user) && loading; + + return { + authLoading, + decks, + editingDeck, + handleCreateDeck, + handleDeleteDeck, + handleEditDeck, + newDeck, + setEditingDeck, + setNewDeck, + setShowCreateModal, + showCreateModal, + showInitialLoading, + showLoggedOut, + user, + }; +} diff --git a/pages/decks.js b/pages/decks.js index 760ecf3..09672a4 100644 --- a/pages/decks.js +++ b/pages/decks.js @@ -1,141 +1,14 @@ -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; import Link from 'next/link'; import Layout from '../components/Layout'; -import DecksCreateModal from '../components/DecksCreateModal'; -import DecksEditModal from '../components/DecksEditModal'; -import DecksGrid from '../components/DecksGrid'; -import { Button } from '../components/ui'; +import DecksPageView from '../components/DecksPageView'; import { useAuth } from '../lib/use-auth'; - -/* 2026-06-04 design-sweep pass: the Tailwind token classes that this - page relied on (bg-bg-secondary, text-text-primary, border-border, - bg-accent-ember, hover:bg-accent-ember-dark, focus:ring-accent-ember) - are NOT defined in tailwind.config.js — they produced zero CSS, - leaving the page visually unstyled (transparent backgrounds, no - borders, no hover states). Sweep replaces those with inline - style={{ ... CSS vars ... }} and the Button / SearchBar primitives - so the page actually renders with the Liquid Glass + corner-border - design system. `rounded-lg` → `rounded-xl` across the board to match - the system standard. */ +import { useDecksPage } from '../lib/use-decks-page.js'; export default function Decks() { - const { user } = useAuth(); - const router = useRouter(); - const [decks, setDecks] = useState([]); - const [loading, setLoading] = useState(true); - const [showCreateModal, setShowCreateModal] = useState(false); - const [editingDeck, setEditingDeck] = useState(null); - const [newDeck, setNewDeck] = useState({ - name: '', - description: '', - format: 'Commander', - is_public: false, - }); + const { user, loading: authLoading } = useAuth(); + const decksPage = useDecksPage({ user, authLoading }); - const fetchDecks = async () => { - try { - const token = localStorage.getItem('auth_token'); - const response = await fetch('/api/decks', { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - setDecks(data); - } else { - console.error('Failed to fetch decks'); - } - } catch (error) { - console.error('Error fetching decks:', error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (user) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- load decks when user is available - fetchDecks(); - } - }, [user]); - - const handleCreateDeck = async (e) => { - e.preventDefault(); - try { - const token = localStorage.getItem('auth_token'); - const response = await fetch('/api/decks', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(newDeck), - }); - - if (response.ok) { - const createdDeck = await response.json(); - setDecks([createdDeck, ...decks]); - setShowCreateModal(false); - setNewDeck({ name: '', description: '', format: 'Commander', is_public: false }); - router.push(`/deck-builder?deck=${createdDeck.id}`); - } else { - console.error('Failed to create deck'); - } - } catch (error) { - console.error('Error creating deck:', error); - } - }; - - const handleEditDeck = async (e) => { - e.preventDefault(); - try { - const token = localStorage.getItem('auth_token'); - const response = await fetch(`/api/decks/${editingDeck.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(editingDeck), - }); - - if (response.ok) { - const updatedDeck = await response.json(); - setDecks(decks.map((deck) => (deck.id === updatedDeck.id ? updatedDeck : deck))); - setEditingDeck(null); - } else { - console.error('Failed to update deck'); - } - } catch (error) { - console.error('Error updating deck:', error); - } - }; - - const handleDeleteDeck = async (deckId) => { - if (!confirm('Are you sure you want to delete this deck? This action cannot be undone.')) { - return; - } - try { - const token = localStorage.getItem('auth_token'); - const response = await fetch(`/api/decks/${deckId}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - }); - - if (response.ok) { - setDecks(decks.filter((deck) => deck.id !== deckId)); - } else { - console.error('Failed to delete deck'); - } - } catch (error) { - console.error('Error deleting deck:', error); - } - }; - - if (!user) { + if (decksPage.showLoggedOut || !user) { return (
@@ -152,7 +25,7 @@ export default function Decks() { ); } - if (loading) { + if (decksPage.showInitialLoading) { return (
@@ -167,72 +40,7 @@ export default function Decks() { return ( -
- {/* Header */} -
-
-

- My Decks -

-

- Build and manage your MTG decks -

-
- -
- - {/* Stats */} -
-
-
- {decks.length} -
-
Total Decks
-
-
-
- {decks.filter((d) => d.format === 'Commander').length} -
-
Commander
-
-
-
- {decks.filter((d) => d.is_public).length} -
-
Public
-
-
-
- {decks.reduce((sum, deck) => sum + (deck.card_count || 0), 0)} -
-
Total Cards
-
-
- - setShowCreateModal(true)} - /> - - setShowCreateModal(false)} - newDeck={newDeck} - setNewDeck={setNewDeck} - onCreate={handleCreateDeck} - /> - - setEditingDeck(null)} - onEdit={handleEditDeck} - /> -
+
); } -- 2.45.2