Der Discord-Login ist jetzt für alle Member nützlich: - /profil: Rang + XP-Fortschritt, Discord-Rollen als Chips, Playtester- Badge, eigener Alpha-Key mit Kopier-Button - Rollen-Selfservice: veröffentlichte Rollen-Menüs direkt im Browser togglen (gleiche Exklusiv-Logik wie die Discord-Buttons; nur Rollen aus aktiven Menüs erlaubt) - Wunsch-Voting im Web: auf /roadmap Wünsche einreichen (postet wie /wunsch in den Voting-Kanal) und upvoten (wish_votes-Tabelle, ein Vote pro Member, unabhängig von Discord-👍) - Member-Gate: Login nur für Mitglieder der konfigurierten Guild; Abgewiesene sehen einen Banner mit Discord-Invite-Link (neue Settings member_gate_enabled + discord_invite_url im System-Tab) - Membership-Check mit 5-Minuten-Cache, Owner immer erlaubt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
6.7 KiB
React
150 lines
6.7 KiB
React
import { useEffect, useMemo, useState } from 'react';
|
|
import { Routes, Route, NavLink, Navigate } 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';
|
|
|
|
function DiscordMark() {
|
|
return (
|
|
<svg className="discord-mark" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path
|
|
fill="currentColor"
|
|
d="M20.32 4.37a19.8 19.8 0 0 0-4.93-1.51 13.78 13.78 0 0 0-.64 1.28 18.27 18.27 0 0 0-5.5 0 12.64 12.64 0 0 0-.64-1.28c-1.71.29-3.37.8-4.93 1.51A20.26 20.26 0 0 0 .1 18.06a19.9 19.9 0 0 0 6.07 3.03c.49-.66.93-1.37 1.3-2.1a12.9 12.9 0 0 1-2.05-.98c.17-.12.34-.25.5-.38a14.2 14.2 0 0 0 12.16 0c.16.13.33.26.5.38-.65.39-1.34.71-2.05.98.37.73.81 1.44 1.3 2.1a19.84 19.84 0 0 0 6.07-3.03 20.2 20.2 0 0 0-3.58-13.69ZM8.02 15.33c-1.18 0-2.16-1.08-2.16-2.42 0-1.33.95-2.42 2.16-2.42 1.21 0 2.18 1.09 2.16 2.42 0 1.34-.95 2.42-2.16 2.42Zm7.96 0c-1.18 0-2.15-1.08-2.15-2.42 0-1.33.95-2.42 2.15-2.42 1.22 0 2.18 1.09 2.16 2.42 0 1.34-.94 2.42-2.16 2.42Z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/* 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 (
|
|
<div className="particles-wrap" aria-hidden="true">
|
|
{particles.map((p) => (
|
|
<span
|
|
key={p.id}
|
|
className="particle"
|
|
style={{ left: p.left, '--dur': p.dur, '--delay': p.delay, bottom: 0 }}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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);
|
|
// 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);
|
|
|
|
useEffect(() => {
|
|
apiGet('/api/me')
|
|
.then((data) => setMe({ scopes: [], ...data, loading: false }))
|
|
.catch(() => setMe({ user: null, admin: false, scopes: [], loading: false }));
|
|
}, []);
|
|
|
|
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 (
|
|
<div className="app">
|
|
<Particles />
|
|
|
|
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
|
|
<NavLink to="/" className="nav-logo">
|
|
D4RKST<span>3R</span>
|
|
</NavLink>
|
|
|
|
<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>}
|
|
{(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>
|
|
) : (
|
|
<a className="btn-login" href="/auth/login">
|
|
<DiscordMark /> Login
|
|
</a>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<main className="main">
|
|
{gated && (
|
|
<div className="gate-banner">
|
|
<span>
|
|
🚪 Der Login ist Mitgliedern des Discord-Servers vorbehalten —
|
|
tritt erst dem Server bei und logg dich dann nochmal ein.
|
|
</span>
|
|
{invite && (
|
|
<a className="btn btn-save" href={invite} target="_blank" rel="noreferrer">
|
|
Zum Discord
|
|
</a>
|
|
)}
|
|
<button className="card-delete" onClick={() => setGated(false)} title="Ausblenden">✕</button>
|
|
</div>
|
|
)}
|
|
<Routes>
|
|
<Route path="/" element={<Navigate to="/devlogs" replace />} />
|
|
<Route path="/devlogs" element={<Devlogs me={me} />} />
|
|
<Route path="/roadmap" element={<Roadmap />} />
|
|
<Route path="/galerie" element={<Galerie />} />
|
|
<Route path="/level" element={<Level />} />
|
|
<Route path="/events" element={<Events />} />
|
|
<Route path="/changelog" element={<Changelog />} />
|
|
<Route path="/profil" element={<Profil me={me} />} />
|
|
<Route path="/commits" element={<Commits me={me} />} />
|
|
<Route path="/settings" element={<Settings me={me} />} />
|
|
<Route path="*" element={<Navigate to="/devlogs" replace />} />
|
|
</Routes>
|
|
</main>
|
|
|
|
<footer className="footer">
|
|
<span className="footer-logo">D4RKST<span>3R</span></span>
|
|
<span className="footer-sep">//</span>
|
|
<a href="https://git.d4rkst3r.de/D4rkst3r" target="_blank" rel="noreferrer">
|
|
git.d4rkst3r.de
|
|
</a>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|