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:
2026-07-30 21:52:59 +02:00
co-authored by Claude Fable 5
parent 3bfa559f2e
commit aa12dff7ab
11 changed files with 551 additions and 15 deletions
+58 -8
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { apiGet } from '../api.js';
import { apiGet, apiPost } from '../api.js';
import { Markdown } from '../markdown.jsx';
const dueFmt = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' });
@@ -83,16 +83,43 @@ function Heatmap({ days }) {
export default function Roadmap() {
const [data, setData] = useState(null);
const [wishes, setWishes] = useState([]);
const [canVote, setCanVote] = useState(false);
const [idea, setIdea] = useState('');
const [submitMsg, setSubmitMsg] = useState('');
const [heatmap, setHeatmap] = useState(null);
const [error, setError] = useState(false);
useEffect(() => {
apiGet('/api/roadmap').then(setData).catch(() => setError(true));
apiGet('/api/wishes').then((d) => setWishes(d.wishes)).catch(() => {});
apiGet('/api/wishes').then((d) => { setWishes(d.wishes); setCanVote(d.canVote); }).catch(() => {});
apiGet('/api/heatmap').then((d) => setHeatmap(d.days)).catch(() => {});
window.scrollTo(0, 0);
}, []);
async function vote(messageId) {
try {
const res = await apiPost(`/api/wishes/${messageId}/vote`);
setWishes((old) => old.map((w) => w.message_id !== messageId
? w
: { ...w, voted: res.voted, score: w.score + (res.voted ? 1 : -1) }));
} catch (e) {
if (e.status === 403) setSubmitMsg('⚠️ Voten dürfen nur Discord-Member.');
}
}
async function submitWish() {
const text = idea.trim();
if (text.length < 5) { setSubmitMsg('⚠️ Etwas mehr Text bitte.'); return; }
try {
await apiPost('/api/wishes', { idea: text });
setIdea('');
setSubmitMsg('✅ Wunsch eingereicht — er steht jetzt auch im Discord zur Abstimmung!');
apiGet('/api/wishes').then((d) => setWishes(d.wishes)).catch(() => {});
} catch (e) {
setSubmitMsg(e.status === 403 ? '⚠️ Einreichen dürfen nur Discord-Member.' : '⚠️ Konnte den Wunsch nicht einreichen.');
}
}
return (
<>
<section className="page-header">
@@ -116,19 +143,42 @@ export default function Roadmap() {
{heatmap && heatmap.length > 0 && <Heatmap days={heatmap} />}
{wishes.length > 0 && (
{(wishes.length > 0 || canVote) && (
<div className="roadmap-group">
<h2 className="settings-title">// Community-Wünsche</h2>
{wishes.map((w, i) => (
<div className="wish-row" key={`${w.created_at}-${i}`}>
<span className="wish-score">👍 {w.score}</span>
<div className="wish-row" key={w.message_id ?? `${w.created_at}-${i}`}>
{canVote && w.message_id ? (
<button
className={`wish-vote${w.voted ? ' voted' : ''}`}
title={w.voted ? 'Vote zurücknehmen' : 'Dafür stimmen'}
onClick={() => vote(w.message_id)}
>
👍 {w.score}
</button>
) : (
<span className="wish-score">👍 {w.score}</span>
)}
<span className="wish-idea">{w.idea}</span>
<span className="wish-author">{w.author}</span>
</div>
))}
<p className="notice" style={{ padding: '.6rem 0 0' }}>
Eigene Idee? Im Discord einfach <code>/wunsch</code> benutzen.
</p>
{canVote ? (
<div className="wish-form">
<input
type="text" maxLength={500} placeholder="Deine Idee für den Server …"
value={idea}
onChange={(e) => setIdea(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') submitWish(); }}
/>
<button className="btn btn-save" onClick={submitWish}>Einreichen</button>
</div>
) : (
<p className="notice" style={{ padding: '.6rem 0 0' }}>
Eigene Idee? Im Discord <code>/wunsch</code> benutzen oder hier einloggen und direkt abstimmen.
</p>
)}
{submitMsg && <p className="notice" style={{ padding: '.6rem 0 0' }}>{submitMsg}</p>}
</div>
)}