Add player name input and default value handling; enhance loading shimmer effect
This commit is contained in:
@@ -2295,6 +2295,7 @@
|
||||
const myRunsSection = qs('run-my-runs-section');
|
||||
const savedRunsListEl = qs('run-saved-runs-list');
|
||||
const saveAccountBtn = qs('run-save-account');
|
||||
const playerNameInput = qs('run-player-name');
|
||||
|
||||
function setRunFormStatus(message, isError, isSuccess){
|
||||
if(!formStatusEl) return;
|
||||
@@ -2309,6 +2310,23 @@
|
||||
listStatusEl.classList.toggle('error-text', !!isError);
|
||||
}
|
||||
|
||||
function showRunSubmissionsShimmer(){
|
||||
if(!submissionsEl) return;
|
||||
submissionsEl.textContent = '';
|
||||
for(let i = 0; i < 4; i += 1){
|
||||
const card = document.createElement('article');
|
||||
card.className = 'submission-card submission-card--shimmer';
|
||||
card.setAttribute('aria-hidden', 'true');
|
||||
submissionsEl.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
function applyRunPlayerDefault(){
|
||||
if(!playerNameInput || !fedlServerUsername) return;
|
||||
if(String(playerNameInput.value || '').trim() !== '') return;
|
||||
playerNameInput.value = fedlServerUsername;
|
||||
}
|
||||
|
||||
function renderRunSubmissions(runs){
|
||||
submissionsEl.innerHTML = '';
|
||||
if(!runs.length){
|
||||
@@ -2417,6 +2435,9 @@
|
||||
levelOptionsEl.innerHTML = titles.map(title=>`<option value="${escapeAttr(title)}"></option>`).join('');
|
||||
}).catch(err=>console.error(err));
|
||||
|
||||
showRunSubmissionsShimmer();
|
||||
setRunListStatus('Loading recent submissions…');
|
||||
|
||||
loadRuns().then(runs=>{
|
||||
const sortedRuns = runs.slice().sort((a,b)=>new Date(b.submittedAt) - new Date(a.submittedAt));
|
||||
renderRunSubmissions(sortedRuns);
|
||||
@@ -2450,6 +2471,7 @@
|
||||
|
||||
document.addEventListener('fedl-auth-updated', ()=>{
|
||||
renderMySavedRuns();
|
||||
applyRunPlayerDefault();
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function(event){
|
||||
@@ -2483,6 +2505,7 @@
|
||||
}
|
||||
clearRunsCache();
|
||||
form.reset();
|
||||
applyRunPlayerDefault();
|
||||
const okMsg = fedlServerUsername
|
||||
? `Run submitted successfully. It is linked to your account (${fedlServerUsername}) for moderators.`
|
||||
: 'Run submitted successfully. The admin panel can review it now.';
|
||||
@@ -2506,6 +2529,7 @@
|
||||
.finally(()=>{
|
||||
renderMySavedRuns();
|
||||
fedlUpdateAuthNav();
|
||||
applyRunPlayerDefault();
|
||||
});
|
||||
|
||||
loadRunPage();
|
||||
@@ -2561,9 +2585,9 @@
|
||||
setSignupStatus('Account created successfully. Loading your data…', 'success');
|
||||
return fedlPullUserStateToLocal(data.userId);
|
||||
}).then(()=>{
|
||||
setSignupStatus('You are signed in. Redirecting to the roulette page…', 'success');
|
||||
setSignupStatus('You are signed in. Redirecting to the home page…', 'success');
|
||||
setTimeout(()=>{
|
||||
window.location.href = 'roulette.html';
|
||||
window.location.href = 'index.html';
|
||||
}, FEDL_AUTH_REDIRECT_MS);
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
@@ -2612,9 +2636,9 @@
|
||||
setLoginStatus('Signed in successfully. Loading your data…', 'success');
|
||||
return fedlPullUserStateToLocal(data.userId);
|
||||
}).then(()=>{
|
||||
setLoginStatus('Welcome back. Redirecting to the roulette page…', 'success');
|
||||
setLoginStatus('Welcome back. Redirecting to the home page…', 'success');
|
||||
setTimeout(()=>{
|
||||
window.location.href = 'roulette.html';
|
||||
window.location.href = 'index.html';
|
||||
}, FEDL_AUTH_REDIRECT_MS);
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
@@ -2628,7 +2652,7 @@
|
||||
fedlRefreshAuthState().finally(()=>{
|
||||
fedlUpdateAuthNav();
|
||||
if ((page === 'signup' || page === 'login') && fedlServerUsername) {
|
||||
window.location.replace('roulette.html');
|
||||
window.location.replace('index.html');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<label class="control-block" for="run-player-name">
|
||||
<span>Player Name</span>
|
||||
<input id="run-player-name" name="playerName" type="text" placeholder="Your in-game name" required />
|
||||
<span class="muted small">When you are logged in, your account username is filled here automatically (you can change it).</span>
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="run-level-title">
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# FEDL HTTP server
|
||||
|
||||
Node.js static file + JSON API server for the FEDL site. It serves the repo root (`../`) as files, exposes REST-style JSON endpoints, and broadcasts **Server-Sent Events (SSE)** when list or run data changes.
|
||||
|
||||
## Running
|
||||
|
||||
From the **repository root**:
|
||||
|
||||
```bash
|
||||
node server/server.js
|
||||
```
|
||||
|
||||
Or from `server/`:
|
||||
|
||||
```bash
|
||||
node server.js
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `PORT` | `8090` | Listen port |
|
||||
| `HOST` | `127.0.0.1` | Bind address |
|
||||
| `ADMIN_PASSWORD` | _(empty)_ | If set, **HTTP Basic** password for admin-only routes (username can be anything; password must match) |
|
||||
| `AREDL_ACCESS_TOKEN` | _(empty)_ | Bearer token for AREDL import APIs |
|
||||
| `AREDL_API_KEY` | _(empty)_ | API key header for AREDL import APIs |
|
||||
|
||||
Console output shows the effective `PORT`, `HOST`, data paths, and whether admin protection is enabled.
|
||||
|
||||
## Base path
|
||||
|
||||
All HTTP handling assumes a **URL prefix** of `/fedl`. Incoming paths are normalized: if the request path starts with `/fedl`, that prefix is stripped and the remainder is used for routing and static files.
|
||||
|
||||
**Examples** (with defaults, no reverse proxy):
|
||||
|
||||
- Site root (often `index.html`): `http://127.0.0.1:8090/fedl` or `http://127.0.0.1:8090/fedl/`
|
||||
- API list: `http://127.0.0.1:8090/fedl/api/list`
|
||||
- Events: `http://127.0.0.1:8090/fedl/events`
|
||||
|
||||
If you mount the app at a different path in production, change the `BASE` constant in `server.js` to match.
|
||||
|
||||
## CORS
|
||||
|
||||
Responses to API routes set permissive CORS headers (`Access-Control-Allow-Origin: *`, allowed methods **GET**, **POST**, **PUT**, **DELETE**, **HEAD**, **OPTIONS**, and `Authorization` + `Content-Type` allowed). Browsers may send a preflight **OPTIONS** request; the server answers **204**.
|
||||
|
||||
## Data files (under `server/`)
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `data.txt` | Pipe-separated demon list source (`category\|position\|title\|url` lines) |
|
||||
| `runs.json` | Run submission queue (moderation) |
|
||||
| `users.json` | Registered FEDL accounts (hashed passwords) |
|
||||
| `sessions.json` | Bearer session tokens + expiry |
|
||||
| `userdata.json` | Per-user synced state (roulette, list %, saved runs, roulette slots) |
|
||||
|
||||
These files are created on demand where noted below. Use `.gitignore` for `users.json`, `sessions.json`, and `userdata.json` if they contain real data.
|
||||
|
||||
## Server-Sent Events
|
||||
|
||||
### `GET /events`
|
||||
|
||||
- **Response:** `text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`
|
||||
- **Initial line:** `retry: 3000`
|
||||
- **Events:** `list-update`, `runs-update` with JSON payloads (e.g. `{ updatedAt: "ISO-8601" }`)
|
||||
|
||||
Clients reconnect using the `retry` hint after drops.
|
||||
|
||||
## Public JSON API
|
||||
|
||||
Unless noted, bodies are **JSON** with `Content-Type: application/json`. Errors use `{ "error": "message" }` when applicable.
|
||||
|
||||
### Demon list
|
||||
|
||||
#### `GET /api/list`
|
||||
|
||||
- **200:** `{ "items": [ { "level", "position", "title", "url" } ], "text": "raw file text" }`
|
||||
- **500:** could not read `data.txt`
|
||||
|
||||
#### `PUT /api/list`
|
||||
|
||||
- **Auth:** Admin Basic (if `ADMIN_PASSWORD` is set)
|
||||
- **Body:** `{ "text": "full data.txt content" }`
|
||||
- **200:** `{ "ok": true }` — writes `data.txt`, emits `list-update`
|
||||
|
||||
### Run queue
|
||||
|
||||
#### `GET /api/runs`
|
||||
|
||||
- **200:** `{ "items": [ run, ... ] }` — each run includes fields from `normalizeRun` (see below)
|
||||
|
||||
#### `POST /api/runs`
|
||||
|
||||
- **Optional:** `Authorization: Bearer <session-token>` — associates submission with account (`accountUserId`, `accountUsername` on stored run)
|
||||
- **Body:** `{ "playerName", "levelTitle", "videoUrl", "percent", "rawFootageUrl?", "notes?" }` (required fields enforced server-side)
|
||||
- **201:** `{ "ok": true, "item": run }`
|
||||
- **400:** validation / invalid JSON
|
||||
|
||||
Run objects include (among others): `id`, `playerName`, `levelTitle`, `videoUrl`, `percent`, `rawFootageUrl`, `notes`, `status`, `reviewedBy`, `reviewNotes`, `submittedAt`, `updatedAt`, `accountUserId`, `accountUsername`.
|
||||
|
||||
#### `PUT /api/runs/:id` / `DELETE /api/runs/:id`
|
||||
|
||||
- **Auth:** Admin Basic
|
||||
- **PUT body:** fields merged via `normalizeRun` with existing run
|
||||
200 / 404 / 400 as appropriate; emits `runs-update` on success
|
||||
|
||||
#### `POST /api/runs/bulk-approve`
|
||||
|
||||
- **Auth:** Admin Basic
|
||||
- **Body:** `{ "playerName": "exact match", "reviewNotes?": "..." }`
|
||||
- **200:** `{ "ok": true, "approved": number, "playerName": "..." }` — approves all **pending** runs whose `playerName` matches (case-insensitive); may emit `runs-update`
|
||||
|
||||
### Auth (FEDL user accounts)
|
||||
|
||||
#### `POST /api/auth/signup`
|
||||
|
||||
- **Body:** `{ "username", "password" }` — username normalized to lowercase; pattern 3–24 chars `[a-z0-9_]`, password min 8 chars
|
||||
- **201:** `{ "ok": true, "token", "userId", "username" }`
|
||||
- **400 / 409:** validation or username taken
|
||||
|
||||
#### `POST /api/auth/login`
|
||||
|
||||
- **Body:** `{ "username", "password" }`
|
||||
- **200:** `{ "ok": true, "token", "userId", "username" }`
|
||||
- **401:** invalid credentials
|
||||
|
||||
#### `POST /api/auth/logout`
|
||||
|
||||
- **Optional:** `Authorization: Bearer <token>` — revokes that session
|
||||
- **200:** `{ "ok": true }`
|
||||
|
||||
#### `GET /api/auth/me`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **200:** `{ "userId", "username" }`
|
||||
- **401:** invalid or expired token
|
||||
|
||||
### User state (synced client storage)
|
||||
|
||||
#### `GET /api/user/state`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **200:** `{ "data": { "roulettePick", "levelPercents", "savedRuns", "rouletteSlots" } }` — shapes are normalized on read; missing keys default safely
|
||||
- **401:** not signed in
|
||||
|
||||
#### `PUT /api/user/state`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **Body:** `{ "data": { ...same fields as above... } }` — server sanitizes `savedRuns` and `rouletteSlots` (size limits and field trimming)
|
||||
- **200:** `{ "ok": true }`
|
||||
- **401 / 400**
|
||||
|
||||
### Imports (admin + external APIs)
|
||||
|
||||
#### `POST /api/import/pointercrate`
|
||||
|
||||
- **Auth:** Admin Basic
|
||||
- Fetches Pointercrate records, maps to run shape, appends to `runs.json`
|
||||
- **200:** summary object with counts (see implementation)
|
||||
|
||||
#### `POST /api/import/aredl`
|
||||
|
||||
- **Auth:** Admin Basic
|
||||
- Requires `AREDL_ACCESS_TOKEN` or `AREDL_API_KEY` configured
|
||||
- **200:** summary / **500** on configuration or API errors
|
||||
|
||||
#### `POST /api/import/targeted`
|
||||
|
||||
- **Auth:** Admin Basic
|
||||
- **Body:** `{ "source": "pointercrate" | "aredl", "filter": "player" | "level", "query": "string" }`
|
||||
- Filters remote records and appends matching runs
|
||||
|
||||
## Static files
|
||||
|
||||
For **GET** / **HEAD** requests that do not match an API or `/events` handler, the server maps the path (after `BASE` strip) to files under the **repository root** (`../` from `server/`). Default document for `/` is `index.html`.
|
||||
|
||||
Unsupported methods receive **405**.
|
||||
|
||||
## Options preflight
|
||||
|
||||
`OPTIONS` on any path: **204** with CORS headers.
|
||||
+11
@@ -267,6 +267,17 @@ table.levels-table .btn{padding:9px 14px}
|
||||
.run-my-runs-head{margin-bottom:14px}
|
||||
.run-saved-card-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px}
|
||||
.submission-card{padding:18px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)}
|
||||
@keyframes fedl-shimmer{
|
||||
0%{background-position:100% 0}
|
||||
100%{background-position:-100% 0}
|
||||
}
|
||||
.submission-card--shimmer{
|
||||
min-height:118px;
|
||||
background:linear-gradient(90deg,rgba(255,255,255,0.04) 0%,rgba(255,255,255,0.1) 45%,rgba(255,255,255,0.04) 90%);
|
||||
background-size:200% 100%;
|
||||
animation:fedl-shimmer 1.15s ease-in-out infinite;
|
||||
pointer-events:none
|
||||
}
|
||||
.submission-card-top{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
||||
.submission-card strong{font-size:1.05rem}
|
||||
.submission-card p{margin:10px 0 0 0;color:#dce8f5;line-height:1.55}
|
||||
|
||||
Reference in New Issue
Block a user