From b2ea950a3c1d77f6aca173c81f06c299b8d518ce Mon Sep 17 00:00:00 2001 From: varutasu <104105839+varutasu@users.noreply.github.com> Date: Sat, 13 Jun 2026 01:14:46 -0500 Subject: [PATCH] refactor(deck): useDeckDetail + DeckDetailView (Brief 3) (#146) Extract detail page state into useDeckDetail and layout into DeckDetailView; pages/deck/[id].js is a thin loading/not-found gated composer. Co-authored-by: Cursor --- components/DeckDetailView.js | 75 +++++++++++++++++++ lib/use-deck-detail.js | 74 +++++++++++++++++++ pages/deck/[id].js | 139 +++-------------------------------- 3 files changed, 158 insertions(+), 130 deletions(-) create mode 100644 components/DeckDetailView.js create mode 100644 lib/use-deck-detail.js diff --git a/components/DeckDetailView.js b/components/DeckDetailView.js new file mode 100644 index 0000000..d3897aa --- /dev/null +++ b/components/DeckDetailView.js @@ -0,0 +1,75 @@ +import Link from 'next/link'; +import DeckDetailCardList from './DeckDetailCardList'; +import DeckDetailStatsSidebar from './DeckDetailStatsSidebar'; +import { Button } from './ui'; +import { getDeckFormatIcon } from '../lib/deck-format-utils.js'; + +export default function DeckDetailView({ + deck, + groupBy, + groupedCards, + isOwner, + setGroupBy, + stats, +}) { + return ( +
+
+
+
+ + ← Back to Decks + +
+
+ {getDeckFormatIcon(deck.format)} +

+ {deck.name} +

+ {deck.is_public && ( + + Public + + )} +
+

+ by {deck.creator_username} • {deck.format} • {stats.totalCards} cards +

+ {deck.description && ( +

+ {deck.description} +

+ )} +
+ + {isOwner && ( +
+ + + +
+ )} +
+ +
+ + +
+
+ ); +} diff --git a/lib/use-deck-detail.js b/lib/use-deck-detail.js new file mode 100644 index 0000000..f7168e3 --- /dev/null +++ b/lib/use-deck-detail.js @@ -0,0 +1,74 @@ +import { useState, useEffect, useMemo } from 'react'; +import { useRouter } from 'next/router'; +import { computeDeckStats } from './deck-builder-stats.js'; +import { groupDeckCards } from './deck-detail-grouping.js'; + +/** + * Deck detail page state and derived data (god-component split). + */ +export function useDeckDetail({ user = null, authLoading = true } = {}) { + const router = useRouter(); + const { id: deckId } = router.query; + + const [deck, setDeck] = useState(null); + const [loading, setLoading] = useState(true); + const [groupBy, setGroupBy] = useState('type'); + + const fetchDeck = async () => { + try { + const token = localStorage.getItem('auth_token'); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + + const response = await fetch(`/api/decks/${deckId}`, { headers }); + + if (response.ok) { + const data = await response.json(); + setDeck(data); + } else { + console.error('Failed to fetch deck'); + router.push('/decks'); + } + } catch (error) { + console.error('Error fetching deck:', error); + router.push('/decks'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (deckId) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- load deck when route id changes + fetchDeck(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- refetch when route deck id changes + }, [deckId]); + + const stats = useMemo( + () => (deck ? computeDeckStats(deck.cards || []) : null), + [deck] + ); + + const groupedCards = useMemo( + () => (deck ? groupDeckCards(deck.cards, groupBy) : {}), + [deck, groupBy] + ); + + const isOwner = Boolean(user && deck && deck.user_id === user.userId); + const showDeckLoading = loading; + const showDeckNotFound = !loading && !deck; + + return { + authLoading, + deck, + deckId, + groupBy, + groupedCards, + isOwner, + setGroupBy, + showDeckLoading, + showDeckNotFound, + stats, + user, + }; +} diff --git a/pages/deck/[id].js b/pages/deck/[id].js index 4e546ee..1bbc539 100644 --- a/pages/deck/[id].js +++ b/pages/deck/[id].js @@ -1,63 +1,14 @@ -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; import Link from 'next/link'; import Layout from '../../components/Layout'; -import DeckDetailCardList from '../../components/DeckDetailCardList'; -import DeckDetailStatsSidebar from '../../components/DeckDetailStatsSidebar'; -import { Button } from '../../components/ui'; -import { computeDeckStats } from '../../lib/deck-builder-stats.js'; -import { groupDeckCards } from '../../lib/deck-detail-grouping.js'; -import { getDeckFormatIcon } from '../../lib/deck-format-utils.js'; +import DeckDetailView from '../../components/DeckDetailView'; import { useAuth } from '../../lib/use-auth'; - -/* 2026-06-04 design-sweep pass: this page used the broken Tailwind - token classes (bg-bg-*, text-text-*, bg-accent-ember, - hover:bg-accent-ember-dark, border-accent-ember) that don't - resolve in tailwind.config.js. Sweep replaces them with inline - CSS variables + Button primitive + glass-panel surface + the - nav-item-active / nav-item-hover utilities so the page renders - with the Liquid Glass design system. */ +import { useDeckDetail } from '../../lib/use-deck-detail.js'; export default function DeckDetail() { - const { user } = useAuth(); - const router = useRouter(); - const { id: deckId } = router.query; - - const [deck, setDeck] = useState(null); - const [loading, setLoading] = useState(true); - const [groupBy, setGroupBy] = useState('type'); + const { user, loading: authLoading } = useAuth(); + const detail = useDeckDetail({ user, authLoading }); - const fetchDeck = async () => { - try { - const token = localStorage.getItem('auth_token'); - const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; - - const response = await fetch(`/api/decks/${deckId}`, { headers }); - - if (response.ok) { - const data = await response.json(); - setDeck(data); - } else { - console.error('Failed to fetch deck'); - router.push('/decks'); - } - } catch (error) { - console.error('Error fetching deck:', error); - router.push('/decks'); - } finally { - setLoading(false); - } - } - - useEffect(() => { - if (deckId) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- load deck when route id changes - fetchDeck(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- refetch when route deck id changes - }, [deckId]); - - if (loading) { + if (detail.showDeckLoading) { return (
@@ -70,15 +21,12 @@ export default function DeckDetail() { ); } - if (!deck) { + if (detail.showDeckNotFound) { return (
-

+

Deck not found

-
- {/* Header */} -
-
-
- - ← Back to Decks - -
-
- {getDeckFormatIcon(deck.format)} -

- {deck.name} -

- {deck.is_public && ( - - Public - - )} -
-

- by {deck.creator_username} • {deck.format} • {stats.totalCards} cards -

- {deck.description && ( -

- {deck.description} -

- )} -
- - {isOwner && ( -
- - - -
- )} -
- -
- - - -
-
+ ); -} \ No newline at end of file +}