Compare commits
2 Commits
af67ed6255
...
3582fca2d9
| Author | SHA1 | Date | |
|---|---|---|---|
| 3582fca2d9 | |||
| 0305cc8d51 |
2
build.sh
2
build.sh
@@ -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 .
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user