Jede Idee bekommt ihre eigene Seite mit der Unterhaltung aus dem Thread
Vorbild windrose.support: Klick auf eine Idee, und man sieht sie ganz — Stand, Begruendung, wer sie eingereicht hat, und darunter, was dazu geschrieben wurde. Der Unterschied bleibt, wo geschrieben wird. Die Beitraege sind aus dem Discord-Thread gespiegelt, nicht hier getippt. Deshalb fuehrt der einzige Knopf am Ende in den Thread und nicht in ein Formular. Ein zweiter Ort fuer dieselbe Unterhaltung waere genau das, was ich beim Konzept als das Falsche bezeichnet habe. Bisher wurde nur mitgezaehlt. Jetzt landen die Beitraege in einer eigenen Tabelle — mit Bearbeiten und Loeschen, sonst stuende auf der Webseite fuer immer, was in Discord laengst zurueckgenommen wurde. Reine Bild-Posts ohne Text bleiben draussen, die haetten hier nichts zu sagen. Der Kommentar-Zaehler wird jetzt aus der Tabelle gezaehlt statt in einer Spalte mitgefuehrt. Eine Wahrheit statt zwei, die auseinanderlaufen koennen — dieselbe Ueberlegung wie beim Zuspruch. Nebenbei geprueft, weil es haette schiefgehen koennen: /api/wishes/suche und /api/wishes/:id liegen auf derselben Ebene. Der Router bevorzugt die feste Route vor der mit Platzhalter — nachgestellt mit find-my-way, nicht aus dem Gedaechtnis behauptet. Geprueft: im Browser die Seite mit drei Beitraegen und die ohne (kein Thread, keine Begruendung, Hinweis statt Liste), der Weg von der Roadmap dorthin ohne Neuladen, Migration erneut durchgespielt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+42
-4
@@ -14,7 +14,8 @@ import {
|
||||
ModalBuilder, MessageFlags, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle,
|
||||
} from 'discord.js';
|
||||
import {
|
||||
saveWish, setWishThread, bumpWishKommentare, getWishCategory, listWishCategories,
|
||||
saveWish, setWishThread, wishByThread, getWishCategory, listWishCategories,
|
||||
saveWishComment, updateWishComment, deleteWishComment,
|
||||
} from '../db.js';
|
||||
import { votingChannelId, publicUrl, brandColor, brandFooter } from '../runtime-settings.js';
|
||||
import { renderTemplate } from '../templates.js';
|
||||
@@ -223,14 +224,51 @@ export async function handleWunschModal(interaction) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Beiträge in Wunsch-Threads mitzählen — das ist der Kommentar-Zähler */
|
||||
/**
|
||||
* Beiträge in Wunsch-Threads mitschreiben.
|
||||
*
|
||||
* Gespiegelt statt gezählt, damit die Detailseite die Unterhaltung zeigen
|
||||
* kann. Bearbeiten und Löschen ziehen mit — sonst stünde auf der Webseite für
|
||||
* immer, was in Discord längst zurückgenommen wurde.
|
||||
*/
|
||||
export function registerWishThreads(client) {
|
||||
client.on(Events.MessageCreate, (message) => {
|
||||
try {
|
||||
if (message.author?.bot || !message.channel?.isThread?.()) return;
|
||||
bumpWishKommentare(message.channelId);
|
||||
const wunsch = wishByThread(message.channelId);
|
||||
if (!wunsch) return;
|
||||
const text = message.content?.trim();
|
||||
// Reine Bild-Posts haben keinen Text — die zeigt die Seite nicht
|
||||
if (!text) return;
|
||||
saveWishComment({
|
||||
message_id: message.id,
|
||||
wish_id: wunsch.id,
|
||||
author: message.member?.displayName ?? message.author.username,
|
||||
author_id: message.author.id,
|
||||
avatar: message.author.displayAvatarURL?.({ size: 64 }) ?? null,
|
||||
content: text.slice(0, 2000),
|
||||
created_at: new Date(message.createdTimestamp).toISOString().slice(0, 19).replace('T', ' '),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[voting] Kommentar-Zähler:', error);
|
||||
console.error('[voting] Kommentar nicht gespeichert:', error);
|
||||
}
|
||||
});
|
||||
|
||||
client.on(Events.MessageUpdate, (alt, neu) => {
|
||||
try {
|
||||
if (!neu?.channel?.isThread?.()) return;
|
||||
const text = neu.content?.trim();
|
||||
if (text) updateWishComment(neu.id, text.slice(0, 2000));
|
||||
} catch (error) {
|
||||
console.error('[voting] Kommentar nicht geändert:', error);
|
||||
}
|
||||
});
|
||||
|
||||
client.on(Events.MessageDelete, (message) => {
|
||||
try {
|
||||
deleteWishComment(message.id);
|
||||
} catch (error) {
|
||||
console.error('[voting] Kommentar nicht entfernt:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,6 +315,37 @@ db.exec(`
|
||||
db.exec('ALTER TABLE wishes ADD COLUMN kommentare INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
// Was im Thread unter einem Wunsch geschrieben wird. Gespiegelt, nicht
|
||||
// gezaehlt: die Detailseite soll die Unterhaltung zeigen koennen, und 50
|
||||
// Threads je Seitenaufruf bei Discord abzufragen waere nicht zu bezahlen.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS wish_comments (
|
||||
message_id TEXT PRIMARY KEY,
|
||||
wish_id INTEGER NOT NULL,
|
||||
author TEXT,
|
||||
author_id TEXT,
|
||||
avatar TEXT,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wish_comments ON wish_comments (wish_id, created_at);
|
||||
`);
|
||||
const insertCommentStmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO wish_comments
|
||||
(message_id, wish_id, author, author_id, avatar, content, created_at)
|
||||
VALUES (@message_id, @wish_id, @author, @author_id, @avatar, @content, @created_at)
|
||||
`);
|
||||
const updateCommentStmt = db.prepare('UPDATE wish_comments SET content = ? WHERE message_id = ?');
|
||||
const deleteCommentStmt = db.prepare('DELETE FROM wish_comments WHERE message_id = ?');
|
||||
const listCommentsStmt = db.prepare(
|
||||
'SELECT * FROM wish_comments WHERE wish_id = ? ORDER BY created_at LIMIT ?'
|
||||
);
|
||||
const dropCommentsStmt = db.prepare('DELETE FROM wish_comments WHERE wish_id = ?');
|
||||
export const saveWishComment = (c) => insertCommentStmt.run(c);
|
||||
export const updateWishComment = (messageId, text) => updateCommentStmt.run(text, messageId).changes > 0;
|
||||
export const deleteWishComment = (messageId) => deleteCommentStmt.run(messageId).changes > 0;
|
||||
export const wishComments = (wishId, limit = 200) => listCommentsStmt.all(wishId, limit);
|
||||
|
||||
const listWishCatsStmt = db.prepare('SELECT * FROM wish_categories ORDER BY sort, id');
|
||||
const getWishCatStmt = db.prepare('SELECT * FROM wish_categories WHERE id = ?');
|
||||
const insertWishCatStmt = db.prepare(
|
||||
@@ -338,7 +369,8 @@ export const deleteWishCategory = db.transaction((id) => {
|
||||
/** Zuspruch = eigene Stimmen + nicht mehr zuordenbarer Sockel aus der Altzeit */
|
||||
const WUNSCH_FELDER = `
|
||||
w.id, w.message_id, w.thread_id, w.idea, w.author, w.author_id, w.status,
|
||||
w.status_grund, w.status_at, w.created_at, w.kommentare, w.category_id,
|
||||
w.status_grund, w.status_at, w.created_at, w.category_id,
|
||||
(SELECT COUNT(*) FROM wish_comments c WHERE c.wish_id = w.id) AS kommentare,
|
||||
(SELECT k.name FROM wish_categories k WHERE k.id = w.category_id) AS kategorie,
|
||||
(SELECT k.emoji FROM wish_categories k WHERE k.id = w.category_id) AS kategorie_emoji,
|
||||
w.bonus + (SELECT COUNT(*) FROM wish_votes v WHERE v.wish_id = w.id) AS score,
|
||||
@@ -351,9 +383,7 @@ const insertWish = db.prepare(
|
||||
'INSERT INTO wishes (message_id, author, author_id, idea, category_id) VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
const setWishThreadStmt = db.prepare('UPDATE wishes SET thread_id = ? WHERE id = ?');
|
||||
const bumpKommentareStmt = db.prepare(
|
||||
'UPDATE wishes SET kommentare = kommentare + 1 WHERE thread_id = ?'
|
||||
);
|
||||
const wishByThreadStmt = db.prepare('SELECT * FROM wishes WHERE thread_id = ?');
|
||||
const setWishCategoryStmt = db.prepare('UPDATE wishes SET category_id = ? WHERE id = ?');
|
||||
const setWishMessageStmt = db.prepare('UPDATE wishes SET message_id = ? WHERE id = ?');
|
||||
const wishByMessageStmt = db.prepare('SELECT * FROM wishes WHERE message_id = ?');
|
||||
@@ -387,18 +417,18 @@ export const saveWish = (messageId, author, idea, authorId = null, categoryId =
|
||||
insertWish.run(messageId, author, authorId, idea, categoryId ?? null).lastInsertRowid;
|
||||
export const setWishThread = (id, threadId) => setWishThreadStmt.run(threadId, id);
|
||||
export const setWishCategory = (id, categoryId) => setWishCategoryStmt.run(categoryId ?? null, id);
|
||||
/** @returns {boolean} true, wenn der Thread zu einem Wunsch gehoerte */
|
||||
export const bumpWishKommentare = (threadId) => bumpKommentareStmt.run(threadId).changes > 0;
|
||||
export const wishByThread = (threadId) => wishByThreadStmt.get(threadId) ?? null;
|
||||
export const setWishMessage = (id, messageId) => setWishMessageStmt.run(messageId, id);
|
||||
export const wishByMessage = (messageId) => wishByMessageStmt.get(messageId) ?? null;
|
||||
export const getWish = (id) => wishByIdStmt.get(id) ?? null;
|
||||
export const topWishes = (limit = 20, sortierung = 'top') =>
|
||||
(topWishesStmts[sortierung] ?? topWishesStmts.top).all(limit);
|
||||
export const searchWishes = (text, limit = 8) => searchWishesStmt.all(`%${text}%`, limit);
|
||||
export const deleteWish = (id) => {
|
||||
export const deleteWish = db.transaction((id) => {
|
||||
dropVotesStmt.run(id);
|
||||
dropCommentsStmt.run(id);
|
||||
return deleteWishStmt.run(id).changes > 0;
|
||||
};
|
||||
});
|
||||
export const setWishStatus = (id, status, grund) =>
|
||||
setWishStatusStmt.run(status, grund || null, id).changes > 0;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
saveTemplate, listTemplates, deleteTemplate,
|
||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||
getWish, searchWishes, setWishStatus, mergeWish, deleteWish, wishVoters, setWishCategory,
|
||||
wishComments,
|
||||
listWishCategories, createWishCategory, updateWishCategory, deleteWishCategory,
|
||||
logAudit, listAudit, playerHistory, uptimeBuckets, watchdogBuckets, watchdogDays,
|
||||
heartbeatBuckets, heartbeatFirst,
|
||||
@@ -1984,6 +1985,27 @@ ${rssItems}
|
||||
return { voted: toggleWishVote(id, user.id) };
|
||||
});
|
||||
|
||||
// Ein einzelner Wunsch mit allem, was im Thread darunter steht.
|
||||
// Muss vor /api/wishes/:id/... stehen? Nein — andere Tiefe, kein Konflikt.
|
||||
app.get('/api/wishes/:id', async (request, reply) => {
|
||||
const id = Number(request.params.id);
|
||||
const wunsch = getWish(id);
|
||||
if (!wunsch) return reply.code(404).send({ error: 'Wunsch nicht gefunden' });
|
||||
const user = getSessionUser(request);
|
||||
const gid = discordGuildId();
|
||||
return {
|
||||
wunsch: {
|
||||
...wunsch,
|
||||
voted: user ? myWishVotes(user.id).includes(id) : false,
|
||||
thread_url: gid && wunsch.thread_id
|
||||
? `https://discord.com/channels/${gid}/${wunsch.thread_id}`
|
||||
: null,
|
||||
},
|
||||
kommentare: wishComments(id),
|
||||
canVote: Boolean(user),
|
||||
};
|
||||
});
|
||||
|
||||
// Suche vor dem Einreichen — Doppler gar nicht erst entstehen lassen
|
||||
app.get('/api/wishes/suche', async (request) => {
|
||||
const q = String(request.query?.q ?? '').trim();
|
||||
|
||||
Reference in New Issue
Block a user