refactor(decks): extract edit modal and deck grid (Brief 2) #142
3 changed files with 192 additions and 166 deletions
80
components/DecksEditModal.js
Normal file
80
components/DecksEditModal.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { Modal, Input, Button } from './ui';
|
||||||
|
import { DECK_INPUT_FIELD_CLASS } from '../lib/deck-format-utils.js';
|
||||||
|
|
||||||
|
export default function DecksEditModal({
|
||||||
|
editingDeck,
|
||||||
|
setEditingDeck,
|
||||||
|
onClose,
|
||||||
|
onEdit,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Modal open={!!editingDeck} onClose={onClose} title="Edit Deck" size="md">
|
||||||
|
{editingDeck && (
|
||||||
|
<form onSubmit={onEdit} 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={onClose} className="flex-1">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="primary" className="flex-1">
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
components/DecksGrid.js
Normal file
98
components/DecksGrid.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Button } from './ui';
|
||||||
|
import { DECK_FORMAT_BADGE_STYLE, getDeckFormatIcon } from '../lib/deck-format-utils.js';
|
||||||
|
|
||||||
|
export default function DecksGrid({ decks, onEditDeck, onDeleteDeck, onCreateDeck }) {
|
||||||
|
if (decks.length === 0) {
|
||||||
|
return (
|
||||||
|
<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={onCreateDeck}>
|
||||||
|
Create Your First Deck
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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
|
||||||
|
type="button"
|
||||||
|
onClick={() => onEditDeck({ ...deck })}
|
||||||
|
className="transition-colors"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
aria-label="Edit deck"
|
||||||
|
>
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onDeleteDeck(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>
|
||||||
|
);
|
||||||
|
}
|
||||||
178
pages/decks.js
178
pages/decks.js
|
|
@ -3,8 +3,9 @@ import { useRouter } from 'next/router';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
import DecksCreateModal from '../components/DecksCreateModal';
|
import DecksCreateModal from '../components/DecksCreateModal';
|
||||||
import { Modal, Input, Button } from '../components/ui';
|
import DecksEditModal from '../components/DecksEditModal';
|
||||||
import { DECK_FORMAT_BADGE_STYLE, DECK_INPUT_FIELD_CLASS, getDeckFormatIcon } from '../lib/deck-format-utils.js';
|
import DecksGrid from '../components/DecksGrid';
|
||||||
|
import { Button } from '../components/ui';
|
||||||
import { useAuth } from '../lib/use-auth';
|
import { useAuth } from '../lib/use-auth';
|
||||||
|
|
||||||
/* 2026-06-04 design-sweep pass: the Tailwind token classes that this
|
/* 2026-06-04 design-sweep pass: the Tailwind token classes that this
|
||||||
|
|
@ -210,93 +211,12 @@ export default function Decks() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Decks Grid */}
|
<DecksGrid
|
||||||
{decks.length === 0 ? (
|
decks={decks}
|
||||||
<div className="text-center py-12">
|
onEditDeck={setEditingDeck}
|
||||||
<div className="text-6xl mb-4">🃏</div>
|
onDeleteDeck={handleDeleteDeck}
|
||||||
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
onCreateDeck={() => setShowCreateModal(true)}
|
||||||
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
|
<DecksCreateModal
|
||||||
isOpen={showCreateModal}
|
isOpen={showCreateModal}
|
||||||
|
|
@ -306,84 +226,12 @@ export default function Decks() {
|
||||||
onCreate={handleCreateDeck}
|
onCreate={handleCreateDeck}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<DecksEditModal
|
||||||
open={!!editingDeck}
|
editingDeck={editingDeck}
|
||||||
|
setEditingDeck={setEditingDeck}
|
||||||
onClose={() => setEditingDeck(null)}
|
onClose={() => setEditingDeck(null)}
|
||||||
title="Edit Deck"
|
onEdit={handleEditDeck}
|
||||||
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>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue