Files
d4rkbot/frontend/src/pages/Home.jsx
T
D4rkst3randClaude Fable 5 5360c8229f
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Englisch als zweite Sprache fuer die oeffentlichen Seiten
Hub und Bot-Produktseite lassen sich jetzt auf Englisch umstellen. Die Wahl
haengt am Browser (Deutsch nur, wenn der Browser es will) und bleibt im
localStorage stehen.

- src/i18n.jsx: Context mit t(), tOr(), Datums- und Zahlenformat der Sprache.
  Keine Bibliothek, die Woerterbuecher werden mitgebaut — kein Nachlade-Blitzer.
- Platzhalter im %s-Stil, dieselbe Schreibweise wie in den Bot-Texten.
- Umschalter in der Navigation, zusaetzlich im Schubladen-Menue: .auth ist auf
  schmalen Schirmen ausgeblendet, sonst waere der Wechsel dort unerreichbar.
- Datums- und Zahlenformat folgen mit (31.07.2026 / 31/07/2026, 45.231 / 45,231).
- Die Funktionsliste kommt weiter deutsch vom Server; englische Modulnamen
  liegen unter feat.<id> im Woerterbuch. Fehlt einer, bleibt der Servertext
  stehen — ein neues Modul verschwindet nie von der Seite.

Das Config-Panel und die Discord-Texte bleiben deutsch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:41:32 +02:00

143 lines
6.9 KiB
React

// 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';
import { useT, useFmt } from '../i18n.jsx';
import {
IconDevlog, IconRoadmap, IconServer, IconGallery, IconLevel, IconEvents,
IconMessage, IconArrowRight, IconLink,
} from '../icons.jsx';
const TILES = [
['/devlogs', IconDevlog, 'nav.devlog', 'home.tile.devlog'],
['/roadmap', IconRoadmap, 'nav.roadmap', 'home.tile.roadmap'],
['/server', IconServer, 'nav.server', 'home.tile.server'],
['/galerie', IconGallery, 'nav.gallery', 'home.tile.gallery'],
['/level', IconLevel, 'nav.level', 'home.tile.level'],
['/events', IconEvents, 'nav.events', 'home.tile.events'],
];
/** 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 { t } = useT();
const { zahl } = useFmt();
const [stats, setStats] = useState(null);
const [servers, setServers] = useState(null);
const [latest, setLatest] = useState(null);
const [invite, setInvite] = useState(null);
const [services, setServices] = useState([]);
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(() => {});
apiGet('/api/services').then((d) => setServices(d.services)).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">{t('home.tag')}</div>
<h1 className="page-title">
{t('home.welcome')}<br />{t('home.at')} D4RKST<span className="home-accent">3</span>R
</h1>
<p className="page-subtitle">{t('home.subtitle')}</p>
<div className="home-cta">
{invite && (
<a className="btn btn-save with-icon" href={invite} target="_blank" rel="noreferrer">
<IconMessage /> {t('home.discord')}
</a>
)}
<Link className="btn btn-ghost home-cta-alt with-icon" to="/devlogs">
<IconDevlog /> {t('home.readDevlogs')}
</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">{zahl(stats.members)}</span>
<span className="srv-sum-label">{t('home.members')}</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">{t('home.serversOnline')}</span>
</div>
)}
{online.length > 0 && (
<div className="srv-sum-item">
<span className="srv-sum-n">{players}</span>
<span className="srv-sum-label">{t('home.playersNow')}</span>
</div>
)}
</div>
)}
<div className="home-tiles">
{TILES.map(([to, Icon, titleKey, descKey]) => (
<Link className="card home-tile" to={to} key={to}>
<span className="home-tile-icon"><Icon size={26} /></span>
<span className="home-tile-title">{t(titleKey)}</span>
<span className="home-tile-desc">{t(descKey)}</span>
</Link>
))}
</div>
{services.length > 0 && (
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
<h2 className="settings-title">{t('home.services')}</h2>
<div className="home-tiles">
{/* Dienst-Icons pflegt der Owner selbst — daher bleiben es Emojis */}
{services.map((s) => (
<a className="card home-tile" href={s.url} target="_blank" rel="noreferrer" key={s.id}>
<span className="home-tile-icon home-tile-emoji">{s.icon}</span>
<span className="home-tile-title with-icon">
{s.name} <IconLink size={14} className="home-tile-ext" />
</span>
{s.description && <span className="home-tile-desc">{s.description}</span>}
</a>
))}
</div>
</div>
)}
{latest && (
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
<h2 className="settings-title">{t('home.latestDevlog')}</h2>
<Link to="/devlogs" className="card home-devlog">
<div className="card-body">
<Markdown text={teaser(latest.content)} />
</div>
<span className="home-devlog-more with-icon">{t('home.more')} <IconArrowRight size={14} /></span>
</Link>
</div>
)}
</section>
</>
);
}