deckhearth/pages/decks.js
Randall Stillwell 31788ed4d1 refactor(decks): extract format utils and create modal (Brief 1)
Add shared deck format helpers and DecksCreateModal; decks page keeps
the edit modal inline for Brief 2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 21:20:45 -05:00

390 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { Modal, Input, Button } from '../components/ui';
import { DECK_FORMAT_BADGE_STYLE, DECK_INPUT_FIELD_CLASS, getDeckFormatIcon } from '../lib/deck-format-utils.js';
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. */
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 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) {
return (
<Layout user={user}>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Please log in to view your decks
</h1>
<Link href="/login" style={{ color: 'var(--accent-ember)' }} className="hover:underline">
Go to Login
</Link>
</div>
</div>
</Layout>
);
}
if (loading) {
return (
<Layout user={user}>
<div className="flex items-center justify-center min-h-screen">
<div
className="animate-spin rounded-full h-32 w-32 border-b-2"
style={{ borderColor: 'var(--accent-ember)' }}
/>
</div>
</Layout>
);
}
return (
<Layout user={user}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
My Decks
</h1>
<p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
Build and manage your MTG decks
</p>
</div>
<Button variant="primary" onClick={() => setShowCreateModal(true)}>
Create New Deck
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="glass-panel rounded-2xl p-6">
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{decks.length}
</div>
<div style={{ color: 'var(--text-secondary)' }}>Total Decks</div>
</div>
<div className="glass-panel rounded-2xl p-6">
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{decks.filter((d) => d.format === 'Commander').length}
</div>
<div style={{ color: 'var(--text-secondary)' }}>Commander</div>
</div>
<div className="glass-panel rounded-2xl p-6">
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{decks.filter((d) => d.is_public).length}
</div>
<div style={{ color: 'var(--text-secondary)' }}>Public</div>
</div>
<div className="glass-panel rounded-2xl p-6">
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
{decks.reduce((sum, deck) => sum + (deck.card_count || 0), 0)}
</div>
<div style={{ color: 'var(--text-secondary)' }}>Total Cards</div>
</div>
</div>
{/* Decks Grid */}
{decks.length === 0 ? (
<div className="text-center py-12">
<div className="text-6xl mb-4">🃏</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
No decks yet
</h3>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Create your first deck to get started
</p>
<Button variant="primary" onClick={() => setShowCreateModal(true)}>
Create Your First Deck
</Button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{decks.map((deck) => (
<div key={deck.id} className="glass-panel rounded-2xl p-6 transition-shadow">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center space-x-2">
<span className="text-2xl">{getDeckFormatIcon(deck.format)}</span>
<span
className="px-2 py-1 rounded-full text-xs font-medium"
style={DECK_FORMAT_BADGE_STYLE}
>
{deck.format}
</span>
</div>
<div className="flex space-x-2">
<button
onClick={() => setEditingDeck({ ...deck })}
className="transition-colors"
style={{ color: 'var(--text-secondary)' }}
aria-label="Edit deck"
>
</button>
<button
onClick={() => handleDeleteDeck(deck.id)}
className="transition-colors"
style={{ color: 'var(--text-secondary)' }}
aria-label="Delete deck"
>
🗑
</button>
</div>
</div>
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{deck.name}
</h3>
{deck.description && (
<p
className="text-sm mb-4 line-clamp-2"
style={{ color: 'var(--text-secondary)' }}
>
{deck.description}
</p>
)}
<div
className="flex justify-between items-center text-sm mb-4"
style={{ color: 'var(--text-secondary)' }}
>
<span>{deck.card_count || 0} cards</span>
{deck.is_public && (
<span style={{ color: 'var(--accent-ember)' }}>Public</span>
)}
</div>
<div className="flex space-x-2">
<Link href={`/deck-builder?deck=${deck.id}`} className="flex-1">
<Button variant="primary" size="md" className="w-full">
Edit Deck
</Button>
</Link>
<Link href={`/deck/${deck.id}`} className="flex-1">
<Button variant="secondary" size="md" className="w-full">
View
</Button>
</Link>
</div>
</div>
))}
</div>
)}
<DecksCreateModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
newDeck={newDeck}
setNewDeck={setNewDeck}
onCreate={handleCreateDeck}
/>
<Modal
open={!!editingDeck}
onClose={() => setEditingDeck(null)}
title="Edit Deck"
size="md"
>
{editingDeck && (
<form onSubmit={handleEditDeck} className="space-y-4">
<Input
label="Deck Name *"
required
value={editingDeck.name}
onChange={(e) => setEditingDeck({ ...editingDeck, name: e.target.value })}
/>
<div>
<label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="edit-deck-format"
>
Format
</label>
<select
id="edit-deck-format"
value={editingDeck.format}
onChange={(e) => setEditingDeck({ ...editingDeck, format: e.target.value })}
className={DECK_INPUT_FIELD_CLASS}
>
<option value="Commander">Commander</option>
<option value="Standard">Standard</option>
<option value="Modern">Modern</option>
<option value="Legacy">Legacy</option>
</select>
</div>
<div>
<label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="edit-deck-description"
>
Description
</label>
<textarea
id="edit-deck-description"
value={editingDeck.description || ''}
onChange={(e) => setEditingDeck({ ...editingDeck, description: e.target.value })}
className={DECK_INPUT_FIELD_CLASS}
rows="3"
/>
</div>
<label className="flex items-center">
<input
type="checkbox"
checked={editingDeck.is_public}
onChange={(e) => setEditingDeck({ ...editingDeck, is_public: e.target.checked })}
className="mr-2"
style={{ accentColor: 'var(--accent-ember)' }}
/>
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Make deck public
</span>
</label>
<div className="flex space-x-3 pt-2">
<Button
type="button"
variant="secondary"
onClick={() => setEditingDeck(null)}
className="flex-1"
>
Cancel
</Button>
<Button type="submit" variant="primary" className="flex-1">
Save Changes
</Button>
</div>
</form>
)}
</Modal>
</div>
</Layout>
);
}