Member-Bereich: Profilseite, Rollen-Selfservice, Web-Voting, Member-Gate
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>
This commit is contained in:
@@ -9,6 +9,7 @@ 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 (
|
||||
@@ -50,6 +51,9 @@ 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')
|
||||
@@ -57,6 +61,10 @@ export default function App() {
|
||||
.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 });
|
||||
@@ -79,6 +87,7 @@ export default function App() {
|
||||
<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>
|
||||
@@ -99,6 +108,20 @@ export default function App() {
|
||||
</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} />} />
|
||||
@@ -107,6 +130,7 @@ export default function App() {
|
||||
<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 />} />
|
||||
|
||||
Reference in New Issue
Block a user