Files
catherine-league/nextjs/src/app/admin/(dashboard)/players/PlayersAdminClient.tsx
2026-03-31 16:09:03 +09:00

185 lines
5.7 KiB
TypeScript

'use client';
import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
import style from '@/styles/admin.module.scss';
type Char = { id: number; name_id: string; name: string; name_jp: string };
type PlayerRow = {
id: number;
player_key: string;
player_name: string;
description: string | null;
image: string | null;
character_id: string | null;
name_id?: string;
name_jp?: string;
};
export function PlayersAdminClient() {
const [players, setPlayers] = useState<PlayerRow[]>([]);
const [characters, setCharacters] = useState<Char[]>([]);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const [playerKey, setPlayerKey] = useState('');
const [playerName, setPlayerName] = useState('');
const [description, setDescription] = useState('');
const [image, setImage] = useState('');
const [characterId, setCharacterId] = useState('');
const load = useCallback(async () => {
setError('');
const [pr, cr] = await Promise.all([
fetch('/api/admin/players', { credentials: 'include' }),
fetch('/api/admin/characters', { credentials: 'include' }),
]);
const pj = await pr.json().catch(() => ({}));
const cj = await cr.json().catch(() => ({}));
if (!pr.ok) {
setError((pj as { error?: string }).error ?? 'Failed to load players');
setLoading(false);
return;
}
if (!cr.ok) {
setError((cj as { error?: string }).error ?? 'Failed to load characters');
setLoading(false);
return;
}
setPlayers((pj as { players: PlayerRow[] }).players ?? []);
setCharacters((cj as { characters: Char[] }).characters ?? []);
setLoading(false);
}, []);
useEffect(() => {
load();
}, [load]);
async function addPlayer(e: React.FormEvent) {
e.preventDefault();
setError('');
const res = await fetch('/api/admin/players', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
player_key: playerKey,
player_name: playerName,
description,
image: image || null,
character_id: characterId,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setError((data as { error?: string }).error ?? 'Create failed');
return;
}
setPlayerKey('');
setPlayerName('');
setDescription('');
setImage('');
setCharacterId('');
await load();
}
async function remove(id: number) {
if (!window.confirm('Delete this player?')) return;
setError('');
const res = await fetch(`/api/admin/players/${id}`, { method: 'DELETE', credentials: 'include' });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setError((data as { error?: string }).error ?? 'Delete failed');
return;
}
await load();
}
return (
<div className={style.card}>
<p>
<Link href="/admin"> Dashboard</Link>
</p>
<h1 className={style.title}>Players</h1>
{error ? <p className={style.error}>{error}</p> : null}
{loading ? <p>Loading</p> : null}
{!loading && (
<>
<table className={style.table}>
<thead>
<tr>
<th>Key</th>
<th>Name</th>
<th>Character</th>
<th />
</tr>
</thead>
<tbody>
{players.map((p) => (
<tr key={p.id}>
<td>{p.player_key}</td>
<td>{p.player_name}</td>
<td>{p.name_jp ?? p.character_id}</td>
<td>
<button type="button" className={style.btnDanger} onClick={() => remove(p.id)}>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
<hr className={style.hr} />
<h2 className={style.title} style={{ fontSize: '1.1rem' }}>
Add player
</h2>
<form className={style.form} style={{ maxWidth: 480 }} onSubmit={addPlayer}>
<label className={style.label}>
player_key
<input className={style.input} value={playerKey} onChange={(e) => setPlayerKey(e.target.value)} required />
</label>
<label className={style.label}>
player_name
<input className={style.input} value={playerName} onChange={(e) => setPlayerName(e.target.value)} required />
</label>
<label className={style.label}>
description
<textarea
className={`${style.input} ${style.textarea}`}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</label>
<label className={style.label}>
image URL (optional)
<input className={style.input} value={image} onChange={(e) => setImage(e.target.value)} />
</label>
<label className={style.label}>
character
<select
className={style.input}
value={characterId}
onChange={(e) => setCharacterId(e.target.value)}
required
>
<option value=""></option>
{characters.map((c) => (
<option key={c.id} value={c.id}>
{c.name_jp} ({c.name_id})
</option>
))}
</select>
</label>
<button className={style.btn} type="submit">
Add
</button>
</form>
</>
)}
</div>
);
}