Compare commits

...

2 Commits

Author SHA1 Message Date
3582fca2d9 chore: bump up version 2026-03-31 16:23:10 +09:00
0305cc8d51 feat: allowed player edit and fix for css 2026-03-31 16:22:56 +09:00
3 changed files with 118 additions and 29 deletions

View File

@@ -1,2 +1,2 @@
APP_VERSION=1.0.1 APP_VERSION=1.0.2
docker buildx build --platform linux/arm64 -f nextjs/Dockerfile -t cfc-web:$APP_VERSION . docker buildx build --platform linux/arm64 -f nextjs/Dockerfile -t cfc-web:$APP_VERSION .

View File

@@ -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' }}>

View File

@@ -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 {