Switch to Long-Polling, add Turniertabellen-Anzeige für ODJM D 2026
This commit is contained in:
88
app.js
88
app.js
@@ -3,37 +3,40 @@
|
|||||||
* Haupt-Application
|
* Haupt-Application
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PGN_URL = 'https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn';
|
|
||||||
const REFRESH_INTERVAL = 10000; // 10 Sekunden
|
|
||||||
const PLAYER_NAME = 'Kiesewetter, Lara';
|
const PLAYER_NAME = 'Kiesewetter, Lara';
|
||||||
|
|
||||||
let board = null;
|
let board = null;
|
||||||
let chess = null;
|
let chess = null;
|
||||||
let currentGame = null;
|
let currentGame = null;
|
||||||
let allLaraGames = [];
|
let allLaraGames = [];
|
||||||
let refreshTimer = null;
|
|
||||||
let countdown = 0;
|
|
||||||
let serverLastFetch = null;
|
let serverLastFetch = null;
|
||||||
let laraColor = null;
|
let laraColor = null;
|
||||||
let currentMoveIndex = -1;
|
let currentMoveIndex = -1;
|
||||||
let userSelectedGame = false;
|
let userSelectedGame = false;
|
||||||
let userScrolledMoves = false;
|
let userScrolledMoves = false;
|
||||||
|
let lastMtime = 0;
|
||||||
|
let pollId = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt die PGN-Datei und aktualisiert die Anzeige
|
* Lädt die PGN-Datei und aktualisiert die Anzeige
|
||||||
*/
|
*/
|
||||||
async function loadPGN(showOverlay = true) {
|
async function loadPGN(showOverlay = true) {
|
||||||
|
const currentPollId = ++pollId;
|
||||||
if (showOverlay) showLoading(true);
|
if (showOverlay) showLoading(true);
|
||||||
hideError();
|
hideError();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [pgnResponse, statusResponse] = await Promise.all([
|
const [pgnResponse, statusResponse] = await Promise.all([
|
||||||
fetch('http://localhost:8111/pgn'),
|
fetch(`http://localhost:8111/pgn?since=${lastMtime}`),
|
||||||
fetch('http://localhost:8111/status').catch(() => null)
|
fetch('http://localhost:8111/status').catch(() => null)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (currentPollId !== pollId) return;
|
||||||
if (!pgnResponse.ok) throw new Error(`HTTP ${pgnResponse.status}`);
|
if (!pgnResponse.ok) throw new Error(`HTTP ${pgnResponse.status}`);
|
||||||
|
|
||||||
|
const mtimeHeader = pgnResponse.headers.get('X-Cache-Mtime');
|
||||||
|
if (mtimeHeader) lastMtime = parseFloat(mtimeHeader);
|
||||||
|
|
||||||
if (statusResponse && statusResponse.ok) {
|
if (statusResponse && statusResponse.ok) {
|
||||||
const status = await statusResponse.json();
|
const status = await statusResponse.json();
|
||||||
serverLastFetch = status.last_fetch ? status.last_fetch * 1000 : null;
|
serverLastFetch = status.last_fetch ? status.last_fetch * 1000 : null;
|
||||||
@@ -62,10 +65,12 @@ async function loadPGN(showOverlay = true) {
|
|||||||
updateMovesList();
|
updateMovesList();
|
||||||
updateAllGamesList();
|
updateAllGamesList();
|
||||||
updateTimestamp();
|
updateTimestamp();
|
||||||
|
updateStandings();
|
||||||
|
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (currentPollId !== pollId) return;
|
||||||
console.error('Fehler beim Laden:', error);
|
console.error('Fehler beim Laden:', error);
|
||||||
showError(`Fehler: ${error.message}`);
|
showError(`Fehler: ${error.message}`);
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -367,6 +372,49 @@ function updateAllGamesList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die Turniertabelle vom Proxy und zeigt Laras Platzierung an
|
||||||
|
*/
|
||||||
|
function updateStandings() {
|
||||||
|
fetch('http://localhost:8111/standings')
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Laden');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
const container = document.getElementById('standings-content');
|
||||||
|
if (!data || data.error) {
|
||||||
|
container.innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="standings-rank">${data.rank}.</div>
|
||||||
|
<div class="standings-rank-label">Tabellenplatz</div>
|
||||||
|
<div class="standings-header">${data.round_info || 'nach Runde 1'}</div>
|
||||||
|
<div class="standings-row">
|
||||||
|
<span class="standings-label">Punkte</span>
|
||||||
|
<span class="standings-value">${data.points}</span>
|
||||||
|
</div>
|
||||||
|
<div class="standings-row">
|
||||||
|
<span class="standings-label">Siege</span>
|
||||||
|
<span class="standings-value">${data.wins}</span>
|
||||||
|
</div>
|
||||||
|
<div class="standings-row">
|
||||||
|
<span class="standings-label">Unentschieden</span>
|
||||||
|
<span class="standings-value">${data.draws}</span>
|
||||||
|
</div>
|
||||||
|
<div class="standings-row">
|
||||||
|
<span class="standings-label">Niederlagen</span>
|
||||||
|
<span class="standings-value">${data.losses}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
document.getElementById('standings-content').innerHTML =
|
||||||
|
'<div class="standings-loading">Daten nicht verfügbar</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format clock string
|
* Format clock string
|
||||||
*/
|
*/
|
||||||
@@ -386,25 +434,19 @@ function updateTimestamp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start auto-refresh
|
* Start long-polling: nach jeder Antwort sofort die nächste Anfrage stellen
|
||||||
*/
|
*/
|
||||||
function startAutoRefresh() {
|
function startAutoRefresh() {
|
||||||
countdown = REFRESH_INTERVAL / 1000;
|
document.getElementById('refresh-timer').textContent = '● Live';
|
||||||
|
document.getElementById('refresh-timer').style.color = '#4ade80';
|
||||||
|
|
||||||
if (refreshTimer) clearInterval(refreshTimer);
|
async function poll() {
|
||||||
|
while (true) {
|
||||||
refreshTimer = setInterval(() => {
|
await loadPGN(false);
|
||||||
countdown--;
|
await new Promise(r => setTimeout(r, 30000));
|
||||||
const mins = Math.floor(countdown / 60);
|
|
||||||
const secs = countdown % 60;
|
|
||||||
document.getElementById('refresh-timer').textContent =
|
|
||||||
`Nächstes Update in: ${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
||||||
|
|
||||||
if (countdown <= 0) {
|
|
||||||
countdown = REFRESH_INTERVAL / 1000;
|
|
||||||
loadPGN(false);
|
|
||||||
}
|
}
|
||||||
}, 1000);
|
}
|
||||||
|
poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -424,11 +466,11 @@ function hideError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manual refresh button
|
* Manual refresh button – startet neuen Long-Poll-Zyklus
|
||||||
*/
|
*/
|
||||||
document.getElementById('refresh-btn').addEventListener('click', () => {
|
document.getElementById('refresh-btn').addEventListener('click', () => {
|
||||||
countdown = REFRESH_INTERVAL / 1000;
|
pollId++;
|
||||||
loadPGN();
|
loadPGN(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -54,6 +54,12 @@
|
|||||||
<h3>Alle Partien von Lara</h3>
|
<h3>Alle Partien von Lara</h3>
|
||||||
<div id="all-games-list"></div>
|
<div id="all-games-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="standings-panel">
|
||||||
|
<h3>Turniertabelle ODJM D 2026</h3>
|
||||||
|
<div id="standings-content">
|
||||||
|
<div class="standings-loading">Lade Tabellenstand...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
157
server.py
157
server.py
@@ -7,30 +7,38 @@ Lädt die PGN-Datei und stellt sie mit CORS-Headern bereit.
|
|||||||
import http.server
|
import http.server
|
||||||
import socketserver
|
import socketserver
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
PGN_URL = os.environ.get("PGN_URL", "https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn")
|
PGN_URL = os.environ.get("PGN_URL", "https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn")
|
||||||
|
STANDINGS_URL = "https://www.deutsche-schachjugend.de/2026/odjm-d/tabelle/"
|
||||||
PORT = int(os.environ.get("PORT", 8111))
|
PORT = int(os.environ.get("PORT", 8111))
|
||||||
CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache")
|
CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache")
|
||||||
CACHE_FILE = os.path.join(CACHE_DIR, "gesamt-utf8.pgn")
|
CACHE_FILE = os.path.join(CACHE_DIR, "gesamt-utf8.pgn")
|
||||||
CACHE_TTL = int(os.environ.get("CACHE_TTL", 30)) # Sekunden
|
STANDINGS_CACHE_FILE = os.path.join(CACHE_DIR, "standings.json")
|
||||||
|
CACHE_TTL = int(os.environ.get("CACHE_TTL", 31)) # Sekunden
|
||||||
|
|
||||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
last_fetch_time = None
|
last_fetch_time = None
|
||||||
|
last_standings_fetch_time = None
|
||||||
|
|
||||||
|
|
||||||
def fetch_pgn():
|
def fetch_pgn():
|
||||||
"""Lädt die PGN-Datei von der URL als Bytes."""
|
"""Lädt die PGN-Datei von der URL als Bytes."""
|
||||||
global last_fetch_time
|
global last_fetch_time
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(PGN_URL, headers={"User-Agent": "Mozilla/5.0"})
|
headers = {"User-Agent": "Mozilla/5.0"}
|
||||||
with urllib.request.urlopen(req, timeout=30) as response:
|
if last_fetch_time:
|
||||||
|
headers["modified_since"] = str(last_fetch_time)
|
||||||
|
req = urllib.request.Request(PGN_URL, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=31) as response:
|
||||||
data = response.read()
|
data = response.read()
|
||||||
last_fetch_time = time.time()
|
last_fetch_time = time.time()
|
||||||
return data
|
return data
|
||||||
@@ -39,43 +47,117 @@ def fetch_pgn():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_pgn_content():
|
def get_pgn_content_longpoll(since=0):
|
||||||
"""Gibt PGN-Inhalt als Bytes zurück, nutzt Cache wenn möglich."""
|
"""Long-Poll: Gibt PGN-Inhalt + mtime zurück.
|
||||||
|
Wenn Cache seit `since` unverändert ist, wird bis zu 31s gewartet."""
|
||||||
|
deadline = time.time() + 31
|
||||||
|
|
||||||
|
while time.time() < deadline:
|
||||||
|
if os.path.exists(CACHE_FILE):
|
||||||
|
mtime = os.path.getmtime(CACHE_FILE)
|
||||||
|
if mtime > since:
|
||||||
|
with open(CACHE_FILE, "rb") as f:
|
||||||
|
return f.read(), mtime
|
||||||
|
age = time.time() - mtime
|
||||||
|
if age >= CACHE_TTL:
|
||||||
|
content = fetch_pgn()
|
||||||
|
if content:
|
||||||
|
with open(CACHE_FILE, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
new_mtime = os.path.getmtime(CACHE_FILE)
|
||||||
|
if new_mtime > since:
|
||||||
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] PGN via Long-Poll aktualisiert ({len(content)} Bytes)")
|
||||||
|
return content, new_mtime
|
||||||
|
else:
|
||||||
|
content = fetch_pgn()
|
||||||
|
if content:
|
||||||
|
with open(CACHE_FILE, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
return content, os.path.getmtime(CACHE_FILE)
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Timeout – aktuellen Stand zurückgeben
|
||||||
|
if os.path.exists(CACHE_FILE):
|
||||||
|
with open(CACHE_FILE, "rb") as f:
|
||||||
|
return f.read(), os.path.getmtime(CACHE_FILE)
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_standings():
|
||||||
|
"""Lädt die Tabellenseite und parst Laras Platzierung."""
|
||||||
|
global last_standings_fetch_time
|
||||||
|
try:
|
||||||
|
headers = {"User-Agent": "Mozilla/5.0"}
|
||||||
|
if last_standings_fetch_time:
|
||||||
|
headers["modified_since"] = str(last_standings_fetch_time)
|
||||||
|
req = urllib.request.Request(STANDINGS_URL, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=31) as response:
|
||||||
|
html = response.read().decode("utf-8", errors="replace")
|
||||||
|
last_standings_fetch_time = time.time()
|
||||||
|
|
||||||
|
# Rundeninfo extrahieren
|
||||||
|
round_info = ""
|
||||||
|
m = re.search(r"Tabellenstand\s+nach\s+der\s+(\d+)\.\s*Runde", html)
|
||||||
|
if m:
|
||||||
|
round_info = f"nach der {m.group(1)}. Runde"
|
||||||
|
|
||||||
|
# Tabellenzeilen suchen
|
||||||
|
rows = re.findall(r"<tr[^>]*>(.*?)</tr>", html, re.DOTALL | re.IGNORECASE)
|
||||||
|
for row in rows:
|
||||||
|
if "Lara Kiesewetter" not in row:
|
||||||
|
continue
|
||||||
|
cells = re.findall(r"<td[^>]*>(.*?)</td>", row, re.DOTALL)
|
||||||
|
clean = [re.sub(r"<[^>]+>", "", c).strip() for c in cells]
|
||||||
|
if len(clean) >= 9:
|
||||||
|
return {
|
||||||
|
"rank": clean[0],
|
||||||
|
"player": "Lara Kiesewetter",
|
||||||
|
"wins": clean[5],
|
||||||
|
"draws": clean[6],
|
||||||
|
"losses": clean[7],
|
||||||
|
"points": clean[8],
|
||||||
|
"round_info": round_info,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Laden der Tabelle: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_standings_content():
|
||||||
|
"""Gibt Tabellenstand als Dict zurück, nutzt Cache."""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
# Prüfe Cache
|
if os.path.exists(STANDINGS_CACHE_FILE):
|
||||||
if os.path.exists(CACHE_FILE):
|
age = now - os.path.getmtime(STANDINGS_CACHE_FILE)
|
||||||
age = now - os.path.getmtime(CACHE_FILE)
|
if age < 300: # 5 Minuten Cache
|
||||||
if age < CACHE_TTL:
|
with open(STANDINGS_CACHE_FILE, "r") as f:
|
||||||
with open(CACHE_FILE, "rb") as f:
|
return json.load(f)
|
||||||
return f.read()
|
|
||||||
|
|
||||||
# Cache verfallen oder nicht vorhanden -> neu laden
|
|
||||||
content = fetch_pgn()
|
|
||||||
if content:
|
|
||||||
with open(CACHE_FILE, "wb") as f:
|
|
||||||
f.write(content)
|
|
||||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] PGN aktualisiert ({len(content)} Bytes)")
|
|
||||||
return content
|
|
||||||
|
|
||||||
# Fallback: alter Cache
|
|
||||||
if os.path.exists(CACHE_FILE):
|
|
||||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Verwende alten Cache")
|
|
||||||
with open(CACHE_FILE, "rb") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
data = fetch_standings()
|
||||||
|
if data:
|
||||||
|
with open(STANDINGS_CACHE_FILE, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] Tabellenstand aktualisiert")
|
||||||
|
return data
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class PGNHandler(http.server.BaseHTTPRequestHandler):
|
class PGNHandler(http.server.BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/pgn":
|
parsed_path = urllib.parse.urlparse(self.path)
|
||||||
content = get_pgn_content()
|
|
||||||
|
if parsed_path.path == "/pgn":
|
||||||
|
params = urllib.parse.parse_qs(parsed_path.query)
|
||||||
|
since = float(params.get("since", [0])[0])
|
||||||
|
content, mtime = get_pgn_content_longpoll(since)
|
||||||
if content:
|
if content:
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header("Cache-Control", "no-cache")
|
self.send_header("Cache-Control", "no-cache")
|
||||||
|
self.send_header("X-Cache-Mtime", str(mtime))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(content)
|
self.wfile.write(content)
|
||||||
else:
|
else:
|
||||||
@@ -83,9 +165,9 @@ class PGNHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b'{"error": "Konnte PGN nicht laden"}')
|
self.wfile.write(json.dumps({"error": "Konnte PGN nicht laden"}).encode())
|
||||||
|
|
||||||
elif self.path == "/status":
|
elif parsed_path.path == "/status":
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
@@ -99,6 +181,23 @@ class PGNHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
}
|
}
|
||||||
self.wfile.write(json.dumps(status).encode())
|
self.wfile.write(json.dumps(status).encode())
|
||||||
|
|
||||||
|
elif parsed_path.path == "/standings":
|
||||||
|
data = get_standings_content()
|
||||||
|
if data:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Cache-Control", "max-age=300")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(data).encode())
|
||||||
|
else:
|
||||||
|
self.send_response(502)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.end_headers()
|
||||||
|
err = {"error": "Tabellendaten nicht verf\u00fcgbar"}
|
||||||
|
self.wfile.write(json.dumps(err).encode())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Statische Dateien aus dem Verzeichnis
|
# Statische Dateien aus dem Verzeichnis
|
||||||
if self.path == "/":
|
if self.path == "/":
|
||||||
|
|||||||
55
style.css
55
style.css
@@ -168,18 +168,69 @@ header h1 {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#moves-panel, #all-games-panel {
|
#moves-panel, #all-games-panel, #standings-panel {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#moves-panel h3, #all-games-panel h3 {
|
#moves-panel h3, #all-games-panel h3, #standings-panel h3 {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
color: #e94560;
|
color: #e94560;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#standings-content {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#standings-content .standings-loading {
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standings-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.standings-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standings-label {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standings-value {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standings-rank {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e94560;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standings-rank-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standings-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #ffd700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
#moves-list {
|
#moves-list {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user