Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8b3d5c622 | ||
|
|
4deab7e461 | ||
|
|
2fb319043b | ||
|
|
3b65631d5c | ||
|
|
09f7af9413 | ||
|
|
898271dd7f | ||
|
|
7c7f260138 | ||
|
|
2ece1d4bb8 | ||
|
|
47c33b28a2 | ||
|
|
a79c363cc6 | ||
|
|
455a77b2c7 | ||
|
|
0ed4d70680 | ||
|
|
5f05b37887 | ||
|
|
d7f0eb2c7b | ||
|
|
3eeb4f7cb4 | ||
|
|
52ff0fb0f5 | ||
|
|
986cab3436 | ||
|
|
dfb42e6393 | ||
|
|
6ddb843e97 | ||
|
|
016d998263 | ||
|
|
f27fcf7bf7 | ||
|
|
07c69d1b30 | ||
|
|
94a3dbda28 | ||
|
|
cdb6e03560 | ||
|
|
c3c74863ef | ||
|
|
8581b01029 | ||
|
|
29b8a4c132 | ||
|
|
85d73922c1 | ||
|
|
14004a21df | ||
|
|
6bbaac2870 | ||
|
|
89f427e634 | ||
|
|
14641723e2 | ||
|
|
aee26eb540 | ||
|
|
f902c80dc6 | ||
|
|
8bf6445cf6 | ||
|
|
b53ab78028 | ||
|
|
7b0fc6c7ad | ||
|
|
b87c3f8e94 | ||
|
|
e2733d9d24 | ||
|
|
9db5e39a56 | ||
|
|
22eeb8293c | ||
|
|
f1ba614429 | ||
|
|
1b349062b4 | ||
|
|
7395b78198 | ||
|
|
7db049187a | ||
|
|
eb1ca87277 | ||
|
|
08a8a5bad1 | ||
|
|
cf614feba6 | ||
|
|
d11b47cb46 | ||
|
|
d16a6a3d68 | ||
|
|
b693c0205a | ||
|
|
142fd5a6a0 | ||
|
|
f5c26caeba | ||
|
|
7086fbfbee | ||
|
|
2e8070e442 | ||
|
|
477185ec17 | ||
|
|
426adc3043 | ||
|
|
f012aa3dc4 | ||
|
|
aac09d2c8b | ||
|
|
453e6662e0 | ||
|
|
09fced106d | ||
|
|
751002d02a | ||
|
|
6bfe12cc42 | ||
|
|
4ecf58dba3 | ||
|
|
3d6fccaf3c | ||
|
|
2d6c25dc83 | ||
|
|
158c3f335c | ||
|
|
be4cc0bf1c | ||
|
|
da39fd4fd0 | ||
|
|
b4c6c48958 | ||
|
|
de621e50e9 | ||
|
|
52531abcc7 | ||
|
|
cdc65ec881 | ||
|
|
9d8906e94c | ||
|
|
06c6beb0b1 | ||
|
|
feb6dd2923 | ||
|
|
7698b2c169 | ||
|
|
11253b209e | ||
|
|
feb50ad389 | ||
|
|
c16f64aa08 | ||
|
|
90310fd671 | ||
|
|
00baa8b204 | ||
|
|
463fa222ff | ||
|
|
5acf920143 | ||
|
|
b39c334fdf | ||
|
|
96d2ca2d4a | ||
|
|
d29719131c | ||
|
|
906ef15e4b | ||
|
|
c239d8c062 | ||
|
|
3d1005a72c | ||
|
|
63d4ec0009 | ||
|
|
46a95d3a19 | ||
|
|
fddc92ae72 | ||
|
|
98668db533 | ||
|
|
b1a6873db3 | ||
|
|
cc68d81440 | ||
|
|
e79d3b0a0d | ||
|
|
4c2f6ed5a0 | ||
|
|
df238b1448 | ||
|
|
10f3639c75 | ||
|
|
93a3f497c0 | ||
|
|
fe65e66d16 | ||
|
|
60aa221e47 | ||
|
|
fdc700c558 | ||
|
|
349756891e | ||
|
|
8009102cd4 | ||
|
|
23a3235afc | ||
|
|
55b8c1a7d4 | ||
|
|
e5691e95cf | ||
|
|
edf2fae76c | ||
|
|
52d8d60760 | ||
|
|
d5b6ba26dc | ||
|
|
f1f040aaa0 | ||
|
|
8940f7cec8 | ||
|
|
58da99b91f | ||
|
|
34d34fc890 | ||
|
|
b3d92b5f6e |
+11
@@ -0,0 +1,11 @@
|
||||
# Server-side auth and user data (local only)
|
||||
server/users.json
|
||||
server/sessions.json
|
||||
server/userdata.json
|
||||
server/data.txt
|
||||
server/runs.json
|
||||
server/messages.json
|
||||
|
||||
# Environment variables
|
||||
server/.env
|
||||
server/config.json
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>401 - Unauthorized</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>403 - Forbidden</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>404 - Not Found</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>429 - Too Many Requests</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>500 - Internal Server Error</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>502 - Bad Gateway</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>503 - Service Unavailable</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 miles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,37 +1,45 @@
|
||||
# GD FEDL
|
||||
|
||||
A lightweight Geometry Dash FEDL-style website with a built-in Node.js server and browser-based list manager.
|
||||
GD FEDL is a Geometry Dash list site built as a multi-page static frontend with a lightweight Node.js backend. It supports live list data, run submissions, admin moderation, account login, synced user state, community posts, bug reports, and direct messages.
|
||||
|
||||
It includes:
|
||||
## What It Includes
|
||||
|
||||
- a home page
|
||||
- a list page for ranked level videos
|
||||
- a manage page for editing the list in the browser
|
||||
- a players page
|
||||
- a rules page
|
||||
- a Node.js server with a JSON API
|
||||
- `index.html` landing page
|
||||
- `lists.html` ranked list browser with search, filters, and video playback
|
||||
- `run.html` run submission page
|
||||
- `admelist.html` admin page for list editing and run moderation
|
||||
- `players.html` player progress tracker
|
||||
- `guess.html` rank guessing game
|
||||
- `roulette.html` demon roulette picker
|
||||
- `rules.html` rules page
|
||||
- `signup.html` and `login.html` account pages
|
||||
- `post.html` and `messages.html` community features
|
||||
- `contact.html` bug report / contact page
|
||||
- `errors.html` plus standalone HTTP-style error pages
|
||||
|
||||
## Project Files
|
||||
## Tech Stack
|
||||
|
||||
- `index.html` - home page
|
||||
- `lists.html` - main list view
|
||||
- `manage.html` - browser UI for adding, editing, and deleting list entries
|
||||
- `players.html` - player manager
|
||||
- `rules.html` - submission rules and mod guidelines
|
||||
- `app.js` - client-side logic for the list and players pages
|
||||
- `styles.css` - site styling
|
||||
- `server.js` - Node.js server and API
|
||||
- `data.json` - primary list storage generated by the server
|
||||
- `data.txt` - compatibility export rewritten by the server
|
||||
- `package.json` - start script for the Node server
|
||||
- Plain HTML, CSS, and JavaScript
|
||||
- No build step for the main site
|
||||
- Node.js `http` server
|
||||
- Server-Sent Events for live updates
|
||||
- Browser `localStorage` / `sessionStorage`
|
||||
- JSON and text files for simple local persistence
|
||||
|
||||
## How The List Data Works
|
||||
## Project Structure
|
||||
|
||||
The site now serves list data through `GET /api/levels`.
|
||||
- `app.js` shared frontend logic
|
||||
- `styles.css` shared site styling
|
||||
- `data.txt` static fallback list data
|
||||
- `level-ids.txt` optional local level ID lookup data for roulette
|
||||
- `server/server.js` backend for list data, auth, posts, messages, bug reports, and run submissions
|
||||
- `server/README.md` backend API details
|
||||
- `server/LINUX_SERVICE_SETUP.txt` example `systemd` setup
|
||||
- `start-server.command` macOS launcher for the Node server
|
||||
|
||||
The server stores entries in `data.json` and also keeps `data.txt` updated automatically for compatibility.
|
||||
## Data Files
|
||||
|
||||
Legacy `data.txt` lines still use this format:
|
||||
The main list format is one entry per line:
|
||||
|
||||
```txt
|
||||
category|position|title|url
|
||||
@@ -43,45 +51,107 @@ Example:
|
||||
new|1|Flamewall|https://youtu.be/x4Io4zkWVRw
|
||||
```
|
||||
|
||||
## Running The Site
|
||||
The server also stores runtime data in `server/`:
|
||||
|
||||
Start the Node server:
|
||||
- `data.txt` live list source
|
||||
- `runs.json` submitted runs
|
||||
- `users.json` registered accounts
|
||||
- `sessions.json` login sessions
|
||||
- `userdata.json` synced user state
|
||||
- `posts.json` community posts
|
||||
- `bugreports.json` bug reports
|
||||
- `messages.json` private messages
|
||||
|
||||
Most JSON files are created automatically when first needed. `server/data.txt` is the one file you should create yourself before first run.
|
||||
|
||||
## Main Backend Features
|
||||
|
||||
- `GET /api/list` and `PUT /api/list` for live list data
|
||||
- `GET /api/runs`, `POST /api/runs`, `PUT /api/runs/:id`, `DELETE /api/runs/:id`
|
||||
- `POST /api/runs/bulk-approve` for admin moderation
|
||||
- `POST /api/auth/signup`, `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/me`
|
||||
- `GET /api/user/state`, `PUT /api/user/state` for synced client state
|
||||
- `GET /api/posts`, `POST /api/posts`, like/comment routes, and post deletion
|
||||
- `GET /api/bugreports`, `POST /api/bugreports`, admin update/delete routes
|
||||
- `GET /api/messages`, `POST /api/messages`, conversation lookup, and user search
|
||||
- `GET /events` for SSE updates
|
||||
- Pointercrate and AREDL import routes for admins
|
||||
|
||||
See [server/README.md](/Users/miles/Documents/GitHub/fedl/server/README.md) for the fuller API reference.
|
||||
|
||||
## Local Setup
|
||||
|
||||
1. Make sure Node.js is installed.
|
||||
2. Create the live list data file if it does not exist yet:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
printf "new|1|Example Level|https://youtu.be/example\n" > server/data.txt
|
||||
```
|
||||
|
||||
Then open:
|
||||
3. Start the server:
|
||||
|
||||
```bash
|
||||
node server/server.js
|
||||
```
|
||||
|
||||
4. Open the site at:
|
||||
|
||||
```txt
|
||||
http://localhost:8000
|
||||
http://127.0.0.1:8090/fedl/
|
||||
```
|
||||
|
||||
## Editing The List
|
||||
To expose it on your local network:
|
||||
|
||||
Open:
|
||||
```bash
|
||||
HOST=0.0.0.0 PORT=8090 node server/server.js
|
||||
```
|
||||
|
||||
On macOS you can also run:
|
||||
|
||||
```txt
|
||||
http://localhost:8000/manage.html
|
||||
./start-server.command
|
||||
```
|
||||
|
||||
From there you can:
|
||||
## Environment Variables
|
||||
|
||||
- add entries
|
||||
- edit existing entries
|
||||
- delete entries
|
||||
- search the current list
|
||||
- `HOST` server bind host, default `127.0.0.1`
|
||||
- `PORT` server port, default `8090`
|
||||
- `ADMIN_PASSWORD` admin password for protected write routes
|
||||
- `AREDL_ACCESS_TOKEN` bearer token for AREDL imports
|
||||
- `AREDL_API_KEY` API key for AREDL imports
|
||||
|
||||
The server also exposes these API routes:
|
||||
Example:
|
||||
|
||||
- `GET /api/levels`
|
||||
- `POST /api/levels`
|
||||
- `PUT /api/levels/:id`
|
||||
- `DELETE /api/levels/:id`
|
||||
```bash
|
||||
ADMIN_PASSWORD=changeme HOST=0.0.0.0 PORT=8090 node server/server.js
|
||||
```
|
||||
|
||||
## Frontend Live Server Note
|
||||
|
||||
`app.js` uses a hardcoded live server base:
|
||||
|
||||
```txt
|
||||
https://raspberrypi-1.tail46eacb.ts.net/fedl
|
||||
```
|
||||
|
||||
That means:
|
||||
|
||||
- the frontend will try the remote live server first
|
||||
- if the live server probe fails, the app redirects to `offlineindex.html`
|
||||
- if you want local development against your own server, update `TESTING_MODE` or `liveServerBase` in `app.js`
|
||||
|
||||
## Notes
|
||||
|
||||
- On first run, the server will migrate the current `data.txt` into `data.json`
|
||||
- Player data is stored in the browser with `localStorage`
|
||||
- The players list is local to each browser/device
|
||||
- No external npm packages are required
|
||||
- `players.html` stores local player progress in the browser, with optional server sync when signed in
|
||||
- admin-protected routes use HTTP Basic auth
|
||||
- account auth uses bearer tokens from the backend
|
||||
- `roulette.html` can use `level-ids.txt` before falling back to external lookups
|
||||
- the backend serves the repo root as static files under the `/fedl` base path
|
||||
|
||||
## Linux Service
|
||||
|
||||
For `systemd` setup notes, see [server/LINUX_SERVICE_SETUP.txt](/Users/miles/Documents/GitHub/fedl/server/LINUX_SERVICE_SETUP.txt).
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](/Users/miles/Documents/GitHub/fedl/LICENSE).
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content="About FEDL - Fair Extreme Demon List, your hub for the Geometry Dash demon list">
|
||||
<meta name="keywords" content="demon list, about, geometry dash, fedl, ranked demons">
|
||||
<title>About — GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="about">
|
||||
<header>
|
||||
<h1>About</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="rules-page">
|
||||
<section class="rule-section">
|
||||
<h2>What is FEDL?</h2>
|
||||
<p><strong>FEDL</strong> stands for <strong>Fair Extreme Demon List</strong>. We are a community-driven Geometry Dash demon list focused on providing the most up-to-date, fair, and least-restrictive rankings in the GD demon community.</p>
|
||||
<p>Our goal is to make demon verification accessible to everyone while maintaining accurate and current rankings that reflect the ever-evolving state of the game.</p>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>How It Works</h2>
|
||||
<p>Players submit their demon completions through our run submission system. Each submission is reviewed by our moderation team to verify the run meets our guidelines. Once approved, the run is linked to the corresponding demon on our list.</p>
|
||||
<p>The list is updated in real-time as new demons are verified, ensuring players always have access to the most current rankings.</p>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>Features</h2>
|
||||
<ul class="rules-list">
|
||||
<li><strong>Browse the List</strong> - Search and filter through all ranked demons by title, level ID, or rank position. Jump between rank ranges for quick navigation.</li>
|
||||
<li><strong>Player Rankings</strong> - Track the top players based on their verified runs and total points earned.</li>
|
||||
<li><strong>Submit Runs</strong> - Submit your own completions with video links and notes for review by our moderators.</li>
|
||||
<li><strong>Roulette</strong> - Feeling adventurous? Spin the roulette to get a random demon from the list as your next challenge.</li>
|
||||
<li><strong>Guess Game</strong> - Test your demon knowledge by guessing if a level is ranked higher or lower than shown.</li>
|
||||
<li><strong>Messages</strong> - Receive updates on your submissions and communicate with the team.</li>
|
||||
<li><strong>Themes</strong> - Customize your experience with 14 different color themes including Light, Dark, Ocean, Cyberpunk, Fire, Galaxy, and more.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>Moderation</h2>
|
||||
<p>Our moderation team reviews all submissions to ensure fairness and accuracy. We verify completion videos, check for compliance with our rules, and provide feedback when runs are rejected.</p>
|
||||
<p>Moderators include <strong>wolf_reaper90</strong>, <strong>dioxyx</strong>, and <strong>steve</strong>. Contact us through the <a href="contact.html" style="color:var(--accent)">Contact page</a> for any moderation-related questions.</p>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
<ul class="rules-list">
|
||||
<li><strong>/</strong> - Focus the search bar</li>
|
||||
<li><strong>H</strong> - Go to Home</li>
|
||||
<li><strong>L</strong> - Go to Lists</li>
|
||||
<li><strong>R</strong> - Go to Run submission</li>
|
||||
<li><strong>?</strong> - Go to Rules</li>
|
||||
<li><strong>Shift+W / Shift+E</strong> - Go to Admin Panel (mods only)</li>
|
||||
<li><strong>Esc</strong> - Close modals</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>Community</h2>
|
||||
<p>Join our Discord server to connect with other demon hunters, ask questions about rules, discuss level placements, share your progress, and get support from our team.</p>
|
||||
<p><a href="https://discord.gg/5HyUKQrN" target="_blank" rel="noopener noreferrer" class="btn" style="display:inline-block;margin-top:12px">Join Our Discord</a></p>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>Theme Options</h2>
|
||||
<p>FEDL includes 14 different themes to customize your viewing experience. Visit the <a href="account.html" style="color:var(--accent)">Account page</a> to choose from themes like Default, Light, Ocean, Cyberpunk, Matrix, Fire, Galaxy, and more!</p>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>Coming Soon</h2>
|
||||
<ul class="rules-list">
|
||||
<li><strong>Geode Mod</strong> - A dedicated FEDL companion mod for Geometry Dash with quick list access, roulette tools, queue tracking, and built-in validation help.</li>
|
||||
<li><strong>Mobile App</strong> - A dedicated app for FEDL is on the way. Stay tuned for updates.</li>
|
||||
<li><strong>OAuth Sign-in</strong> - Sign in with Google, Discord, or other platforms for easier access.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="rule-section">
|
||||
<h2>Credits</h2>
|
||||
<p>FEDL is built and maintained by the community. Thanks to all the moderators who review submissions, the players who submit runs, and everyone who helps keep the demon list up to date.</p>
|
||||
<p class="muted">Version 1.0 | Last updated April 2026</p>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Account — GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="account">
|
||||
<header>
|
||||
<h1>Account</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="account-main">
|
||||
<section class="panel account-hero">
|
||||
<p class="hero-kicker">FEDL account</p>
|
||||
<h2>Manage your account</h2>
|
||||
<p class="muted">Change your password and view your account details.</p>
|
||||
<p id="account-overview-status" class="muted" role="status"></p>
|
||||
<div class="account-summary-grid">
|
||||
<article class="account-summary-card">
|
||||
<span class="account-summary-label">Username</span>
|
||||
<strong id="account-username">Loading…</strong>
|
||||
</article>
|
||||
<article class="account-summary-card">
|
||||
<span class="account-summary-label">Joined</span>
|
||||
<strong id="account-created">Loading…</strong>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel account-panel">
|
||||
<h3>Theme</h3>
|
||||
<p class="muted">Choose your preferred color scheme.</p>
|
||||
<div class="theme-options">
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="dark" checked>
|
||||
<span class="theme-preview theme-preview-dark"></span>
|
||||
<span>Default</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="light">
|
||||
<span class="theme-preview theme-preview-light"></span>
|
||||
<span>Light</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="blue">
|
||||
<span class="theme-preview theme-preview-blue"></span>
|
||||
<span>Ocean</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="midnight">
|
||||
<span class="theme-preview theme-preview-midnight"></span>
|
||||
<span>Midnight</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="cyberpunk">
|
||||
<span class="theme-preview theme-preview-cyberpunk"></span>
|
||||
<span>Cyberpunk</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="earth">
|
||||
<span class="theme-preview theme-preview-earth"></span>
|
||||
<span>Earth</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="retro">
|
||||
<span class="theme-preview theme-preview-retro"></span>
|
||||
<span>Retro</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="matrix">
|
||||
<span class="theme-preview theme-preview-matrix"></span>
|
||||
<span>Matrix</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="synthwave">
|
||||
<span class="theme-preview theme-preview-synthwave"></span>
|
||||
<span>Synthwave</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="fire">
|
||||
<span class="theme-preview theme-preview-fire"></span>
|
||||
<span>Fire</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="galaxy">
|
||||
<span class="theme-preview theme-preview-galaxy"></span>
|
||||
<span>Galaxy</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="candy">
|
||||
<span class="theme-preview theme-preview-candy"></span>
|
||||
<span>Candy</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="highcontrast">
|
||||
<span class="theme-preview theme-preview-highcontrast"></span>
|
||||
<span>High Contrast</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="original">
|
||||
<span class="theme-preview theme-preview-original"></span>
|
||||
<span>Original</span>
|
||||
</label>
|
||||
</div>
|
||||
<p id="account-theme-status" class="muted" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section class="panel account-panel">
|
||||
<h3>Password reset</h3>
|
||||
<p class="muted">Forgot your password? Request a reset code which will be sent to your messages.</p>
|
||||
<p id="account-reset-status" class="muted" role="status"></p>
|
||||
<div class="auth-actions">
|
||||
<button type="button" class="btn" id="account-reset-email-btn">Send reset to my messages</button>
|
||||
<a class="text-link" href="reset-password.html">Open reset page</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel account-panel">
|
||||
<h3>Change password</h3>
|
||||
<p class="muted">Use this if you already know your current password and just want to swap it.</p>
|
||||
<form id="account-password-form" class="auth-form">
|
||||
<label class="admin-password-field" for="account-current-password">
|
||||
<span>Current password</span>
|
||||
<input id="account-current-password" name="currentPassword" type="password" autocomplete="current-password" required />
|
||||
</label>
|
||||
<label class="admin-password-field" for="account-new-password">
|
||||
<span>New password</span>
|
||||
<input id="account-new-password" name="newPassword" type="password" autocomplete="new-password" required minlength="8" />
|
||||
</label>
|
||||
<label class="admin-password-field" for="account-confirm-password">
|
||||
<span>Confirm new password yaa</span>
|
||||
<input id="account-confirm-password" name="confirmPassword" type="password" autocomplete="new-password" required minlength="8" />
|
||||
</label>
|
||||
<p id="account-password-status" class="muted" role="status"></p>
|
||||
<div class="auth-actions">
|
||||
<button type="submit" class="btn" id="account-password-submit">Update password</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Edit List - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="admelist" class="admin-locked">
|
||||
<header id="admin-page-header">
|
||||
<h1>FEDL Admin</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="admin-shell">
|
||||
<section id="admin-login-screen" class="admin-login-screen">
|
||||
<div class="admin-login-card">
|
||||
<p class="hero-kicker">Admin Access</p>
|
||||
<h2>Enter the admin password</h2>
|
||||
<p class="muted">Submit the server password to unlock the list editor and run moderation tools.</p>
|
||||
<form id="admin-login-form" class="admin-login-form">
|
||||
<label class="admin-password-field" for="admin-password">
|
||||
<span>Admin password</span>
|
||||
<input id="admin-password" type="password" placeholder="Enter server password" autocomplete="current-password" />
|
||||
</label>
|
||||
<div class="admin-login-actions">
|
||||
<button id="admin-login-submit" type="submit" class="btn">Submit</button>
|
||||
<p id="admin-auth-status" class="muted">Saved only in this browser session.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="admin-shell-content" class="admin-shell-content" hidden>
|
||||
<section class="admin-tabs">
|
||||
<button type="button" class="admin-tab active" data-tab="stats">Stats</button>
|
||||
<button type="button" class="admin-tab" data-tab="list">List Editor</button>
|
||||
<button type="button" class="admin-tab" data-tab="runs">Run Queue</button>
|
||||
<button type="button" class="admin-tab" data-tab="mods">Mods</button>
|
||||
<button type="button" class="admin-tab" data-tab="bugs">Bug Reports</button>
|
||||
<button type="button" class="admin-tab" data-tab="import">Import</button>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-stats">
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Statistics</p>
|
||||
<h2>Overview</h2>
|
||||
<p id="admin-stats-status" class="muted">Loading stats...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-box">
|
||||
<strong id="stat-total-runs">--</strong>
|
||||
<span>Total Runs</span>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<strong id="stat-pending-runs">--</strong>
|
||||
<span>Pending</span>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<strong id="stat-levels-count">--</strong>
|
||||
<span>Levels</span>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<strong id="stat-bug-reports">--</strong>
|
||||
<span>Open Reports</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-list" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">List Editor</p>
|
||||
<h2>Manage the live list</h2>
|
||||
<p id="admin-status" class="muted">Loading list data...</p>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<input id="admin-search" type="text" placeholder="Search rows..." />
|
||||
<button id="add-row" type="button" class="btn ghost-btn">Add Row</button>
|
||||
<button id="save-list" type="button" class="btn">Save List</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:90px">#</th>
|
||||
<th style="width:140px">Level</th>
|
||||
<th>Title</th>
|
||||
<th>Video URL</th>
|
||||
<th style="width:120px">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-list-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="padding:16px">
|
||||
<button id="add-row-bottom" type="button" class="btn ghost-btn">Add Row</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-runs" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Run Queue</p>
|
||||
<h2>Review submitted runs</h2>
|
||||
<p id="runs-admin-status" class="muted">Loading run submissions...</p>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<input id="run-search" type="text" placeholder="Search submissions..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:130px">Status</th>
|
||||
<th style="width:160px">Player</th>
|
||||
<th style="width:110px">Percent</th>
|
||||
<th>Level</th>
|
||||
<th style="width:180px">Submitted</th>
|
||||
<th style="width:240px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="run-admin-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-bugs" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Reports</p>
|
||||
<h2>View and manage bug reports</h2>
|
||||
<p id="bug-reports-admin-status" class="muted">Loading bug reports...</p>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<input id="bug-report-search" type="text" placeholder="Search reports..." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:100px">Category</th>
|
||||
<th style="width:100px">Status</th>
|
||||
<th style="width:160px">Submitted By</th>
|
||||
<th>Subject</th>
|
||||
<th style="width:180px">Submitted</th>
|
||||
<th style="width:180px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bug-reports-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-import" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Import tool</p>
|
||||
<h2>External lists & bulk approve</h2>
|
||||
<p id="import-status" class="muted">Pull records from Pointercrate or AREDL (AREDL needs a server API key or token). Targeted import loads victories from the API by player or level, adds them as approved runs with notes set to "Valid run". Bulk approve approves pending FEDL submissions only. Full imports run one page at a time to limit rate usage.</p>
|
||||
</div>
|
||||
<div class="admin-actions admin-import-actions">
|
||||
<button id="import-pointercrate" type="button" class="btn ghost-btn">Import Pointercrate</button>
|
||||
<button id="import-aredl" type="button" class="btn ghost-btn">Import AREDL</button>
|
||||
<button id="import-targeted-open" type="button" class="btn ghost-btn">Import by player or level…</button>
|
||||
<button id="bulk-approve-open" type="button" class="btn ghost-btn">Bulk-approve by player…</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="import-targeted-modal" class="video-modal bulk-approve-modal" hidden>
|
||||
<div class="inner bulk-approve-modal-card" role="dialog" aria-modal="true" aria-labelledby="import-targeted-title">
|
||||
<p class="hero-kicker">Import tool</p>
|
||||
<h2 id="import-targeted-title">Import victories from API</h2>
|
||||
<p class="muted">Choose a list, match by player or level, then import. New runs are <strong>approved</strong> with submission and review notes <strong>Valid run</strong>. Duplicates (same video URL) are skipped.</p>
|
||||
<form id="import-targeted-form" class="admin-login-form bulk-approve-form">
|
||||
<label class="admin-password-field" for="import-targeted-source">
|
||||
<span>List API</span>
|
||||
<select id="import-targeted-source" required>
|
||||
<option value="pointercrate">Pointercrate</option>
|
||||
<option value="aredl">AREDL</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="import-targeted-mode">
|
||||
<span class="import-targeted-mode-label">Match</span>
|
||||
<label class="import-targeted-radio"><input type="radio" name="import-targeted-filter" value="player" checked /> Player</label>
|
||||
<label class="import-targeted-radio"><input type="radio" name="import-targeted-filter" value="level" /> Level</label>
|
||||
</div>
|
||||
<label class="admin-password-field" for="import-targeted-query">
|
||||
<span id="import-targeted-query-label">Player name</span>
|
||||
<input id="import-targeted-query" type="text" placeholder="Name as on the list" autocomplete="off" required />
|
||||
</label>
|
||||
<p class="muted small import-targeted-hint">Pointercrate: use the <strong>exact</strong> player or demon name from the list, or a numeric id (player id / demon id). The old API filters were unreliable; names are resolved correctly now. AREDL prefers an exact name match, then substring, across scanned pages.</p>
|
||||
<div class="admin-login-actions bulk-approve-actions">
|
||||
<button type="submit" class="btn" id="import-targeted-submit">Import as valid runs</button>
|
||||
<button type="button" class="btn ghost-btn" id="import-targeted-cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bulk-approve-modal" class="video-modal bulk-approve-modal" hidden>
|
||||
<div class="inner bulk-approve-modal-card" role="dialog" aria-modal="true" aria-labelledby="bulk-approve-title">
|
||||
<p class="hero-kicker">Import tool</p>
|
||||
<h2 id="bulk-approve-title">Bulk-approve by player</h2>
|
||||
<p class="muted">Only submissions still marked <strong>pending</strong> are updated. Approved and rejected runs are left unchanged.</p>
|
||||
<form id="bulk-approve-form" class="admin-login-form bulk-approve-form">
|
||||
<label class="admin-password-field" for="bulk-approve-player">
|
||||
<span>Player name</span>
|
||||
<input id="bulk-approve-player" type="text" placeholder="Exact name from the run queue" autocomplete="off" required />
|
||||
</label>
|
||||
<p id="bulk-approve-preview" class="muted" aria-live="polite"></p>
|
||||
<label class="admin-password-field" for="bulk-approve-notes">
|
||||
<span>Review notes (optional)</span>
|
||||
<input id="bulk-approve-notes" type="text" placeholder="Shown on each approved run" autocomplete="off" />
|
||||
</label>
|
||||
<div class="admin-login-actions bulk-approve-actions">
|
||||
<button type="submit" class="btn" id="bulk-approve-submit">Approve all pending</button>
|
||||
<button type="button" class="btn ghost-btn" id="bulk-approve-cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Contact Us - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="contact">
|
||||
<header>
|
||||
<h1>Contact Us</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="run-shell">
|
||||
<section class="panel run-panel">
|
||||
<div class="run-head">
|
||||
<div>
|
||||
<p class="hero-kicker">FEDL Contact</p>
|
||||
<h2>Report a bug or send feedback</h2>
|
||||
<p class="muted run-intro">Found a bug? Have a suggestion? Fill out the form below and the admins will review it. If you <a href="login.html">log in</a>, your username will be attached to the report.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="contact-form" class="run-form">
|
||||
<label class="control-block" for="contact-category">
|
||||
<span>Category</span>
|
||||
<select id="contact-category" name="category" required>
|
||||
<option value="bug">Bug Report</option>
|
||||
<option value="suggestion">Suggestion</option>
|
||||
<option value="question">Question</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="contact-subject">
|
||||
<span>Subject</span>
|
||||
<input id="contact-subject" name="subject" type="text" placeholder="Brief summary of the issue" required />
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="contact-description">
|
||||
<span>Description</span>
|
||||
<textarea id="contact-description" name="description" rows="8" placeholder="Describe the bug, include steps to reproduce if applicable, and any screenshots or links that might help." required></textarea>
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="contact-email">
|
||||
<span>Email (optional)</span>
|
||||
<input id="contact-email" name="email" type="email" placeholder="Your email if you'd like a response" />
|
||||
</label>
|
||||
|
||||
<div class="run-actions">
|
||||
<button id="contact-submit" type="submit" class="btn">Submit Report</button>
|
||||
<p id="contact-form-status" class="muted">The server needs to be running for submissions.</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside class="panel run-side-panel">
|
||||
<p class="status-label">What to report</p>
|
||||
<h2>Before you submit</h2>
|
||||
<div class="run-checklist">
|
||||
<article>
|
||||
<strong>Bug details</strong>
|
||||
<p>Include what you were doing when the bug occurred and what you expected to happen.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Steps to reproduce</strong>
|
||||
<p>List the steps needed to trigger the bug so we can try to replicate it.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Screenshots or videos</strong>
|
||||
<p>Links to screenshots or videos can help us understand the issue better.</p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,352 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "legacy-1",
|
||||
"level": "new",
|
||||
"position": "1",
|
||||
"title": "Flamewall",
|
||||
"url": "https://youtu.be/x4Io4zkWVRw?si=tf0DBaWahOPPEWxO"
|
||||
},
|
||||
{
|
||||
"id": "legacy-2",
|
||||
"level": "new",
|
||||
"position": "2",
|
||||
"title": "Thinking Space II",
|
||||
"url": "https://youtu.be/CELNmHwln_c?si=soP6SzRn92G1vKbl"
|
||||
},
|
||||
{
|
||||
"id": "legacy-3",
|
||||
"level": "new",
|
||||
"position": "4",
|
||||
"title": "Amethyst",
|
||||
"url": "https://youtu.be/4lfkzz1VCbA?si=6NNeWiH-nnwu21Cb"
|
||||
},
|
||||
{
|
||||
"id": "legacy-4",
|
||||
"level": "new",
|
||||
"position": "3",
|
||||
"title": "Tidal Wave",
|
||||
"url": "https://youtu.be/9fsZ014qB3s?si=oj4E4ceyNtRVcDvd"
|
||||
},
|
||||
{
|
||||
"id": "legacy-5",
|
||||
"level": "new",
|
||||
"position": "5",
|
||||
"title": "Orbit",
|
||||
"url": "https://youtu.be/QKcv8DkNPd0?si=cKBFqp7GRqmfupgJ"
|
||||
},
|
||||
{
|
||||
"id": "legacy-6",
|
||||
"level": "new",
|
||||
"position": "6",
|
||||
"title": "BOOBAWAMBA",
|
||||
"url": "https://youtu.be/20fYiqLAo_E?si=QSSixz31rWeeQ_8B"
|
||||
},
|
||||
{
|
||||
"id": "legacy-7",
|
||||
"level": "new",
|
||||
"position": "7",
|
||||
"title": "Nullscapes",
|
||||
"url": "https://youtu.be/EztneTPp5CU?si=ljn-S_bnGYwOoUyV"
|
||||
},
|
||||
{
|
||||
"id": "legacy-8",
|
||||
"level": "new",
|
||||
"position": "8",
|
||||
"title": "Quantuese Processing",
|
||||
"url": "https://youtu.be/j5NC0u1Q91Q?si=FvUEY2kdDtNMqWSD"
|
||||
},
|
||||
{
|
||||
"id": "legacy-9",
|
||||
"level": "new",
|
||||
"position": "9",
|
||||
"title": "Subsuming Vortex",
|
||||
"url": "https://youtu.be/0eYG1ogJpIQ?si=7XRxfDsmMVb0G9Si"
|
||||
},
|
||||
{
|
||||
"id": "legacy-10",
|
||||
"level": "new",
|
||||
"position": "10",
|
||||
"title": "Andromeda",
|
||||
"url": "https://youtu.be/mk3TDemdkC0?si=va13_RKAJUyFvhcv"
|
||||
},
|
||||
{
|
||||
"id": "legacy-11",
|
||||
"level": "new",
|
||||
"position": "11",
|
||||
"title": "Every End",
|
||||
"url": "https://youtu.be/AO--mVVFtKI?si=gIT5vxyc1FZ4_8Jj"
|
||||
},
|
||||
{
|
||||
"id": "legacy-12",
|
||||
"level": "new",
|
||||
"position": "12",
|
||||
"title": "Silent Clubstep",
|
||||
"url": "https://youtu.be/GR4OMkS3SN8?si=CMNz8bPBaUU94wiy"
|
||||
},
|
||||
{
|
||||
"id": "legacy-13",
|
||||
"level": "new",
|
||||
"position": "13",
|
||||
"title": "Anathema",
|
||||
"url": "https://youtu.be/_uKwmjHmySI?si=PgXwX7Z--wCay34b"
|
||||
},
|
||||
{
|
||||
"id": "legacy-14",
|
||||
"level": "new",
|
||||
"position": "14",
|
||||
"title": "Acheron",
|
||||
"url": "https://youtu.be/sBKR6aUorzA?si=0A9S2_W8gi1awZ81"
|
||||
},
|
||||
{
|
||||
"id": "legacy-15",
|
||||
"level": "new",
|
||||
"position": "15",
|
||||
"title": "Ashley Wave Trials",
|
||||
"url": "https://youtu.be/aTxt76U3e2Q?si=RVcEcOTOfs7NGWRi"
|
||||
},
|
||||
{
|
||||
"id": "legacy-16",
|
||||
"level": "new",
|
||||
"position": "16",
|
||||
"title": "Avernus",
|
||||
"url": "https://youtu.be/16Zh8jssanc?si=0o6d2dH4p_J7UjKB"
|
||||
},
|
||||
{
|
||||
"id": "legacy-17",
|
||||
"level": "new",
|
||||
"position": "17",
|
||||
"title": "Menace",
|
||||
"url": "https://youtu.be/nnkgghxxsEE?si=OuUQfD7WbXVooYyX"
|
||||
},
|
||||
{
|
||||
"id": "legacy-18",
|
||||
"level": "new",
|
||||
"position": "18",
|
||||
"title": "Spectre",
|
||||
"url": "https://youtu.be/MzsSLKJrLSI?si=-bXtr_jG96kTCzy4"
|
||||
},
|
||||
{
|
||||
"id": "legacy-19",
|
||||
"level": "new",
|
||||
"position": "19",
|
||||
"title": "Abyss Of Darkness",
|
||||
"url": "https://youtu.be/ejJkpqcMMCY?si=Rpp4HEi1kOMh-p0h"
|
||||
},
|
||||
{
|
||||
"id": "legacy-20",
|
||||
"level": "new",
|
||||
"position": "20",
|
||||
"title": "Defeated Circles",
|
||||
"url": "https://youtu.be/nU5AQPzd2YA?si=6eKDMPh-2QjDBBOF"
|
||||
},
|
||||
{
|
||||
"id": "legacy-21",
|
||||
"level": "new",
|
||||
"position": "21",
|
||||
"title": "Tunnel Of Despair",
|
||||
"url": "https://youtu.be/LpS4JREhW98?si=Z6_DRV5YZTEA0TeQ"
|
||||
},
|
||||
{
|
||||
"id": "legacy-22",
|
||||
"level": "new",
|
||||
"position": "22",
|
||||
"title": "Subterminal Point",
|
||||
"url": "https://youtu.be/h2wmRMgACH4?si=nDD0-bmLqPgcdVJP"
|
||||
},
|
||||
{
|
||||
"id": "legacy-23",
|
||||
"level": "new",
|
||||
"position": "23",
|
||||
"title": "KOCMOC",
|
||||
"url": "https://youtu.be/2CxE-UWCIG4?si=9xz4hBjtNKe8Rcbs"
|
||||
},
|
||||
{
|
||||
"id": "legacy-24",
|
||||
"level": "new",
|
||||
"position": "24",
|
||||
"title": "Slaughterhouse",
|
||||
"url": "https://youtu.be/kpcF1-QAHQc?si=7ybF9EbcjqHzloLM"
|
||||
},
|
||||
{
|
||||
"id": "legacy-25",
|
||||
"level": "new",
|
||||
"position": "25",
|
||||
"title": "Kyouki",
|
||||
"url": "https://youtu.be/KDa5c0CJTHs?si=WU0sPusFLB7Jtkam"
|
||||
},
|
||||
{
|
||||
"id": "legacy-26",
|
||||
"level": "new",
|
||||
"position": "26",
|
||||
"title": "The Lightning Rod",
|
||||
"url": "https://youtu.be/nQDTi077O6M?si=Kqp2Od3lJx1YWTDB"
|
||||
},
|
||||
{
|
||||
"id": "legacy-27",
|
||||
"level": "new",
|
||||
"position": "27",
|
||||
"title": "Based After Based",
|
||||
"url": "https://youtu.be/yQBFyUvB3lY?si=Cmxbc5jrTrNlcmG5"
|
||||
},
|
||||
{
|
||||
"id": "legacy-28",
|
||||
"level": "new",
|
||||
"position": "28",
|
||||
"title": "CHIL",
|
||||
"url": "https://youtu.be/DROMiCc2ZRM?si=_bR8CCnqJGVCex9F"
|
||||
},
|
||||
{
|
||||
"id": "legacy-29",
|
||||
"level": "new",
|
||||
"position": "29",
|
||||
"title": "Sakupen Circles",
|
||||
"url": "https://youtu.be/ofG2mJi9kEA?si=3yv78_jxidKPhtL5"
|
||||
},
|
||||
{
|
||||
"id": "legacy-30",
|
||||
"level": "new",
|
||||
"position": "30",
|
||||
"title": "Deimos (ItsHybrid)",
|
||||
"url": "https://youtu.be/b2yHaIk5zio?si=AMIuVFR-EW4CK8_P"
|
||||
},
|
||||
{
|
||||
"id": "legacy-31",
|
||||
"level": "new",
|
||||
"position": "31",
|
||||
"title": "Eyes In The Water",
|
||||
"url": "https://youtu.be/yvLwiOy3KEA?si=1991xs25WTEoTbE0"
|
||||
},
|
||||
{
|
||||
"id": "legacy-32",
|
||||
"level": "new",
|
||||
"position": "32",
|
||||
"title": "Voltage",
|
||||
"url": "https://youtu.be/wBRvBN9tmlc?si=GU2md9I7ZnUV7Rfr"
|
||||
},
|
||||
{
|
||||
"id": "legacy-33",
|
||||
"level": "new",
|
||||
"position": "33",
|
||||
"title": "Firework",
|
||||
"url": "https://youtu.be/QBe5x2o9v2w?si=dhPg9YcCRjwngxU9"
|
||||
},
|
||||
{
|
||||
"id": "legacy-34",
|
||||
"level": "new",
|
||||
"position": "34",
|
||||
"title": "Silentlocked",
|
||||
"url": "https://youtu.be/O-IQeUdEGvI?si=KeSHH6kwZoSVGINT"
|
||||
},
|
||||
{
|
||||
"id": "legacy-35",
|
||||
"level": "new",
|
||||
"position": "35",
|
||||
"title": "poocubed",
|
||||
"url": "https://youtu.be/fzL31vai1ms?si=9Gwk7RDtbEFxOJ6t"
|
||||
},
|
||||
{
|
||||
"id": "legacy-36",
|
||||
"level": "new",
|
||||
"position": "36",
|
||||
"title": "KOSETSU",
|
||||
"url": "https://youtu.be/hZ8vFX8z_BU?si=P6fFal0yazJbje2O"
|
||||
},
|
||||
{
|
||||
"id": "legacy-37",
|
||||
"level": "new",
|
||||
"position": "37",
|
||||
"title": "Through The Gates",
|
||||
"url": "https://youtu.be/4yHA6jux5UI?si=nstk3A6FDN3QR-qm"
|
||||
},
|
||||
{
|
||||
"id": "legacy-38",
|
||||
"level": "new",
|
||||
"position": "38",
|
||||
"title": "Saul Goodman",
|
||||
"url": "https://youtu.be/hjs5PjUaw9k?si=qJpGrru21d50gO5R"
|
||||
},
|
||||
{
|
||||
"id": "legacy-39",
|
||||
"level": "new",
|
||||
"position": "39",
|
||||
"title": "The Salt Factory",
|
||||
"url": "https://youtu.be/lQ7M-Sgov24?si=nXYVL0XGy5NOJLXn"
|
||||
},
|
||||
{
|
||||
"id": "legacy-40",
|
||||
"level": "new",
|
||||
"position": "40",
|
||||
"title": "Snowbound",
|
||||
"url": "https://youtu.be/cjHwgbtAkXU?si=ghJbRJCkN-m0zu60"
|
||||
},
|
||||
{
|
||||
"id": "legacy-41",
|
||||
"level": "new",
|
||||
"position": "41",
|
||||
"title": "CONVOLSION",
|
||||
"url": "https://youtu.be/qeRKuyU3eGI?si=UPaem0b3V8fX0L1r"
|
||||
},
|
||||
{
|
||||
"id": "legacy-42",
|
||||
"level": "new",
|
||||
"position": "42",
|
||||
"title": "The Apocalyptic Trilogy",
|
||||
"url": "https://youtu.be/RUBbpsTR5eU?si=mEsPtyMLrQgP5fWl"
|
||||
},
|
||||
{
|
||||
"id": "legacy-43",
|
||||
"level": "new",
|
||||
"position": "43",
|
||||
"title": "MINUSdry",
|
||||
"url": "https://youtu.be/YvA8ehhzz0Q?si=DT6DfrXOQ41swSIR"
|
||||
},
|
||||
{
|
||||
"id": "legacy-44",
|
||||
"level": "new",
|
||||
"position": "44",
|
||||
"title": "Sevvend Clubstep",
|
||||
"url": "https://youtu.be/TvA8EJTJFCc?si=Rm9HyExGYpeGn4qs"
|
||||
},
|
||||
{
|
||||
"id": "legacy-45",
|
||||
"level": "new",
|
||||
"position": "45",
|
||||
"title": "Edge Of Destiny",
|
||||
"url": "https://youtu.be/rUphe3H59yU?si=dZ8LgDyqif1ET_k1"
|
||||
},
|
||||
{
|
||||
"id": "legacy-46",
|
||||
"level": "new",
|
||||
"position": "46",
|
||||
"title": "The Halucination",
|
||||
"url": "https://youtu.be/tYbXsNkO9HE?si=b8hmXS98BAjDlPzF"
|
||||
},
|
||||
{
|
||||
"id": "legacy-47",
|
||||
"level": "new",
|
||||
"position": "47",
|
||||
"title": "CONVULSION",
|
||||
"url": "https://youtu.be/qeRKuyU3eGI?si=qJN026t6RcGjU3iW"
|
||||
},
|
||||
{
|
||||
"id": "legacy-48",
|
||||
"level": "new",
|
||||
"position": "48",
|
||||
"title": "Solar Flare",
|
||||
"url": "https://youtu.be/eHQNgty8ypY?si=ZdUIODdeDYLAXeHI"
|
||||
},
|
||||
{
|
||||
"id": "legacy-49",
|
||||
"level": "new",
|
||||
"position": "49",
|
||||
"title": "LIMBO",
|
||||
"url": "https://youtu.be/kXYMbaMVOZg?si=fWm4ngeSxBhFZadA"
|
||||
},
|
||||
{
|
||||
"id": "legacy-50",
|
||||
"level": "new",
|
||||
"position": "50",
|
||||
"title": "The Catacombs",
|
||||
"url": "https://youtu.be/T9-75lfKVQg?si=zEH6vP1617n5666I"
|
||||
}
|
||||
]
|
||||
@@ -1,14 +1,14 @@
|
||||
new|1|Flamewall|https://youtu.be/x4Io4zkWVRw?si=tf0DBaWahOPPEWxO
|
||||
new|2|Thinking Space II|https://youtu.be/CELNmHwln_c?si=soP6SzRn92G1vKbl
|
||||
new|4|Amethyst|https://youtu.be/4lfkzz1VCbA?si=6NNeWiH-nnwu21Cb
|
||||
new|3|Tidal Wave|https://youtu.be/9fsZ014qB3s?si=oj4E4ceyNtRVcDvd
|
||||
new|4|Amethyst|https://youtu.be/4lfkzz1VCbA?si=6NNeWiH-nnwu21Cb
|
||||
new|5|Orbit|https://youtu.be/QKcv8DkNPd0?si=cKBFqp7GRqmfupgJ
|
||||
new|6|BOOBAWAMBA|https://youtu.be/20fYiqLAo_E?si=QSSixz31rWeeQ_8B
|
||||
new|7|Nullscapes|https://youtu.be/EztneTPp5CU?si=ljn-S_bnGYwOoUyV
|
||||
new|8|Quantuese Processing|https://youtu.be/j5NC0u1Q91Q?si=FvUEY2kdDtNMqWSD
|
||||
new|9|Subsuming Vortex|https://youtu.be/0eYG1ogJpIQ?si=7XRxfDsmMVb0G9Si
|
||||
new|10|Andromeda|https://youtu.be/mk3TDemdkC0?si=va13_RKAJUyFvhcv
|
||||
new|11|Every End|https://youtu.be/AO--mVVFtKI?si=gIT5vxyc1FZ4_8Jj
|
||||
new|10|Every End|https://youtu.be/AO--mVVFtKI?si=gIT5vxyc1FZ4_8Jj
|
||||
new|11|Andromeda|https://youtu.be/mk3TDemdkC0?si=va13_RKAJUyFvhcv
|
||||
new|12|Silent Clubstep|https://youtu.be/GR4OMkS3SN8?si=CMNz8bPBaUU94wiy
|
||||
new|13|Anathema|https://youtu.be/_uKwmjHmySI?si=PgXwX7Z--wCay34b
|
||||
new|14|Acheron|https://youtu.be/sBKR6aUorzA?si=0A9S2_W8gi1awZ81
|
||||
@@ -20,31 +20,316 @@ new|19|Abyss Of Darkness|https://youtu.be/ejJkpqcMMCY?si=Rpp4HEi1kOMh-p0h
|
||||
new|20|Defeated Circles|https://youtu.be/nU5AQPzd2YA?si=6eKDMPh-2QjDBBOF
|
||||
new|21|Tunnel Of Despair|https://youtu.be/LpS4JREhW98?si=Z6_DRV5YZTEA0TeQ
|
||||
new|22|Subterminal Point|https://youtu.be/h2wmRMgACH4?si=nDD0-bmLqPgcdVJP
|
||||
new|23|KOCMOC|https://youtu.be/2CxE-UWCIG4?si=9xz4hBjtNKe8Rcbs
|
||||
new|24|Slaughterhouse|https://youtu.be/kpcF1-QAHQc?si=7ybF9EbcjqHzloLM
|
||||
new|25|Kyouki|https://youtu.be/KDa5c0CJTHs?si=WU0sPusFLB7Jtkam
|
||||
new|26|The Lightning Rod|https://youtu.be/nQDTi077O6M?si=Kqp2Od3lJx1YWTDB
|
||||
new|27|Based After Based|https://youtu.be/yQBFyUvB3lY?si=Cmxbc5jrTrNlcmG5
|
||||
new|28|CHIL|https://youtu.be/DROMiCc2ZRM?si=_bR8CCnqJGVCex9F
|
||||
new|29|Sakupen Circles|https://youtu.be/ofG2mJi9kEA?si=3yv78_jxidKPhtL5
|
||||
new|30|Deimos (ItsHybrid)|https://youtu.be/b2yHaIk5zio?si=AMIuVFR-EW4CK8_P
|
||||
new|31|Eyes In The Water|https://youtu.be/yvLwiOy3KEA?si=1991xs25WTEoTbE0
|
||||
new|32|Voltage|https://youtu.be/wBRvBN9tmlc?si=GU2md9I7ZnUV7Rfr
|
||||
new|33|Firework|https://youtu.be/QBe5x2o9v2w?si=dhPg9YcCRjwngxU9
|
||||
new|34|Silentlocked|https://youtu.be/O-IQeUdEGvI?si=KeSHH6kwZoSVGINT
|
||||
new|35|poocubed|https://youtu.be/fzL31vai1ms?si=9Gwk7RDtbEFxOJ6t
|
||||
new|36|KOSETSU|https://youtu.be/hZ8vFX8z_BU?si=P6fFal0yazJbje2O
|
||||
new|37|Through The Gates|https://youtu.be/4yHA6jux5UI?si=nstk3A6FDN3QR-qm
|
||||
new|38|Saul Goodman|https://youtu.be/hjs5PjUaw9k?si=qJpGrru21d50gO5R
|
||||
new|39|The Salt Factory|https://youtu.be/lQ7M-Sgov24?si=nXYVL0XGy5NOJLXn
|
||||
new|40|Snowbound|https://youtu.be/cjHwgbtAkXU?si=ghJbRJCkN-m0zu60
|
||||
new|41|CONVOLSION|https://youtu.be/qeRKuyU3eGI?si=UPaem0b3V8fX0L1r
|
||||
new|42|The Apocalyptic Trilogy|https://youtu.be/RUBbpsTR5eU?si=mEsPtyMLrQgP5fWl
|
||||
new|43|MINUSdry|https://youtu.be/YvA8ehhzz0Q?si=DT6DfrXOQ41swSIR
|
||||
new|44|Sevvend Clubstep|https://youtu.be/TvA8EJTJFCc?si=Rm9HyExGYpeGn4qs
|
||||
new|45|Edge Of Destiny|https://youtu.be/rUphe3H59yU?si=dZ8LgDyqif1ET_k1
|
||||
new|46|The Halucination|https://youtu.be/tYbXsNkO9HE?si=b8hmXS98BAjDlPzF
|
||||
new|47|CONVULSION|https://youtu.be/qeRKuyU3eGI?si=qJN026t6RcGjU3iW
|
||||
new|48|Solar Flare|https://youtu.be/eHQNgty8ypY?si=ZdUIODdeDYLAXeHI
|
||||
new|49|LIMBO|https://youtu.be/kXYMbaMVOZg?si=fWm4ngeSxBhFZadA
|
||||
new|50|The Catacombs|https://youtu.be/T9-75lfKVQg?si=zEH6vP1617n5666I
|
||||
new|23|Penumbral|https://youtu.be/nVHPFf13M-I?si=1TpoGSwOX63uOtj1
|
||||
new|24|KOCMOC|https://youtu.be/2CxE-UWCIG4?si=9xz4hBjtNKe8Rcbs
|
||||
new|25|Slaughterhouse|https://youtu.be/kpcF1-QAHQc?si=7ybF9EbcjqHzloLM
|
||||
new|26|Kyouki|https://youtu.be/KDa5c0CJTHs?si=WU0sPusFLB7Jtkam
|
||||
new|27|The Lightning Rod|https://youtu.be/nQDTi077O6M?si=Kqp2Od3lJx1YWTDB
|
||||
new|28|Based After Based|https://youtu.be/yQBFyUvB3lY?si=Cmxbc5jrTrNlcmG5
|
||||
new|29|CHIL|https://youtu.be/DROMiCc2ZRM?si=_bR8CCnqJGVCex9F
|
||||
new|30|Sakupen Circles|https://youtu.be/ofG2mJi9kEA?si=3yv78_jxidKPhtL5
|
||||
new|31|Deimos (ItsHybrid)|https://youtu.be/b2yHaIk5zio?si=AMIuVFR-EW4CK8_P
|
||||
new|32|Eyes In The Water|https://youtu.be/yvLwiOy3KEA?si=1991xs25WTEoTbE0
|
||||
new|33|Voltage|https://youtu.be/wBRvBN9tmlc?si=GU2md9I7ZnUV7Rfr
|
||||
new|34|Firework|https://youtu.be/QBe5x2o9v2w?si=dhPg9YcCRjwngxU9
|
||||
new|35|Silentlocked|https://youtu.be/O-IQeUdEGvI?si=KeSHH6kwZoSVGINT
|
||||
new|36|poocubed|https://youtu.be/fzL31vai1ms?si=9Gwk7RDtbEFxOJ6t
|
||||
new|37|KOSETSU|https://youtu.be/hZ8vFX8z_BU?si=P6fFal0yazJbje2O
|
||||
new|38|Through The Gates|https://youtu.be/4yHA6jux5UI?si=nstk3A6FDN3QR-qm
|
||||
new|39|Saul Goodman|https://youtu.be/hjs5PjUaw9k?si=qJpGrru21d50gO5R
|
||||
new|40|The Salt Factory|https://youtu.be/lQ7M-Sgov24?si=nXYVL0XGy5NOJLXn
|
||||
new|41|Solar Flare|https://youtu.be/eHQNgty8ypY?si=ZdUIODdeDYLAXeHI
|
||||
new|42|Snowbound|https://youtu.be/cjHwgbtAkXU?si=ghJbRJCkN-m0zu60
|
||||
new|43|CONVOLSION|https://youtu.be/qeRKuyU3eGI?si=UPaem0b3V8fX0L1r
|
||||
new|44|The Apocalyptic Trilogy|https://youtu.be/RUBbpsTR5eU?si=mEsPtyMLrQgP5fWl
|
||||
new|45|MINUSdry|https://youtu.be/YvA8ehhzz0Q?si=DT6DfrXOQ41swSIR
|
||||
new|46|Sevvend Clubstep|https://youtu.be/TvA8EJTJFCc?si=Rm9HyExGYpeGn4qs
|
||||
new|47|Edge Of Destiny|https://youtu.be/rUphe3H59yU?si=dZ8LgDyqif1ET_k1
|
||||
new|48|The Halucination|https://youtu.be/tYbXsNkO9HE?si=b8hmXS98BAjDlPzF
|
||||
new|49|CONVULSION|https://youtu.be/qeRKuyU3eGI?si=qJN026t6RcGjU3iW
|
||||
new|50|LIMBO|https://youtu.be/kXYMbaMVOZg?si=fWm4ngeSxBhFZadA
|
||||
new|51|The Catacombs|https://youtu.be/T9-75lfKVQg?si=zEH6vP1617n5666I
|
||||
new|52|Gaggatronda|https://youtu.be/A4G41vw6Cf0?si=0B8vDSj8vPElRa4i
|
||||
new|53|Codependence (Solo)|https://youtu.be/IKGk41hSnDo?si=1hmZYSDTYOxjWW6u
|
||||
new|54|Belladonna|https://youtu.be/saHSgoU1JQQ?si=SyIGkLNhmE1D8So7
|
||||
new|55|Collapse (Nexel)|https://youtu.be/RCFpXyf6bn0?si=WMfEkJJRqN6xaLxZ
|
||||
new|56|Mayhem|https://youtu.be/U6e6DD32noM?si=YCWNMvQ34ohce3H3
|
||||
new|57|Walter White|https://youtu.be/KzK62PkWOJ0?si=UQdOGms7z-pG-XPp
|
||||
new|58|CROMACAVE|https://youtu.be/tfpsMZI6z-o?si=OG_8nmb3MUpR-EBz
|
||||
new|59|The Plunge|https://youtu.be/8gW1-_u1K0Y?si=Sei6SdC7G-QzrKdy
|
||||
new|60|Infinite Chaos|https://youtu.be/wOMVERxU5IA?si=xig-kQM3x3e5fH1k
|
||||
new|61|Damascus|https://youtu.be/MWQyCUGt76Y?si=p3xkwoLEbK0p-ucw
|
||||
new|62|Operation Evolution|https://youtu.be/WsVF-ayipvY?si=SGz8C0T9IXeu1YeR
|
||||
new|63|SARYYX NEVER CLEAR|https://youtu.be/rRlyvmjcz_4?si=v6Eva9LHV2iLzNaf
|
||||
new|64|The Yangire|https://youtu.be/V3FjIorIfWg?si=5DR0YRTHFMw8dZ7z
|
||||
new|65|Decks Dark|https://youtu.be/l52ApE4HWkY?si=85C832hCxG0PUThq. hi
|
||||
new|66|Climax|https://youtu.be/E4ppq2WeAAs?si=dOWHqoBCSC_3liFu
|
||||
new|67|ORDINARY|https://youtu.be/4QiHP3jpGnk?si=0plaftXpA8nOrUf8
|
||||
new|68|wavterminal|https://youtu.be/dlvsCE3xYkQ?si=ihNU2fqTufiJ3t7N
|
||||
new|69|Loops Of Fury|https://youtu.be/9HBG3i5zbw8?si=7SnwcoJkF5Wke-im
|
||||
new|70|Sinister Silence|https://youtu.be/dJCdTMQxHMo?si=eXhBEf1AiZRxnwr_
|
||||
new|71|arcturus|https://youtu.be/a_Bqa8l9Xtc?si=Tz6FaY_wAld1xjGq
|
||||
new|72|Cimmerian Shade|https://youtu.be/lUNvV1k19HA?si=P9_B0sDua9WZ_bQj
|
||||
new|73|PSYCHOPATH|https://youtu.be/1luL7tTE8p8?si=YKhXZzpUsFUntBQ2
|
||||
new|74|Midnight|https://youtu.be/NhkN_a9cNi8?si=kxmLQdZfSmiXjV6s
|
||||
new|75|Tartarus|https://youtu.be/JptG7rwti1U?si=HCtGcPUmKQEtJlB9
|
||||
new|76|Sonic Wave Infinity|https://youtu.be/C3-l_fFikpQ?si=-tbOdlhQ49L9WeOe
|
||||
new|77|BEELINE (solo)|https://youtu.be/nt6T9YDb6gw?si=isN4Q0jDZnuMsldv
|
||||
new|78|Jigsaw|https://youtu.be/FPfSqlI0T30?si=xb8vXJ2girhi-dbs
|
||||
new|79|Waterfall|https://youtu.be/VKuVdqneG9Y?si=8CRjd1ttXRM8mWLh
|
||||
new|80|THE JET ENGINE|https://youtu.be/8Q20XU__LoQ?si=NKm0fq3qFgiX53e9
|
||||
new|81|Coalescence|https://youtu.be/do-a2EzZ4R8?si=zEIRXm5OR97WKgX1
|
||||
new|82|BPATA MPAKA|https://youtu.be/sSbHYzYgYfA?si=7g9yQ1XnoZtQrqgP
|
||||
new|83|The Wonder Of You|https://youtu.be/iQ3RzYCvLvA?si=JU5AW_dP1Mgmxq7A
|
||||
new|84|Delta|https://youtu.be/_rr3J8DkIm4?si=BU9VW8h-wKoWAcRJ
|
||||
new|85|The Golden|https://youtu.be/wpS5PSHmji0?si=J7dT2Srdo_LqTrd7
|
||||
new|86|Terminal Rampancy|https://youtu.be/29hRhhqQTLc?si=5GGOrtxUootbEY-F
|
||||
new|87|Natural Disaster|https://youtu.be/_0GqdSFmG_8?si=4kUa8hJqK-bT70nI
|
||||
new|88|Viprin UFO|https://youtu.be/LTMW9Py-nHE?si=vIBNIDOhvQi9FpVd
|
||||
new|89|Oblivion|https://youtu.be/ZcmDAm50BB0?si=otqXWvzIRzL1L3wq
|
||||
new|90|NETWORK|(Agat3) https://youtu.be/K7wJi01J5D8?si=hEJIwYn1J9RY_-XG
|
||||
new|91|UNKNOWN|https://youtu.be/a1sQxQqU6EM?si=737UNiYWoU5CM0zQ
|
||||
new|92|Verdant Landscape|https://youtu.be/wjZxkS5KC0A?si=WFTg6aixL7Z0YAGp
|
||||
new|93|Levigo|https://youtu.be/AROmUo7DNkE?si=eHXmi5PmBW94sXRl
|
||||
new|94|ATOMIC CANNON Mk III|https://youtu.be/xlAI_wVvnx8?si=3UJXMPHYGKH-qDyC
|
||||
new|95|Shukketsu|https://youtu.be/nkG4hKJIz2g?si=KNly0dPd063HIlKU
|
||||
new|96|Checked Steam|https://youtu.be/cq6047eYGXs?si=yE7kCbjdAp_7iMlM
|
||||
new|97|WOBBLING MACHINE (Solo)|https://youtu.be/RKX6VOsICOg?si=QjLbBucxWJhI3TW0
|
||||
new|98|Critical Heat|https://youtu.be/UjYIt2jy_BY?si=5Jl3FoAOEPSGL1t-
|
||||
new|99|Crystal Crusher (Solo)|https://youtu.be/RKX6VOsICOg?si=ZPPM3qjbx2KraxYW
|
||||
new|100|limbo but uwu ig idk|https://youtu.be/3B36N-qfOD8?si=NshDR7CO9whAkga9
|
||||
new|101|The Paroxysm of Rage|https://youtu.be/nq3i1qJQeEk?si=-xey2Qd9hrtu_AZU
|
||||
new|102|paranoia (amplitron)|https://youtu.be/Aw83i8Zmh_0?si=0GwEgBHuexCHsuhk
|
||||
new|103|Blood Echo|https://youtu.be/fSvk4tLlQzQ?si=a6pIZkJ58uM2y5gs
|
||||
new|104|Aerial Gleam|https://youtu.be/eyZjKtrGVYw?si=qmsOlWAezCCgYeGb
|
||||
new|105|DISCONNECT|https://youtu.be/HxHo-LzwSao?si=PsuqKQi9DL-iYtUW
|
||||
new|106|VOID|https://youtu.be/kN1FUoucoSc?si=NjE2E12FF9urJL45
|
||||
new|107|Starlit Stroll|https://youtu.be/U7MM74dTXuI?si=WofCw6_u86gVLFSb
|
||||
new|108|Trueffet|https://youtu.be/yHESkazVD4Q?si=Wo04Qh1dOGEDb3JH
|
||||
new|109|Henken|https://youtu.be/M--2oQKrV68?si=WsNcHkxpVO3BGW6T
|
||||
new|110|Graceful|https://youtu.be/5kOD37vQHKE?si=VXPDbYRAZstaq_Oy
|
||||
new|111|Kenos|https://youtu.be/Bs1kVySdUtI?si=4DAwPGeehZX6YzEJ
|
||||
new|112|azure blast|https://youtu.be/TN0FQ3KBv4w?si=JVSf1iXaez8sSOw0
|
||||
new|113|Fragile|https://youtu.be/XfXzGv0oALw?si=0Blg4XOudYEomR2A
|
||||
new|114|Starlight Summit|https://youtu.be/LB9xx41M4Ns?si=xC9MsTIzlM44AdhE
|
||||
new|115|chrome hearts|https://youtu.be/GK0Z8rgziH4?si=orYnpQGMPKPQTW_O
|
||||
new|116|Esfera|https://youtu.be/GK0Z8rgziH4?si=1km0bCL22Egbl-Xk
|
||||
new|117|Destruction 19|https://youtu.be/UUyzGg_4mqs?si=uJw74R5zv3GwYiag
|
||||
new|118|NEUTRA|https://youtu.be/0fNBFwNS1qg?si=sqeqmMqFotN6jgGh
|
||||
new|119|Time Lapse|https://youtu.be/MnI1mM_wjzc?si=FEmB2fWSeNqJDIwv
|
||||
new|120|Dark Dimension|https://youtu.be/g2DrT9bHjOI?si=TuVvUXo1XDGlNB5_
|
||||
new|121|Guideless Goobering|https://youtu.be/pFxdGKYQrcg?si=nQ3kVcqOq8UEt6uB
|
||||
new|122|Hard Machine|https://youtu.be/z3grmsqAaas?si=xBvAriHwGl-IneUd
|
||||
new|123|Swing Swing|https://youtu.be/_hL9hFU3WI4?si=4ihgAE4n8VrN4lsk
|
||||
new|124|DISSONANCE|https://youtu.be/IfFser-f9c0?si=P2D23zMCX96QPEL8
|
||||
new|125|Zodiac|https://youtu.be/N4QjElo58_o?si=0PMsj7NgBfUUIzLZ
|
||||
new|126|Goober Rage Stage|https://youtu.be/1rrouUCPXhY?si=2o6cHwApb4o-08jL
|
||||
new|127|Crackhead Circles|https://youtu.be/_4MDq8Us5gM?si=KO4JZZNs5mYpzAg3
|
||||
new|128|Lotus Flower|https://youtu.be/pTgOHcwJhd0?si=k92uqgcP9o-Ovvxr
|
||||
new|129|Scream Machine|https://youtu.be/n-KeSGEX7Jg?si=dY8omM-22Iju1uck
|
||||
new|130|Axinie|https://youtu.be/LjfO2fqnozI?si=KuTXmwjvM6_t45rS
|
||||
new|131|Widestep|https://youtu.be/TN8fHN_XV3o?si=UdbpxVI9fvujr4RU
|
||||
new|132|Judgement Knights|https://youtu.be/MzzY8nWfxnk?si=zAXlxJZFiKCGlu9A
|
||||
new|133|Keres|https://youtu.be/MFFIjZP7PxQ?si=lkc9tRawW-XNv6-7
|
||||
new|134|Lithium|https://youtu.be/IfVM56qNVdA?si=AIEw5JbV9OQW60eJ
|
||||
new|135|IRIS|https://youtu.be/WiFwAU8pgr4?si=Q0fTY2Hd2oQLBLaW
|
||||
new|136|Cold Sweat|https://youtu.be/tBpEPXGhQug?si=0s6Mh429yBF_dkea
|
||||
new|137|Galeforce|https://youtu.be/G6VF2Gvmpz0?si=WzSIerGhoz3u0fcV
|
||||
new|138|in this|https://youtu.be/gQiOdxOyNGg?si=gTmiy_pThUKYLcb0
|
||||
new|139|Frost Spirit|https://youtu.be/KXbaRnRpm58?si=LHfUVqpxgRYtEmkH
|
||||
new|140|Thinking Space|https://youtu.be/iHf2nanWjvE?si=stgikKc4PPw8F7vp
|
||||
new|141|Promethean|https://youtu.be/19kE_Yo8puE?si=0AcDFf0OjghIgdpX
|
||||
new|142|Ascent|https://youtu.be/sII8zQZIJlg?si=gHbK7-s-CLho4vM0
|
||||
new|143|Dry Out Copyable 2|https://youtu.be/FXW9J9CaREU?si=6Be51250LT0SQLHy
|
||||
new|144|Renevant|https://youtu.be/2Y8kBw8YxO8?si=UQS31gMvOew6fsrc
|
||||
new|145|ConClusion|https://youtu.be/wpuPFFqnJFY?si=CEHAmw_k9_WjadYw
|
||||
new|146|Trotil|https://youtu.be/kblnO7LfQn4?si=Lq_V_8BrvzGo-KAH
|
||||
new|147|We Are Not The Same (Solo)|https://youtu.be/yLLd4mpeYC4?si=5Cugl6UYnBcx8A0a
|
||||
new|148|Disconnected Descent|https://youtu.be/TP7OAAVH-uA?si=2_eKfx19pc3xyZFt
|
||||
new|149|Instinct (KrmaL)|https://youtu.be/6W3Gvi9ft0w?si=o-AAu2Yz7G5Ytm2Y
|
||||
new|150|Calculator Core|https://youtu.be/VrgNFsF2NGw?si=VxW5clIFp8YLf0nv
|
||||
new|151|Sky Shredder|https://youtu.be/Z-69-4RKRgk?si=O96OhtDsQ1BFgJLG
|
||||
new|152|Crimson Planet|https://www.youtube.com/live/euMInLjyG6k?si=09vneTJfThjnpACw
|
||||
new|153|shimmer (amplitron)|https://youtu.be/J89j-_l7ezg?si=fMAyAXx2w028BXR8
|
||||
new|154|DIRECTIONS|https://youtu.be/-SEgSgxpRxo?si=ZLxK5XD8B8pPiVsp
|
||||
new|155|ATOMIC CANNON Mk II|https://youtu.be/4-lOnZsfhNQ?si=ON-czwtJawFMgZc_
|
||||
new|156|Ringy Paracosm|https://youtu.be/RIDuh4IVGiw?si=9s892WxqybVKVYlP
|
||||
new|157|Cognition|https://youtu.be/enl0GOyskPs?si=qe18-qXwxoUkokyt
|
||||
new|158|Axiom Asterism|https://youtu.be/LnFdka3HekA?si=ehLne5DftKG33cQH
|
||||
new|159|Indivine|https://youtu.be/g2CVts-tX70?si=XfKVwmDp5HoTj_--
|
||||
new|160|Neon Skyline|https://youtu.be/HvGjIAeLv_8?si=sEu3tWgtJJrcbHxd
|
||||
new|161|Cosmic Cyclone|https://youtu.be/0I9xCfTOpXg?si=2Q1_twV5HH3zLOMN
|
||||
new|162|CORRODERE|https://youtu.be/-V87Bh44lAU?si=zIbxyCYezyK_dAna
|
||||
new|163|SARY NEVER CLEAR|https://youtu.be/23u3SlwrOMw?si=GMUrk4CONI8n-RHy
|
||||
new|164|qoUEO|https://youtu.be/PJbfroH0sok?si=lQM4JwbyXlwc85td
|
||||
new|165|Rigel|https://youtu.be/liJFeLHcW_Q?si=UaqSqF1aJLB6kt6t
|
||||
new|166|Scrubbabingo force|https://youtu.be/eT_izuCxGUU?si=_VzD8E46NCZMx11v
|
||||
new|167|RUST|https://youtu.be/mEXGlrNMyR4?si=HQuHo3l0QAtY4hPO
|
||||
new|168|Call Me Maybe|https://youtu.be/Jc-98ND5OJk?si=3EsBQ7paobUcgnel
|
||||
new|169|ta1LSD0ll|https://youtu.be/w-NH14vgMb8?si=xocgmpg6Jn92_Evs
|
||||
new|170|SAND SAILOR|https://youtu.be/QPmzn9X77Uo?si=vWrkuwERNWZbYcTb
|
||||
new|171|Gloxinia|https://youtu.be/MJh_6Z5v6bM?si=Lyzx4IoWnWQfZELk
|
||||
new|172|Akashic Records|https://youtu.be/qnZnV6q-mt4?si=7n69Q8erUDa58c0H
|
||||
new|173|Cobwebs|https://youtu.be/bgZ85rCaEGY?si=gEsYkSGnk6G5qP_r
|
||||
new|174|meow hard|https://youtu.be/jOhGH2TckDA?si=ZhjMOsVjzFxDPFZm
|
||||
new|175|Lucid Nightmares|https://youtu.be/G7hBE5qWx6M?si=D8M1hqmsA9k3oVOU
|
||||
new|176|RUTHLESS|https://youtu.be/9jwVeYtc6H4?si=meyxbaKJqCe5Fums
|
||||
new|177|Coral Cave|https://youtu.be/kTOM1nSEF3I?si=XpkVW2-qTylokjI-
|
||||
new|178|SUPERHATEMEWORLD|https://youtu.be/fhmgZYCJ29M?si=yjgUX5sohJ0ej1uW
|
||||
new|179|Launchpad Labyrinth|https://youtu.be/XELUzw7UbNU?si=rxyVGXKE74rHb8DT
|
||||
new|180|Tenkai (Solo)|https://youtu.be/AM2KHSZkjcc?si=bkHkgj1CO1yv31vv
|
||||
new|181|Sharpscapes|https://youtu.be/wM-J6fLIs14?si=4njb0wT6coAwDDZB
|
||||
new|182|obsession|https://youtu.be/OjXKjQXnhjA?si=5QdHmozIHvdl4I4-
|
||||
new|183|Horros (Solo)|https://youtu.be/OS46C-zvqDA?si=2DKf_LRA2PwIjAtG
|
||||
new|184|Omega Interface|https://youtu.be/sVngEgj0who?si=CyRbOzs4OKeB4StS
|
||||
new|185|Amalgam|https://youtu.be/82DDR2BjXSI?si=xm-EDHngoNovfmzf
|
||||
new|186|Ragnarok|https://youtu.be/aGtXqn9HL2o?si=Hhkr5oZGdAF4iMgZ
|
||||
new|187|Sides Of My Mind (Solo)|https://youtu.be/d3d2KPVgCAk?si=L-zr7NZGRrWqiDuJ
|
||||
new|188|Ykds1479ymdppr|https://youtu.be/Wom_gMcauUI?si=UVV5h9rfRPws1Wr5
|
||||
new|189|The Art of the Blade|https://youtu.be/KSJl4A9eUW4?si=PmbP4G67aQEaeyyh
|
||||
new|190|NYUTOPIA|https://youtu.be/TPyZKiTOUQA?si=5_D_ddUqaz7xSDpS
|
||||
new|191|me when machine gang (Solo)|https://youtu.be/C8uSFs01qwc?si=dgFXyQFS1rKk3ug9
|
||||
new|192|Deep Bass|https://youtu.be/5uu8xXgz_4s?si=jnpzgqcYgIxNS5m6
|
||||
new|193|Pandemonium (Cersia)|https://youtu.be/OKEHsJPaeQY?si=cv5MRfDMZjs4nwno
|
||||
new|194|Silent Club|https://youtu.be/NjEiHIokTGM?si=wmVPS04r8wi_jgNG
|
||||
new|195|Descent Into Exile|https://youtu.be/whtt_WkG_20?si=Ly3S1-pgtmJfDgpI
|
||||
new|196|The Rupture|https://youtu.be/SaU454afkKY?si=QcXiCR2boQsd8iWg
|
||||
new|197|no jokes|https://youtu.be/oVhGYiQ8fBc?si=gtjiunr2HOryqsGn
|
||||
new|198|Jupiter My Favourite|https://youtu.be/adVQ_yflcGM?si=9l3Eo1YxpL7SQ5o1
|
||||
new|199|Kappa|https://youtu.be/eIfc1zUuOPU?si=OETCzxDyPm4VtXhI
|
||||
new|200|FARBIDI THEORY|https://youtu.be/MQD6klwhcsM?si=-8ZQ1fakCN5HVqAw
|
||||
new|201|LD50|https://youtu.be/2OiS9mVDlmQ?si=XvEHVQlMZvYv7eUM
|
||||
new|202|Bloom|https://youtu.be/oZTyTgeb7q4?si=MC61WABnqDPSl377
|
||||
new|203|the ocean of fish|https://youtu.be/dNHUg49I3sM?si=chQLiCsZmoPjhh6E
|
||||
new|204|BarbarosFinaleFinale|https://youtu.be/F32ekXnS3a4?si=UW9apT8TfrCh6NEL
|
||||
new|205|Nabil Let Go|https://youtu.be/IRYire6RkUg?si=h50D6hCRKxr8O4wR
|
||||
new|206|Bloodlust|https://youtu.be/5SzKetF2btw?si=hs8FajAMmW52a2DD
|
||||
new|207|Asterios|https://youtu.be/YG3taML8WjA?si=p68EsJTC4tJ58g4q
|
||||
new|208|Sank|https://youtu.be/6ZuEWp1dJ0c?si=eR2pCKpXj_JPVDUEz
|
||||
new|209|Escape Room|https://youtu.be/sZebhp6gFuI?si=HrzmtVkFY9OZtJBa
|
||||
new|210|Jesse Pinkman|https://youtu.be/fYcBtp4X8GA?si=HlxPFd0TEfufE5Qz
|
||||
new|211|Moving Forward|https://youtu.be/2wYnXim6TYY?si=z66XmjVqWN3w5Xo0
|
||||
new|212|X84|https://youtu.be/ZzGVEmS5D8s?si=WRnQYxFWB9uFAmJi
|
||||
new|213|Excruciation Chamber|https://youtu.be/BdkuNdFfY-o?si=QeVwcyv4Sx27exQI
|
||||
new|214|Terminux|https://youtu.be/-UgocJ3WYn4?si=uSiVYs6waOj5y8pz
|
||||
new|215|Twilight|https://youtu.be/5bZ3RQP7fjs?si=EqntxnyH46JPb4tM
|
||||
new|216|IthacropoliX|https://youtu.be/n4nbu_fZcf4?si=3AojnEZYAGP3BBPQ
|
||||
new|217|CITRA|https://youtu.be/O_kYXvwqrWo?si=EjQHq04G3caSRj5H
|
||||
new|218|Nhelv|https://youtu.be/k117h9HUt6s?si=O-tw91m3_FFR0mYN
|
||||
new|219|Awedsy|https://youtu.be/hx5ebE8e6Eo?si=aoXlzm82otqvYXmJ
|
||||
new|220|TORN|https://youtu.be/QUpzux7AoTI?si=5BBH3x-j2x62ttNl
|
||||
new|221|MewneI|https://youtu.be/yWfbbEbTK9w?si=LHHUHaA3zc7Yy_uY
|
||||
new|222|Deimos (EndLevel)|https://youtu.be/FgxsNzQnF5o?si=yalVUa7cMm-_KVXC
|
||||
new|223|Congregation|https://youtu.be/KVlcdvGYcj0?si=uwMTvD45yMSST6jW
|
||||
new|224|Gracefully|https://youtu.be/5RBn1nk5wb4?si=YzBymZ_FrG1fGeK0
|
||||
new|225|DsinK|https://youtu.be/EsQAFvKqo3U?si=9DApZCy2SmlFOnLo
|
||||
new|226|Sigma|https://youtu.be/3INdO0esQRw?si=LTYmo4jqmiNECG4W
|
||||
new|227|Sazerix|https://youtu.be/l8aBStUgYOs?si=xxfUBcsVHJDeiutP
|
||||
new|228|Pootis Engage XTREME|https://youtu.be/2SqKRRphIVI?si=e__iWF4KfDo_QrsR
|
||||
new|229|Aronia|https://youtu.be/eJe3i5KyKOk?si=LpTBN7qyPb4VIYDq
|
||||
new|230|Exosphere|https://youtu.be/HRbBub341W4?si=tPr0acTWVgsYiSfj
|
||||
new|231|untitled unmastered|https://youtu.be/zzPsYgyU4n4?si=GhsZ-FF2GtgI-E9f
|
||||
new|232|Pagoda|https://youtu.be/KVomsNjGalQ?si=gOl163Fh0uJgbxpxhttps://youtu.be/npIZ8TGMDvk?si=4HY9BHGn-5jf8JXH
|
||||
new|233|Fog|https://youtu.be/Kt_fxnOYeco?si=LnB8xdFEpvNvEUED
|
||||
new|234|Knights of Thunder|https://youtu.be/npIZ8TGMDvk?si=A7qhkz-Y7-Vif1l2
|
||||
new|235|ATOMIC CANNON|https://youtu.be/VZS5Yn9Y-OI?si=PTnl4Z7QFIUx1YNI
|
||||
new|236|Cicatrize|https://youtu.be/QfF3jgPQZk0?si=h5Ov2QbMoNCo0aVk
|
||||
new|237|Dust Bowl|https://youtu.be/EXuxHOAAVrI?si=uOOvtL7gLoZ0odwr
|
||||
new|238|Gustavo Fring|https://youtu.be/OB0YiqyEp2E?si=7pkhUZ6Fzuu4HVVi
|
||||
new|239|Ploink|https://youtu.be/BV19Dg09LN0?si=kTEGTO_tyhpA_SoZ
|
||||
new|240|Fragmented|https://youtu.be/RnLoliyy4k0?si=_Vuwadp_AEsTKuDX
|
||||
new|241|Ouroboros|https://youtu.be/hLTnewDC2Hs?si=OnPDUTPAQAQCcU0W
|
||||
new|242|FRIDAY|https://youtu.be/YUoFhrN3aKw?si=ma7I1IAtU0UbrSDu
|
||||
new|243|CONNECT|https://youtu.be/oo_PqnEHZvo?si=q1jV7efm_cq_XPYM
|
||||
new|244|Ourwa|https://youtu.be/BImzHJnr5Ks?si=_ZT5WifsgFTBMK9O
|
||||
new|245|XRAY|https://youtu.be/zk1G8Spw1hg?si=YL20_J-29p0x-iD2
|
||||
new|246|Visible Ray|https://youtu.be/hHpaB752peM?si=syGmQ7hSd536vRCp
|
||||
new|247|the wiener|https://youtu.be/rgMruLorH1c?si=hQ6-z1GvwBCNzk8A
|
||||
new|248|Hardry|https://youtu.be/59JlNpDlhH8?si=fXzYUMwfmEjMPmQa
|
||||
new|249|Ghoul|https://youtu.be/W1kgdxV_8E0?si=9ULxBYkdf5TaoHX6
|
||||
new|250|kowareta|https://youtu.be/wxWBFaZYHPM?si=7-9C-9-oa5jrNIrC
|
||||
new|251|Apotheosis|https://youtu.be/X261xjwc3HQ?si=7-DnqQMviuTWtoOG
|
||||
new|252|EGO KILLING|https://www.youtube.com/watch?v=xnCsfVFygC4
|
||||
new|253|Frozen Cave|https://www.youtube.com/watch?v=BFaqgkw-Hg0
|
||||
new|254|Dump|https://www.youtube.com/watch?v=UDrCb0rzuQo
|
||||
new|255|SZYSLAK|https://www.youtube.com/watch?v=CUov9IB5wjg
|
||||
new|256|Th3Dev0n|https://www.youtube.com/watch?v=f7s6i0M1n-g
|
||||
new|257|The Yandere|https://www.youtube.com/watch?v=MN6Dra6yAys
|
||||
new|258|2 Comme le vent|https://www.youtube.com/watch?v=P7tLT3QEHw4
|
||||
new|259|Death Corridor Z|https://www.youtube.com/watch?v=9799msdH394
|
||||
new|260|Arctic Lights|https://www.youtube.com/watch?v=yd5cH9nvDvg
|
||||
new|261|PROTON LAUNCHER|https://www.youtube.com/watch?v=8stjtuvzfyk
|
||||
new|262|Dedohexdragon|https://www.youtube.com/watch?v=z_zegPqfqr4
|
||||
new|263|ROCKY ROAD|https://www.youtube.com/watch?v=uQqB3cVh_AM
|
||||
new|264|Plasma Pulse Finale|https://www.youtube.com/watch?v=xqzx9cu6XoA
|
||||
new|265|CHROMA FINALE|https://www.youtube.com/watch?v=ByJ9rz4-WUI
|
||||
new|266|Spectrum Cyclone|https://www.youtube.com/watch?v=IN0XeMo2SmM
|
||||
new|267|Icefall|https://www.youtube.com/watch?v=9dy1afi-fo4
|
||||
new|268|Storming Summit|https://www.youtube.com/watch?v=ymmMnZLpfQU
|
||||
new|269|CRIDIUM|https://www.youtube.com/watch?v=IS3AcckWUT8
|
||||
new|270|REPENTLESS|https://www.youtube.com/watch?v=-lexD6huBlg
|
||||
new|271|Specthys (Solo)|https://www.youtube.com/watch?v=QEhqvQvEfNM
|
||||
new|272|Alpha|https://www.youtube.com/watch?v=x9s3nSRUM7A
|
||||
new|273|Fever Dream (icedcave)|https://www.youtube.com/watch?v=Mc3vZeM_VYw
|
||||
new|274|Shutdown|https://www.youtube.com/watch?v=A-A6_ybDY2I
|
||||
new|275|Calamity|https://www.youtube.com/watch?v=PwOPRxp6KjY
|
||||
new|276|Aquatic Auroras|https://www.youtube.com/watch?v=cD0zZ9OJkBE
|
||||
new|277|Cersia Madness|https://www.youtube.com/watch?v=vGHOD8OWgdY
|
||||
new|278|Piano Woman|https://www.youtube.com/watch?v=xCc_h4grIvY
|
||||
new|279|RAM|https://www.youtube.com/watch?v=5cl7WAQJKdY
|
||||
new|280|GBS|https://www.youtube.com/watch?v=zBmIYg2dpk4
|
||||
new|281|nevermore|https://www.youtube.com/watch?v=VSxu7YnyGsQ
|
||||
new|282|WORSTBRAINMAP|https://www.youtube.com/watch?v=GWjIIFktU04
|
||||
new|283|Midnight Tokyo|https://www.youtube.com/watch?v=ALeB-Svv0BQ
|
||||
new|284|Sparkling|https://www.youtube.com/watch?v=4r9hkMxpiQw
|
||||
new|285|of Ambrosia|https://www.youtube.com/watch?v=0KtPsPVpj78
|
||||
new|286|Mirrored Calamity|https://www.youtube.com/watch?v=5Wo0eFR8Lwk
|
||||
new|287|Iris of a Puppeteer|https://www.youtube.com/watch?v=DK3BShj1zYc
|
||||
new|288|Requiem|https://www.youtube.com/watch?v=sRCQ1JjhBq4
|
||||
new|289|Dance of the Violins|https://www.youtube.com/watch?v=PYwI_ODrrlw
|
||||
new|290|Eternal Moment|https://www.youtube.com/watch?v=FbPJ2fclXHQ
|
||||
new|291|Edge of the World|https://www.youtube.com/watch?v=QWy1RnPDCWk
|
||||
new|292|RANDOM ACCESS MEMORY|https://www.youtube.com/watch?v=6_pYfJKKpRY
|
||||
new|293|SisuSonic|https://www.youtube.com/watch?v=epTEemVtIqY
|
||||
new|294|Beyond Hell|https://www.youtube.com/watch?v=RRtIiUBaVo4
|
||||
new|295|ConTroller|https://www.youtube.com/watch?v=kXLBuSDagOE
|
||||
new|296|Omega|https://www.youtube.com/watch?v=spYufsgr3NU
|
||||
new|297|AKIRA|https://www.youtube.com/watch?v=RHCCZkf4UFM
|
||||
new|298|Gamma|https://www.youtube.com/watch?v=HTXRWzS2aw8
|
||||
new|299|Golden Club|https://www.youtube.com/watch?v=YBs04o1ZVQY
|
||||
new|300|network (by VanDerVals)|https://www.youtube.com/watch?v=uA5i_jBW2hY
|
||||
new|301|Molten Gear|https://www.youtube.com/watch?v=NMRaJu5BiAI
|
||||
new|302|Spacial Rend|https://www.youtube.com/watch?v=VgiRJRBzacM
|
||||
new|303|Hyper Paracosm|https://www.youtube.com/watch?v=zQbU_2To0bg
|
||||
new|304|Collect All Pets|https://www.youtube.com/watch?v=NArTpUWoOQ0
|
||||
new|305|ADOMANIA|https://www.youtube.com/watch?v=-AJl65nhTEA
|
||||
new|306|Time Extreme|https://www.youtube.com/watch?v=gzqeoJK_UvM
|
||||
new|307|Wasureta|https://www.youtube.com/watch?v=9jkCtNDbDt0
|
||||
new|308|Ryft|https://www.youtube.com/watch?v=Wxjk0A09XNU
|
||||
new|309|Macabre|https://www.youtube.com/watch?v=b2kGqEY8E1Q
|
||||
new|310|OVERKILL|https://www.youtube.com/watch?v=aBDP_9-Sm-w
|
||||
new|311|Cybernetic Crescent|https://www.youtube.com/watch?v=0yKtq1PJeDQ
|
||||
new|312|ELLIPSISM|https://www.youtube.com/watch?v=tp6-hpqtnWk
|
||||
new|313|reeses potentially|https://www.youtube.com/watch?v=v45Ei-punX4
|
||||
new|314|OVERKILL|https://www.youtube.com/watch?v=aBDP_9-Sm-w
|
||||
new|315|Cybernetic Crescent|https://www.youtube.com/watch?v=0yKtq1PJeDQ
|
||||
new|316|ELLIPSISM|https://www.youtube.com/watch?v=tp6-hpqtnWk
|
||||
new|317|reeses potentially|https://www.youtube.com/watch?v=v45Ei-punX4
|
||||
new|318|Sewers|https://www.youtube.com/watch?v=YBWx2Dm10Ag
|
||||
new|319|Sink|https://www.youtube.com/watch?v=qX6yzZey9Ng
|
||||
new|320|the DEADLANDS|https://www.youtube.com/watch?v=pC-rUt9Z5dI
|
||||
new|321|FIFTY THREE|https://www.youtube.com/watch?v=JF4vQZC4rBI
|
||||
new|322|ASHES|https://www.youtube.com/watch?v=cmYjLL1k5Z0
|
||||
new|323|Necromancer|https://youtu.be/y-NwRnHqaJI?si=hldrpzkr7URUxjek
|
||||
new|324|Rage|https://youtu.be/RW_i0Kx3gDA?si=HCGnK8oMp1LxdlS8
|
||||
new|325|Kjdf8314jlfssf|https://youtu.be/FgOR1jwsXww?si=RZc_QNZssV8cx5IX
|
||||
new|326|Sonic Wave Rebirth|https://youtu.be/wXxPNaqTZKU?si=fsJyrSuf7LafVM9X
|
||||
new|327|Sonic Wave|https://youtu.be/GpXTwPgweAk?si=SJ3F-6VyszYB3ene
|
||||
new|328|Icotact|https://youtu.be/dd1rG5DLdc4?si=UyfeaCYu4_f9viW4
|
||||
new|329|IMMORTALITY|https://youtu.be/tl4trfW1JSo?si=hshKMrfXFgsP1EW7
|
||||
new|330|Cersia Difficult|https://youtu.be/8Vi_wD2wp-w?si=aQmFMcsFfHb4xLAx
|
||||
new|331|LUMINA|https://youtu.be/ug9mjzRT7Ao?si=x1aYQMbf7LJoUlag
|
||||
new|332|Dual Pulse|https://youtu.be/CRLoL1gnuiE?si=k2kJ-Ts_r3V21Z2U
|
||||
new|333|BRKMGB8GJCZ|https://youtu.be/PtAn8xfwK5w?si=Zm9l4aNL3ZJublVi
|
||||
new|334|Sephiroth|https://youtu.be/VZThVeKm7lI?si=uY2vdmf0WmARldU_
|
||||
new|1250|niwa|https://www.youtube.com/watch?v=vegDZr7jh-M
|
||||
+4
-1
@@ -1,17 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Error Pages - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="error-shell">
|
||||
<header>
|
||||
<h1>Error Pages</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="manage.html">Manage</a>
|
||||
<a href="guess.html">Guess Game</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Guess The Rank - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="guess">
|
||||
<header>
|
||||
<h1>Guess The Rank</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="roulette-shell">
|
||||
<section class="panel roulette-panel roulette-guess-panel">
|
||||
<div class="roulette-head">
|
||||
<div>
|
||||
<p class="roulette-kicker">Guessing Game</p>
|
||||
<h2>Where Is It Ranked?</h2>
|
||||
<p class="muted roulette-intro">A level title shows up, you guess its place on the list, and you get higher or lower hints until you solve it or run out of tries.</p>
|
||||
</div>
|
||||
<button class="btn roulette-btn" id="guess-start" type="button">Start Round</button>
|
||||
</div>
|
||||
|
||||
<div class="roulette-card guess-card">
|
||||
<label class="control-block control-select" for="guess-mode">
|
||||
<span>Mode</span>
|
||||
<select id="guess-mode">
|
||||
<option value="casual">Casual - 6 guesses</option>
|
||||
<option value="standard" selected>Standard - 4 guesses</option>
|
||||
<option value="hard">Hard - 3 guesses</option>
|
||||
<option value="marathon">Marathon - 8 guesses</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<p class="roulette-status muted" id="guess-status">Start a round to get a level.</p>
|
||||
<h3 id="guess-level-title">No level selected</h3>
|
||||
<p class="roulette-rank" id="guess-attempts">Tries left: 4</p>
|
||||
<p class="roulette-note muted" id="guess-feedback">Pick a mode, start a round, and enter a rank number like 1, 15, or 42.</p>
|
||||
|
||||
<form class="guess-form" id="guess-form">
|
||||
<label class="guess-label" for="guess-input">Your rank guess</label>
|
||||
<div class="guess-controls">
|
||||
<input id="guess-input" type="number" min="1" inputmode="numeric" placeholder="Guess the rank" />
|
||||
<button class="btn" id="guess-submit" type="submit">Guess</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="guess-answer muted" id="guess-answer">The correct rank will show here if you run out of guesses.</p>
|
||||
<a class="btn roulette-open" id="guess-open" href="#" target="_blank" rel="noopener noreferrer" hidden>View Video</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="panel roulette-rules-card">
|
||||
<p class="roulette-kicker">Modes</p>
|
||||
<h2>How this page works</h2>
|
||||
<div class="roulette-rule-list">
|
||||
<article>
|
||||
<strong>Casual</strong>
|
||||
<p>Six guesses for a more forgiving round if you just want to learn the list.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Standard</strong>
|
||||
<p>Four guesses for the default version of the game.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Hard</strong>
|
||||
<p>Three guesses if you want the original tougher setup.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Marathon</strong>
|
||||
<p>Eight guesses for longer rounds and closer narrowing.</p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+171
-23
@@ -1,36 +1,183 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>GD fedl</title>
|
||||
<meta name="description" content="FEDL - Browse ranked demons, check demon list rules, play demon games, and submit demon runs. Your hub for the Geometry Dash demon list.">
|
||||
<meta name="keywords" content="demon list, ranked demons, geometry dash, demon ranking, challenge demon">
|
||||
<title>FEDL - Ranked Demon List Hub for Geometry Dash</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="index">
|
||||
<header>
|
||||
<h1>GD fedl</h1>
|
||||
<nav>
|
||||
<a href="manage.html">Manage</a>
|
||||
<a href="players.html">Players</a>
|
||||
<header role="banner">
|
||||
<h1>FEDL</h1>
|
||||
<nav role="navigation" aria-label="Main navigation">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="panel">
|
||||
<h2>Welcome</h2>
|
||||
<p class="small muted">Use the top navigation to view players or fedls. This UI follows a compact table-first layout for quick browsing.</p>
|
||||
<section class="home-hero">
|
||||
<div class="home-copy panel">
|
||||
<p class="hero-kicker">FEDL Hub</p>
|
||||
<h2>Track ranked demons, learn submission rules, and master the challenge.</h2>
|
||||
<p class="hero-text">FEDL is your quick home base for browsing ranked demons, checking submission rules, spinning a roulette challenge, and keeping up with the list without digging through a bunch of scattered pages.</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a class="btn" href="lists.html">Open The List</a>
|
||||
<a class="btn" href="signup.html">Sign up</a>
|
||||
<a class="btn ghost-btn" href="guess.html">Play Guess Game</a>
|
||||
<a class="btn ghost-btn" href="run.html">Submit A Run</a>
|
||||
<a class="btn ghost-btn" href="rules.html">Submission Rules</a>
|
||||
<a class="btn ghost-btn" href="roulette.html">Spin Roulette</a>
|
||||
</div>
|
||||
|
||||
<div class="hero-stats" aria-label="FEDL overview">
|
||||
<article class="stat-card">
|
||||
<strong id="hero-total-levels">--</strong>
|
||||
<span>Ranked Levels</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-top-entry">--</strong>
|
||||
<span>#1 Demon</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-last-slot">--</strong>
|
||||
<span>Verified Runs</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-total-players">--</strong>
|
||||
<span>Top Player</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-recent-runs">--</strong>
|
||||
<span>Levels Added This Week</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-list-updated">--</strong>
|
||||
<span>Last Updated</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-stage panel">
|
||||
<p class="stage-label">Live Snapshot</p>
|
||||
<h3>Top 10 levels</h3>
|
||||
<p class="muted">A fast preview of what is waiting inside the list page right now.</p>
|
||||
<div class="featured-list" id="featured-list">
|
||||
<article class="featured-card skeleton-card">
|
||||
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
|
||||
<strong class="skeleton skeleton-title"></strong>
|
||||
<p class="skeleton skeleton-text"></p>
|
||||
</article>
|
||||
<article class="featured-card skeleton-card">
|
||||
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
|
||||
<strong class="skeleton skeleton-title"></strong>
|
||||
<p class="skeleton skeleton-text"></p>
|
||||
</article>
|
||||
<article class="featured-card skeleton-card">
|
||||
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
|
||||
<strong class="skeleton skeleton-title"></strong>
|
||||
<p class="skeleton skeleton-text"></p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
<section class="layout">
|
||||
<div class="panel" style="flex:1">
|
||||
<h3>Quick Links</h3>
|
||||
<a class="btn" href="lists.html">View Lists</a>
|
||||
<a class="btn" href="manage.html" style="margin-left:8px">Manage</a>
|
||||
<a class="btn" href="players.html" style="margin-left:8px">Players</a>
|
||||
<a class="btn" href="rules.html" style="margin-left:8px">Rules</a>
|
||||
<a class="btn" href="roulette.html" style="margin-left:8px">Roulette</a>
|
||||
<a class="btn" href="errors.html" style="margin-left:8px">Errors</a>
|
||||
|
||||
<section class="home-section-grid">
|
||||
<article class="panel info-panel">
|
||||
<p class="status-label">Browse Faster</p>
|
||||
<h3>The list page is built for quick checking</h3>
|
||||
<p class="muted">Search by title, jump between rank ranges, and open completion videos without leaving the site.</p>
|
||||
<a class="text-link" href="lists.html">Go to lists</a>
|
||||
</article>
|
||||
|
||||
<article class="panel info-panel">
|
||||
<p class="status-label">Stay Legit</p>
|
||||
<h3>The rules page explains what submissions need</h3>
|
||||
<p class="muted">Use it to review raw footage requirements, mod rules, skip policies, and approval expectations before you submit.</p>
|
||||
<a class="text-link" href="rules.html">Read the rules</a>
|
||||
</article>
|
||||
|
||||
<article class="panel info-panel">
|
||||
<p class="status-label">Send Proof</p>
|
||||
<h3>The run page sends completions into the live queue</h3>
|
||||
<p class="muted">Drop in your video, add raw footage and notes, and let the admin panel pick it up for review.</p>
|
||||
<a class="text-link" href="run.html">Open submissions</a>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel spotlight-panel">
|
||||
<div class="spotlight-copy">
|
||||
<p class="status-label">Start Here</p>
|
||||
<h2>What you can do on FEDL</h2>
|
||||
<p class="spotlight-text">This site is meant to be simple: find ranked demons, watch linked runs, check moderation expectations, and get back to playing without unnecessary clutter.</p>
|
||||
</div>
|
||||
|
||||
<div class="spotlight-grid">
|
||||
<article class="spotlight-card">
|
||||
<h3>Lists</h3>
|
||||
<p>Browse the ranking, filter by placement ranges, and watch linked videos from each entry.</p>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Rules</h3>
|
||||
<p>Check completion requirements, accepted tools, and what moderators expect from a valid run.</p>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Guess Game</h3>
|
||||
<p>See a random level name, pick a mode, and guess its rank with higher or lower hints.</p>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Roulette</h3>
|
||||
<p>Pull a random demon with rank and level ID details for your next roulette attempt.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel discord-panel download-panel">
|
||||
<div class="discord-copy">
|
||||
<div class="discord-badge">
|
||||
<span class="discord-logo" aria-hidden="true">📱</span>
|
||||
<div>
|
||||
<p class="discord-label">Download</p>
|
||||
<h2>Geode Mod (Coming Soon)</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="discord-text">Geode Mod is a dedicated FEDL companion mod for Geometry Dash demon players with quick list access, roulette tools, queue tracking, and built-in validation help.</p>
|
||||
<ul class="feature-bullet-list">
|
||||
<li>Fast access to the full ranked demon list with real-time updates</li>
|
||||
<li>Roulette generator with saved pick history</li>
|
||||
<li>Submission queue tracking with status markers and moderator notes</li>
|
||||
<li>Integrated rules checklist and run validation tips</li>
|
||||
</ul>
|
||||
<p class="discord-text discord-footnote">Launching soon for Geode, mobile, and desktop.</p>
|
||||
</div>
|
||||
<div class="discord-actions">
|
||||
<button class="btn discord-btn" disabled>Coming soon</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel discord-panel">
|
||||
<div class="discord-copy">
|
||||
<div class="discord-badge">
|
||||
<span class="discord-logo" aria-hidden="true">📱</span>
|
||||
<div>
|
||||
<p class="discord-label">Update</p>
|
||||
<h2>App Coming Soon</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="discord-text">A dedicated FEDL app is on the way. Stay tuned for updates.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -38,7 +185,7 @@
|
||||
<div class="discord-copy">
|
||||
<div class="discord-badge">
|
||||
<span class="discord-logo" aria-hidden="true">
|
||||
<img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d7f4ef6498ac018f2c55_Symbol.svg" alt="Discord logo" />
|
||||
<img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d7f4ef6498ac018f2c55_Symbol.svg" alt="Discord logo" loading="lazy" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="discord-label">Community</p>
|
||||
@@ -46,7 +193,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="discord-text">Join the FEDL Discord to ask questions about rules, talk about levels, share progress, report issues, and get help from moderators and other players.</p>
|
||||
<p class="discord-text">Join the FEDL Discord to ask about rules, talk about placements, share progress, report problems, and get help from moderators and other players.</p>
|
||||
|
||||
<div class="discord-highlights">
|
||||
<span>Rule help</span>
|
||||
@@ -56,10 +203,11 @@
|
||||
</div>
|
||||
|
||||
<div class="discord-actions">
|
||||
<a class="btn discord-btn" href="https://discord.gg/vA8GcdCG" target="_blank" rel="noopener noreferrer">Join Now</a>
|
||||
<a class="btn discord-btn" href="https://discord.gg/7yUjEVF7uP" target="_blank" rel="noopener noreferrer">Join Now</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
<script src="app.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+61
-21
@@ -1,47 +1,87 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Lists — GD fedl</title>
|
||||
<meta name="description" content="Browse the complete FEDL ranked demon list. Search and filter demons by rank, difficulty, and placement. Find your next challenge demon today.">
|
||||
<meta name="keywords" content="demon list, ranked demons, demon ranking, geometry dash demons, hardest demons">
|
||||
<title>Complete Ranked Demon List - FEDL Rankings</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="lists">
|
||||
<header>
|
||||
<h1>fedl</h1>
|
||||
<h1>FEDL Ranked Demon List</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="manage.html">Manage</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="panel">
|
||||
<h2>Levels</h2>
|
||||
<div class="panel list-sidebar-panel">
|
||||
<p class="sidebar-kicker">Quick Jump</p>
|
||||
<h2>Rank ranges</h2>
|
||||
<p class="muted">Jump straight to a section of the list and narrow things down fast.</p>
|
||||
<ul id="levels"></ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="panel content">
|
||||
<div class="search-row">
|
||||
<h2 id="list-title">All Videos</h2>
|
||||
<input id="search" type="text" placeholder="Search videos or titles..." />
|
||||
<select id="level-filter"><option value="all">All Levels</option></select>
|
||||
</div>
|
||||
<section class="content list-page-shell">
|
||||
<section class="panel list-hero-panel">
|
||||
<div class="list-hero-copy">
|
||||
<p class="hero-kicker">FEDL Rankings</p>
|
||||
<h2 id="list-title">FEDL List</h2>
|
||||
<p class="muted list-hero-text">Browse the full ranking, search for a level by name, filter by placement range, and open linked videos directly from the table.</p>
|
||||
</div>
|
||||
<div class="list-hero-badge">
|
||||
<strong>Live list</strong>
|
||||
<span>Updates pull from the current FEDL data source when available.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table">
|
||||
<thead>
|
||||
<tr><th style="width:80px">#</th><th>Title</th><th style="width:140px">Action</th></tr>
|
||||
</thead>
|
||||
<tbody id="list-area"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<section class="panel list-controls-panel">
|
||||
<div class="search-row">
|
||||
<label class="control-block" for="search">
|
||||
<span>Search</span>
|
||||
<input id="search" type="text" placeholder="Search titles..." />
|
||||
</label>
|
||||
<label class="control-block control-select" for="level-filter">
|
||||
<span>Filter</span>
|
||||
<select id="level-filter"><option value="all">All Levels</option></select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel list-table-panel">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<p class="status-label">List Table</p>
|
||||
<h3>Current placements</h3>
|
||||
</div>
|
||||
<p class="muted table-hint">Click <strong>Open</strong> to watch the video. With a profile from Roulette, use <strong>My %</strong> to track progress per level (this device only).</p>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table">
|
||||
<thead>
|
||||
<tr><th style="width:80px">#</th><th>Title</th><th style="width:100px">My %</th><th style="width:140px">Action</th></tr>
|
||||
</thead>
|
||||
<tbody id="list-area"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Log in — GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="login">
|
||||
<header>
|
||||
<h1>Log in</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="auth-main">
|
||||
<section class="panel auth-panel">
|
||||
<p class="hero-kicker">FEDL account</p>
|
||||
<h2>Welcome back</h2>
|
||||
<p class="muted">Sign in to load your saved roulette pick and per-level progress from the server.</p>
|
||||
<form id="login-form" class="auth-form">
|
||||
<label class="admin-password-field" for="login-username">
|
||||
<span>Username</span>
|
||||
<input id="login-username" name="username" type="text" autocomplete="username" required />
|
||||
</label>
|
||||
<label class="admin-password-field" for="login-password">
|
||||
<span>Password</span>
|
||||
<input id="login-password" name="password" type="password" autocomplete="current-password" required />
|
||||
</label>
|
||||
<div class="cf-turnstile" data-sitekey="0x4AAAAAAC_RrrNCJVX_wbHb"></div>
|
||||
<p id="login-status" class="muted" role="status"></p>
|
||||
<div class="auth-actions">
|
||||
<button type="submit" class="btn" id="login-submit">Log in</button>
|
||||
<a class="text-link" href="signup.html">Need an account? Sign up</a>
|
||||
<a class="text-link" href="reset-password.html">Forgot password?</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Manage List - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body data-page="manage">
|
||||
<header>
|
||||
<h1>Manage FEDL</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="panel manage-hero">
|
||||
<p class="rules-kicker">Node Manager</p>
|
||||
<h2>Edit the list without touching raw text files</h2>
|
||||
<p class="muted small">This page saves through the Node server API and keeps both <code>data.json</code> and <code>data.txt</code> in sync.</p>
|
||||
</section>
|
||||
|
||||
<section class="panel manage-grid">
|
||||
<div>
|
||||
<h2 id="manage-form-title">Add Entry</h2>
|
||||
<form id="level-form" class="manage-form">
|
||||
<input type="hidden" id="level-id" />
|
||||
<label>
|
||||
Category
|
||||
<input id="level-category" type="text" value="new" placeholder="new" />
|
||||
</label>
|
||||
<label>
|
||||
Position
|
||||
<input id="level-position" type="number" min="1" placeholder="1" required />
|
||||
</label>
|
||||
<label>
|
||||
Title
|
||||
<input id="level-title" type="text" placeholder="Level title" required />
|
||||
</label>
|
||||
<label>
|
||||
Video URL
|
||||
<input id="level-url" type="url" placeholder="https://youtube.com/watch?v=example" />
|
||||
</label>
|
||||
<div class="manage-actions">
|
||||
<button class="btn" type="submit" id="level-submit">Save Entry</button>
|
||||
<button class="btn ghost-btn" type="button" id="level-reset">Clear</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Quick Notes</h2>
|
||||
<ul class="rules-list">
|
||||
<li>Entries are stored by the server in JSON for easier editing and API access.</li>
|
||||
<li>The legacy text format is still rewritten automatically for compatibility.</li>
|
||||
<li>Sorting on the public list still follows the numeric position field.</li>
|
||||
</ul>
|
||||
<p class="manage-status muted" id="manage-status">Loading current entries from the server.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="search-row manage-search-row">
|
||||
<h2>Current Entries</h2>
|
||||
<input id="manage-search" type="text" placeholder="Search by title or category..." />
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:100px">#</th>
|
||||
<th style="width:140px">Category</th>
|
||||
<th>Title</th>
|
||||
<th style="width:220px">Video</th>
|
||||
<th style="width:180px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Messages — GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
<style>
|
||||
.dm-container{display:flex;height:calc(100vh - 120px);gap:20px;margin-top:20px}
|
||||
.dm-sidebar{width:300px;flex-shrink:0;background:var(--panel);border-radius:12px;padding:16px;overflow-y:auto}
|
||||
.dm-sidebar h3{margin:0 0 16px 0;font-size:1rem;color:var(--muted)}
|
||||
.dm-conversation{cursor:pointer;padding:12px;border-radius:8px;margin-bottom:8px;transition:background .2s}
|
||||
.dm-conversation:hover{background:rgba(255,255,255,0.05)}
|
||||
.dm-conversation.active{background:rgba(0,170,255,0.15)}
|
||||
.dm-conversation-name{font-weight:600;color:var(--accent-warm)}
|
||||
.dm-conversation-preview{font-size:0.85rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.dm-conversation-time{font-size:0.75rem;color:var(--muted);margin-top:4px}
|
||||
.dm-conversation.unread .dm-conversation-name{color:var(--accent)}
|
||||
.dm-main{flex:1;background:var(--panel);border-radius:12px;display:flex;flex-direction:column}
|
||||
.dm-header{padding:16px;border-bottom:1px solid rgba(255,255,255,0.1);display:flex;justify-content:space-between;align-items:center}
|
||||
.dm-header h3{margin:0}
|
||||
.dm-messages{flex:1;padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:12px}
|
||||
.dm-message{padding:12px 16px;border-radius:12px;max-width:70%;word-wrap:break-word}
|
||||
.dm-message.sent{background:var(--accent);color:var(--bg);align-self:flex-end;border-bottom-right-radius:4px}
|
||||
.dm-message.received{background:rgba(255,255,255,0.1);align-self:flex-start;border-bottom-left-radius:4px}
|
||||
.dm-message.system{background:rgba(255,184,77,0.12);border:1px solid rgba(255,184,77,0.25);align-self:stretch;max-width:90%}
|
||||
.dm-message.system .dm-message-content{white-space:pre-wrap}
|
||||
.dm-message-time{font-size:0.7rem;opacity:0.7;margin-top:4px}
|
||||
.dm-input-row{display:flex;gap:12px;padding:16px;border-top:1px solid rgba(255,255,255,0.1)}
|
||||
.dm-input-row input{flex:1;padding:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);color:var(--text);font-size:1rem}
|
||||
.dm-input-row input:focus{outline:none;border-color:var(--accent)}
|
||||
.dm-compose{background:var(--panel);padding:20px;border-radius:12px;margin-bottom:20px}
|
||||
.dm-compose h3{margin:0 0 16px 0}
|
||||
.dm-compose input,.dm-compose input:focus{width:100%;padding:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);color:var(--text);font-size:1rem;margin-bottom:12px;box-sizing:border-box}
|
||||
.dm-compose .btn{width:100%}
|
||||
.dm-user-search{position:relative}
|
||||
.dm-user-dropdown{position:absolute;top:100%;left:0;right:0;background:var(--panel);border:1px solid rgba(255,255,255,0.1);border-radius:8px;max-height:200px;overflow-y:auto;z-index:100}
|
||||
.dm-user-dropdown div{padding:10px 12px;cursor:pointer}
|
||||
.dm-user-dropdown div:hover{background:rgba(255,255,255,0.05)}
|
||||
.empty-state{text-align:center;padding:40px;color:var(--muted)}
|
||||
.no-conversation-selected{flex:1;display:flex;align-items:center;justify-content:center;color:var(--muted)}
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="messages">
|
||||
<header>
|
||||
<h1>Messages</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
<span class="fedl-auth-nav"></span>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<div class="dm-compose" id="dmCompose">
|
||||
<h3>New Message</h3>
|
||||
<div class="dm-user-search">
|
||||
<input type="text" id="userSearch" placeholder="Search for a user..." autocomplete="off">
|
||||
<div class="dm-user-dropdown" id="userDropdown" hidden></div>
|
||||
</div>
|
||||
<input type="text" id="selectedUser" placeholder="Selected user" readonly>
|
||||
<input type="text" id="dmContent" placeholder="Type your message...">
|
||||
<button type="button" class="btn" id="sendDmBtn">Send Message</button>
|
||||
</div>
|
||||
<div class="dm-container">
|
||||
<div class="dm-sidebar">
|
||||
<h3>Conversations</h3>
|
||||
<div id="conversationsList"></div>
|
||||
</div>
|
||||
<div class="dm-main">
|
||||
<div class="dm-header">
|
||||
<h3 id="chatWithName">Select a conversation</h3>
|
||||
</div>
|
||||
<div class="dm-messages" id="messagesList">
|
||||
<div class="no-conversation-selected">Select a conversation from the sidebar</div>
|
||||
</div>
|
||||
<div class="dm-input-row" id="messageInputRow" hidden>
|
||||
<input type="text" id="messageInput" placeholder="Type a message...">
|
||||
<button type="button" class="btn" id="sendMessageBtn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
<script>
|
||||
const TESTING_MODE = false;
|
||||
const BASE = TESTING_MODE ? 'http://127.0.0.1:8090/fedl' : 'https://raspberrypi-1.tail46eacb.ts.net/fedl';
|
||||
const AUTH_TOKEN_KEY = 'fedl_auth_token';
|
||||
let currentUser = null;
|
||||
let currentUserId = null;
|
||||
let conversations = [];
|
||||
let selectedConversation = null;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(AUTH_TOKEN_KEY) || '';
|
||||
}
|
||||
|
||||
function setToken(t) {
|
||||
if (t) localStorage.setItem(AUTH_TOKEN_KEY, t);
|
||||
else localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
function api(path, options = {}) {
|
||||
const opts = Object.assign({ headers: { 'Content-Type': 'application/json' } }, options);
|
||||
const tok = getToken();
|
||||
if (tok) opts.headers['Authorization'] = `Bearer ${tok}`;
|
||||
if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
|
||||
return fetch(BASE + path, opts).then(r => r.json().then(j => ({ ok: r.ok, status: r.status, data: j })));
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function formatDate(ts) {
|
||||
const d = new Date(ts);
|
||||
const diff = Date.now() - d;
|
||||
if (diff < 60000) return 'just now';
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
||||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function checkAuth() {
|
||||
const tok = getToken();
|
||||
if (!tok) { window.location.href = 'login.html?return=' + encodeURIComponent(window.location.href); return Promise.resolve(null); }
|
||||
return api('/api/auth/me').then(res => {
|
||||
if (res.ok && res.data && res.data.userId) {
|
||||
currentUser = res.data.username;
|
||||
currentUserId = res.data.userId;
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
}).catch(() => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function updateUi() {
|
||||
const nav = document.querySelector('.fedl-auth-nav');
|
||||
if (nav) {
|
||||
nav.innerHTML = `<span class="muted">Hi, <strong>${escapeHtml(currentUser)}</strong></span> <a href="login.html" class="btn ghost-btn small-btn" onclick="setToken('');">Log out</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadConversations() {
|
||||
api('/api/messages').then(res => {
|
||||
if (res.ok && res.data && res.data.conversations) {
|
||||
conversations = res.data.conversations;
|
||||
renderConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderConversations() {
|
||||
const list = document.getElementById('conversationsList');
|
||||
|
||||
const hasSystemMessages = conversations.some(c => c.userId === 'system');
|
||||
const userConversations = conversations.filter(c => c.userId !== 'system');
|
||||
|
||||
let html = '';
|
||||
|
||||
if (hasSystemMessages) {
|
||||
html += `
|
||||
<div class="dm-conversation system ${selectedConversation === 'system' ? 'active' : ''}" data-user="system" style="border-left: 3px solid var(--accent-warm);">
|
||||
<div class="dm-conversation-name" style="color: var(--accent-warm);">System Notifications</div>
|
||||
<div class="dm-conversation-preview">Password resets and alerts</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!userConversations.length && !hasSystemMessages) {
|
||||
list.innerHTML = '<div class="empty-state">No conversations yet</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
html += userConversations.map(c => `
|
||||
<div class="dm-conversation ${c.unreadCount > 0 ? 'unread' : ''} ${selectedConversation === c.userId ? 'active' : ''}" data-user="${escapeHtml(c.userId)}">
|
||||
<div class="dm-conversation-name">${escapeHtml(c.username)}</div>
|
||||
<div class="dm-conversation-preview">${escapeHtml(c.lastMessage.content)}</div>
|
||||
<div class="dm-conversation-time">${formatDate(c.lastMessage.timestamp)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
list.innerHTML = html;
|
||||
list.querySelectorAll('.dm-conversation').forEach(el => {
|
||||
el.addEventListener('click', () => selectConversation(el.dataset.user));
|
||||
});
|
||||
}
|
||||
|
||||
function selectConversation(userId) {
|
||||
selectedConversation = userId;
|
||||
if (userId === 'system') {
|
||||
document.getElementById('chatWithName').textContent = 'System Notifications';
|
||||
document.getElementById('messageInputRow').hidden = true;
|
||||
loadSystemMessages();
|
||||
} else {
|
||||
const conv = conversations.find(c => c.userId === userId);
|
||||
document.getElementById('chatWithName').textContent = conv ? conv.username : 'Chat';
|
||||
document.getElementById('messageInputRow').hidden = false;
|
||||
loadMessages(userId);
|
||||
}
|
||||
renderConversations();
|
||||
}
|
||||
|
||||
function loadSystemMessages() {
|
||||
api('/api/messages/system').then(res => {
|
||||
if (res.ok && res.data && res.data.messages) {
|
||||
renderMessages(res.data.messages, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadMessages(userId) {
|
||||
api('/api/messages/' + userId).then(res => {
|
||||
if (res.ok && res.data && res.data.messages) {
|
||||
renderMessages(res.data.messages);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderMessages(messages, isSystem = false) {
|
||||
const list = document.getElementById('messagesList');
|
||||
if (!messages.length) {
|
||||
list.innerHTML = '<div class="empty-state">No messages yet.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = messages.map(m => {
|
||||
const isCode = m.type === 'password_reset';
|
||||
const content = isCode
|
||||
? `<strong style="color: var(--accent); font-family: monospace; font-size: 1.1em; display: block; margin: 8px 0; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 8px; word-break: break-all;">${escapeHtml(m.content)}</strong>`
|
||||
: escapeHtml(m.content);
|
||||
return `
|
||||
<div class="dm-message ${isSystem ? 'system' : (m.fromUserId === currentUserId ? 'sent' : 'received')}" style="${isSystem ? 'background: rgba(255,184,77,0.15); border: 1px solid rgba(255,184,77,0.3); max-width: 85%;' : ''}">
|
||||
<div class="dm-message-content">${content}</div>
|
||||
<div class="dm-message-time">${formatDate(m.timestamp)}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
list.scrollTop = list.scrollHeight;
|
||||
}
|
||||
|
||||
const userSearchInput = document.getElementById('userSearch');
|
||||
const userDropdown = document.getElementById('userDropdown');
|
||||
const selectedUserInput = document.getElementById('selectedUser');
|
||||
|
||||
userSearchInput.addEventListener('input', async () => {
|
||||
const query = userSearchInput.value.trim();
|
||||
if (query.length < 2) {
|
||||
userDropdown.hidden = true;
|
||||
return;
|
||||
}
|
||||
const res = await api('/api/users/search', { method: 'POST', body: { query } });
|
||||
if (res.ok && res.data && res.data.results) {
|
||||
userDropdown.innerHTML = res.data.results.map(u =>
|
||||
`<div data-user-id="${u.userId}" data-username="${u.username}">${escapeHtml(u.username)}</div>`
|
||||
).join('');
|
||||
userDropdown.hidden = false;
|
||||
userDropdown.querySelectorAll('div').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
selectedUserInput.value = el.dataset.userId;
|
||||
selectedUserInput.dataset.username = el.dataset.username;
|
||||
userSearchInput.value = el.dataset.username;
|
||||
userDropdown.hidden = true;
|
||||
});
|
||||
});
|
||||
} else if (res.data && res.data.error) {
|
||||
console.error('User search error:', res.data.error);
|
||||
userDropdown.innerHTML = '<div style="padding:10px;color:var(--muted)">Error: ' + res.data.error + '</div>';
|
||||
userDropdown.hidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
userSearchInput.addEventListener('blur', () => {
|
||||
setTimeout(() => userDropdown.hidden = true, 200);
|
||||
});
|
||||
|
||||
document.getElementById('sendDmBtn').addEventListener('click', async () => {
|
||||
const toUserId = selectedUserInput.value;
|
||||
const content = document.getElementById('dmContent').value.trim();
|
||||
if (!toUserId || !content) return;
|
||||
const res = await api('/api/messages', { method: 'POST', body: { toUserId, content } });
|
||||
if (res.ok) {
|
||||
document.getElementById('dmContent').value = '';
|
||||
selectedUserInput.value = '';
|
||||
userSearchInput.value = '';
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('sendMessageBtn').addEventListener('click', async () => {
|
||||
const content = document.getElementById('messageInput').value.trim();
|
||||
if (!selectedConversation || !content) return;
|
||||
const res = await api('/api/messages', { method: 'POST', body: { toUserId: selectedConversation, content } });
|
||||
if (res.ok) {
|
||||
document.getElementById('messageInput').value = '';
|
||||
loadMessages(selectedConversation);
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('messageInput').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') document.getElementById('sendMessageBtn').click();
|
||||
});
|
||||
|
||||
checkAuth().then(() => { updateUi(); loadConversations(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>FEDL Offline</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="offlineindex">
|
||||
<header>
|
||||
<h1>FEDL Offline</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="home-hero">
|
||||
<div class="home-copy panel">
|
||||
<p class="hero-kicker">Offline Mode</p>
|
||||
<h2>FEDL is using the local list right now.</h2>
|
||||
<p class="hero-text">Live features are unavailable at the moment, but you can still browse the ranked list from the local <code>data.txt</code> file and keep using the static pages.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn" href="lists.html">Browse list</a>
|
||||
<a class="btn ghost-btn" href="rules.html">Read rules</a>
|
||||
<a class="btn ghost-btn" href="roulette.html">Open roulette</a>
|
||||
</div>
|
||||
<div class="hero-stats" aria-label="FEDL offline overview">
|
||||
<article class="stat-card">
|
||||
<strong id="hero-total-levels">--</strong>
|
||||
<span>Levels from local data</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-top-entry">--</strong>
|
||||
<span>Current top entry</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-last-slot">Local</strong>
|
||||
<span>Status</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-stage panel">
|
||||
<p class="stage-label">Local Snapshot</p>
|
||||
<h3>Top 10 levels</h3>
|
||||
<p class="muted">Pulled from the bundled list file so the site still has something useful to show.</p>
|
||||
<div class="featured-list" id="featured-list">
|
||||
<article class="featured-card">
|
||||
<span class="featured-rank">#--</span>
|
||||
<strong>Loading list data...</strong>
|
||||
<p>Reading the local list file.</p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="panel list-table-panel">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<p class="status-label">Local List</p>
|
||||
<h3>Top 10 from data.txt</h3>
|
||||
</div>
|
||||
<p class="muted table-hint">This preview stays available even when live features are down.</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table">
|
||||
<thead>
|
||||
<tr><th style="width:110px">Rank</th><th>Title</th></tr>
|
||||
</thead>
|
||||
<tbody id="offline-list-area">
|
||||
<tr><td colspan="2" class="muted">Loading local list...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<p class="hero-kicker">Offline Fallback</p>
|
||||
<h2>Static pages are still available</h2>
|
||||
<p class="hero-text">You can keep browsing the list, rules, roulette, and other static pages while the live parts of the site are unavailable.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn" href="lists.html">Browse full list</a>
|
||||
<a class="btn ghost-btn" href="players.html">View players</a>
|
||||
<a class="btn ghost-btn" href="rules.html">Read rules</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content="FEDL - Browse ranked demons, check demon list rules, play demon games, and submit demon runs. Your hub for the Geometry Dash demon list.">
|
||||
<meta name="keywords" content="demon list, ranked demons, geometry dash, demon ranking, challenge demon">
|
||||
<title>FEDL - Ranked Demon List Hub for Geometry Dash</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="onepage">
|
||||
<header role="banner">
|
||||
<h1>FEDL</h1>
|
||||
<nav role="navigation" aria-label="Main navigation">
|
||||
<a href="#" data-nav="home">Home</a>
|
||||
<a href="#" data-nav="lists">Lists</a>
|
||||
<a href="#" data-nav="players">Players</a>
|
||||
<a href="#" data-nav="run">Submit Run</a>
|
||||
<a href="#" data-nav="roulette">Roulette</a>
|
||||
<a href="#" data-nav="guess">Guess Game</a>
|
||||
<a href="#" data-nav="rules">Rules</a>
|
||||
<a href="#" data-nav="messages">Messages</a>
|
||||
<a href="#" data-nav="contact">Contact</a>
|
||||
<a href="#" data-nav="about">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section id="page-home" class="spa-page">
|
||||
<section class="home-hero">
|
||||
<div class="home-copy panel">
|
||||
<p class="hero-kicker">FEDL Hub</p>
|
||||
<h2>Track ranked demons, learn submission rules, and master the challenge.</h2>
|
||||
<p class="hero-text">FEDL is your quick home base for browsing ranked demons, checking submission rules, spinning a roulette challenge, and keeping up with the list without digging through a bunch of scattered pages.</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a class="btn" href="#" data-nav="lists">Open The List</a>
|
||||
<a class="btn" href="signup.html">Sign up</a>
|
||||
<a class="btn ghost-btn" href="#" data-nav="guess">Play Guess Game</a>
|
||||
<a class="btn ghost-btn" href="#" data-nav="run">Submit A Run</a>
|
||||
<a class="btn ghost-btn" href="#" data-nav="rules">Submission Rules</a>
|
||||
<a class="btn ghost-btn" href="#" data-nav="roulette">Spin Roulette</a>
|
||||
</div>
|
||||
|
||||
<div class="hero-stats" aria-label="FEDL overview">
|
||||
<article class="stat-card">
|
||||
<strong id="hero-total-levels">--</strong>
|
||||
<span>Ranked Levels</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-top-entry">--</strong>
|
||||
<span>#1 Demon</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-last-slot">--</strong>
|
||||
<span>Verified Runs</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-total-players">--</strong>
|
||||
<span>Top Player</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-recent-runs">--</strong>
|
||||
<span>Levels Added This Week</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<strong id="hero-list-updated">--</strong>
|
||||
<span>Last Updated</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-stage panel">
|
||||
<p class="stage-label">Live Snapshot</p>
|
||||
<h3>Top 10 levels</h3>
|
||||
<p class="muted">A fast preview of what is waiting inside the list page right now.</p>
|
||||
<div class="featured-list" id="featured-list">
|
||||
<article class="featured-card skeleton-card">
|
||||
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
|
||||
<strong class="skeleton skeleton-title"></strong>
|
||||
<p class="skeleton skeleton-text"></p>
|
||||
</article>
|
||||
<article class="featured-card skeleton-card">
|
||||
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
|
||||
<strong class="skeleton skeleton-title"></strong>
|
||||
<p class="skeleton skeleton-text"></p>
|
||||
</article>
|
||||
<article class="featured-card skeleton-card">
|
||||
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
|
||||
<strong class="skeleton skeleton-title"></strong>
|
||||
<p class="skeleton skeleton-text"></p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="home-section-grid">
|
||||
<article class="panel info-panel">
|
||||
<p class="status-label">Browse Faster</p>
|
||||
<h3>The list page is built for quick checking</h3>
|
||||
<p class="muted">Search by title, jump between rank ranges, and open completion videos without leaving the site.</p>
|
||||
<a class="text-link" href="#" data-nav="lists">Go to lists</a>
|
||||
</article>
|
||||
|
||||
<article class="panel info-panel">
|
||||
<p class="status-label">Stay Legit</p>
|
||||
<h3>The rules page explains what submissions need</h3>
|
||||
<p class="muted">Use it to review raw footage requirements, mod rules, skip policies, and approval expectations before you submit.</p>
|
||||
<a class="text-link" href="#" data-nav="rules">Read the rules</a>
|
||||
</article>
|
||||
|
||||
<article class="panel info-panel">
|
||||
<p class="status-label">Send Proof</p>
|
||||
<h3>The run page sends completions into the live queue</h3>
|
||||
<p class="muted">Drop in your video, add raw footage and notes, and let the admin panel pick it up for review.</p>
|
||||
<a class="text-link" href="#" data-nav="run">Open submissions</a>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel spotlight-panel">
|
||||
<div class="spotlight-copy">
|
||||
<p class="status-label">Start Here</p>
|
||||
<h2>What you can do on FEDL</h2>
|
||||
<p class="spotlight-text">This site is meant to be simple: find ranked demons, watch linked runs, check moderation expectations, and get back to playing without unnecessary clutter.</p>
|
||||
</div>
|
||||
|
||||
<div class="spotlight-grid">
|
||||
<article class="spotlight-card">
|
||||
<h3>Lists</h3>
|
||||
<p>Browse the ranking, filter by placement ranges, and watch linked videos from each entry.</p>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Rules</h3>
|
||||
<p>Check completion requirements, accepted tools, and what moderators expect from a valid run.</p>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Guess Game</h3>
|
||||
<p>See a random level name, pick a mode, and guess its rank with higher or lower hints.</p>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Roulette</h3>
|
||||
<p>Pull a random demon with rank and level ID details for your next roulette attempt.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel discord-panel download-panel">
|
||||
<div class="discord-copy">
|
||||
<div class="discord-badge">
|
||||
<span class="discord-logo" aria-hidden="true">📱</span>
|
||||
<div>
|
||||
<p class="discord-label">Download</p>
|
||||
<h2>Geode Mod (Coming Soon)</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="discord-text">Geode Mod is a dedicated FEDL companion mod for Geometry Dash demon players with quick list access, roulette tools, queue tracking, and built-in validation help.</p>
|
||||
<ul class="feature-bullet-list">
|
||||
<li>Fast access to the full ranked demon list with real-time updates</li>
|
||||
<li>Roulette generator with saved pick history</li>
|
||||
<li>Submission queue tracking with status markers and moderator notes</li>
|
||||
<li>Integrated rules checklist and run validation tips</li>
|
||||
</ul>
|
||||
<p class="discord-text discord-footnote">Launching soon for Geode, mobile, and desktop.</p>
|
||||
</div>
|
||||
<div class="discord-actions">
|
||||
<button class="btn discord-btn" disabled>Coming soon</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel discord-panel">
|
||||
<div class="discord-copy">
|
||||
<div class="discord-badge">
|
||||
<span class="discord-logo" aria-hidden="true">📱</span>
|
||||
<div>
|
||||
<p class="discord-label">Update</p>
|
||||
<h2>App Coming Soon</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="discord-text">A dedicated FEDL app is on the way. Stay tuned for updates.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel discord-panel">
|
||||
<div class="discord-copy">
|
||||
<div class="discord-badge">
|
||||
<span class="discord-logo" aria-hidden="true">
|
||||
<img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d7f4ef6498ac018f2c55_Symbol.svg" alt="Discord logo" loading="lazy" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="discord-label">Community</p>
|
||||
<h2>Join Our Discord</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="discord-text">Join the FEDL Discord to ask about rules, talk about placements, share progress, report problems, and get help from moderators and other players.</p>
|
||||
|
||||
<div class="discord-highlights">
|
||||
<span>Rule help</span>
|
||||
<span>Level discussion</span>
|
||||
<span>Moderator support</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="discord-actions">
|
||||
<a class="btn discord-btn" href="https://discord.gg/5HyUKQrN" target="_blank" rel="noopener noreferrer">Join Now</a>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-lists" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<div class="list-controls">
|
||||
<div class="search-row">
|
||||
<input id="list-search" type="text" placeholder="Search by title..." aria-label="Search list" />
|
||||
<button id="list-add-row" type="button" class="btn ghost-btn" hidden>Add Level</button>
|
||||
<button id="list-save-btn" type="button" class="btn" hidden>Save</button>
|
||||
</div>
|
||||
<div class="list-jump">
|
||||
<button type="button" class="btn ghost-btn small-btn" data-jump="1">1-25</button>
|
||||
<button type="button" class="btn ghost-btn small-btn" data-jump="26">26-50</button>
|
||||
<button type="button" class="btn ghost-btn small-btn" data-jump="51">51-75</button>
|
||||
<button type="button" class="btn ghost-btn small-btn" data-jump="76">76-100</button>
|
||||
<button type="button" class="btn ghost-btn small-btn" data-jump="100">100+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table" id="list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:80px">Rank</th>
|
||||
<th style="width:180px">ID</th>
|
||||
<th>Title</th>
|
||||
<th style="width:200px">Video</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="list-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-players" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<div class="list-controls">
|
||||
<div class="search-row">
|
||||
<input id="player-search" type="text" placeholder="Search players..." aria-label="Search players" />
|
||||
<select id="player-filter">
|
||||
<option value="all">All players</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width:100px">Runs</th>
|
||||
<th style="width:120px">Best</th>
|
||||
<th style="width:100px">Points</th>
|
||||
<th>Top Levels</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="players-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-run" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<h2>Submit a Run</h2>
|
||||
<p class="muted">Submit your completion to be reviewed by a moderator.</p>
|
||||
<form id="run-form" class="run-form">
|
||||
<label class="admin-password-field">
|
||||
<span>Player Name</span>
|
||||
<input id="run-player-name" type="text" placeholder="Who completed this?" required />
|
||||
</label>
|
||||
<label class="admin-password-field">
|
||||
<span>Level Name</span>
|
||||
<input id="run-level-title" type="text" placeholder="Exact name from the list" required />
|
||||
</label>
|
||||
<label class="admin-password-field">
|
||||
<span>Video URL</span>
|
||||
<input id="run-video-url" type="url" placeholder="YouTube or video link" required />
|
||||
</label>
|
||||
<label class="admin-password-field">
|
||||
<span>Percent</span>
|
||||
<input id="run-percent" type="number" min="1" max="100" value="100" required />
|
||||
<span class="muted small">% (100 for 100%, 80 for 80%)</span>
|
||||
</label>
|
||||
<label class="admin-password-field">
|
||||
<span>Raw Footage URL (optional)</span>
|
||||
<input id="run-raw-url" type="url" placeholder="Link to raw gameplay if different" />
|
||||
</label>
|
||||
<label class="admin-password-field">
|
||||
<span>Notes (optional)</span>
|
||||
<textarea id="run-notes" placeholder="Any additional info for moderators"></textarea>
|
||||
</label>
|
||||
<p id="run-form-status" class="muted"></p>
|
||||
<button type="submit" class="btn">Submit Run</button>
|
||||
</form>
|
||||
<div id="run-list-section">
|
||||
<h3>Your Submissions</h3>
|
||||
<div id="run-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-roulette" class="spa-page" hidden>
|
||||
<section class="panel roulette-panel">
|
||||
<h2>Roulette</h2>
|
||||
<p class="muted">Spin the wheel for a random demon challenge.</p>
|
||||
<div id="roulette-result" class="roulette-result">
|
||||
<p class="muted">Click spin to get a random demon.</p>
|
||||
</div>
|
||||
<button id="roulette-spin" type="button" class="btn">Spin</button>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-guess" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<h2>Guess the Rank</h2>
|
||||
<p class="muted">A random level name will appear. Guess if it's ranked higher or lower!</p>
|
||||
<div id="guess-game">
|
||||
<p id="guess-level" class="guess-level">???</p>
|
||||
<div class="guess-buttons">
|
||||
<button id="guess-higher" class="btn">Higher ▲</button>
|
||||
<button id="guess-lower" class="btn">Lower ▼</button>
|
||||
</div>
|
||||
<p id="guess-result" class="muted"></p>
|
||||
<p id="guess-score">Score: 0</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-rules" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<h2>Submission Rules</h2>
|
||||
<div class="rules-content">
|
||||
<h3>Requirements</h3>
|
||||
<ul>
|
||||
<li>Video must show full completion from start to finish</li>
|
||||
<li>Raw footage must be available upon request</li>
|
||||
<li>No cheats, hacks, or glitches allowed</li>
|
||||
<li>Level must be on the ranked list</li>
|
||||
</ul>
|
||||
<h3>Verification Process</h3>
|
||||
<ol>
|
||||
<li>Submit your run with video link</li>
|
||||
<li>Wait for moderator review</li>
|
||||
<li>Provide raw footage if requested</li>
|
||||
<li>Run gets approved or rejected with notes</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-messages" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<h2>Messages</h2>
|
||||
<p class="muted">Password resets and system messages appear here.</p>
|
||||
<div id="messages-list">
|
||||
<p class="muted">No messages.</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-contact" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<h2>Contact</h2>
|
||||
<form id="contact-form">
|
||||
<label class="admin-password-field">
|
||||
<span>Subject</span>
|
||||
<input id="contact-subject" type="text" required />
|
||||
</label>
|
||||
<label class="admin-password-field">
|
||||
<span>Message</span>
|
||||
<textarea id="contact-message" required></textarea>
|
||||
</label>
|
||||
<button type="submit" class="btn">Send</button>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="page-about" class="spa-page" hidden>
|
||||
<section class="panel">
|
||||
<h2>About FEDL</h2>
|
||||
<p>FEDL is a Geometry Dash demon list hub. Browse ranked demons, submit runs, and track your progress.</p>
|
||||
<p class="muted">Version 1.0</p>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<script src="onepage.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
// One Page App - FEDL
|
||||
(function(){
|
||||
function qs(id){return document.getElementById(id)}
|
||||
function escapeHtml(str){return String(str||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||||
function escapeAttr(str){return escapeHtml(str).replace(/'/g,''');}
|
||||
|
||||
const page = document.body.dataset.page;
|
||||
const isFileProtocol = window.location.protocol === 'file:';
|
||||
const TESTING_MODE = false;
|
||||
const liveServerBase = TESTING_MODE ? 'http://127.0.0.1:8090/fedl' : 'https://server.fedl.site/fedl';
|
||||
const canUseLiveServer = !isFileProtocol || !!liveServerBase;
|
||||
const liveApiUrl = `${liveServerBase}/api/list`;
|
||||
const liveRunsUrl = `${liveServerBase}/api/runs`;
|
||||
const liveDataFileUrl = `${liveServerBase}/server/data.txt`;
|
||||
|
||||
function liveApiPath(path){
|
||||
const p = String(path || '').startsWith('/') ? path : `/${path}`;
|
||||
return `${liveServerBase}${p}`;
|
||||
}
|
||||
|
||||
let cachedItems = null;
|
||||
let cachedRuns = null;
|
||||
|
||||
function loadItems(){
|
||||
if(cachedItems) return Promise.resolve(cachedItems);
|
||||
return fetch(liveApiUrl, {cache:'no-store'}).then(r=>r.text()).then(txt=>{
|
||||
cachedItems = txt.split(/\r?\n/).map(l=>l.trim()).filter(Boolean).map(l=>{
|
||||
const parts = l.split('|').map(p=>p.trim());
|
||||
return {level:parts[0]||'',position:parts[1]||'',title:parts[2]||'',url:parts[3]||''};
|
||||
});
|
||||
return cachedItems;
|
||||
}).catch(()=>[]);
|
||||
}
|
||||
|
||||
function loadRuns(){
|
||||
if(cachedRuns) return Promise.resolve(cachedRuns);
|
||||
return fetch(liveRunsUrl, {cache:'no-store'}).then(r=>r.json()).then(data=>{
|
||||
cachedRuns = Array.isArray(data.items) ? data.items : [];
|
||||
return cachedRuns;
|
||||
}).catch(()=>[]);
|
||||
}
|
||||
|
||||
function calculatePoints(rank){
|
||||
if(rank<1||rank>1000) return 0;
|
||||
if(rank===1) return 100;
|
||||
if(rank<=10) return 90-rank+1;
|
||||
if(rank<=25) return 80-rank+1;
|
||||
if(rank<=50) return 60-rank+1;
|
||||
if(rank<=100) return 40-rank+1;
|
||||
if(rank<=200) return 20-rank+1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const spaPage = window.location.hash.slice(1) || 'home';
|
||||
const pages = document.querySelectorAll('.spa-page');
|
||||
|
||||
function showPage(pageName){
|
||||
pages.forEach(p => p.hidden = true);
|
||||
const pageEl = document.getElementById('page-' + pageName);
|
||||
if(pageEl) pageEl.hidden = false;
|
||||
history.replaceState(null, '', '#' + pageName);
|
||||
loadPageContent(pageName);
|
||||
}
|
||||
|
||||
function loadPageContent(pageName){
|
||||
if(pageName === 'lists' || pageName === 'home'){
|
||||
const listBody = qs('list-body');
|
||||
if(listBody && !listBody.dataset.loaded){
|
||||
listBody.dataset.loaded = 'true';
|
||||
loadItems().then(items=>{
|
||||
const ranked = items.slice().sort((a,b)=>(Number(a.position)||0)-(Number(b.position)||0));
|
||||
renderListTable(ranked);
|
||||
if(qs('hero-total-levels')){
|
||||
qs('hero-total-levels').textContent = String(ranked.length);
|
||||
qs('hero-top-entry').textContent = ranked[0]?.title || 'Unavailable';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if(pageName === 'players'){
|
||||
const playersBody = qs('players-body');
|
||||
const searchEl = qs('player-search');
|
||||
if(playersBody && searchEl && !playersBody.dataset.loaded){
|
||||
playersBody.dataset.loaded = 'true';
|
||||
Promise.all([loadRuns(), loadItems()]).then(([runs, items])=>{
|
||||
const lookup = new Map(items.map(item => [String(item.title || '').toLowerCase(), Number(item.position) || 9999]));
|
||||
const map = new Map();
|
||||
runs.filter(run => String(run.status || '').toLowerCase() === 'approved').forEach(run => {
|
||||
const playerName = String(run.playerName || '').trim();
|
||||
if(!playerName) return;
|
||||
const key = playerName.toLowerCase();
|
||||
let entry = map.get(key);
|
||||
if(!entry){
|
||||
entry = {name: playerName, runs: 0, bestRank: 9999, points: 0, topLevels: new Set()};
|
||||
map.set(key, entry);
|
||||
}
|
||||
entry.runs += 1;
|
||||
const rank = lookup.get(String(run.levelTitle || '').toLowerCase()) || 9999;
|
||||
if(rank > 0 && rank < entry.bestRank) entry.bestRank = rank;
|
||||
if(rank > 0 && rank < 1000) entry.points += calculatePoints(rank);
|
||||
if(run.levelTitle) entry.topLevels.add(String(run.levelTitle).trim());
|
||||
});
|
||||
const players = Array.from(map.values()).map(entry => ({
|
||||
name: entry.name,
|
||||
runs: entry.runs,
|
||||
bestRank: entry.bestRank === 9999 ? '—' : `#${entry.bestRank}`,
|
||||
points: entry.points,
|
||||
topLevels: Array.from(entry.topLevels).slice(0, 3).join(', ')
|
||||
})).sort((a,b) => b.points - a.points || a.name.localeCompare(b.name));
|
||||
|
||||
function render(){
|
||||
const query = (searchEl.value || '').toLowerCase();
|
||||
const filtered = players.filter(p => !query || p.name.toLowerCase().includes(query));
|
||||
if(!filtered.length){
|
||||
playersBody.innerHTML = '<tr><td colspan="5" class="muted">No player data found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
playersBody.innerHTML = filtered.slice(0, 100).map(p => `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(p.name)}</strong></td>
|
||||
<td>${p.runs}</td>
|
||||
<td>${p.bestRank}</td>
|
||||
<td>${p.points}</td>
|
||||
<td>${escapeHtml(p.topLevels)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
searchEl.addEventListener('input', render);
|
||||
render();
|
||||
});
|
||||
}
|
||||
}
|
||||
if(pageName === 'run'){
|
||||
const runForm = qs('run-form');
|
||||
if(runForm && !runForm.dataset.loaded){
|
||||
runForm.dataset.loaded = 'true';
|
||||
const formStatusEl = qs('run-form-status');
|
||||
runForm.addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
const playerName = qs('run-player-name').value.trim();
|
||||
const levelTitle = qs('run-level-title').value.trim();
|
||||
const videoUrl = qs('run-video-url').value.trim();
|
||||
const percent = qs('run-percent').value.trim();
|
||||
const rawUrl = qs('run-raw-url').value.trim();
|
||||
const notes = qs('run-notes').value.trim();
|
||||
if(!playerName || !levelTitle || !videoUrl || !percent){
|
||||
formStatusEl.textContent = 'Please fill in all required fields.';
|
||||
return;
|
||||
}
|
||||
formStatusEl.textContent = 'Submitting...';
|
||||
fetch(liveRunsUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ playerName, levelTitle, videoUrl, percent, rawFootageUrl: rawUrl, notes })
|
||||
}).then(r => {
|
||||
if(!r.ok) throw new Error('Failed');
|
||||
formStatusEl.textContent = 'Run submitted successfully!';
|
||||
runForm.reset();
|
||||
}).catch(() => {
|
||||
formStatusEl.textContent = 'Failed to submit run.';
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
if(pageName === 'roulette'){
|
||||
const spinBtn = qs('roulette-spin');
|
||||
const resultEl = qs('roulette-result');
|
||||
if(spinBtn && resultEl && !spinBtn.dataset.loaded){
|
||||
spinBtn.dataset.loaded = 'true';
|
||||
spinBtn.addEventListener('click', function(){
|
||||
loadItems().then(items => {
|
||||
if(!items.length){
|
||||
resultEl.innerHTML = '<p class="muted">No levels loaded.</p>';
|
||||
return;
|
||||
}
|
||||
const random = items[Math.floor(Math.random() * items.length)];
|
||||
resultEl.innerHTML = `
|
||||
<p><strong>${escapeHtml(random.title || 'Untitled')}</strong></p>
|
||||
<p class="muted">Rank #${random.position || '?'}</p>
|
||||
<p class="muted">${random.level || ''}</p>
|
||||
`;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
if(pageName === 'guess'){
|
||||
const higherBtn = qs('guess-higher');
|
||||
const lowerBtn = qs('guess-lower');
|
||||
const levelEl = qs('guess-level');
|
||||
const resultEl = qs('guess-result');
|
||||
const scoreEl = qs('guess-score');
|
||||
if(higherBtn && lowerBtn && levelEl && !higherBtn.dataset.loaded){
|
||||
higherBtn.dataset.loaded = 'true';
|
||||
let currentLevel = null;
|
||||
let score = 0;
|
||||
let revealed = false;
|
||||
function newRound(){
|
||||
loadItems().then(items => {
|
||||
if(!items.length) return;
|
||||
currentLevel = items[Math.floor(Math.random() * items.length)];
|
||||
levelEl.textContent = currentLevel.title || '???';
|
||||
resultEl.textContent = '';
|
||||
revealed = false;
|
||||
});
|
||||
}
|
||||
higherBtn.addEventListener('click', function(){
|
||||
if(revealed || !currentLevel) return;
|
||||
revealed = true;
|
||||
const rank = Number(currentLevel.position) || 9999;
|
||||
const actual = Math.random() * 100;
|
||||
if(actual > 50){
|
||||
score++;
|
||||
resultEl.textContent = `Correct! It was #${rank}`;
|
||||
}else{
|
||||
resultEl.textContent = `Wrong! It was #${rank}`;
|
||||
}
|
||||
scoreEl.textContent = `Score: ${score}`;
|
||||
setTimeout(newRound, 2000);
|
||||
});
|
||||
lowerBtn.addEventListener('click', function(){
|
||||
if(revealed || !currentLevel) return;
|
||||
revealed = true;
|
||||
const rank = Number(currentLevel.position) || 9999;
|
||||
const actual = Math.random() * 100;
|
||||
if(actual < 50){
|
||||
score++;
|
||||
resultEl.textContent = `Correct! It was #${rank}`;
|
||||
}else{
|
||||
resultEl.textContent = `Wrong! It was #${rank}`;
|
||||
}
|
||||
scoreEl.textContent = `Score: ${score}`;
|
||||
setTimeout(newRound, 2000);
|
||||
});
|
||||
newRound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderListTable(items){
|
||||
const tbody = qs('list-body');
|
||||
if(!tbody) return;
|
||||
const searchInput = qs('list-search');
|
||||
const query = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
const filtered = items.filter(item => !query || (item.title||'').toLowerCase().includes(query) || (item.level||'').toLowerCase().includes(query));
|
||||
if(!filtered.length){
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="muted">No levels found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = filtered.map(item => `
|
||||
<tr>
|
||||
<td>#${escapeHtml(item.position || '')}</td>
|
||||
<td>${escapeHtml(item.level || '')}</td>
|
||||
<td>${escapeHtml(item.title || '')}</td>
|
||||
<td>${item.url ? `<a href="${escapeAttr(item.url)}" target="_blank">Video</a>` : ''}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
document.querySelectorAll('.skeleton-card').forEach(el => el.remove());
|
||||
const featuredList = qs('featured-list');
|
||||
if(featuredList){
|
||||
const top10 = items.slice(0, 10);
|
||||
featuredList.innerHTML = top10.map(item => `
|
||||
<article class="featured-card">
|
||||
<span class="featured-rank">#${escapeHtml(item.position || '--')}</span>
|
||||
<strong>${escapeHtml(item.title || 'Untitled')}</strong>
|
||||
<p>${item.url ? 'Video link ready.' : 'No video linked.'}</p>
|
||||
</article>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
const navLinks = document.querySelectorAll('nav a[data-nav]');
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
showPage(this.dataset.nav);
|
||||
});
|
||||
});
|
||||
|
||||
if(spaPage && document.getElementById('page-' + spaPage)){
|
||||
showPage(spaPage);
|
||||
}else{
|
||||
showPage('home');
|
||||
}
|
||||
|
||||
// Search listener
|
||||
const listSearch = qs('list-search');
|
||||
if(listSearch){
|
||||
listSearch.addEventListener('input', ()=>{
|
||||
loadItems().then(items=>{
|
||||
const ranked = items.slice().sort((a,b)=>(Number(a.position)||0)-(Number(b.position)||0));
|
||||
renderListTable(ranked);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "gd-fedl",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Node-powered FEDL list manager",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
+63
-14
@@ -1,36 +1,85 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Players — GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="players">
|
||||
<header>
|
||||
<h1>Players</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="manage.html">Manage</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="panel">
|
||||
<h2>Add Player</h2>
|
||||
<form id="player-form" style="display:flex;gap:8px">
|
||||
<input id="player-name" placeholder="Player name" required />
|
||||
<button class="btn" type="submit">Add</button>
|
||||
</form>
|
||||
</section>
|
||||
<main class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="panel list-sidebar-panel">
|
||||
<p class="sidebar-kicker">Quick jump</p>
|
||||
<h2>Player groups</h2>
|
||||
<p class="muted">Jump to players by first letter.</p>
|
||||
<ul id="player-groups"></ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Players</h2>
|
||||
<ul id="players"></ul>
|
||||
<section class="content list-page-shell">
|
||||
<section class="panel list-hero-panel">
|
||||
<div class="list-hero-copy">
|
||||
<p class="hero-kicker">Player tracker</p>
|
||||
<h2 id="list-title">Local player roster</h2>
|
||||
<p class="muted list-hero-text">Manage your player list in browser storage with the same searchable table experience as the FEDL demonlist UI.</p>
|
||||
</div>
|
||||
<div class="list-hero-badge">
|
||||
<strong>Local only</strong>
|
||||
<span>Player names are stored in this browser only.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel list-controls-panel">
|
||||
<div class="search-row">
|
||||
<label class="control-block" for="search">
|
||||
<span>Search</span>
|
||||
<input id="search" type="text" placeholder="Search player names..." />
|
||||
</label>
|
||||
<label class="control-block control-select" for="group-filter">
|
||||
<span>Filter</span>
|
||||
<select id="group-filter"><option value="all">All players</option></select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel list-table-panel">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<p class="status-label">Player table</p>
|
||||
<h3>Saved players</h3>
|
||||
</div>
|
||||
<p class="muted table-hint">Use the search box or letter filter to locate players quickly.</p>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th style="width:80px">Runs</th><th style="width:80px">Points</th><th style="width:100px">Best rank</th><th style="width:180px">Top levels</th></tr>
|
||||
</thead>
|
||||
<tbody id="players-area"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Reset password — GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="reset-password">
|
||||
<header>
|
||||
<h1>Reset password</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="login.html">Log in</a>
|
||||
<a href="signup.html">Sign up</a>
|
||||
<a href="account.html">Account</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="account-main">
|
||||
<section class="panel account-panel">
|
||||
<p class="hero-kicker">FEDL account</p>
|
||||
<h2>Request a reset code</h2>
|
||||
<p class="muted">Enter your username to get a reset code sent to your messages.</p>
|
||||
<form id="reset-request-form" class="auth-form">
|
||||
<label class="admin-password-field" for="reset-identifier">
|
||||
<span>Username</span>
|
||||
<input id="reset-identifier" name="identifier" type="text" autocomplete="username" required />
|
||||
</label>
|
||||
<p id="reset-request-status" class="muted" role="status"></p>
|
||||
<div class="auth-actions">
|
||||
<button type="submit" class="btn" id="reset-request-submit">Get reset code</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="reset-section" class="panel account-panel" style="display:none;">
|
||||
<h2>Enter your reset code</h2>
|
||||
<p class="muted">Enter the reset code you received above.</p>
|
||||
<div id="reset-code-display" style="display:none;"></div>
|
||||
<form id="reset-password-form" class="auth-form">
|
||||
<label class="admin-password-field" for="reset-token">
|
||||
<span>Reset code</span>
|
||||
<input id="reset-token" name="token" type="text" autocomplete="off" required />
|
||||
</label>
|
||||
<label class="admin-password-field" for="reset-new-password">
|
||||
<span>New password</span>
|
||||
<input id="reset-new-password" name="newPassword" type="password" autocomplete="new-password" required minlength="8" />
|
||||
</label>
|
||||
<label class="admin-password-field" for="reset-confirm-password">
|
||||
<span>Confirm new password</span>
|
||||
<input id="reset-confirm-password" name="confirmPassword" type="password" autocomplete="new-password" required minlength="8" />
|
||||
</label>
|
||||
<p id="reset-password-status" class="muted" role="status"></p>
|
||||
<div class="auth-actions">
|
||||
<button type="submit" class="btn" id="reset-password-submit">Set new password</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
# Allow all pages on fedl.site
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Subdomains should have their own robots.txt files
|
||||
# to define their crawling policies
|
||||
+105
-31
@@ -1,57 +1,131 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Demon Roulette - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="roulette">
|
||||
<header>
|
||||
<h1>Demon Roulette</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="manage.html">Manage</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="panel roulette-rules">
|
||||
<p class="roulette-kicker">Rules</p>
|
||||
<h2>How Roulette Works</h2>
|
||||
<ul class="rules-list">
|
||||
<li>Extreme Demon Roulette is a community challenge built around a roulette of hard demons from a chosen list.</li>
|
||||
<li>You can use a main list, extended list, legacy list, or a custom list like this FEDL list.</li>
|
||||
<li>A common format is to start at <strong>1%</strong> and raise the goal by <strong>1%</strong> for each new level until you fail or finish the challenge.</li>
|
||||
<li>Some versions allow skips, rerolls, or custom limits, depending on the rules you want to use.</li>
|
||||
<li>This page shows the demon name, rank, and level ID for the selected result.</li>
|
||||
<li>Percent targets are stored in <code>level-ids.txt</code> using the format <code>title|levelId|percent</code>.</li>
|
||||
<li>If a level ID is missing in that file, this page will try to look it up through the Geometry Dash community API automatically.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="panel roulette-panel">
|
||||
<div class="roulette-head">
|
||||
<div>
|
||||
<p class="roulette-kicker">Random Challenge</p>
|
||||
<h2>Demon Roulette</h2>
|
||||
<p class="muted">Spin the FEDL list to get a random demon for your roulette run.</p>
|
||||
<section class="roulette-shell">
|
||||
<div class="roulette-main-stack">
|
||||
<section class="panel roulette-account-panel">
|
||||
<p class="roulette-kicker">Your profile</p>
|
||||
<h2 class="roulette-account-heading">Save roulette & progress</h2>
|
||||
<p class="muted small roulette-account-intro">Profiles are stored only in this browser. Each profile remembers your last roulette demon and a progress % per level (also shown on the list page).</p>
|
||||
<p class="muted small roulette-login-sync-hint" id="roulette-login-sync-hint" hidden>
|
||||
<a href="login.html">Log in</a> or <a href="signup.html">sign up</a> to sync roulette, save slots, and % across browsers.
|
||||
</p>
|
||||
<div class="roulette-slots-block" id="roulette-slots-block">
|
||||
<p class="roulette-slots-heading">Roulette save slots</p>
|
||||
<p class="muted small" id="roulette-slots-hint">Use three slots to park different demons. Requires an active profile or a signed-in account.</p>
|
||||
<div class="roulette-slot-rows">
|
||||
<div class="roulette-slot-row">
|
||||
<span class="roulette-slot-title">Slot 1</span>
|
||||
<span class="roulette-slot-label muted" id="roulette-slot-label-1">Empty</span>
|
||||
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-save-1">Save</button>
|
||||
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-load-1">Load</button>
|
||||
</div>
|
||||
<div class="roulette-slot-row">
|
||||
<span class="roulette-slot-title">Slot 2</span>
|
||||
<span class="roulette-slot-label muted" id="roulette-slot-label-2">Empty</span>
|
||||
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-save-2">Save</button>
|
||||
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-load-2">Load</button>
|
||||
</div>
|
||||
<div class="roulette-slot-row">
|
||||
<span class="roulette-slot-title">Slot 3</span>
|
||||
<span class="roulette-slot-label muted" id="roulette-slot-label-3">Empty</span>
|
||||
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-save-3">Save</button>
|
||||
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-load-3">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn roulette-btn" id="roulette-spin" type="button">Spin</button>
|
||||
<div class="roulette-account-controls">
|
||||
<label class="roulette-account-label" for="roulette-account-select">Active profile</label>
|
||||
<select id="roulette-account-select" class="roulette-account-select"></select>
|
||||
<div class="roulette-account-create-row">
|
||||
<input id="roulette-account-new" type="text" placeholder="New profile name" autocomplete="username" />
|
||||
<button type="button" class="btn ghost-btn" id="roulette-account-create">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn ghost-btn roulette-restore-btn" id="roulette-restore" hidden>Restore last spin</button>
|
||||
</section>
|
||||
<section class="panel roulette-panel">
|
||||
<div class="roulette-head">
|
||||
<div>
|
||||
<p class="roulette-kicker">Random Challenge</p>
|
||||
<h2>Demon Roulette</h2>
|
||||
<p class="muted roulette-intro">Spin the FEDL list to get a random demon for your next roulette run, practice grind, or bad decision.</p>
|
||||
</div>
|
||||
<button class="btn roulette-btn" id="roulette-spin" type="button">Spin</button>
|
||||
</div>
|
||||
|
||||
<div class="roulette-card" id="roulette-card">
|
||||
<p class="roulette-status muted" id="roulette-status">Press spin to choose a demon.</p>
|
||||
<h3 id="roulette-title">No demon selected</h3>
|
||||
<p class="roulette-rank" id="roulette-rank">Rank: -</p>
|
||||
<p class="roulette-meta" id="roulette-level-id">Level ID: -</p>
|
||||
<p class="roulette-note muted" id="roulette-note">Level IDs come from your local file first, then API lookup if needed.</p>
|
||||
<div class="roulette-progress-block" id="roulette-progress-row" hidden>
|
||||
<label class="roulette-progress-label" for="roulette-percent">Your % on this level</label>
|
||||
<div class="roulette-progress-input-row">
|
||||
<input id="roulette-percent" type="text" inputmode="decimal" placeholder="e.g. 47" autocomplete="off" />
|
||||
<span class="muted">%</span>
|
||||
<button type="button" class="btn small-btn" id="roulette-percent-submit">Submit %</button>
|
||||
</div>
|
||||
<p id="roulette-percent-hint" class="muted small roulette-percent-hint"></p>
|
||||
</div>
|
||||
<a class="btn roulette-open" id="roulette-open" href="#" target="_blank" rel="noopener noreferrer" hidden>View Video</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="roulette-card" id="roulette-card">
|
||||
<p class="roulette-status muted" id="roulette-status">Press spin to choose a demon.</p>
|
||||
<h3 id="roulette-title">No demon selected</h3>
|
||||
<p class="roulette-rank" id="roulette-rank">Rank: -</p>
|
||||
<p class="roulette-meta" id="roulette-level-id">Level ID: -</p>
|
||||
<p class="roulette-note muted" id="roulette-note">Level IDs come from your local file first, then API lookup if needed.</p>
|
||||
<a class="btn roulette-open" id="roulette-open" href="#" target="_blank" rel="noopener noreferrer" hidden>Open Video</a>
|
||||
</div>
|
||||
<aside class="panel roulette-rules-card">
|
||||
<p class="roulette-kicker">How It Works</p>
|
||||
<h2>Quick rules</h2>
|
||||
<div class="roulette-rule-list">
|
||||
<article>
|
||||
<strong>Pick from a list</strong>
|
||||
<p>Use FEDL or any demon list format you want for your roulette session.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Raise the target</strong>
|
||||
<p>A common setup starts at 1% and increases by 1% after each new level.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Use your own rules</strong>
|
||||
<p>Skips, rerolls, and loss conditions can be adjusted to match your challenge format.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Extra data included</strong>
|
||||
<p>This page shows rank and level ID, and it tries API lookup when local info is missing.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Want another mode?</strong>
|
||||
<p>Open the separate Guess Game page if you want to figure out a level's rank with higher or lower hints.</p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+9
-1
@@ -1,20 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Rules - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body class="rules-view">
|
||||
<script src="app.js" defer></script>
|
||||
<header>
|
||||
<h1>FEDL Rules</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="manage.html">Manage</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Submit Run - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="run">
|
||||
<header>
|
||||
<h1>Submit Run</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="run-shell">
|
||||
<section class="panel run-panel">
|
||||
<div class="run-head">
|
||||
<div>
|
||||
<p class="hero-kicker">FEDL Submission</p>
|
||||
<h2>Send in a run</h2>
|
||||
<p class="muted run-intro">Submit your completion video, attach raw footage if you have it hosted, and leave notes for the moderators. If you <a href="login.html">log in</a>, you can save multiple runs to your account (drafts) and queue submissions will show your username to moderators.</p>
|
||||
</div>
|
||||
<div class="run-head-badge">
|
||||
<strong>Live queue</strong>
|
||||
<span>Submissions land in the admin panel instantly when the Node server is running.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="run-form" class="run-form">
|
||||
<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). Starting in 2027 you will need an account to submit a run.</span>
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="run-level-title">
|
||||
<span>Level</span>
|
||||
<input id="run-level-title" name="levelTitle" type="text" list="run-level-options" placeholder="Level title" required />
|
||||
<datalist id="run-level-options"></datalist>
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="run-video-url">
|
||||
<span>Completion Video URL</span>
|
||||
<input id="run-video-url" name="videoUrl" type="url" placeholder="https://youtu.be/..." required />
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="run-percent">
|
||||
<span>Percent</span>
|
||||
<input id="run-percent" name="percent" type="number" min="1" max="100" placeholder="100" required />
|
||||
</label>
|
||||
|
||||
<label class="control-block" for="run-raw-footage-url">
|
||||
<span>Raw Footage URL</span>
|
||||
<input id="run-raw-footage-url" name="rawFootageUrl" type="url" placeholder="Optional raw footage link" />
|
||||
</label>
|
||||
|
||||
<label class="control-block run-notes-block" for="run-notes">
|
||||
<span>Notes For Mods</span>
|
||||
<textarea id="run-notes" name="notes" rows="6" placeholder="Mention handcam, custom copy info, bug fix approval, or anything the moderators should know."></textarea>
|
||||
</label>
|
||||
|
||||
<div class="run-actions">
|
||||
<button id="run-submit" type="submit" class="btn">Submit Run</button>
|
||||
<button type="button" id="run-save-account" class="btn ghost-btn" hidden>Save to my account</button>
|
||||
<p id="run-form-status" class="muted">The server needs to be running for live submissions.</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside class="panel run-side-panel">
|
||||
<p class="status-label">Checklist</p>
|
||||
<h2>Before you submit</h2>
|
||||
<div class="run-checklist">
|
||||
<article>
|
||||
<strong>Completion video</strong>
|
||||
<p>Make sure the completion link is public and actually shows the run you want reviewed.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Raw footage</strong>
|
||||
<p>Add the raw footage link here if you have it hosted somewhere moderators can access.</p>
|
||||
</article>
|
||||
<article>
|
||||
<strong>Important notes</strong>
|
||||
<p>Call out solo or 2-player variants, custom copies, LDM use, and handcam details in the notes box.</p>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="panel run-my-runs-panel" id="run-my-runs-section" hidden>
|
||||
<div class="submission-header run-my-runs-head">
|
||||
<div>
|
||||
<p class="hero-kicker">Your account</p>
|
||||
<h3>Runs saved to your account</h3>
|
||||
<p class="muted">Keep multiple drafts here while you work on links or notes. They sync when you are signed in.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="run-saved-runs-list" class="submission-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel submission-panel">
|
||||
<div class="table-header submission-header">
|
||||
<div>
|
||||
<p class="status-label">Submission Queue</p>
|
||||
<h3>Recent run submissions</h3>
|
||||
</div>
|
||||
<p id="run-list-status" class="muted">Loading recent submissions...</p>
|
||||
</div>
|
||||
<div id="run-submissions" class="submission-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,207 +0,0 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { URL } = require('url');
|
||||
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
const PORT = Number(process.env.PORT) || 8000;
|
||||
const ROOT = __dirname;
|
||||
const DATA_TXT_PATH = path.join(ROOT, 'data.txt');
|
||||
const DATA_JSON_PATH = path.join(ROOT, 'data.json');
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.txt': 'text/plain; charset=utf-8'
|
||||
};
|
||||
|
||||
function sendJson(res, statusCode, payload) {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store'
|
||||
});
|
||||
res.end(JSON.stringify(payload, null, 2));
|
||||
}
|
||||
|
||||
function sendText(res, statusCode, text, contentType = 'text/plain; charset=utf-8') {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'no-store'
|
||||
});
|
||||
res.end(text);
|
||||
}
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk;
|
||||
if (body.length > 1_000_000) {
|
||||
reject(new Error('Request body too large.'));
|
||||
req.destroy();
|
||||
}
|
||||
});
|
||||
req.on('end', () => resolve(body));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeItem(item, fallbackId) {
|
||||
return {
|
||||
id: String(item.id || fallbackId || Date.now()),
|
||||
level: String(item.level || 'new').trim() || 'new',
|
||||
position: String(item.position || '').trim(),
|
||||
title: String(item.title || '').trim(),
|
||||
url: String(item.url || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function parseTxtData(txt) {
|
||||
return txt
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line, index) => {
|
||||
const parts = line.split('|').map(part => part.trim());
|
||||
return normalizeItem({
|
||||
level: parts[0],
|
||||
position: parts[1],
|
||||
title: parts[2],
|
||||
url: parts[3]
|
||||
}, `legacy-${index + 1}`);
|
||||
});
|
||||
}
|
||||
|
||||
function serializeTxtData(items) {
|
||||
return items
|
||||
.map(item => [item.level, item.position, item.title, item.url].join('|'))
|
||||
.join('\n') + '\n';
|
||||
}
|
||||
|
||||
function loadItems() {
|
||||
if (fs.existsSync(DATA_JSON_PATH)) {
|
||||
const raw = fs.readFileSync(DATA_JSON_PATH, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.map((item, index) => normalizeItem(item, `json-${index + 1}`));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(DATA_TXT_PATH)) return [];
|
||||
const txt = fs.readFileSync(DATA_TXT_PATH, 'utf8');
|
||||
const items = parseTxtData(txt);
|
||||
saveItems(items);
|
||||
return items;
|
||||
}
|
||||
|
||||
function saveItems(items) {
|
||||
const normalized = items.map((item, index) => normalizeItem(item, `item-${index + 1}`));
|
||||
fs.writeFileSync(DATA_JSON_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
||||
fs.writeFileSync(DATA_TXT_PATH, serializeTxtData(normalized), 'utf8');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateItem(item) {
|
||||
if (!item.title) return 'Title is required.';
|
||||
if (!item.position) return 'Position is required.';
|
||||
if (!/^\d+$/.test(String(item.position))) return 'Position must be numeric.';
|
||||
return null;
|
||||
}
|
||||
|
||||
function getStaticPath(requestPath) {
|
||||
const cleanPath = requestPath === '/' ? '/index.html' : requestPath;
|
||||
const safePath = path.normalize(cleanPath).replace(/^(\.\.[/\\])+/, '').replace(/^[/\\]+/, '');
|
||||
return path.join(ROOT, safePath);
|
||||
}
|
||||
|
||||
function serveStatic(reqPath, res) {
|
||||
const filePath = getStaticPath(reqPath);
|
||||
if (!filePath.startsWith(ROOT)) {
|
||||
sendText(res, 403, 'Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readFile(filePath, (err, content) => {
|
||||
if (err) {
|
||||
const fallback404 = path.join(ROOT, '404.html');
|
||||
fs.readFile(fallback404, (fallbackErr, fallbackContent) => {
|
||||
if (fallbackErr) {
|
||||
sendText(res, 404, 'Not Found');
|
||||
return;
|
||||
}
|
||||
sendText(res, 404, fallbackContent, 'text/html; charset=utf-8');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
sendText(res, 200, content, MIME_TYPES[ext] || 'application/octet-stream');
|
||||
});
|
||||
}
|
||||
|
||||
function sortItems(items) {
|
||||
return [...items].sort((a, b) => Number(a.position) - Number(b.position));
|
||||
}
|
||||
|
||||
loadItems();
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (pathname === '/api/levels' && req.method === 'GET') {
|
||||
return sendJson(res, 200, { items: sortItems(loadItems()) });
|
||||
}
|
||||
|
||||
if (pathname === '/api/levels' && req.method === 'POST') {
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const payload = normalizeItem(JSON.parse(body), `item-${Date.now()}`);
|
||||
const error = validateItem(payload);
|
||||
if (error) return sendJson(res, 400, { error });
|
||||
const items = loadItems();
|
||||
items.push(payload);
|
||||
return sendJson(res, 201, { item: payload, items: sortItems(saveItems(items)) });
|
||||
} catch (error) {
|
||||
return sendJson(res, 400, { error: 'Invalid JSON request body.' });
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/api/levels/') && (req.method === 'PUT' || req.method === 'DELETE')) {
|
||||
const id = decodeURIComponent(pathname.slice('/api/levels/'.length));
|
||||
const items = loadItems();
|
||||
const index = items.findIndex(item => item.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
return sendJson(res, 404, { error: 'Level not found.' });
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
items.splice(index, 1);
|
||||
return sendJson(res, 200, { items: sortItems(saveItems(items)) });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const payload = normalizeItem({ ...items[index], ...JSON.parse(body), id }, id);
|
||||
const error = validateItem(payload);
|
||||
if (error) return sendJson(res, 400, { error });
|
||||
items[index] = payload;
|
||||
return sendJson(res, 200, { item: payload, items: sortItems(saveItems(items)) });
|
||||
} catch (error) {
|
||||
return sendJson(res, 400, { error: 'Invalid JSON request body.' });
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/api/health') {
|
||||
return sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
return serveStatic(pathname, res);
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`GD FEDL server running at http://localhost:${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
Linux system service setup
|
||||
|
||||
This lets the server start automatically on boot on a Linux device.
|
||||
MAKE A FILE NAME data.txt TO RUN DO THIS FURST
|
||||
|
||||
1. Copy the project to the Linux machine.
|
||||
Example path:
|
||||
/opt/fedl
|
||||
|
||||
2. Make sure Node.js is installed.
|
||||
Check with:
|
||||
node -v
|
||||
|
||||
3. Test the server manually first.
|
||||
In the project folder run:
|
||||
HOST=0.0.0.0 PORT=3000 node server/server.js
|
||||
|
||||
4. Create a systemd service file.
|
||||
Run:
|
||||
sudo nano /etc/systemd/system/fedl.service
|
||||
|
||||
5. Paste this into the service file.
|
||||
|
||||
[Unit]
|
||||
Description=FEDL Node Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/fedl
|
||||
ExecStart=/usr/bin/env node /opt/fedl/server/server.js
|
||||
Environment=HOST=0.0.0.0
|
||||
Environment=PORT=3000
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
User=www-data
|
||||
Group=www-data
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
6. Reload systemd.
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
7. Enable the service.
|
||||
sudo systemctl enable fedl.service
|
||||
|
||||
8. Start the service.
|
||||
sudo systemctl start fedl.service
|
||||
|
||||
9. Check status.
|
||||
sudo systemctl status fedl.service
|
||||
|
||||
10. View logs.
|
||||
sudo journalctl -u fedl.service -f
|
||||
|
||||
Notes
|
||||
|
||||
- If your project is not in /opt/fedl, change WorkingDirectory and ExecStart.
|
||||
- If www-data does not exist on your Linux system, use your own service user.
|
||||
- Make sure port 3000 is allowed through the firewall.
|
||||
- If you use a reverse proxy like Nginx, you can proxy to:
|
||||
http://127.0.0.1:3000
|
||||
@@ -0,0 +1,226 @@
|
||||
# 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 |
|
||||
| `APP_BASE_URL` | _(derived from request)_ | Public site base used in password reset links, for example `https://fedl.site` or `https://server.fedl.site/fedl` |
|
||||
| `SMTP_HOST` | _(empty)_ | SMTP host for password reset emails |
|
||||
| `SMTP_PORT` | `465` | SMTP port |
|
||||
| `SMTP_SECURE` | `true` | Use TLS from connect time; set to `false` only if your mail server accepts plain SMTP |
|
||||
| `SMTP_USER` | _(empty)_ | SMTP username for authenticated mailboxes |
|
||||
| `SMTP_PASS` | _(empty)_ | SMTP password |
|
||||
| `SMTP_FROM` | _(empty)_ | Sender address for password reset emails, e.g. `[email protected]` |
|
||||
| `SMTP_EHLO_NAME` | `fedl.site` | Optional EHLO/HELO name sent to the SMTP server |
|
||||
|
||||
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 + account email) |
|
||||
| `sessions.json` | Bearer session tokens + expiry |
|
||||
| `userdata.json` | Per-user synced state (roulette, list %, saved runs, roulette slots) |
|
||||
| `reset_tokens.json` | One-time password reset tokens + expiry |
|
||||
|
||||
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", "email" }`
|
||||
- **401:** invalid or expired token
|
||||
|
||||
#### `POST /api/auth/request-password-reset`
|
||||
|
||||
- **Body:** `{ "identifier": "username-or-email" }`
|
||||
- **200:** generic success response whether or not a matching account exists
|
||||
|
||||
#### `POST /api/auth/reset-password`
|
||||
|
||||
- **Body:** `{ "token", "newPassword" }`
|
||||
- **200:** `{ "ok": true }`
|
||||
- **400:** invalid/expired token or invalid password
|
||||
|
||||
### Account settings
|
||||
|
||||
#### `GET /api/account`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **200:** account profile including `username`, `email`, `createdAt`, and whether reset email delivery is configured
|
||||
|
||||
#### `PUT /api/account`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **Body:** `{ "email": "[email protected]" }`
|
||||
- **200:** `{ "ok": true, "email": "..." }`
|
||||
|
||||
#### `PUT /api/account/password`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **Body:** `{ "currentPassword", "newPassword" }`
|
||||
- **200:** `{ "ok": true, "token": "new-session-token" }`
|
||||
|
||||
#### `POST /api/account/password-reset-email`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- Sends a reset link to the email saved on the current account
|
||||
- Requires SMTP to be configured server-side
|
||||
|
||||
### 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.
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"2351d1fa28dc758a60b985808c2f3f40815cf067c8087cd99ea00f77b7d75832": {
|
||||
"userId": "usr_mnsa4u5y537c2d59",
|
||||
"expiresAt": "2026-04-10T04:30:30.954Z",
|
||||
"createdAt": "2026-04-10T03:30:30.954Z"
|
||||
},
|
||||
"d1e6f33eccdaa0777c7d4f161223bdeb4d95d94c9bed9911dd754e1c24f67719": {
|
||||
"userId": "usr_mnsa4u5y537c2d59",
|
||||
"expiresAt": "2026-04-10T04:30:55.992Z",
|
||||
"createdAt": "2026-04-10T03:30:55.992Z"
|
||||
},
|
||||
"c9480b7c2d3953a6ad4aa25760949762ad18f3b8388a21685d59bed395737a3b": {
|
||||
"userId": "usr_mnsa4u5y537c2d59",
|
||||
"expiresAt": "2026-04-10T04:31:30.517Z",
|
||||
"createdAt": "2026-04-10T03:31:30.517Z"
|
||||
},
|
||||
"b05688c574332c89ad3a0575185eb7e2725dc12b387913212162a375282e9d3a": {
|
||||
"userId": "usr_mnsa4u5y537c2d59",
|
||||
"expiresAt": "2026-04-10T04:46:39.467Z",
|
||||
"createdAt": "2026-04-10T03:46:39.467Z"
|
||||
}
|
||||
}
|
||||
+2052
File diff suppressed because it is too large
Load Diff
+58
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Sign up — GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="signup">
|
||||
<header>
|
||||
<h1>Sign up</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="auth-main">
|
||||
<section class="panel auth-panel">
|
||||
<p class="hero-kicker">FEDL account</p>
|
||||
<h2>Create an account</h2>
|
||||
<p class="muted">Username and password are stored on your FEDL server. Roulette progress and list % sync to the server when you are signed in. Password resets are sent via the app's messages.</p>
|
||||
<form id="signup-form" class="auth-form">
|
||||
<label class="admin-password-field" for="signup-username">
|
||||
<span>Username</span>
|
||||
<input id="signup-username" name="username" type="text" autocomplete="username" required minlength="3" maxlength="24" pattern="[a-z0-9_]{3,24}" placeholder="letters, numbers, underscore" />
|
||||
</label>
|
||||
<p class="muted small auth-hint">3–24 characters, lowercase letters, numbers, or underscore only.</p>
|
||||
<label class="admin-password-field" for="signup-password">
|
||||
<span>Password</span>
|
||||
<input id="signup-password" name="password" type="password" autocomplete="new-password" required minlength="8" placeholder="At least 8 characters" />
|
||||
</label>
|
||||
<label class="admin-password-field" for="signup-password2">
|
||||
<span>Confirm password</span>
|
||||
<input id="signup-password2" name="password2" type="password" autocomplete="new-password" required minlength="8" />
|
||||
</label>
|
||||
<div class="cf-turnstile" data-sitekey="0x4AAAAAAC_RrrNCJVX_wbHb"></div>
|
||||
<p id="signup-status" class="muted" role="status"></p>
|
||||
<div class="auth-actions">
|
||||
<button type="submit" class="btn" id="signup-submit">Create account</button>
|
||||
<a class="text-link" href="login.html?return=signup.html">Already have an account? Log in</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content="Complete sitemap of all FEDL pages and resources.">
|
||||
<title>Sitemap — FEDL</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
<style>
|
||||
.sitemap-content {
|
||||
max-width: 800px;
|
||||
}
|
||||
.sitemap-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.sitemap-section h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.sitemap-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.sitemap-section li {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.sitemap-section a {
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sitemap-section a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="sitemap">
|
||||
<header>
|
||||
<h1>FEDL - Ranked Demon List</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
<a href="admelist.html">Admin</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="sitemap-content">
|
||||
<h1>Sitemap</h1>
|
||||
<p>Welcome to the FEDL sitemap. Below you'll find all the pages and resources available on our site.</p>
|
||||
|
||||
<section class="sitemap-section">
|
||||
<h2>Main Pages</h2>
|
||||
<ul>
|
||||
<li><a href="index.html">Home</a> - FEDL Hub and overview</li>
|
||||
<li><a href="lists.html">Rankings</a> - Browse the complete ranked demon list</li>
|
||||
<li><a href="rules.html">Rules</a> - Submission rules and guidelines</li>
|
||||
<li><a href="contact.html">Contact</a> - Get in touch with us</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="sitemap-section">
|
||||
<h2>Games & Tools</h2>
|
||||
<ul>
|
||||
<li><a href="guess.html">Guess Game</a> - Test your demon knowledge</li>
|
||||
<li><a href="roulette.html">Roulette</a> - Spin for a random challenge demon</li>
|
||||
<li><a href="players.html">Players</a> - Browse player profiles and stats</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="sitemap-section">
|
||||
<h2>User Account</h2>
|
||||
<ul>
|
||||
<li><a href="login.html">Login</a> - Sign in to your account</li>
|
||||
<li><a href="signup.html">Sign Up</a> - Create a new account</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="sitemap-section">
|
||||
<h2>Submissions & Communication</h2>
|
||||
<ul>
|
||||
<li><a href="run.html">Submit Run</a> - Submit your demon completions</li>
|
||||
<li><a href="admelist.html">Admin</a> - List management tools</li>
|
||||
<li><a href="messages.html">Messages</a> - View and send messages</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="sitemap-section">
|
||||
<h2>Additional Resources</h2>
|
||||
<ul>
|
||||
<li><a href="README.md">About FEDL</a> - Learn more about the project</li>
|
||||
<li><a href="errors.html">Error Reference</a> - Common error pages and solutions</li>
|
||||
<li><a href="offlineindex.html">Offline Index</a> - Browse content offline</li>
|
||||
<li><a href="sitemap.html">Sitemap</a> - This page</li>
|
||||
<li><a href="https://github.com/your-repo">GitHub Repository</a> - View the source code</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="sitemap-section">
|
||||
<h2>Error Pages</h2>
|
||||
<ul>
|
||||
<li><a href="401.html">401 Unauthorized</a> - Authentication required</li>
|
||||
<li><a href="403.html">403 Forbidden</a> - Access denied</li>
|
||||
<li><a href="404.html">404 Not Found</a> - Page not found</li>
|
||||
<li><a href="429.html">429 Too Many Requests</a> - Rate limit exceeded</li>
|
||||
<li><a href="500.html">500 Internal Server Error</a> - Server error</li>
|
||||
<li><a href="502.html">502 Bad Gateway</a> - Gateway error</li>
|
||||
<li><a href="503.html">503 Service Unavailable</a> - Service temporarily unavailable</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<hr style="margin: 2rem 0; opacity: 0.3;">
|
||||
<p style="font-size: 0.9rem; color: #999;">Last updated: April 8, 2026</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://fedl.site/</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>1.0</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/lists.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.95</priority>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/run.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.85</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/guess.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.8</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/players.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.8</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/rules.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.8</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/roulette.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.75</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/contact.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/login.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/signup.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/messages.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/post.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/admelist.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/errors.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.6</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/offlineindex.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.6</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/sitemap.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.6</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/401.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.3</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/403.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.3</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/404.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.3</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/429.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.3</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/500.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.3</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/502.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.3</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://fedl.site/503.html</loc>
|
||||
<lastmod>2026-04-08</lastmod>
|
||||
<priority>0.3</priority>
|
||||
<changefreq>yearly</changefreq>
|
||||
</url>
|
||||
</urlset>
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/zsh
|
||||
cd "/Users/miles/Documents/GitHub/fedl" || exit 1
|
||||
HOST=0.0.0.0 PORT=8090 node server/server.js
|
||||
+434
-42
@@ -1,9 +1,11 @@
|
||||
:root{--bg:#0f1724;--panel:#071326;--accent:#5cc5ff;--muted:#9fb3c8;--text:#e6eef8;--card:#081220}
|
||||
:root{--bg:#0f1724;--panel:#071326;--accent:#5cc5ff;--muted:#9fb3c8;--text:#e6eef8;--card:#081220;--accent-warm:#ffb84d}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;color:var(--text);background:linear-gradient(180deg,#061021 0%,#071426 100%)}
|
||||
header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;background:linear-gradient(90deg,rgba(255,255,255,0.02),transparent);backdrop-filter:blur(4px)}
|
||||
header h1{margin:0;font-size:1.1rem}
|
||||
nav a{color:var(--muted);margin-left:14px;text-decoration:none}
|
||||
html,body{height:100%;margin:0;font-family:"Space Grotesk","Trebuchet MS",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--text);background:var(--bg)}
|
||||
header{position:sticky;top:0;z-index:50;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:14px 20px;background:linear-gradient(90deg,rgba(6,16,33,0.9),rgba(7,20,38,0.72));backdrop-filter:blur(16px);border-bottom:1px solid rgba(255,255,255,0.05)}
|
||||
header h1{margin:0;font-size:1.1rem;letter-spacing:.02em}
|
||||
nav{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px 12px}
|
||||
nav a{color:var(--muted);margin-left:0;text-decoration:none;transition:color .2s ease,opacity .2s ease}
|
||||
nav a:hover{color:var(--text)}
|
||||
main{padding:18px 20px}
|
||||
h1,h2,h3,h4{line-height:1.15}
|
||||
.layout{display:flex;gap:16px;align-items:flex-start}
|
||||
@@ -12,11 +14,13 @@ h1,h2,h3,h4{line-height:1.15}
|
||||
.sidebar h2{margin:0 0 8px 0;font-size:0.95rem}
|
||||
.sidebar ul{list-style:none;padding:0;margin:0}
|
||||
.sidebar li{margin:6px 0}
|
||||
.sidebar-kicker{margin:0 0 8px 0;text-transform:uppercase;letter-spacing:.16em;font-size:.72rem;font-weight:800;color:var(--accent)}
|
||||
.level-link{display:block;width:100%;text-align:left;padding:8px;border-radius:6px;border:0;background:transparent;color:var(--muted);cursor:pointer;font-weight:600}
|
||||
.level-link.active{background:rgba(92,197,255,0.06);color:var(--accent)}
|
||||
.content{flex:1}
|
||||
.panel{background:var(--panel);padding:14px;border-radius:10px;margin-bottom:12px;box-shadow:0 3px 12px rgba(2,6,23,0.6)}
|
||||
.search-row{display:flex;gap:8px;align-items:center;margin-bottom:12px}
|
||||
.home-section-grid,.spotlight-panel,.discord-panel,.list-table-panel,.submission-panel,.run-side-panel{content-visibility:auto;contain-intrinsic-size:1px 500px}
|
||||
.search-row{display:flex;flex-wrap:wrap;gap:14px;align-items:end;margin-bottom:0}
|
||||
.search-row input[type=text]{flex:1;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--text)}
|
||||
.search-row select{padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--text)}
|
||||
.table-wrap{overflow:auto;border-radius:8px}
|
||||
@@ -24,40 +28,287 @@ table.levels-table{width:100%;border-collapse:collapse;background:linear-gradien
|
||||
table.levels-table th,table.levels-table td{padding:12px 14px;border-bottom:1px solid rgba(255,255,255,0.02);text-align:left}
|
||||
table.levels-table th{color:var(--muted);font-size:0.85rem;font-weight:600}
|
||||
table.levels-table tr:hover td{background:rgba(255,255,255,0.01)}
|
||||
.btn{display:inline-block;padding:8px 10px;border-radius:8px;background:var(--accent);color:#042;cursor:pointer;border:none}
|
||||
.levels-table input{width:100%;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
|
||||
.btn{display:inline-block;padding:10px 14px;border-radius:999px;background:var(--accent);color:var(--bg);cursor:pointer;border:none;font-weight:700;text-decoration:none;transition:transform .2s ease,box-shadow .2s ease,background .2s ease,filter .2s ease}
|
||||
.btn:hover{transform:translateY(-1px);box-shadow:0 10px 24px color-mix(in srgb,var(--accent) 22%,transparent);filter:brightness(1.1)}
|
||||
.btn:active{transform:translateY(0)}
|
||||
.muted{color:var(--muted);font-size:0.9rem}
|
||||
.error-text{color:#ff9f9f}
|
||||
.success-text{color:#8cf0ae!important}
|
||||
header nav a.btn{margin-left:14px;text-decoration:none;vertical-align:middle}
|
||||
.small-btn{padding:8px 12px}
|
||||
.danger-btn{background:var(--accent);color:var(--bg)}
|
||||
.admin-shell{padding:18px 20px}
|
||||
.admin-shell-content[hidden]{display:none}
|
||||
.admin-locked{overflow:hidden}
|
||||
.admin-locked #admin-page-header,.admin-locked #admin-shell-content{visibility:hidden}
|
||||
.admin-login-screen{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;padding:30px;background:
|
||||
radial-gradient(circle at top,rgba(255,184,77,0.16),transparent 24%),
|
||||
linear-gradient(180deg,rgba(6,16,33,0.98),rgba(7,20,38,0.99))}
|
||||
.admin-login-card{width:min(100%,520px);padding:34px;border-radius:28px;background:linear-gradient(145deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96) 42%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 35%);border:1px solid rgba(255,184,77,0.16)}
|
||||
.admin-login-card h2{margin:0 0 10px 0;font-size:clamp(2rem,4vw,2.8rem)}
|
||||
.admin-login-form{display:grid;gap:16px;margin-top:20px}
|
||||
.admin-login-actions{display:grid;gap:12px}
|
||||
.admin-login-screen[hidden]{display:none}
|
||||
.admin-panel{padding:20px}
|
||||
.admin-overview-panel{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:24px 26px;border-radius:28px;background:linear-gradient(135deg,rgba(255,184,77,0.12),rgba(7,19,38,0.95));border:1px solid rgba(255,184,77,0.16)}
|
||||
.admin-overview-panel h2{margin:0 0 8px 0;font-size:clamp(1.9rem,4vw,2.8rem)}
|
||||
.admin-overview-badges{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.admin-overview-badges span{padding:10px 14px;border-radius:999px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);color:var(--text);font-weight:700}
|
||||
.admin-password-field{display:grid;gap:8px;width:100%;font-weight:700;color:var(--text)}
|
||||
.admin-password-field span{font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
||||
.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:220px;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
|
||||
.admin-toolbar{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:18px}
|
||||
.admin-toolbar h2{margin:0 0 8px 0}
|
||||
.admin-import-actions{flex-wrap:wrap;justify-content:flex-end}
|
||||
.bulk-approve-modal[hidden]{display:none !important}
|
||||
.bulk-approve-modal-card{max-width:480px}
|
||||
.bulk-approve-modal-card h2{margin:0 0 8px 0;font-size:clamp(1.35rem,3vw,1.75rem)}
|
||||
.bulk-approve-form{margin-top:8px}
|
||||
.bulk-approve-actions{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
|
||||
.import-targeted-mode{display:flex;flex-wrap:wrap;align-items:center;gap:12px 18px;margin:4px 0 8px}
|
||||
.import-targeted-mode-label{font-size:0.88rem;font-weight:700;color:var(--muted)}
|
||||
.import-targeted-radio{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:0.95rem}
|
||||
.import-targeted-radio input{accent-color:var(--accent)}
|
||||
.import-targeted-hint{margin:0 0 4px;line-height:1.45}
|
||||
.admin-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
||||
.admin-table td:last-child{text-align:center}
|
||||
.run-admin-cell{display:grid;gap:6px}
|
||||
.run-admin-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}
|
||||
.run-admin-detail-row td{padding-top:0}
|
||||
.run-admin-detail{display:grid;gap:8px;padding:0 0 14px 0;color:var(--text)}
|
||||
.run-admin-detail a{color:var(--accent)}
|
||||
.hero-kicker{margin:0 0 12px 0;text-transform:uppercase;letter-spacing:.18em;font-size:.75rem;font-weight:800;color:var(--accent-warm)}
|
||||
.home-hero{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(300px,.85fr);gap:22px;align-items:stretch;min-height:calc(100vh - 132px)}
|
||||
.home-copy,.hero-stage{position:relative;overflow:hidden}
|
||||
.home-copy{display:flex;flex-direction:column;justify-content:center;padding:34px;border-radius:28px;background:linear-gradient(145deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96) 42%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 35%);border:1px solid rgba(255,184,77,0.16)}
|
||||
.home-copy::before{content:"";position:absolute;inset:auto -80px -90px auto;width:240px;height:240px;border-radius:50%;background:rgba(92,197,255,0.12);filter:blur(16px)}
|
||||
.home-copy h2{margin:0 0 14px 0;font-size:clamp(2.6rem,6vw,5.3rem);max-width:12ch;position:relative;z-index:1}
|
||||
.hero-text{max-width:60ch;margin:0;position:relative;z-index:1;color:var(--text);line-height:1.75;font-size:1.02rem}
|
||||
.hero-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px;position:relative;z-index:1}
|
||||
.hero-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:28px;position:relative;z-index:1}
|
||||
.stat-card{padding:16px 18px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);backdrop-filter:blur(10px)}
|
||||
.stat-card strong{display:block;margin-bottom:8px;font-size:clamp(1.4rem,3vw,2rem);color:var(--text)}
|
||||
.stat-card span{display:block;color:var(--text);line-height:1.45;font-size:.94rem}
|
||||
.hero-stage{padding:28px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.07),rgba(255,255,255,0.03));border:1px solid rgba(255,255,255,0.08)}
|
||||
.stage-label{margin:0 0 8px 0;font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);font-weight:800}
|
||||
.hero-stage h3{margin:0 0 8px 0;font-size:2rem}
|
||||
.featured-list{display:grid;gap:12px;margin-top:22px;max-height:600px;overflow-y:auto}
|
||||
.featured-card{padding:18px;border-radius:18px;background:var(--panel);border:1px solid rgba(255,255,255,0.08)}
|
||||
.featured-rank{display:inline-flex;align-items:center;justify-content:center;min-width:58px;padding:7px 12px;border-radius:999px;background:rgba(92,197,255,0.14);color:var(--accent);font-size:.82rem;font-weight:800}
|
||||
.featured-card strong{display:block;margin:14px 0 8px 0;font-size:1.1rem}
|
||||
.featured-card p{margin:0;color:var(--text);line-height:1.55}
|
||||
.home-section-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:18px}
|
||||
.info-panel{padding:24px;border-radius:22px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
|
||||
.info-panel h3{margin:0 0 10px 0;font-size:1.45rem}
|
||||
.text-link{display:inline-block;margin-top:18px;color:var(--accent);font-weight:700;text-decoration:none}
|
||||
.text-link:hover{color:#a9e5ff}
|
||||
.spotlight-panel{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:24px;align-items:start;margin-top:18px;padding:28px;border-radius:28px;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 12%,transparent),var(--panel));border:1px solid rgba(92,197,255,0.16)}
|
||||
.spotlight-copy h2{margin:0 0 12px 0;font-size:2.1rem}
|
||||
.spotlight-text{margin:0;color:var(--text);line-height:1.7}
|
||||
.spotlight-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
|
||||
.spotlight-card{padding:18px;border-radius:18px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08)}
|
||||
.spotlight-card h3{margin:0 0 10px 0;font-size:1.08rem}
|
||||
.spotlight-card p{margin:0;color:var(--text);line-height:1.6}
|
||||
.list-page-shell{display:flex;flex-direction:column;gap:16px}
|
||||
.list-sidebar-panel{padding:18px 16px;border-radius:24px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
|
||||
.list-sidebar-panel h2{margin:0 0 10px 0;font-size:1.2rem}
|
||||
.list-sidebar-panel .muted{margin:0 0 16px 0;line-height:1.55}
|
||||
.list-hero-panel{display:flex;align-items:end;justify-content:space-between;gap:18px;padding:28px;border-radius:28px;background:linear-gradient(135deg,rgba(92,197,255,0.14),rgba(7,19,38,0.95));border:1px solid rgba(92,197,255,0.16)}
|
||||
.list-hero-copy h2{margin:0 0 10px 0;font-size:clamp(2rem,4vw,3rem)}
|
||||
.list-hero-text{max-width:60ch;line-height:1.7}
|
||||
.list-hero-badge{max-width:230px;padding:16px 18px;border-radius:18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08)}
|
||||
.list-hero-badge strong{display:block;margin-bottom:8px;font-size:1rem}
|
||||
.list-hero-badge span{display:block;color:var(--text);line-height:1.55;font-size:.92rem}
|
||||
.list-controls-panel{padding:20px 22px;border-radius:24px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.03));border:1px solid rgba(255,255,255,0.06)}
|
||||
.control-block{display:flex;flex:1;flex-direction:column;gap:8px;font-weight:700;color:var(--text)}
|
||||
.control-block span{font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
||||
.control-block input,.control-block select{width:100%;padding:12px 14px;border-radius:14px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
|
||||
.control-select{max-width:220px}
|
||||
.list-table-panel{padding:22px 22px 16px;border-radius:26px;background:linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
|
||||
.table-header{display:flex;align-items:end;justify-content:space-between;gap:16px;margin-bottom:16px}
|
||||
.table-header h3{margin:0;font-size:1.45rem}
|
||||
.table-hint{max-width:320px;line-height:1.6;text-align:right}
|
||||
table.levels-table{border-radius:18px;overflow:hidden}
|
||||
table.levels-table th{background:rgba(255,255,255,0.03)}
|
||||
table.levels-table td{font-size:.98rem}
|
||||
table.levels-table td:last-child{text-align:right}
|
||||
table.levels-table tbody tr:nth-child(even) td{background:rgba(255,255,255,0.018)}
|
||||
table.levels-table .btn{padding:9px 14px}
|
||||
.wip-hero{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr);gap:22px;align-items:stretch;min-height:calc(100vh - 132px)}
|
||||
.wip-copy,.wip-stage{position:relative}
|
||||
.wip-copy{display:flex;flex-direction:column;justify-content:center;padding:32px;border-radius:26px;background:linear-gradient(145deg,rgba(255,184,77,0.1),rgba(7,19,38,0.96) 40%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 36%);border:1px solid rgba(255,184,77,0.16);overflow:hidden}
|
||||
.wip-copy::after{content:"";position:absolute;inset:auto -60px -80px auto;width:220px;height:220px;border-radius:50%;background:rgba(92,197,255,0.12);filter:blur(14px)}
|
||||
.wip-copy h2{margin:0 0 14px 0;font-size:clamp(2.4rem,6vw,5rem);max-width:11ch}
|
||||
.hero-text{max-width:58ch;margin:0;color:var(--text);line-height:1.7;font-size:1.02rem}
|
||||
.hero-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:26px}
|
||||
.wip-stage{display:flex;align-items:center;justify-content:center;padding:12px}
|
||||
.construction-card{width:min(100%,380px);padding:24px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.09);box-shadow:0 30px 80px rgba(0,0,0,0.28);animation:floatCard 4.6s ease-in-out infinite}
|
||||
.construction-pill{display:inline-block;padding:7px 12px;border-radius:999px;background:rgba(255,184,77,0.16);color:#ffd79b;font-size:.78rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
|
||||
.construction-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin:20px 0}
|
||||
.construction-grid span{display:block;aspect-ratio:1;border-radius:16px;background:linear-gradient(135deg,rgba(92,197,255,0.22),rgba(255,184,77,0.08));border:1px solid rgba(255,255,255,0.08);animation:pulseTile 3.4s ease-in-out infinite}
|
||||
.construction-grid span:nth-child(2){animation-delay:.35s}
|
||||
.construction-grid span:nth-child(3){animation-delay:.7s}
|
||||
.construction-grid span:nth-child(4){animation-delay:1.05s}
|
||||
.progress-shell{height:14px;border-radius:999px;background:rgba(255,255,255,0.08);overflow:hidden}
|
||||
.progress-bar{height:100%;width:48%;border-radius:inherit;background:linear-gradient(90deg,var(--accent-warm),var(--accent));animation:progressShift 2.8s ease-in-out infinite}
|
||||
.construction-card p{margin:18px 0 0 0;color:var(--text);line-height:1.6}
|
||||
.wip-status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-top:18px}
|
||||
.status-panel{padding:22px;border-radius:20px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
|
||||
.status-label{margin:0 0 8px 0;font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-warm);font-weight:800}
|
||||
.status-panel h3{margin:0 0 10px 0;font-size:1.4rem}
|
||||
.coming-shell{position:relative;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 132px);overflow:hidden}
|
||||
.coming-panel{position:relative;z-index:1;width:min(100%,720px);padding:42px 30px;text-align:center;border-radius:28px;background:linear-gradient(145deg,rgba(92,197,255,0.12),rgba(7,19,38,0.96) 52%),radial-gradient(circle at top,rgba(255,255,255,0.1),transparent 32%);border:1px solid rgba(92,197,255,0.18);box-shadow:0 24px 70px rgba(2,6,23,0.42)}
|
||||
.coming-panel h2{margin:0 0 14px 0;font-size:clamp(2.8rem,8vw,4.8rem)}
|
||||
.coming-text{max-width:44ch;margin:0 auto;color:var(--text);line-height:1.7}
|
||||
.coming-loader{display:flex;justify-content:center;gap:12px;margin:28px 0 22px}
|
||||
.coming-loader span{width:16px;height:16px;border-radius:50%;background:linear-gradient(180deg,var(--accent-warm),var(--accent));animation:bounceDot 1.2s ease-in-out infinite}
|
||||
.coming-loader span:nth-child(2){animation-delay:.15s}
|
||||
.coming-loader span:nth-child(3){animation-delay:.3s}
|
||||
.dad-joke{margin:0 auto 24px;padding:16px 18px;max-width:42ch;border-radius:18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.07);color:var(--text);line-height:1.65}
|
||||
.coming-orb{position:absolute;border-radius:50%;filter:blur(8px);opacity:.8;animation:drift 8s ease-in-out infinite}
|
||||
.orb-one{top:10%;left:12%;width:180px;height:180px;background:rgba(92,197,255,0.12)}
|
||||
.orb-two{right:10%;bottom:12%;width:220px;height:220px;background:rgba(255,184,77,0.12);animation-delay:1.8s}
|
||||
@keyframes floatCard{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
|
||||
@keyframes pulseTile{0%,100%{transform:scale(1);opacity:.72}50%{transform:scale(1.04);opacity:1}}
|
||||
@keyframes progressShift{0%{transform:translateX(-18%);width:36%}50%{transform:translateX(58%);width:50%}100%{transform:translateX(-18%);width:36%}}
|
||||
@keyframes bounceDot{0%,80%,100%{transform:translateY(0);opacity:.5}40%{transform:translateY(-12px);opacity:1}}
|
||||
@keyframes drift{0%,100%{transform:translate3d(0,0,0)}50%{transform:translate3d(20px,-18px,0)}}
|
||||
.video-modal{position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:1200}
|
||||
.video-modal .inner{width:90%;max-width:960px;background:var(--card);padding:12px;border-radius:8px}
|
||||
.video-modal .inner{width:90%;max-width:960px;max-height:88vh;overflow:auto;background:var(--card);padding:12px;border-radius:8px}
|
||||
.video-modal iframe{width:100%;height:540px;border:0;border-radius:6px}
|
||||
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.video-modal iframe{height:320px}}
|
||||
.modal-runs-wrap{margin-top:16px;padding:16px;border-radius:16px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)}
|
||||
.modal-runs-head{display:flex;align-items:end;justify-content:space-between;gap:12px;margin-bottom:12px}
|
||||
.modal-runs-list{display:grid;gap:10px;max-height:240px;overflow:auto;padding-right:4px}
|
||||
.modal-run-card{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;align-items:center;padding:12px 14px;border-radius:14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06)}
|
||||
.modal-run-card strong,.modal-run-card span,.modal-run-card a{align-self:center;line-height:1.2}
|
||||
.modal-run-card strong{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.modal-run-card span{color:var(--text);font-weight:700;white-space:nowrap}
|
||||
.modal-run-card a{margin:0;white-space:nowrap}
|
||||
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.video-modal iframe{height:320px}.wip-hero{grid-template-columns:1fr;min-height:auto}.wip-copy{padding:28px 22px}.wip-status-grid{grid-template-columns:1fr}.coming-panel{padding:34px 22px}.coming-shell{min-height:auto;padding:30px 0}}
|
||||
|
||||
/* small helpers reused */
|
||||
.small{font-size:0.9rem}
|
||||
.roulette-shell{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr);gap:18px;align-items:start}
|
||||
.roulette-main-stack{display:grid;gap:18px;min-width:0}
|
||||
.roulette-account-panel{padding:22px 24px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
|
||||
.roulette-account-heading{margin:0 0 8px 0;font-size:1.35rem}
|
||||
.roulette-account-intro{margin:0 0 14px;line-height:1.55}
|
||||
.roulette-account-controls{display:grid;gap:10px}
|
||||
.roulette-account-label{font-size:0.88rem;font-weight:700;color:var(--muted)}
|
||||
.roulette-account-select{width:100%;max-width:320px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit}
|
||||
.roulette-account-create-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
||||
.roulette-account-create-row input{flex:1;min-width:160px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit}
|
||||
.roulette-restore-btn{margin-top:12px}
|
||||
.roulette-login-sync-hint{margin:0 0 14px;line-height:1.55}
|
||||
.roulette-login-sync-hint a{color:var(--accent)}
|
||||
.roulette-slots-block{margin:14px 0;padding:14px 0;border-top:1px solid rgba(255,255,255,0.08);border-bottom:1px solid rgba(255,255,255,0.08)}
|
||||
.roulette-slots-heading{margin:0 0 6px 0;font-size:0.95rem;font-weight:800;color:var(--text)}
|
||||
.roulette-slot-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:10px}
|
||||
.roulette-slot-title{min-width:52px;font-weight:700;font-size:0.88rem;color:var(--muted)}
|
||||
.roulette-slot-label{flex:1;min-width:140px;font-size:0.88rem}
|
||||
.roulette-progress-block{margin-top:16px;padding-top:16px;border-top:1px solid rgba(255,255,255,0.08)}
|
||||
.roulette-progress-label{display:block;font-size:0.88rem;font-weight:700;margin-bottom:8px;color:var(--text)}
|
||||
.roulette-progress-input-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
||||
.roulette-progress-input-row input{width:100px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit}
|
||||
.roulette-percent-hint{margin:10px 0 0;line-height:1.55;max-width:52ch}
|
||||
.modal-account-progress{margin:16px 0;padding:14px 16px;border-radius:14px;background:rgba(92,197,255,0.08);border:1px solid rgba(92,197,255,0.15)}
|
||||
.modal-account-label{margin:0 0 8px 0;font-size:0.9rem;font-weight:700}
|
||||
.modal-account-row{display:flex;gap:10px;align-items:center}
|
||||
.modal-account-pct-input{width:100px;padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.05);color:var(--text);font:inherit}
|
||||
.list-my-progress-cell{vertical-align:middle}
|
||||
.list-progress-input{width:100%;max-width:88px;padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit;text-align:center}
|
||||
.roulette-main-column{display:grid;gap:18px}
|
||||
.roulette-rules{margin-bottom:18px}
|
||||
.roulette-panel{margin-top:18px;padding:24px 26px;background:linear-gradient(135deg,rgba(92,197,255,0.12),rgba(7,19,38,0.96));border:1px solid rgba(92,197,255,0.16)}
|
||||
.roulette-panel{margin-top:0;padding:28px;border-radius:28px;background:linear-gradient(135deg,rgba(92,197,255,0.14),rgba(7,19,38,0.96));border:1px solid rgba(92,197,255,0.18)}
|
||||
.roulette-head{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin-bottom:18px}
|
||||
.roulette-kicker{margin:0 0 8px 0;text-transform:uppercase;letter-spacing:0.14em;font-size:0.72rem;font-weight:700;color:var(--accent)}
|
||||
.roulette-head h2{margin:0 0 8px 0;font-size:1.9rem}
|
||||
.roulette-btn{min-width:110px;font-weight:700}
|
||||
.roulette-card{padding:22px;border-radius:14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05)}
|
||||
.roulette-head h2{margin:0 0 8px 0;font-size:clamp(2rem,4vw,3rem)}
|
||||
.roulette-intro{max-width:50ch;line-height:1.7}
|
||||
.roulette-btn{min-width:120px;font-weight:700}
|
||||
.roulette-card{padding:24px;border-radius:22px;background:linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.03));border:1px solid rgba(255,255,255,0.08);box-shadow:0 20px 40px rgba(2,6,23,0.28)}
|
||||
.roulette-status{margin:0 0 8px 0}
|
||||
.roulette-card h3{margin:0 0 8px 0;font-size:1.5rem}
|
||||
.roulette-rank{margin:0 0 16px 0;font-size:1rem;color:#dce8f5}
|
||||
.roulette-meta{margin:0 0 10px 0;font-size:1rem;color:#dce8f5}
|
||||
.roulette-card h3{margin:0 0 10px 0;font-size:clamp(1.6rem,3vw,2.3rem)}
|
||||
.roulette-rank{margin:0 0 16px 0;font-size:1.02rem;color:var(--text)}
|
||||
.roulette-meta{margin:0 0 10px 0;font-size:1rem;color:var(--text)}
|
||||
.roulette-note{margin:10px 0 16px 0}
|
||||
.roulette-open{color:#042}
|
||||
.roulette-guess-panel{background:linear-gradient(135deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96));border-color:rgba(255,184,77,0.18)}
|
||||
.guess-card{display:grid;gap:12px}
|
||||
.guess-form{display:grid;gap:10px;margin-top:4px}
|
||||
.guess-label{font-size:.9rem;font-weight:700;color:var(--text)}
|
||||
.guess-controls{display:flex;gap:12px;align-items:center}
|
||||
.guess-controls input{flex:1;padding:12px 14px;border-radius:12px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text);font:inherit}
|
||||
.guess-answer{margin:0}
|
||||
.roulette-rules-card{padding:24px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
|
||||
.roulette-rules-card h2{margin:0 0 16px 0;font-size:1.8rem}
|
||||
.roulette-rule-list{display:grid;gap:14px}
|
||||
.roulette-rule-list article{padding:16px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06)}
|
||||
.roulette-rule-list strong{display:block;margin-bottom:8px;font-size:1rem;color:var(--text)}
|
||||
.roulette-rule-list p{margin:0;color:var(--text);line-height:1.6}
|
||||
.run-shell{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr);gap:18px;align-items:start}
|
||||
.run-panel{padding:28px;border-radius:28px;background:linear-gradient(135deg,rgba(92,197,255,0.14),rgba(7,19,38,0.96));border:1px solid rgba(92,197,255,0.18)}
|
||||
.run-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:20px}
|
||||
.run-head h2{margin:0 0 10px 0;font-size:clamp(2rem,4vw,3rem)}
|
||||
.run-intro{max-width:58ch;line-height:1.7}
|
||||
.run-head-badge{max-width:240px;padding:16px 18px;border-radius:18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08)}
|
||||
.run-head-badge strong{display:block;margin-bottom:8px}
|
||||
.run-head-badge span{display:block;color:var(--text);line-height:1.55;font-size:.92rem}
|
||||
.run-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
|
||||
.run-notes-block{grid-column:1/-1}
|
||||
.run-form textarea{width:100%;padding:12px 14px;border-radius:14px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text);resize:vertical;min-height:150px}
|
||||
.run-actions{grid-column:1/-1;display:flex;flex-wrap:wrap;align-items:center;gap:14px}
|
||||
.run-side-panel{padding:24px 22px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
|
||||
.run-side-panel h2{margin:0 0 16px 0;font-size:1.8rem}
|
||||
.run-checklist{display:grid;gap:14px}
|
||||
.run-checklist article{padding:16px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06)}
|
||||
.run-checklist strong{display:block;margin-bottom:8px}
|
||||
.run-checklist p{margin:0;color:var(--text);line-height:1.6}
|
||||
.submission-panel{margin-top:18px;padding:22px 22px 16px;border-radius:26px;background:linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
|
||||
.submission-header{margin-bottom:18px}
|
||||
.submission-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
|
||||
.run-my-runs-panel{margin-top:8px}
|
||||
.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:var(--text);line-height:1.55}
|
||||
.submission-meta{font-size:.92rem;color:var(--muted)!important}
|
||||
.submission-links{display:flex;flex-wrap:wrap;gap:14px;margin-top:14px}
|
||||
.status-pill{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:999px;font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
|
||||
.status-pending{background:rgba(255,184,77,0.16);color:#ffd07d}
|
||||
.status-approved{background:rgba(70,196,121,0.16);color:#8cf0ae}
|
||||
.status-rejected{background:rgba(255,123,123,0.16);color:#ffaeae}
|
||||
.discord-panel{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-top:18px;padding:26px 28px;background:linear-gradient(135deg,rgba(88,101,242,0.18),rgba(7,19,38,0.95));border:1px solid rgba(88,101,242,0.22)}
|
||||
.download-panel{background:linear-gradient(135deg,rgba(45,221,149,0.18),rgba(7,19,38,0.95));border:1px solid rgba(45,221,149,0.22)}
|
||||
.discord-copy{flex:1}
|
||||
.discord-badge{display:flex;align-items:center;gap:16px;margin-bottom:14px}
|
||||
.discord-logo{display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:18px;background:#5865f2;color:#fff;box-shadow:0 10px 24px rgba(88,101,242,0.32)}
|
||||
.discord-logo{display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:18px;background:#5865f2;color:var(--text);box-shadow:0 10px 24px rgba(88,101,242,0.32)}
|
||||
.discord-logo img{width:34px;height:34px;display:block}
|
||||
.discord-label{margin:0 0 6px 0;text-transform:uppercase;letter-spacing:0.14em;font-size:0.72rem;font-weight:700;color:#bfc7ff}
|
||||
.discord-label{margin:0 0 6px 0;text-transform:uppercase;letter-spacing:0.14em;font-size:0.72rem;font-weight:700;color:var(--text)}
|
||||
.discord-panel h2{margin:0;font-size:1.9rem}
|
||||
.discord-text{max-width:720px;margin:0 0 16px 0;line-height:1.65;color:#dce8f5}
|
||||
.discord-text{max-width:720px;margin:0 0 16px 0;line-height:1.65;color:var(--text)}
|
||||
.discord-footnote{margin-top:16px;margin-bottom:0}
|
||||
.feature-bullet-list{margin:0;padding-left:22px;color:#e0ffe9}
|
||||
.feature-bullet-list li{margin:0 0 10px 0;line-height:1.55}
|
||||
.download-panel .discord-text{color:#e0ffe9}
|
||||
.discord-highlights{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.discord-highlights span{padding:8px 12px;border-radius:999px;background:rgba(255,255,255,0.07);color:#eef3ff;font-size:0.92rem}
|
||||
.discord-actions{display:flex;align-items:center;justify-content:center}
|
||||
.discord-btn{padding:12px 18px;background:#5865f2;color:#fff;font-weight:700}
|
||||
.discord-btn{padding:12px 18px;background:#5865f2;color:var(--text);font-weight:700}
|
||||
.download-panel .discord-btn{background:#1fce7a}
|
||||
.rules-view header{padding:18px 28px}
|
||||
.rules-view header h1{font-size:2rem;font-weight:800;letter-spacing:0.02em}
|
||||
.rules-view main{display:flex;justify-content:center;padding:40px 20px 64px}
|
||||
@@ -66,11 +317,11 @@ table.levels-table tr:hover td{background:rgba(255,255,255,0.01)}
|
||||
.rules-kicker{margin:0 0 10px 0;text-transform:uppercase;letter-spacing:0.18em;font-size:0.72rem;color:var(--accent);font-weight:700}
|
||||
.rules-page h2{margin:0 0 12px 0;font-size:2.1rem;font-weight:800}
|
||||
.rule-section + .rule-section{margin-top:24px;padding-top:24px;border-top:1px solid rgba(255,255,255,0.06)}
|
||||
.rule-section h3{margin:0 0 12px 0;font-size:1.28rem;font-weight:750;color:#f4f8ff}
|
||||
.rule-section h3{margin:0 0 12px 0;font-size:1.28rem;font-weight:750;color:var(--text)}
|
||||
.rule-section h4{margin:18px 0 10px 0;font-size:0.95rem;font-weight:800;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent)}
|
||||
.rule-section p{margin:0 0 12px 0;line-height:1.7;color:#dce8f5}
|
||||
.rule-section p{margin:0 0 12px 0;line-height:1.7;color:var(--text)}
|
||||
.rules-list{margin:0;padding-left:22px}
|
||||
.rules-list li{margin:0 0 8px 0;line-height:1.55;color:#dce8f5}
|
||||
.rules-list li{margin:0 0 8px 0;line-height:1.55;color:var(--text)}
|
||||
.error-shell main{min-height:calc(100vh - 84px);display:flex;align-items:center;justify-content:center;padding:32px 20px 56px}
|
||||
.error-page{width:min(100%,980px);padding:0;overflow:hidden;border:1px solid rgba(92,197,255,0.12);background:
|
||||
linear-gradient(140deg,rgba(92,197,255,0.1),rgba(7,19,38,0.95) 34%),
|
||||
@@ -80,15 +331,33 @@ linear-gradient(180deg,rgba(8,18,32,0.98),rgba(7,19,38,0.94))}
|
||||
.error-copy{padding:34px 36px}
|
||||
.error-code{display:inline-block;margin:0 0 16px 0;padding:8px 14px;border-radius:999px;background:rgba(92,197,255,0.1);color:var(--accent);font-size:0.84rem;font-weight:800;letter-spacing:0.16em;text-transform:uppercase}
|
||||
.error-copy h2{margin:0 0 12px 0;font-size:clamp(2.2rem,5vw,4.6rem);font-weight:900;letter-spacing:0.03em}
|
||||
.error-copy p{margin:0 0 14px 0;max-width:56ch;line-height:1.7;color:#dce8f5}
|
||||
.error-copy p{margin:0 0 14px 0;max-width:56ch;line-height:1.7;color:var(--text)}
|
||||
.error-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:24px}
|
||||
.error-actions .btn{font-weight:700;text-decoration:none}
|
||||
.ghost-btn{background:transparent;color:var(--text);border:1px solid rgba(255,255,255,0.14)}
|
||||
.auth-main{max-width:520px;margin:0 auto;padding:28px 20px 48px}
|
||||
.auth-panel{padding:28px 26px}
|
||||
.auth-panel h2{margin:0 0 8px 0;font-size:1.45rem}
|
||||
.auth-form{display:grid;gap:16px;margin-top:18px}
|
||||
.auth-form .auth-hint{margin:-6px 0 0 0;font-size:.82rem}
|
||||
.auth-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:8px}
|
||||
.account-main{max-width:820px;margin:0 auto;padding:28px 20px 48px;display:grid;gap:18px}
|
||||
.account-hero,.account-panel{padding:28px 26px}
|
||||
.account-hero{background:linear-gradient(145deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96) 42%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 35%);border:1px solid rgba(255,184,77,0.16);border-radius:28px}
|
||||
.account-hero h2,.account-panel h2,.account-panel h3{margin:0 0 8px 0}
|
||||
.account-summary-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:18px}
|
||||
.account-summary-card{padding:18px;border-radius:18px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08)}
|
||||
.account-summary-label{display:block;margin-bottom:8px;font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}
|
||||
.account-summary-card strong{font-size:1.15rem;color:var(--text)}
|
||||
.account-panel{border-radius:26px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
|
||||
header nav .fedl-auth-nav{margin-left:14px;display:inline-flex;align-items:center;flex-wrap:wrap;gap:8px 10px;vertical-align:middle}
|
||||
header nav .fedl-auth-nav a{color:var(--muted)}
|
||||
header nav .fedl-auth-nav a:hover{color:var(--text)}
|
||||
.error-side{display:flex;flex-direction:column;justify-content:space-between;gap:18px;padding:34px 30px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))}
|
||||
.error-side h3{margin:0 0 10px 0;font-size:1rem;letter-spacing:0.04em;text-transform:uppercase;color:#eef6ff}
|
||||
.error-side h3{margin:0 0 10px 0;font-size:1rem;letter-spacing:0.04em;text-transform:uppercase;color:var(--text)}
|
||||
.error-list{margin:0;padding:0;list-style:none}
|
||||
.error-list li + li{margin-top:10px}
|
||||
.error-list a{display:block;padding:12px 14px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);color:#eaf2fb;text-decoration:none}
|
||||
.error-list a{display:block;padding:12px 14px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);color:var(--text);text-decoration:none}
|
||||
.error-list a span{display:block;color:var(--muted);font-size:0.88rem}
|
||||
.error-tip{margin:0;padding:14px 16px;border-radius:12px;background:rgba(92,197,255,0.08);border:1px solid rgba(92,197,255,0.12);color:#d9ecff}
|
||||
.error-collection{width:min(100%,1100px)}
|
||||
@@ -97,20 +366,143 @@ linear-gradient(180deg,rgba(8,18,32,0.98),rgba(7,19,38,0.94))}
|
||||
.status-card{display:block;padding:18px;border-radius:14px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06);text-decoration:none;color:var(--text)}
|
||||
.status-card strong{display:block;margin-bottom:6px;font-size:1.65rem}
|
||||
.status-card span{display:block;color:var(--muted);line-height:1.5}
|
||||
.search-row input[type=number],.search-row input[type=url],.search-row input:not([type]){flex:1;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--text)}
|
||||
.btn + .btn{margin-left:8px}
|
||||
.manage-hero h2{margin:0 0 8px 0;font-size:2rem}
|
||||
.manage-grid{display:grid;grid-template-columns:minmax(320px,1.05fr) minmax(260px,0.95fr);gap:22px}
|
||||
.manage-form{display:grid;gap:14px}
|
||||
.manage-form label{display:grid;gap:8px;font-weight:600;color:#eaf3fe}
|
||||
.manage-form input{padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.02);color:var(--text)}
|
||||
.manage-actions{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.manage-actions .btn + .btn{margin-left:0}
|
||||
.manage-status{margin-top:18px;line-height:1.6}
|
||||
.manage-search-row{margin-bottom:0}
|
||||
.table-actions{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.table-actions .btn{margin-left:0}
|
||||
.table-link{color:var(--accent);text-decoration:none}
|
||||
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.video-modal iframe{height:320px}.roulette-head{flex-direction:column;align-items:flex-start}.discord-panel{flex-direction:column;align-items:flex-start;padding:22px 20px}.discord-panel h2{font-size:1.55rem}.discord-actions{width:100%;justify-content:flex-start}.rules-view header{padding:16px 18px}.rules-view header h1{font-size:1.55rem}.rules-page{padding:24px 20px}.rules-page h2{font-size:1.65rem}}
|
||||
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.sidebar .panel{height:auto}.video-modal iframe{height:320px}.modal-runs-head,.modal-run-card{grid-template-columns:1fr}.home-hero{grid-template-columns:1fr;min-height:auto}.home-copy,.hero-stage{padding:24px 22px}.hero-stats,.home-section-grid,.spotlight-grid{grid-template-columns:1fr}.spotlight-panel{grid-template-columns:1fr;padding:24px 22px}.list-hero-panel,.table-header,.search-row,.roulette-head,.guess-controls{flex-direction:column;align-items:flex-start}.control-select{max-width:none;width:100%}.roulette-shell{grid-template-columns:1fr}.discord-panel{flex-direction:column;align-items:flex-start;padding:22px 20px}.discord-panel h2{font-size:1.55rem}.discord-actions{width:100%;justify-content:flex-start}.rules-view header{padding:16px 18px}.rules-view header h1{font-size:1.55rem}.rules-page{padding:24px 20px}.rules-page h2{font-size:1.65rem}.guess-controls input,.guess-controls .btn{width:100%}}
|
||||
@media(max-width:900px){.error-grid{grid-template-columns:1fr}.error-copy,.error-side{padding:24px 20px}.error-copy h2{font-size:2.5rem}}
|
||||
@media(max-width:900px){.manage-grid{grid-template-columns:1fr}}
|
||||
@media(max-width:900px){.admin-overview-panel,.admin-toolbar{flex-direction:column;align-items:stretch}.admin-actions{flex-direction:column;align-items:stretch}.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:0;width:100%}.run-shell{grid-template-columns:1fr}.run-head{flex-direction:column;align-items:flex-start}.run-form{grid-template-columns:1fr}.admin-login-screen{padding:18px}.admin-login-card{padding:26px 22px}}
|
||||
@media(max-width:640px){header{padding:12px 16px;flex-wrap:wrap;gap:10px}header h1{font-size:1rem}nav{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}nav a{padding:8px 12px;border-radius:20px;font-size:0.85rem}header nav a.btn{margin-left:0}.home-hero{grid-template-columns:1fr;min-height:auto;gap:16px}.home-copy,.hero-stage{padding:20px 18px}.home-copy h2{font-size:2rem;max-width:none}.hero-text{font-size:0.95rem}.hero-actions{flex-direction:column;align-items:stretch;gap:10px}.hero-actions .btn,.roulette-btn{width:100%;text-align:center}.hero-stats{grid-template-columns:1fr;gap:10px}.spotlight-panel{grid-template-columns:1fr;padding:20px 18px}.spotlight-grid{grid-template-columns:1fr;gap:12px}.home-section-grid{grid-template-columns:1fr;gap:12px}.info-panel{padding:18px 16px}.list-hero-panel,.list-controls-panel,.list-table-panel,.roulette-panel,.roulette-rules-card,.run-panel,.run-side-panel,.submission-panel,.account-hero,.account-panel{padding:18px 16px}.table-header{flex-direction:column;align-items:flex-start;gap:12px}.search-row{flex-direction:column;align-items:stretch}.control-select{max-width:none;width:100%}.table-hint{text-align:left}.roulette-shell,.guess-controls{flex-direction:column;gap:12px}.guess-controls input,.guess-controls .btn{width:100%}.account-summary-grid{grid-template-columns:1fr}.theme-options{grid-template-columns:repeat(2,1fr);gap:12px}.run-admin-actions{flex-direction:column;align-items:stretch}}
|
||||
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:14px;margin-top:16px}
|
||||
.stat-box{padding:16px;border-radius:14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);text-align:center}
|
||||
.stat-box strong{display:block;margin-bottom:6px;font-size:1.75rem;color:var(--accent)}
|
||||
.stat-box span{display:block;color:var(--muted);font-size:.9rem}
|
||||
.stat-box-small{padding:12px}
|
||||
.stat-box-small strong{font-size:1.25rem}
|
||||
.stat-box-small span{font-size:.8rem}
|
||||
.bug-report-detail-row td{padding-top:0}
|
||||
.bug-report-detail{display:grid;gap:8px;padding:0 0 14px 0;color:var(--text)}
|
||||
.bug-report-detail a{color:var(--accent)}
|
||||
.admin-tabs{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
|
||||
.admin-tab{padding:10px 18px;border-radius:999px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:var(--muted);cursor:pointer;font-weight:700;transition:all .2s ease}
|
||||
.admin-tab:hover{background:rgba(255,255,255,0.08);color:var(--text)}
|
||||
.admin-tab.active{background:var(--accent);color:var(--bg)}
|
||||
.admin-tab-panel{display:block}
|
||||
.admin-tab-panel[hidden]{display:none}
|
||||
|
||||
.theme-options{display:flex;gap:16px;flex-wrap:wrap;margin:16px 0}
|
||||
.theme-option{display:flex;flex-direction:column;align-items:center;gap:8px;cursor:pointer}
|
||||
.theme-option input{display:none}
|
||||
.theme-option:has(input:checked) .theme-preview{outline:2px solid var(--accent);outline-offset:2px}
|
||||
.theme-preview{width:60px;height:40px;border-radius:8px;border:1px solid rgba(255,255,255,0.1)}
|
||||
.theme-preview-dark{background:linear-gradient(135deg,#0f1724,#1a2744)}
|
||||
.theme-preview-light{background:linear-gradient(135deg,#f0f4f8,#d9e2ec)}
|
||||
.theme-preview-blue{background:linear-gradient(135deg,#0d1b2a,#1b3a5f)}
|
||||
.theme-preview-midnight{background:linear-gradient(135deg,#0a0a12,#1a1a2e)}
|
||||
.theme-preview-cyberpunk{background:linear-gradient(135deg,#0f0f1a,#2d1b4e)}
|
||||
.theme-preview-earth{background:linear-gradient(135deg,#1a2f1a,#2d4a2d)}
|
||||
.theme-preview-retro{background:linear-gradient(135deg,#2b1a0a,#4a2c17)}
|
||||
.theme-preview-matrix{background:linear-gradient(135deg,#000a00,#001100)}
|
||||
.theme-preview-synthwave{background:linear-gradient(135deg,#1a0a2e,#ff2a6d)}
|
||||
.theme-preview-fire{background:linear-gradient(135deg,#1a0505,#ff4500)}
|
||||
.theme-preview-galaxy{background:linear-gradient(135deg,#0a0612,#e056fd)}
|
||||
.theme-preview-candy{background:linear-gradient(135deg,#fdf2f8,#f472b6)}
|
||||
.theme-preview-highcontrast{background:linear-gradient(135deg,#000,#fff)}
|
||||
.theme-preview-original{background:linear-gradient(135deg,#0f1724,#071326)}
|
||||
|
||||
[data-theme="cyberpunk"] .btn{box-shadow:0 0 12px var(--accent), inset 0 0 8px rgba(0,255,159,0.1);position:relative;overflow:hidden}
|
||||
[data-theme="cyberpunk"] .btn::after{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,255,159,0.3),transparent);animation:cyberglitch 3s infinite}
|
||||
@keyframes cyberglitch{0%{left:-100%}50%,100%{left:100%}}
|
||||
[data-theme="cyberpunk"] .panel{border:1px solid rgba(0,255,159,0.3);box-shadow:0 0 20px rgba(0,255,159,0.1),inset 0 0 30px rgba(0,255,159,0.02)}
|
||||
[data-theme="cyberpunk"] h1,[data-theme="cyberpunk"] h2,[data-theme="cyberpunk"] h3{text-shadow:0 0 10px var(--accent)}
|
||||
[data-theme="cyberpunk"] header{position:relative;overflow:hidden}
|
||||
[data-theme="cyberpunk"] header::after{content:"";position:absolute;bottom:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:scanline 4s linear infinite}
|
||||
@keyframes scanline{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
||||
[data-theme="earth"] .panel{border:1px solid rgba(132,204,22,0.2);background:linear-gradient(180deg,var(--panel),color-mix(in srgb,var(--accent) 5%,var(--panel)))}
|
||||
[data-theme="earth"] header{background:linear-gradient(90deg,color-mix(in srgb,#1a2f1a 90%,var(--accent)),color-mix(in srgb,#2d4a2d 80%,var(--accent)))}
|
||||
[data-theme="earth"] .btn{border:2px solid var(--accent);box-shadow:0 4px 12px color-mix(in srgb,var(--accent) 30%,transparent);position:relative;overflow:hidden}
|
||||
[data-theme="earth"] .btn::before{content:"🌿";position:absolute;left:6px;font-size:0.7rem;z-index:1}
|
||||
[data-theme="earth"] .account-summary-card{position:relative}
|
||||
[data-theme="earth"] .account-summary-card::before{content:"🌲";position:absolute;top:-8px;right:-4px;font-size:1rem}
|
||||
[data-theme="earth"] nav a:hover{color:var(--accent-warm)}
|
||||
[data-theme="earth"] .hero-kicker::before{content:"🍃 "}
|
||||
[data-theme="earth"] .hero-kicker::after{content:" 🍃"}
|
||||
[data-theme="earth"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 10%,transparent),transparent 30%),linear-gradient(180deg,var(--bg),var(--panel))}
|
||||
[data-theme="earth"] header{position:relative}
|
||||
[data-theme="earth"] header::after{content:"";position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,var(--accent),transparent)}
|
||||
[data-theme="earth"] nav a:hover{color:var(--accent-warm);transform:scale(1.05)}
|
||||
[data-theme="dark"] .btn{border-radius:8px;box-shadow:0 4px 15px rgba(92,197,255,0.3)}
|
||||
[data-theme="dark"] .btn:hover{box-shadow:0 6px 25px rgba(92,197,255,0.5)}
|
||||
[data-theme="light"] .btn{border-radius:4px;background:linear-gradient(135deg,var(--accent),#0ea5e9);border:2px solid #0369a1}
|
||||
[data-theme="light"] .btn:hover{transform:translateY(-2px);box-shadow:0 6px 15px rgba(2,132,199,0.4)}
|
||||
[data-theme="blue"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 15%,transparent),transparent 28%),linear-gradient(180deg,#071826 0%,#0d1b2a 100%)}
|
||||
[data-theme="blue"] .btn{border-radius:20px;clip-path:polygon(10px 0,100% 0,calc(100% - 10px) 100%,0 100%)}
|
||||
[data-theme="blue"] .btn:hover{animation:waveshiver 0.5s ease}
|
||||
@keyframes waveshiver{0%,100%{transform:translateX(0)}25%{transform:translateX(2px)}75%{transform:translateX(-2px)}}
|
||||
[data-theme="midnight"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 10%,transparent),transparent 28%),linear-gradient(180deg,#05050a 0%,#0a0a12 100%)}
|
||||
[data-theme="midnight"] .btn{border-radius:2px 8px 2px 8px;border:1px solid var(--accent);background:transparent;color:var(--accent)}
|
||||
[data-theme="midnight"] .btn:hover{background:var(--accent);color:var(--bg);animation:pulse 1s infinite}
|
||||
@keyframes pulse{0%,100%{box-shadow:0 0 5px var(--accent)}50%{box-shadow:0 0 20px var(--accent)}}
|
||||
[data-theme="cyberpunk"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 12%,transparent),transparent 28%),linear-gradient(180deg,#0a0512 0%,#0f0f1a 100%)}
|
||||
[data-theme="cyberpunk"] .btn{border-radius:2px;clip-path:polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,8px 100%,0 calc(100% - 8px));border:1px solid var(--accent);box-shadow:0 0 15px var(--accent),inset 0 0 10px rgba(0,255,159,0.2);position:relative;overflow:hidden}
|
||||
[data-theme="cyberpunk"] .btn::after{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,255,159,0.4),transparent);animation:cyberglitch 2s infinite}
|
||||
@keyframes cyberglitch{0%{left:-100%}50%,100%{left:100%}}
|
||||
[data-theme="retro"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 10%,transparent),transparent 28%),linear-gradient(180deg,#120c05 0%,#1a1208 100%);position:relative}
|
||||
[data-theme="retro"] body::after{content:"";position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.3) 2px,rgba(0,0,0,0.3) 4px);pointer-events:none;z-index:9999;animation:scanlines 0.1s linear infinite}
|
||||
[data-theme="retro"] .panel{border:2px dashed var(--accent);box-shadow:inset 0 0 20px rgba(255,159,28,0.1)}
|
||||
[data-theme="retro"] .btn{border-radius:0;border:3px double var(--accent);background:transparent;color:var(--accent);text-transform:uppercase;letter-spacing:2px;position:relative}
|
||||
[data-theme="retro"] .btn:hover{background:var(--accent);color:var(--bg);box-shadow:0 0 15px var(--accent)}
|
||||
@keyframes scanlines{0%{background-position:0 0}100%{background-position:0 4px}}
|
||||
[data-theme="matrix"] body{background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,255,0,0.03) 2px,rgba(0,255,0,0.03) 4px),linear-gradient(180deg,#000a00,#001100);position:relative;overflow:hidden}
|
||||
[data-theme="matrix"] body::before{content:"101011001 01101001 10101010 01010101";position:fixed;top:0;left:0;right:0;bottom:0;font-family:monospace;font-size:14px;line-height:20px;color:rgba(0,255,0,0.05);overflow:hidden;pointer-events:none;white-space:pre-wrap;word-break:break-all;animation:matrixfall 20s linear infinite;z-index:-1}
|
||||
@keyframes matrixfall{0%{transform:translateY(0)}100%{transform:translateY(-50%)}}
|
||||
[data-theme="matrix"] .panel{border:1px solid var(--accent);box-shadow:0 0 10px var(--accent)}
|
||||
[data-theme="matrix"] .btn{font-family:monospace;border-radius:0;border:1px solid var(--accent);background:#001100;text-shadow:0 0 5px var(--accent);letter-spacing:1px;position:relative;overflow:hidden}
|
||||
[data-theme="matrix"] .btn::after{content:">";position:absolute;right:8px;opacity:0;transition:opacity 0.2s}
|
||||
[data-theme="matrix"] .btn:hover::after{opacity:1}
|
||||
[data-theme="matrix"] header::before{content:"wake up, neo...";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-family:monospace;font-size:0.6rem;opacity:0.1;color:var(--accent);white-space:nowrap}
|
||||
[data-theme="synthwave"] body{background:linear-gradient(180deg,#1a0a2e 0%,#2d1b4e 50%,#0f0f1a 100%);position:relative}
|
||||
[data-theme="synthwave"] body::after{content:"";position:fixed;bottom:0;left:0;right:0;height:200px;background:linear-gradient(0deg,rgba(5,217,232,0.1) 0%,transparent 100%);clip-path:polygon(0 100%,5% 80%,10% 90%,15% 70%,20% 85%,25% 60%,30% 75%,35% 65%,40% 80%,45% 55%,50% 70%,55% 60%,60% 75%,65% 50%,70% 65%,75% 55%,80% 70%,85% 60%,90% 75%,95% 65%,100% 80%,100% 100%);pointer-events:none}
|
||||
[data-theme="synthwave"] .panel{border:2px solid var(--accent);box-shadow:0 0 20px var(--accent),inset 0 0 20px rgba(255,42,109,0.1)}
|
||||
[data-theme="synthwave"] .btn{border-radius:0;clip-path:polygon(15px 0,100% 0,100% calc(100% - 15px),calc(100% - 15px) 100%,0 100%,0 15px);background:linear-gradient(135deg,var(--accent),#05d9e8);color:#1a0a2e;font-weight:900;text-transform:uppercase;position:relative}
|
||||
[data-theme="synthwave"] .btn::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.3),transparent);animation:synthwaveglitch 2s infinite}
|
||||
@keyframes synthwaveglitch{0%{transform:translateX(-100%)}50%,100%{transform:translateX(100%)}}
|
||||
[data-theme="synthwave"] h1,[data-theme="synthwave"] h2,[data-theme="synthwave"] h3{font-family:"Courier New",monospace;text-transform:uppercase;letter-spacing:0.1em}
|
||||
[data-theme="fire"] body{background:radial-gradient(ellipse at bottom,rgba(255,69,0,0.2) 0%,transparent 60%),linear-gradient(180deg,#1a0505,#2d0a0a);position:relative}
|
||||
[data-theme="fire"] body::after{content:"";position:fixed;inset:0;background:radial-gradient(ellipse at bottom,rgba(255,100,0,0.1) 0%,transparent 70%);pointer-events:none;animation:flameflicker 3s ease-in-out infinite}
|
||||
@keyframes flameflicker{0%,100%{opacity:0.5}50%{opacity:1}}
|
||||
[data-theme="fire"] .panel{border:1px solid rgba(255,69,0,0.4);box-shadow:0 0 30px rgba(255,69,0,0.2)}
|
||||
[data-theme="fire"] .btn{border-radius:50px;background:linear-gradient(180deg,#ff4500,#ff6b35);box-shadow:0 4px 20px rgba(255,69,0,0.5),inset 0 2px 0 rgba(255,255,255,0.2);border:none;text-shadow:0 1px 2px rgba(0,0,0,0.3);position:relative;overflow:hidden}
|
||||
[data-theme="fire"] .btn::after{content:"";position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:linear-gradient(45deg,transparent,rgba(255,255,255,0.1),transparent);transform:rotate(45deg);animation:fireglint 2s infinite}
|
||||
@keyframes fireglint{0%{transform:translateX(-100%) rotate(45deg)}100%{transform:translateX(100%) rotate(45deg)}}
|
||||
[data-theme="fire"] header{background:linear-gradient(90deg,rgba(255,69,0,0.2),rgba(255,170,0,0.2))}
|
||||
[data-theme="galaxy"] body{background:radial-gradient(circle at 30% 20%,rgba(224,86,253,0.15),transparent 50%),radial-gradient(circle at 70% 80%,rgba(249,202,36,0.1),transparent 40%),linear-gradient(180deg,#0a0612,#150f25);position:relative}
|
||||
[data-theme="galaxy"] body::before{content:"";position:fixed;inset:0;background-image:radial-gradient(2px 2px at 20px 30px,rgba(255,255,255,0.3),transparent),radial-gradient(2px 2px at 40px 70px,rgba(255,255,255,0.2),transparent),radial-gradient(1px 1px at 90px 40px,rgba(255,255,255,0.4),transparent),radial-gradient(2px 2px at 130px 80px,rgba(255,255,255,0.2),transparent),radial-gradient(1px 1px at 160px 120px,rgba(255,255,255,0.5),transparent);background-size:200px 200px;animation:twinkle 4s ease-in-out infinite;pointer-events:none}
|
||||
@keyframes twinkle{0%,100%{opacity:0.5}50%{opacity:1}}
|
||||
[data-theme="galaxy"] .panel{background:linear-gradient(135deg,var(--panel),color-mix(in srgb,var(--accent) 20%,var(--card)));border:1px solid rgba(224,86,253,0.3)}
|
||||
[data-theme="galaxy"] .btn{border-radius:30px;background:linear-gradient(135deg,var(--accent),var(--accent-warm));color:#0a0612;box-shadow:0 0 20px color-mix(in srgb,var(--accent) 50%,transparent);position:relative;overflow:hidden}
|
||||
[data-theme="galaxy"] .btn::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.4),transparent);animation:galaxysparkle 2s infinite}
|
||||
@keyframes galaxysparkle{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
||||
[data-theme="galaxy"] header{background:linear-gradient(90deg,rgba(224,86,253,0.2),rgba(249,202,36,0.1))}
|
||||
[data-theme="candy"] body{background:radial-gradient(circle at top left,rgba(244,114,182,0.2),transparent 40%),radial-gradient(circle at bottom right,rgba(52,211,153,0.15),transparent 40%),linear-gradient(180deg,#fdf2f8,#fce7f3);position:relative;overflow:hidden}
|
||||
[data-theme="candy"] body::before{content:"";position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M30 5 L35 25 L55 30 L35 35 L30 55 L25 35 L5 30 L25 25 Z' fill='none' stroke='rgba(244,114,182,0.08)' stroke-width='1'/%3E%3C/svg%3E");pointer-events:none;animation:drift 20s linear infinite}
|
||||
@keyframes drift{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
|
||||
[data-theme="candy"] .panel{background:linear-gradient(180deg,var(--panel),#fff);border:2px solid rgba(244,114,182,0.3);border-radius:20px}
|
||||
[data-theme="candy"] .btn{border-radius:25px;background:linear-gradient(135deg,#f472b6,#34d399);box-shadow:0 6px 20px rgba(244,114,182,0.4);border:none;position:relative;overflow:hidden}
|
||||
[data-theme="candy"] .btn::before{content:"";position:absolute;top:50%;left:50%;width:0;height:0;background:rgba(255,255,255,0.5);border-radius:50%;transform:translate(-50%,-50%);transition:width 0.3s,height 0.3s}
|
||||
[data-theme="candy"] .btn:active::before{width:200px;height:200px}
|
||||
[data-theme="candy"] header{background:linear-gradient(90deg,rgba(244,114,182,0.15),rgba(52,211,153,0.1))}
|
||||
[data-theme="candy"] nav a:hover{color:var(--accent)}
|
||||
[data-theme="highcontrast"] body{background:#000}
|
||||
[data-theme="highcontrast"] .panel{border:3px solid var(--accent)}
|
||||
[data-theme="highcontrast"] .btn{background:var(--accent);color:var(--bg);border:3px solid var(--text);border-radius:0;font-weight:900;letter-spacing:1px;text-transform:uppercase}
|
||||
[data-theme="highcontrast"] .btn:hover{background:var(--text);color:var(--bg);box-shadow:0 0 0 3px var(--accent)}
|
||||
[data-theme="highcontrast"] header{border-bottom:3px solid var(--accent)}
|
||||
[data-theme="highcontrast"] nav a{font-weight:700}
|
||||
[data-theme="highcontrast"] nav a:hover{background:var(--accent);color:var(--bg)}
|
||||
.skeleton{background:linear-gradient(90deg,rgba(255,255,255,0.05) 25%,rgba(255,255,255,0.1) 50%,rgba(255,255,255,0.05) 75%);background-size:200% 100%;animation:skeleton-loading 1.5s ease-in-out infinite;border-radius:4px}
|
||||
.skeleton-text{height:1em;margin:0.5em 0}
|
||||
.skeleton-title{height:1.5em;width:60%;margin:0.5em 0}
|
||||
.skeleton-box{height:80px;border-radius:8px}
|
||||
@keyframes skeleton-loading{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||||
.page-transition{opacity:0;transition:opacity 0.3s ease}
|
||||
.page-transition.loaded{opacity:1}
|
||||
body{animation:page-enter 0.3s ease}
|
||||
@keyframes page-enter{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<head>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
|
||||
crossorigin="anonymous"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content="FEDL Support - Get help with submissions, rules, and technical issues.">
|
||||
<meta name="keywords" content="support, help, fedl, geometry dash, demon list">
|
||||
<title>FEDL - Support</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/png" href="pageicon.png">
|
||||
</head>
|
||||
<body data-page="support">
|
||||
<header>
|
||||
<h1>FEDL - Support</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
<a href="lists.html">Lists</a>
|
||||
<a href="players.html">Players</a>
|
||||
<a href="run.html">Submit Run</a>
|
||||
<a href="roulette.html">Roulette</a>
|
||||
<a href="rules.html">Rules</a>
|
||||
<a href="messages.html">Messages</a>
|
||||
<a href="contact.html">Contact</a>
|
||||
<a href="about.html">About</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section class="panel">
|
||||
<p class="status-label">Support</p>
|
||||
<h2>Need Help?</h2>
|
||||
<p class="muted">Find answers to common questions below. If you still need assistance, use the contact page to reach out.</p>
|
||||
|
||||
<details class="faq-item">
|
||||
<summary>How do I submit a run?</summary>
|
||||
<p>Go to the Submit Run page, fill out the form with your video link, raw footage, and notes. Your submission will be reviewed by moderators.</p>
|
||||
</details>
|
||||
|
||||
<details class="faq-item">
|
||||
<summary>Why was my run rejected?</summary>
|
||||
<p>Common reasons include missing raw footage, mod usage violations, or incomplete submission details. Check the Rules page for full requirements.</p>
|
||||
</details>
|
||||
|
||||
<details class="faq-item">
|
||||
<summary>How are demons ranked?</summary>
|
||||
<p>Demon rankings are based on difficulty, creativity, and popularity. The list is regularly updated by our moderation team.</p>
|
||||
</details>
|
||||
|
||||
<details class="faq-item">
|
||||
<summary>Can I suggest a level for the list?</summary>
|
||||
<p>Yes! Use the contact page to suggest levels. Include the level ID and why you think it deserves to be ranked.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<p class="status-label">Advertisements</p>
|
||||
<h2>Support FEDL</h2>
|
||||
<p class="muted">This website displays ads to help cover hosting and maintenance costs. Thank you for your support!</p>
|
||||
|
||||
<div class="ad-container">
|
||||
<ins class="adsbygoogle"
|
||||
data-ad-client="YOUR-AD-CLIENT-ID"
|
||||
data-ad-slot="YOUR-AD-SLOT-ID"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"></ins>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<p class="status-label">Resources</p>
|
||||
<h2>Quick Links</h2>
|
||||
<div class="spotlight-grid">
|
||||
<article class="spotlight-card">
|
||||
<h3>Rules</h3>
|
||||
<p>Learn about submission requirements and moderation policies.</p>
|
||||
<a class="text-link" href="rules.html">View Rules</a>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Submit Run</h3>
|
||||
<p>Send in your completion for review.</p>
|
||||
<a class="text-link" href="run.html">Submit</a>
|
||||
</article>
|
||||
<article class="spotlight-card">
|
||||
<h3>Contact</h3>
|
||||
<p>Get in touch with the moderation team.</p>
|
||||
<a class="text-link" href="contact.html">Contact Us</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
|
||||
<script>
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user