feat: some changes and admin
This commit is contained in:
184
nextjs/src/app/admin/(dashboard)/players/PlayersAdminClient.tsx
Normal file
184
nextjs/src/app/admin/(dashboard)/players/PlayersAdminClient.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user