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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user