feat: allowed player edit and fix for css
This commit is contained in:
@@ -85,6 +85,37 @@ export function PlayersAdminClient() {
|
|||||||
await load();
|
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) {
|
async function remove(id: number) {
|
||||||
if (!window.confirm('Delete this player?')) return;
|
if (!window.confirm('Delete this player?')) return;
|
||||||
setError('');
|
setError('');
|
||||||
@@ -107,30 +138,73 @@ export function PlayersAdminClient() {
|
|||||||
{loading ? <p>Loading…</p> : null}
|
{loading ? <p>Loading…</p> : null}
|
||||||
{!loading && (
|
{!loading && (
|
||||||
<>
|
<>
|
||||||
<table className={style.table}>
|
{players.map((p) => (
|
||||||
<thead>
|
<div
|
||||||
<tr>
|
key={p.id}
|
||||||
<th>Key</th>
|
style={{ marginBottom: 24, paddingBottom: 16, borderBottom: '1px solid #eee' }}
|
||||||
<th>Name</th>
|
>
|
||||||
<th>Character</th>
|
<p style={{ margin: '0 0 8px', fontSize: '0.85rem', color: '#666' }}>
|
||||||
<th />
|
id: {p.id}
|
||||||
</tr>
|
{p.name_jp ? ` · ${p.name_jp}` : ''}
|
||||||
</thead>
|
</p>
|
||||||
<tbody>
|
<label className={style.label}>
|
||||||
{players.map((p) => (
|
player_key
|
||||||
<tr key={p.id}>
|
<input
|
||||||
<td>{p.player_key}</td>
|
className={style.input}
|
||||||
<td>{p.player_name}</td>
|
value={p.player_key}
|
||||||
<td>{p.name_jp ?? p.character_id}</td>
|
onChange={(e) => updateLocal(p.id, { player_key: e.target.value })}
|
||||||
<td>
|
/>
|
||||||
<button type="button" className={style.btnDanger} onClick={() => remove(p.id)}>
|
</label>
|
||||||
Delete
|
<label className={style.label}>
|
||||||
</button>
|
player_name
|
||||||
</td>
|
<input
|
||||||
</tr>
|
className={style.input}
|
||||||
))}
|
value={p.player_name}
|
||||||
</tbody>
|
onChange={(e) => updateLocal(p.id, { player_name: e.target.value })}
|
||||||
</table>
|
/>
|
||||||
|
</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} />
|
<hr className={style.hr} />
|
||||||
<h2 className={style.title} style={{ fontSize: '1.1rem' }}>
|
<h2 className={style.title} style={{ fontSize: '1.1rem' }}>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
animation: bounce 1s;
|
animation: bounce 1s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Day + night: Firefox does not transition between two background-image URLs.
|
||||||
|
// Stack night in ::after and fade opacity (works everywhere).
|
||||||
.background {
|
.background {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -30,14 +32,27 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
background-image: url('https://static.catherine-fc.com/media/bgimage.png');
|
background-image: url('https://static.catherine-fc.com/media/bgimage.png');
|
||||||
background-repeat: none;
|
background-repeat: no-repeat;
|
||||||
|
background-position: center top;
|
||||||
width: 1366px;
|
width: 1366px;
|
||||||
height: 401px;
|
height: 401px;
|
||||||
animation: upAndDown 10s linear infinite;
|
animation: upAndDown 10s linear infinite;
|
||||||
transition: background-image 0.5s ease-in-out;
|
&::after {
|
||||||
&:hover {
|
content: '';
|
||||||
transition: background-image 0.5s ease-in-out;
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background-image: url('https://static.catherine-fc.com/media/bgimagenight.png');
|
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 {
|
.logoHeader {
|
||||||
|
|||||||
Reference in New Issue
Block a user