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 (
{particles.map((p) => (
))}
);
}
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 (
{drawer && (
)}
{gated && (
{t('gate.text')}
{invite && (
{t('gate.join')}
)}
)}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Frei angelegte Seiten — muss hinter allen festen Routen stehen */}
} />
} />
);
}