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
+112
View File
@@ -0,0 +1,112 @@
// Startseite: Hero mit CTAs, Live-Zahlen, neuestes Devlog und Bereichs-Kacheln.
// Nutzt nur öffentliche APIs — alles best effort, die Seite steht auch ohne Daten.
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { apiGet } from '../api.js';
import { Markdown } from '../markdown.jsx';
const TILES = [
['/devlogs', '📔', 'Devlog', 'Entwicklungs-Updates, frisch aus dem Editor'],
['/roadmap', '🗺️', 'Roadmap', 'Meilensteine + Community-Wünsche zum Abstimmen'],
['/server', '🎮', 'Server', 'Live-Status aller Game-Server mit Verlauf'],
['/galerie', '🖼️', 'Galerie', 'Screenshots aus der Community'],
['/level', '📈', 'Level', 'XP-Bestenliste — wer redet am meisten mit?'],
['/events', '📅', 'Events', 'Playtests, Streams und was sonst ansteht'],
];
/** Devlog-Text für den Teaser kürzen (erste Sätze, ohne Titel-Zeile) */
function teaser(content) {
const body = content
.split(/\n{2,}/)
.filter((b) => !/^Devlog\s*[—–-]/i.test(b.trim()) && !/^\*[^*]+\*$/s.test(b.trim()))
.join('\n\n');
return body.length > 420 ? `${body.slice(0, 420).trimEnd()}` : body;
}
export default function Home() {
const [stats, setStats] = useState(null);
const [servers, setServers] = useState(null);
const [latest, setLatest] = useState(null);
const [invite, setInvite] = useState(null);
useEffect(() => {
apiGet('/api/serverstats').then(setStats).catch(() => {});
apiGet('/api/servers').then((d) => setServers(d.servers)).catch(() => {});
apiGet('/api/devlogs?page=1').then((d) => setLatest(d.items?.[0] ?? null)).catch(() => {});
apiGet('/api/invite').then((d) => setInvite(d.invite)).catch(() => {});
window.scrollTo(0, 0);
}, []);
const online = servers?.filter((s) => s.online) ?? [];
const players = online.reduce((sum, s) => sum + (s.players ?? 0), 0);
return (
<>
<section className="page-header home-hero">
<div className="page-bg-text">D4RKST3R</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Community-Hub</div>
<h1 className="page-title">
Willkommen<br />bei D4RKST<span className="home-accent">3</span>R
</h1>
<p className="page-subtitle">
Devlogs, Roadmap, Server-Status und alles aus der Community
an einem Ort, direkt aus dem Discord.
</p>
<div className="home-cta">
{invite && <a className="btn btn-save" href={invite} target="_blank" rel="noreferrer">💬 Zum Discord</a>}
<Link className="btn btn-ghost home-cta-alt" to="/devlogs">📔 Devlogs lesen</Link>
</div>
</div>
</section>
<section className="content">
{(stats || servers) && (
<div className="srv-summary home-stats">
{stats?.members > 0 && (
<div className="srv-sum-item">
<span className="srv-sum-n">{stats.members.toLocaleString('de-DE')}</span>
<span className="srv-sum-label">Member im Discord</span>
</div>
)}
{servers?.length > 0 && (
<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>
)}
{online.length > 0 && (
<div className="srv-sum-item">
<span className="srv-sum-n">{players}</span>
<span className="srv-sum-label">Spieler gerade drauf</span>
</div>
)}
</div>
)}
<div className="home-tiles">
{TILES.map(([to, icon, title, desc]) => (
<Link className="card home-tile" to={to} key={to}>
<span className="home-tile-icon">{icon}</span>
<span className="home-tile-title">{title}</span>
<span className="home-tile-desc">{desc}</span>
</Link>
))}
</div>
{latest && (
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
<h2 className="settings-title">// Neuestes Devlog</h2>
<Link to="/devlogs" className="card home-devlog">
<div className="card-body">
<Markdown text={teaser(latest.content)} />
</div>
<span className="home-devlog-more">Weiterlesen </span>
</Link>
</div>
)}
</section>
</>
);
}