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

267 lines
8.3 KiB
TypeScript

'use client';
import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
import {
btn,
btnDanger,
card,
error as errorClass,
formWide,
hr,
input,
label,
metaLine,
rowActions,
sectionBlock,
sectionTitle,
textarea,
title,
} from '@/lib/adminUi';
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;
};
const field = `${input} ${textarea}`;
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();
}
function updateLocal(id: number, patch: Partial<PlayerRow>) {
setPlayers((prev) => prev.map((r) => (r.id === id ? { ...r, ...patch } : r)));
}
async function save(row: PlayerRow) {
setError('');
const cid = row.character_id != null && row.character_id !== '' ? String(row.character_id) : '';
if (!cid) {
setError('Character is required');
return;
}
const res = await fetch(`/api/admin/players/${row.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
player_key: row.player_key,
player_name: row.player_name,
description: row.description ?? '',
image: row.image === '' || row.image == null ? null : row.image,
character_id: cid,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setError((data as { error?: string }).error ?? 'Save failed');
return;
}
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={card}>
<p>
<Link href="/admin"> Dashboard</Link>
</p>
<h1 className={title}>Players</h1>
{error ? <p className={errorClass}>{error}</p> : null}
{loading ? <p>Loading</p> : null}
{!loading && (
<>
{players.map((p) => (
<div key={p.id} className={sectionBlock}>
<p className={metaLine}>
id: {p.id}
{p.name_jp ? ` · ${p.name_jp}` : ''}
</p>
<label className={label}>
player_key
<input
className={input}
value={p.player_key}
onChange={(e) => updateLocal(p.id, { player_key: e.target.value })}
/>
</label>
<label className={label}>
player_name
<input
className={input}
value={p.player_name}
onChange={(e) => updateLocal(p.id, { player_name: e.target.value })}
/>
</label>
<label className={label}>
description
<textarea
className={field}
value={p.description ?? ''}
onChange={(e) => updateLocal(p.id, { description: e.target.value })}
/>
</label>
<label className={label}>
image URL (optional)
<input
className={input}
value={p.image ?? ''}
onChange={(e) => updateLocal(p.id, { image: e.target.value || null })}
/>
</label>
<label className={label}>
character
<select
className={input}
value={p.character_id != null && p.character_id !== '' ? String(p.character_id) : ''}
onChange={(e) => updateLocal(p.id, { character_id: 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>
<div className={rowActions}>
<button type="button" className={btn} onClick={() => save(p)}>
Save
</button>
<button type="button" className={btnDanger} onClick={() => remove(p.id)}>
Delete
</button>
</div>
</div>
))}
<hr className={hr} />
<h2 className={sectionTitle}>Add player</h2>
<form className={formWide} onSubmit={addPlayer}>
<label className={label}>
player_key
<input className={input} value={playerKey} onChange={(e) => setPlayerKey(e.target.value)} required />
</label>
<label className={label}>
player_name
<input className={input} value={playerName} onChange={(e) => setPlayerName(e.target.value)} required />
</label>
<label className={label}>
description
<textarea className={field} value={description} onChange={(e) => setDescription(e.target.value)} />
</label>
<label className={label}>
image URL (optional)
<input className={input} value={image} onChange={(e) => setImage(e.target.value)} />
</label>
<label className={label}>
character
<select
className={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={btn} type="submit">
Add
</button>
</form>
</>
)}
</div>
);
}