deckhearth/pages/games/[id].js

213 lines
7.6 KiB
JavaScript
Raw Normal View History

import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import Layout from '../../components/Layout';
import CardPreview from '../../components/designer/CardFrame';
import { Button, Input } from '../../components/ui';
import { useAuth } from '../../lib/use-auth';
export default function GameSpace() {
const router = useRouter();
const { id } = router.query;
const { user, loading: authLoading } = useAuth();
const [game, setGame] = useState(null);
const [designs, setDesigns] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [editing, setEditing] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!authLoading && !user) {
router.push('/login');
}
}, [authLoading, user, router]);
useEffect(() => {
if (!user || !id) return undefined;
const loadGame = async () => {
setLoading(true);
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/custom-games/${id}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const data = await response.json();
if (response.ok) {
setGame(data.game);
setDesigns(data.designs);
setName(data.game.name);
setDescription(data.game.description || '');
} else {
setError(data.error || 'Game not found.');
}
} catch {
setError('Failed to load the game.');
} finally {
setLoading(false);
}
};
loadGame();
return undefined;
}, [user, id]);
const handleSave = async () => {
if (!name.trim()) return;
setSaving(true);
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/custom-games/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: JSON.stringify({ name, description }),
});
const data = await response.json();
if (response.ok) {
setGame((prev) => ({ ...prev, name: data.game.name, description: data.game.description }));
setEditing(false);
} else {
setError(data.error || 'Save failed.');
}
} catch {
setError('Save failed.');
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!window.confirm(`Delete "${game?.name}"? Cards keep existing but leave the game.`)) return;
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/custom-games/${id}`, {
method: 'DELETE',
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (response.ok) {
router.push('/games');
} else {
setError('Delete failed.');
}
} catch {
setError('Delete failed.');
}
};
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>
);
}
if (error && !game) {
return (
<Layout user={user}>
<div className="p-6 max-w-2xl mx-auto text-center py-20">
<h1 className="text-xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>{error}</h1>
<Button variant="primary" onClick={() => router.push('/games')}>Back to My Games</Button>
</div>
</Layout>
);
}
return (
<Layout user={user}>
<div className="p-4 sm:p-6 max-w-[1500px] mx-auto space-y-6">
{/* Header */}
<div className="pt-2 flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
{editing ? (
<div className="flex-1 space-y-3">
<Input value={name} onChange={(e) => setName(e.target.value)} maxLength={100} />
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description (optional)"
maxLength={300}
/>
<div className="flex gap-2">
<Button variant="primary" onClick={handleSave} disabled={saving || !name.trim()}>
{saving ? 'Saving…' : 'Save'}
</Button>
<Button variant="secondary" onClick={() => setEditing(false)}>Cancel</Button>
</div>
</div>
) : (
<div>
<h1 className="text-2xl sm:text-3xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
{game.name}
</h1>
<p className="text-base" style={{ color: 'var(--text-secondary)' }}>
{game.description || 'Custom game system'} · {designs.length} card{designs.length === 1 ? '' : 's'}
</p>
</div>
)}
<div className="flex gap-2">
<Button variant="secondary" onClick={() => router.push('/games')}>My Games</Button>
<Button variant="secondary" onClick={() => setEditing(true)}>Edit</Button>
<Button variant="secondary" onClick={handleDelete}>Delete</Button>
<Button variant="primary" onClick={() => router.push(`/designer?game=${game.id}`)}>
+ New Card
</Button>
</div>
</div>
{error && (
<div className="glass-panel rounded-xl px-4 py-3 text-sm" style={{ color: '#f87171' }} role="alert">
{error}
</div>
)}
{/* Designs grid */}
{designs.length === 0 ? (
<div className="text-center py-16">
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
No cards in this game yet
</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
Design the first card for {game.name}.
</p>
<Button variant="primary" size="lg" onClick={() => router.push(`/designer?game=${game.id}`)}>
Open Card Designer
</Button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-5">
{designs.map((design) => (
<div key={design.id} className="glass-panel rounded-2xl p-4 flex flex-col gap-3">
<div
className="cursor-pointer"
onClick={() => router.push(`/designer?id=${design.id}`)}
role="link"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && router.push(`/designer?id=${design.id}`)}
>
<CardPreview design={design} maxWidth={280} />
</div>
<div className="min-w-0">
<p className="font-semibold truncate" style={{ color: 'var(--text-primary)' }}>
{design.name}
</p>
<p className="text-xs truncate" style={{ color: 'var(--text-secondary)' }}>
{design.card_type || 'No type'} {design.mana_cost ? `· ${design.mana_cost}` : ''}
</p>
</div>
<Button variant="secondary" size="sm" onClick={() => router.push(`/designer?id=${design.id}`)}>
Edit
</Button>
</div>
))}
</div>
)}
</div>
</Layout>
);
}