feat: allowed player edit and fix for css

This commit is contained in:
2026-03-31 16:22:56 +09:00
parent af67ed6255
commit 0305cc8d51
2 changed files with 117 additions and 28 deletions

View File

@@ -85,6 +85,37 @@ export function PlayersAdminClient() {
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('');
@@ -107,30 +138,73 @@ export function PlayersAdminClient() {
{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>
{players.map((p) => (
<div
key={p.id}
style={{ marginBottom: 24, paddingBottom: 16, borderBottom: '1px solid #eee' }}
>
<p style={{ margin: '0 0 8px', fontSize: '0.85rem', color: '#666' }}>
id: {p.id}
{p.name_jp ? ` · ${p.name_jp}` : ''}
</p>
<label className={style.label}>
player_key
<input
className={style.input}
value={p.player_key}
onChange={(e) => updateLocal(p.id, { player_key: e.target.value })}
/>
</label>
<label className={style.label}>
player_name
<input
className={style.input}
value={p.player_name}
onChange={(e) => updateLocal(p.id, { player_name: e.target.value })}
/>
</label>
<label className={style.label}>
description
<textarea
className={`${style.input} ${style.textarea}`}
value={p.description ?? ''}
onChange={(e) => updateLocal(p.id, { description: e.target.value })}
/>
</label>
<label className={style.label}>
image URL (optional)
<input
className={style.input}
value={p.image ?? ''}
onChange={(e) => updateLocal(p.id, { image: e.target.value || null })}
/>
</label>
<label className={style.label}>
character
<select
className={style.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={style.rowActions}>
<button type="button" className={style.btn} onClick={() => save(p)}>
Save
</button>
<button type="button" className={style.btnDanger} onClick={() => remove(p.id)}>
Delete
</button>
</div>
</div>
))}
<hr className={style.hr} />
<h2 className={style.title} style={{ fontSize: '1.1rem' }}>

View File

@@ -23,6 +23,8 @@
animation: bounce 1s;
}
}
// Day + night: Firefox does not transition between two background-image URLs.
// Stack night in ::after and fade opacity (works everywhere).
.background {
position: absolute;
top: 0;
@@ -30,14 +32,27 @@
right: 0;
margin: auto;
background-image: url('https://static.catherine-fc.com/media/bgimage.png');
background-repeat: none;
background-repeat: no-repeat;
background-position: center top;
width: 1366px;
height: 401px;
animation: upAndDown 10s linear infinite;
transition: background-image 0.5s ease-in-out;
&:hover {
transition: background-image 0.5s ease-in-out;
&::after {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-image: url('https://static.catherine-fc.com/media/bgimagenight.png');
background-repeat: no-repeat;
background-position: center top;
opacity: 0;
transition: opacity 0.5s ease-in-out;
pointer-events: none;
}
&:hover::after {
opacity: 1;
}
}
.logoHeader {