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
+}