185 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|