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'; import Roadmap from './pages/Roadmap.jsx'; import Galerie from './pages/Galerie.jsx'; import Level from './pages/Level.jsx'; 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'; import Status from './pages/Status.jsx'; import Home from './pages/Home.jsx'; import DevlogDetail from './pages/DevlogDetail.jsx'; import Impressum from './pages/Impressum.jsx'; import Datenschutz from './pages/Datenschutz.jsx'; import Seite from './pages/Seite.jsx'; import { IconChevronDown, IconMenu, IconX, IconLock } from './icons.jsx'; import { useT, LangSwitch } from './i18n.jsx'; // Beschriftung erst beim Rendern uebersetzen — der Pfad bleibt deutsch, // damit geteilte Links weiter funktionieren. const COMMUNITY_LINKS = [ ['/status', 'nav.status'], ['/galerie', 'nav.gallery'], ['/level', 'nav.level'], ['/events', 'nav.events'], ['/changelog', 'nav.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 (
{open &&
setOpen(false)}>{children}
}
); } function DiscordMark() { return ( ); } /* Aufsteigende Neon-Partikel wie auf der Platform-Seite */ function Particles() { const particles = useMemo( () => Array.from({ length: 24 }, (_, i) => ({ id: i, left: `${Math.random() * 100}%`, dur: `${6 + Math.random() * 8}s`, delay: `${Math.random() * 8}s`, })), [] ); return ( ); } 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 { t } = useT(); 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); // Alle veroeffentlichten Seiten. Das Menue zeigt nur die mit „im Menü"; // im Fusszeilen-Bereich stehen sie alle — dort sucht man Rechtliches. const [pages, setPages] = useState([]); useEffect(() => { apiGet('/api/me') .then((data) => setMe({ scopes: [], ...data, loading: false })) .catch(() => setMe({ user: null, admin: false, scopes: [], loading: false })); apiGet('/api/pages').then((d) => setPages(d.pages)).catch(() => {}); }, []); useEffect(() => { if (gated) apiGet('/api/invite').then((d) => setInvite(d.invite)).catch(() => {}); }, [gated]); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 24); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return (
D4RKST3R
{me.loading ? null : me.user ? ( {me.user.avatar && } {me.user.username} } > {t('nav.profile')} {me.admin && {t('nav.commits')}} {t('nav.logout')} ) : ( {t('nav.login')} )}
{drawer && ( )}
{gated && (
{t('gate.text')} {invite && ( {t('gate.join')} )}
)} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Frei angelegte Seiten — muss hinter allen festen Routen stehen */} } /> } />
); }