refactor(decks): extract format utils and create modal (Brief 1) (#136)
Add shared deck format helpers and DecksCreateModal; decks page keeps the edit modal inline for Brief 2. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f83d71abe0
commit
290d79ccf8
3 changed files with 117 additions and 115 deletions
81
components/DecksCreateModal.js
Normal file
81
components/DecksCreateModal.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { Modal, Input, Button } from './ui';
|
||||||
|
import { DECK_INPUT_FIELD_CLASS } from '../lib/deck-format-utils.js';
|
||||||
|
|
||||||
|
export default function DecksCreateModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
newDeck,
|
||||||
|
setNewDeck,
|
||||||
|
onCreate,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} onClose={onClose} title="Create New Deck" size="md">
|
||||||
|
<form onSubmit={onCreate} className="space-y-4">
|
||||||
|
<Input
|
||||||
|
label="Deck Name *"
|
||||||
|
required
|
||||||
|
value={newDeck.name}
|
||||||
|
onChange={(e) => setNewDeck({ ...newDeck, name: e.target.value })}
|
||||||
|
placeholder="Enter deck name"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
className="block text-sm font-medium mb-2"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
htmlFor="create-deck-format"
|
||||||
|
>
|
||||||
|
Format
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="create-deck-format"
|
||||||
|
value={newDeck.format}
|
||||||
|
onChange={(e) => setNewDeck({ ...newDeck, 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="create-deck-description"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="create-deck-description"
|
||||||
|
value={newDeck.description}
|
||||||
|
onChange={(e) => setNewDeck({ ...newDeck, description: e.target.value })}
|
||||||
|
className={DECK_INPUT_FIELD_CLASS}
|
||||||
|
rows="3"
|
||||||
|
placeholder="Describe your deck strategy..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newDeck.is_public}
|
||||||
|
onChange={(e) => setNewDeck({ ...newDeck, 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">
|
||||||
|
Create Deck
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
lib/deck-format-utils.js
Normal file
23
lib/deck-format-utils.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
export const DECK_FORMAT_OPTIONS = ['Commander', 'Standard', 'Modern', 'Legacy'];
|
||||||
|
|
||||||
|
export const DECK_INPUT_FIELD_CLASS = 'input-field w-full';
|
||||||
|
|
||||||
|
export const DECK_FORMAT_BADGE_STYLE = {
|
||||||
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDeckFormatIcon(format) {
|
||||||
|
switch (format) {
|
||||||
|
case 'Commander':
|
||||||
|
return '⚔️';
|
||||||
|
case 'Standard':
|
||||||
|
return '🏆';
|
||||||
|
case 'Modern':
|
||||||
|
return '🔥';
|
||||||
|
case 'Legacy':
|
||||||
|
return '💎';
|
||||||
|
default:
|
||||||
|
return '🃏';
|
||||||
|
}
|
||||||
|
}
|
||||||
126
pages/decks.js
126
pages/decks.js
|
|
@ -2,7 +2,9 @@ import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
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 { Modal, Input, Button } from '../components/ui';
|
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';
|
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
|
||||||
|
|
@ -132,39 +134,6 @@ export default function Decks() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFormatIcon = (format) => {
|
|
||||||
switch (format) {
|
|
||||||
case 'Commander': return '⚔️';
|
|
||||||
case 'Standard': return '🏆';
|
|
||||||
case 'Modern': return '🔥';
|
|
||||||
case 'Legacy': return '💎';
|
|
||||||
default: return '🃏';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Single neutral chip style for format badges — the prior
|
|
||||||
// getFormatColor() returned 5 stale Tailwind colour pairs
|
|
||||||
// (bg-purple-100, bg-blue-100, etc.) that don't fit the
|
|
||||||
// Deck Hearth palette and don't render in dark theme anyway.
|
|
||||||
const formatBadgeStyle = {
|
|
||||||
backgroundColor: 'var(--bg-tertiary)',
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reusable inline-style for the .card surfaces used as stat
|
|
||||||
// cards + deck cards. We can't use the .glass-panel class +
|
|
||||||
// rounded-xl directly because we want the same gradient-border
|
|
||||||
// catch-light effect that the chrome chips have, and inline
|
|
||||||
// styles for that pattern are verbose. Easiest path: opt into
|
|
||||||
// the existing .glass-panel class for the multi-layer bg, then
|
|
||||||
// override border-radius via className. (.glass-panel doesn't
|
|
||||||
// set border-radius so we control it via Tailwind.)
|
|
||||||
const chipBgStyle = {};
|
|
||||||
|
|
||||||
// textarea / select use .input-field which is the Deck-Hearth
|
|
||||||
// standard input class (rounded-2xl + ember focus ring).
|
|
||||||
const inputFieldClass = 'input-field w-full';
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
|
|
@ -215,7 +184,7 @@ export default function Decks() {
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
<div className="glass-panel rounded-2xl p-6" style={chipBgStyle}>
|
<div className="glass-panel rounded-2xl p-6">
|
||||||
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
{decks.length}
|
{decks.length}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -261,10 +230,10 @@ export default function Decks() {
|
||||||
<div key={deck.id} className="glass-panel rounded-2xl p-6 transition-shadow">
|
<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 justify-between items-start mb-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-2xl">{getFormatIcon(deck.format)}</span>
|
<span className="text-2xl">{getDeckFormatIcon(deck.format)}</span>
|
||||||
<span
|
<span
|
||||||
className="px-2 py-1 rounded-full text-xs font-medium"
|
className="px-2 py-1 rounded-full text-xs font-medium"
|
||||||
style={formatBadgeStyle}
|
style={DECK_FORMAT_BADGE_STYLE}
|
||||||
>
|
>
|
||||||
{deck.format}
|
{deck.format}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -329,84 +298,13 @@ export default function Decks() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Modal
|
<DecksCreateModal
|
||||||
open={showCreateModal}
|
isOpen={showCreateModal}
|
||||||
onClose={() => setShowCreateModal(false)}
|
onClose={() => setShowCreateModal(false)}
|
||||||
title="Create New Deck"
|
newDeck={newDeck}
|
||||||
size="md"
|
setNewDeck={setNewDeck}
|
||||||
>
|
onCreate={handleCreateDeck}
|
||||||
<form onSubmit={handleCreateDeck} className="space-y-4">
|
|
||||||
<Input
|
|
||||||
label="Deck Name *"
|
|
||||||
required
|
|
||||||
value={newDeck.name}
|
|
||||||
onChange={(e) => setNewDeck({ ...newDeck, name: e.target.value })}
|
|
||||||
placeholder="Enter deck name"
|
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block text-sm font-medium mb-2"
|
|
||||||
style={{ color: 'var(--text-primary)' }}
|
|
||||||
htmlFor="create-deck-format"
|
|
||||||
>
|
|
||||||
Format
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="create-deck-format"
|
|
||||||
value={newDeck.format}
|
|
||||||
onChange={(e) => setNewDeck({ ...newDeck, format: e.target.value })}
|
|
||||||
className={inputFieldClass}
|
|
||||||
>
|
|
||||||
<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="create-deck-description"
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="create-deck-description"
|
|
||||||
value={newDeck.description}
|
|
||||||
onChange={(e) => setNewDeck({ ...newDeck, description: e.target.value })}
|
|
||||||
className={inputFieldClass}
|
|
||||||
rows="3"
|
|
||||||
placeholder="Describe your deck strategy..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={newDeck.is_public}
|
|
||||||
onChange={(e) => setNewDeck({ ...newDeck, 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={() => setShowCreateModal(false)}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" variant="primary" className="flex-1">
|
|
||||||
Create Deck
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={!!editingDeck}
|
open={!!editingDeck}
|
||||||
|
|
@ -434,7 +332,7 @@ export default function Decks() {
|
||||||
id="edit-deck-format"
|
id="edit-deck-format"
|
||||||
value={editingDeck.format}
|
value={editingDeck.format}
|
||||||
onChange={(e) => setEditingDeck({ ...editingDeck, format: e.target.value })}
|
onChange={(e) => setEditingDeck({ ...editingDeck, format: e.target.value })}
|
||||||
className={inputFieldClass}
|
className={DECK_INPUT_FIELD_CLASS}
|
||||||
>
|
>
|
||||||
<option value="Commander">Commander</option>
|
<option value="Commander">Commander</option>
|
||||||
<option value="Standard">Standard</option>
|
<option value="Standard">Standard</option>
|
||||||
|
|
@ -454,7 +352,7 @@ export default function Decks() {
|
||||||
id="edit-deck-description"
|
id="edit-deck-description"
|
||||||
value={editingDeck.description || ''}
|
value={editingDeck.description || ''}
|
||||||
onChange={(e) => setEditingDeck({ ...editingDeck, description: e.target.value })}
|
onChange={(e) => setEditingDeck({ ...editingDeck, description: e.target.value })}
|
||||||
className={inputFieldClass}
|
className={DECK_INPUT_FIELD_CLASS}
|
||||||
rows="3"
|
rows="3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue