Startseite, Server-Seite v2 mit Logos, Lightbox-Fix, Navbar-Ausrichtung

- Startseite (/): Hero mit CTAs (Discord-Invite + Devlogs), Live-Zahlen
  (Member, Server online, Spieler), Bereichs-Kacheln, neuestes Devlog
  als Teaser — kein Redirect mehr auf /devlogs
- Server-Seite v2: Übersichtsleiste (X/Y online, Spieler gesamt),
  2-Spalten-Grid, Server-Logo (neues Feld image_url, https-only, auch
  als Embed-Thumbnail im Discord-Status), große Spielerzahl, Uptime %
  + Peak aus den 24h-Samples, Adresse als Copy-Button
- Lightbox: rendert jetzt per Portal in <body> (Header/Scanlines
  schienen durchs Overlay), Pfeile stehen neben dem Bild statt drauf,
  Info-Zeile unterm Bild
- Navbar: Community-Dropdown-Button auf Link-Höhe ausgerichtet
  (line-height + align-items)
- Setup Server-Tab: Logo-URL-Feld

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 22:25:30 +02:00
co-authored by Claude Fable 5
parent be11398f7c
commit ef0fb066f7
10 changed files with 316 additions and 68 deletions
+80 -21
View File
@@ -1,5 +1,6 @@
// Öffentliche Server-Status-Seite: Live-Status + 24h-Spielerzahl-Verlauf
// (Daten sammelt der Server-Monitor alle 2 Minuten).
// Öffentliche Server-Status-Seite: Übersichtsleiste, Karte pro Server mit Logo,
// Live-Status, 24h-Spielerzahl-Verlauf, Uptime und Peak.
// Daten sammelt der Server-Monitor alle 2 Minuten.
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
import { SkeletonCards } from '../components/Skeleton.jsx';
@@ -35,39 +36,78 @@ function HistoryChart({ history, max }) {
);
}
/** Uptime (%) und Spieler-Peak aus den 24h-Samples */
function historyStats(history) {
if (history.length === 0) return null;
const online = history.filter((h) => h.online).length;
return {
uptime: Math.round((online / history.length) * 100),
peak: Math.max(0, ...history.map((h) => h.players ?? 0)),
};
}
function ServerCard({ s }) {
const [copied, setCopied] = useState(false);
const pct = s.online && s.max ? Math.round((s.players / s.max) * 100) : 0;
const stats = historyStats(s.history);
async function copyAddress() {
try {
await navigator.clipboard.writeText(s.address);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch { /* Browser ohne Clipboard-Rechte */ }
}
return (
<article className={`card srv-card ${s.online === false ? 'srv-down' : ''}`}>
<div className="srv-head">
<span className="srv-name">{s.icon} {s.name}</span>
<span className={`srv-status ${s.online ? 'on' : s.online === false ? 'off' : ''}`}>
{s.online === null ? '⏳ Warte auf Check' : s.online ? '🟢 Online' : '🔴 Offline'}
{s.ping != null && s.online && <span className="srv-ping"> · {s.ping}ms</span>}
</span>
{s.image_url
? <img className="srv-logo" src={s.image_url} alt="" loading="lazy" />
: <span className="srv-logo srv-logo-emoji">{s.icon}</span>}
<div className="srv-title">
<span className="srv-name">{s.name}</span>
<span className={`srv-status ${s.online ? 'on' : s.online === false ? 'off' : ''}`}>
{s.online === null ? '⏳ Warte auf Check' : s.online ? '🟢 Online' : '🔴 Offline'}
{s.ping != null && s.online && <span className="srv-ping"> · {s.ping}ms</span>}
{s.map && s.online && <span className="srv-map"> · 🗺 {s.map}</span>}
</span>
</div>
{s.online && s.players != null && (
<div className="srv-count">
<span className="srv-count-n">{s.players}</span>
{s.max > 0 && <span className="srv-count-max">/ {s.max}</span>}
</div>
)}
</div>
{s.online && s.players != null && s.max > 0 && (
<>
<div className="xp-bar srv-bar">
<div className="xp-fill" style={{ width: `${Math.min(100, pct)}%` }} />
</div>
<p className="srv-players">
{s.players} / {s.max} Spieler ({pct}%)
{s.map && <span className="srv-map"> · 🗺 {s.map}</span>}
</p>
</>
<div className="xp-bar srv-bar">
<div className="xp-fill" style={{ width: `${Math.min(100, pct)}%` }} />
</div>
)}
{s.history.length > 1 && <HistoryChart history={s.history} max={s.max} />}
{s.history.length > 1 && <p className="srv-chart-label">Spielerzahl · letzte 24h</p>}
{stats && (
<div className="srv-stats">
<span>Spielerzahl · letzte 24h</span>
<span>
Peak <b>{stats.peak}</b> · Uptime{' '}
<b className={stats.uptime >= 99 ? 'up-good' : stats.uptime >= 90 ? '' : 'up-bad'}>
{stats.uptime}%
</b>
</span>
</div>
)}
{(s.connect_url || s.address) && (
<div className="srv-connect">
{s.connect_url && (
<a className="btn btn-save" href={s.connect_url}>Connect</a>
{s.connect_url && <a className="btn btn-save" href={s.connect_url}>🎮 Connect</a>}
{s.address && (
<button className="alpha-key srv-addr" onClick={copyAddress} title="Adresse kopieren">
{copied ? '✓ Kopiert!' : s.address}
</button>
)}
{s.address && <code className="alpha-key srv-addr">{s.address}</code>}
</div>
)}
{s.checkedAt && (
@@ -89,6 +129,9 @@ export default function Server() {
return () => clearInterval(timer);
}, []);
const online = servers?.filter((s) => s.online) ?? [];
const totalPlayers = online.reduce((sum, s) => sum + (s.players ?? 0), 0);
return (
<>
<section className="page-header">
@@ -107,7 +150,23 @@ export default function Server() {
{error && <p className="notice">Server-Status konnte nicht geladen werden.</p>}
{!error && !servers && <SkeletonCards n={2} />}
{servers?.length === 0 && <p className="notice">Noch keine Server eingetragen.</p>}
{servers?.map((s) => <ServerCard s={s} key={s.id} />)}
{servers?.length > 0 && (
<div className="srv-summary">
<div className="srv-sum-item">
<span className="srv-sum-n">{online.length}<span className="srv-sum-of"> / {servers.length}</span></span>
<span className="srv-sum-label">Server online</span>
</div>
<div className="srv-sum-item">
<span className="srv-sum-n">{totalPlayers}</span>
<span className="srv-sum-label">Spieler gerade drauf</span>
</div>
</div>
)}
<div className="srv-grid">
{servers?.map((s) => <ServerCard s={s} key={s.id} />)}
</div>
</section>
</>
);