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:
2026-07-30 22:15:58 +02:00
co-authored by Claude Fable 5
parent 8d470ba26b
commit be11398f7c
17 changed files with 671 additions and 28 deletions
+75 -13
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { Routes, Route, NavLink, Navigate } from 'react-router-dom';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
import { apiGet } from './api.js';
import Devlogs from './pages/Devlogs.jsx';
import Changelog from './pages/Changelog.jsx';
@@ -10,6 +10,33 @@ import Events from './pages/Events.jsx';
import Commits from './pages/Commits.jsx';
import Settings from './pages/Settings.jsx';
import Profil from './pages/Profil.jsx';
import Server from './pages/Server.jsx';
const COMMUNITY_LINKS = [
['/galerie', 'Galerie'],
['/level', 'Level'],
['/events', 'Events'],
['/changelog', 'Changelog'],
];
/** Dropdown in der Navbar (schließt bei Klick daneben und bei Navigation) */
function NavDropdown({ label, active, children }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, []);
return (
<div className={`nav-drop ${open ? 'open' : ''}`} ref={ref}>
<button className={`nav-drop-btn ${active ? 'active' : ''}`} onClick={() => setOpen((o) => !o)}>
{label} <span className="nav-caret"></span>
</button>
{open && <div className="nav-drop-menu" onClick={() => setOpen(false)}>{children}</div>}
</div>
);
}
function DiscordMark() {
return (
@@ -51,6 +78,12 @@ export default function App() {
// Login-Status einmal beim Laden holen
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
const [scrolled, setScrolled] = useState(false);
const [drawer, setDrawer] = useState(false);
const location = useLocation();
const communityActive = COMMUNITY_LINKS.some(([to]) => location.pathname.startsWith(to));
// Drawer bei Seitenwechsel schließen
useEffect(() => { setDrawer(false); }, [location.pathname]);
// Member-Gate: OAuth war okay, aber User ist nicht auf dem Discord (?gate=1)
const [gated, setGated] = useState(() => new URLSearchParams(window.location.search).has('gate'));
const [invite, setInvite] = useState(null);
@@ -83,30 +116,58 @@ export default function App() {
<nav className="nav-links">
<NavLink to="/devlogs">Devlog</NavLink>
<NavLink to="/roadmap">Roadmap</NavLink>
<NavLink to="/galerie">Galerie</NavLink>
<NavLink to="/level">Level</NavLink>
<NavLink to="/events">Events</NavLink>
<NavLink to="/changelog">Changelog</NavLink>
{me.user && <NavLink to="/profil">Profil</NavLink>}
{me.admin && <NavLink to="/commits">Commits</NavLink>}
<NavLink to="/server">Server</NavLink>
<NavDropdown label="Community" active={communityActive}>
{COMMUNITY_LINKS.map(([to, label]) => (
<NavLink to={to} key={to}>{label}</NavLink>
))}
</NavDropdown>
{(me.admin || me.scopes?.length > 0) && <NavLink to="/settings">Setup</NavLink>}
</nav>
<div className="auth">
{me.loading ? null : me.user ? (
<div className="user-chip">
{me.user.avatar && <img className="avatar" src={me.user.avatar} alt="" />}
<span className="username">{me.user.username}</span>
<a className="logout" href="/auth/logout" title="Logout"></a>
</div>
<NavDropdown
label={
<span className="user-chip-inner">
{me.user.avatar && <img className="avatar" src={me.user.avatar} alt="" />}
<span className="username">{me.user.username}</span>
</span>
}
>
<NavLink to="/profil">Profil</NavLink>
{me.admin && <NavLink to="/commits">Commits</NavLink>}
<a href="/auth/logout">Logout</a>
</NavDropdown>
) : (
<a className="btn-login" href="/auth/login">
<DiscordMark /> Login
</a>
)}
</div>
<button className="burger" onClick={() => setDrawer((d) => !d)} aria-label="Menü">
{drawer ? '✕' : '☰'}
</button>
</header>
{drawer && (
<nav className="drawer" onClick={() => setDrawer(false)}>
<NavLink to="/devlogs">Devlog</NavLink>
<NavLink to="/roadmap">Roadmap</NavLink>
<NavLink to="/server">Server</NavLink>
{COMMUNITY_LINKS.map(([to, label]) => (
<NavLink to={to} key={to}>{label}</NavLink>
))}
{me.user && <NavLink to="/profil">Profil</NavLink>}
{me.admin && <NavLink to="/commits">Commits</NavLink>}
{(me.admin || me.scopes?.length > 0) && <NavLink to="/settings">Setup</NavLink>}
{me.user
? <a href="/auth/logout">Logout</a>
: <a href="/auth/login">Login mit Discord</a>}
</nav>
)}
<main className="main">
{gated && (
<div className="gate-banner">
@@ -126,6 +187,7 @@ export default function App() {
<Route path="/" element={<Navigate to="/devlogs" replace />} />
<Route path="/devlogs" element={<Devlogs me={me} />} />
<Route path="/roadmap" element={<Roadmap />} />
<Route path="/server" element={<Server />} />
<Route path="/galerie" element={<Galerie />} />
<Route path="/level" element={<Level />} />
<Route path="/events" element={<Events />} />