UX-Paket: Navbar aufgeräumt, /server-Seite, Lightbox, Skeletons, Audit-Log, Reroll
- Navbar entschlackt: nur noch Devlog · Roadmap · Server · Community · Setup; Galerie/Level/Events/Changelog im Community-Dropdown, Profil/Commits/Logout im Avatar-Menü; Mobile-Burger mit Drawer - Öffentliche /server-Seite: Live-Status-Karten mit Spieler-Balken, Map, Connect-Button und 24h-Spielerzahl-Chart (SVG); Monitor sampelt jede Abfrage in player_history (7 Tage Retention), GET /api/servers liefert nur öffentliche Felder (keine Query-URLs/Hosts) - Lightbox: Galerie- und Devlog-Bilder öffnen im Overlay mit Pfeiltasten, Buttons, Wisch-Gesten und Zähler statt neuem Tab - Skeleton-Loader: schimmernde Platzhalter auf Devlogs, Roadmap, Galerie, Level, Changelog und Server statt "Lade …" - Team-Audit-Log: audit_log-Tabelle (max 500 Einträge), Logging bei Settings/Composer/Vorlagen/Rollen-Menüs/Gameservern/Team/API-Keys/ Branding, GET /api/auditlog (Owner) + Liste im Team-Tab - Giveaway: 🔁 Reroll-Button (Admin-only) + 👥 Teilnehmerliste am Gewinner-Post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// Öffentliche Server-Status-Seite: Live-Status + 24h-Spielerzahl-Verlauf
|
||||
// (Daten sammelt der Server-Monitor alle 2 Minuten).
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
import { SkeletonCards } from '../components/Skeleton.jsx';
|
||||
|
||||
const timeFmt = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
/** 24h-Verlauf als SVG-Fläche; Offline-Samples als rote Punkte auf der Nulllinie */
|
||||
function HistoryChart({ history, max }) {
|
||||
if (history.length < 2) return null;
|
||||
const W = 600, H = 90, PAD = 4;
|
||||
const peak = Math.max(max ?? 0, 1, ...history.map((h) => h.players ?? 0));
|
||||
const x = (i) => PAD + (i / (history.length - 1)) * (W - PAD * 2);
|
||||
const y = (p) => H - PAD - ((p ?? 0) / peak) * (H - PAD * 2);
|
||||
|
||||
const points = history.map((h, i) => `${x(i).toFixed(1)},${y(h.online ? h.players : 0).toFixed(1)}`);
|
||||
const area = `${PAD},${H - PAD} ${points.join(' ')} ${W - PAD},${H - PAD}`;
|
||||
const offline = history.map((h, i) => (h.online ? null : i)).filter((i) => i !== null);
|
||||
|
||||
return (
|
||||
<svg className="srv-chart" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="srvFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="var(--neon)" stopOpacity=".35" />
|
||||
<stop offset="100%" stopColor="var(--neon)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon points={area} fill="url(#srvFill)" />
|
||||
<polyline points={points.join(' ')} fill="none" stroke="var(--neon)" strokeWidth="1.5" />
|
||||
{offline.map((i) => (
|
||||
<circle key={i} cx={x(i)} cy={H - PAD} r="2" fill="#f23f43" />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerCard({ s }) {
|
||||
const pct = s.online && s.max ? Math.round((s.players / s.max) * 100) : 0;
|
||||
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>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{s.history.length > 1 && <HistoryChart history={s.history} max={s.max} />}
|
||||
{s.history.length > 1 && <p className="srv-chart-label">Spielerzahl · letzte 24h</p>}
|
||||
|
||||
{(s.connect_url || s.address) && (
|
||||
<div className="srv-connect">
|
||||
{s.connect_url && (
|
||||
<a className="btn btn-save" href={s.connect_url}>Connect</a>
|
||||
)}
|
||||
{s.address && <code className="alpha-key srv-addr">{s.address}</code>}
|
||||
</div>
|
||||
)}
|
||||
{s.checkedAt && (
|
||||
<p className="srv-checked">Zuletzt geprüft: {timeFmt.format(new Date(s.checkedAt))} Uhr</p>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Server() {
|
||||
const [servers, setServers] = useState(null);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const load = () => apiGet('/api/servers').then((d) => setServers(d.servers)).catch(() => setError(true));
|
||||
load();
|
||||
const timer = setInterval(load, 60_000);
|
||||
window.scrollTo(0, 0);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">SERVER</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Live-Status</div>
|
||||
<h1 className="page-title">Server</h1>
|
||||
<p className="page-subtitle">
|
||||
Alle Game-Server auf einen Blick — aktualisiert alle 2 Minuten.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
{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} />)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user