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
+28 -1
View File
@@ -142,9 +142,11 @@ const deletePlaytester = db.prepare('DELETE FROM playtesters WHERE user_id = ?')
const listPlaytestersStmt = db.prepare(
'SELECT user_id, username, applied_at FROM playtesters ORDER BY applied_at'
);
const hasPlaytesterStmt = db.prepare('SELECT 1 FROM playtesters WHERE user_id = ?');
export const addPlaytester = (userId, username) => insertPlaytester.run(userId, username).changes > 0;
export const removePlaytester = (userId) => deletePlaytester.run(userId).changes > 0;
export const listPlaytesters = () => listPlaytestersStmt.all();
export const isPlaytester = (userId) => Boolean(hasPlaytesterStmt.get(userId));
const hasStarboardStmt = db.prepare('SELECT 1 FROM starboard WHERE message_id = ?');
const insertStarboard = db.prepare(
@@ -206,13 +208,38 @@ const insertWish = db.prepare('INSERT INTO wishes (message_id, author, idea) VAL
const wishScoreStmt = db.prepare('UPDATE wishes SET score = MAX(0, score + ?) WHERE message_id = ?');
const hasWishStmt = db.prepare('SELECT 1 FROM wishes WHERE message_id = ?');
const topWishesStmt = db.prepare(
'SELECT idea, author, score, created_at FROM wishes ORDER BY score DESC, created_at DESC LIMIT ?'
'SELECT message_id, idea, author, score, created_at FROM wishes ORDER BY score DESC, created_at DESC LIMIT ?'
);
export const saveWish = (messageId, author, idea) => insertWish.run(messageId, author, idea);
export const isWish = (messageId) => Boolean(hasWishStmt.get(messageId));
export const bumpWish = (messageId, delta) => wishScoreStmt.run(delta, messageId);
export const topWishes = (limit = 20) => topWishesStmt.all(limit);
// Web-Votes auf Wünsche (Discord-👍 laufen separat über Reactions)
db.exec(`
CREATE TABLE IF NOT EXISTS wish_votes (
message_id TEXT NOT NULL,
user_id TEXT NOT NULL,
PRIMARY KEY (message_id, user_id)
);
`);
const hasWishVoteStmt = db.prepare('SELECT 1 FROM wish_votes WHERE message_id = ? AND user_id = ?');
const insertWishVote = db.prepare('INSERT OR IGNORE INTO wish_votes (message_id, user_id) VALUES (?, ?)');
const deleteWishVote = db.prepare('DELETE FROM wish_votes WHERE message_id = ? AND user_id = ?');
const myWishVotesStmt = db.prepare('SELECT message_id FROM wish_votes WHERE user_id = ?');
/** Web-Vote togglen; passt den Score mit an. @returns {boolean} true = jetzt gevotet */
export function toggleWishVote(messageId, userId) {
if (hasWishVoteStmt.get(messageId, userId)) {
deleteWishVote.run(messageId, userId);
wishScoreStmt.run(-1, messageId);
return false;
}
insertWishVote.run(messageId, userId);
wishScoreStmt.run(1, messageId);
return true;
}
export const myWishVotes = (userId) => myWishVotesStmt.all(userId).map((r) => r.message_id);
const insertGiveaway = db.prepare(`
INSERT INTO giveaways (message_id, channel_id, prize, winners, ends_at)
VALUES (@message_id, @channel_id, @prize, @winners, @ends_at)