From b6b767acb778c66e3849ea7e6cd427dfaa6d4ab7 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Sun, 2 Aug 2026 09:14:59 +0200 Subject: [PATCH] Bis fuenf Eintraege Knoepfe statt Auswahlmenue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Du hattest recht. Bei vier Anliegen ist ein Auswahlmenue der schlechtere Weg: zwei Klicks statt einem, und die Moeglichkeiten sieht man erst nach dem Aufklappen. Ein Knopf ist beides — sichtbar und sofort. Umgekehrt gilt es nicht immer. Ab sechs wird die Knopfreihe zur Wand und Discord bricht sie um; dann ist das Menue das ruhigere Bild. Also nicht "Knoepfe statt Menue", sondern der Uebergang: bis fuenf Knoepfe, darueber Menue. Fuenf, weil genau so viele in eine Reihe passen. Betroffen waren nur zwei Stellen — Ticket-Anliegen und Wunsch-Bereiche. Alles andere im Bot waren ohnehin schon Knoepfe. Eine Sache verliert man dabei: die graue Beschreibungszeile, die ein Menue unter jedem Eintrag anzeigt. Die steht jetzt im Text des Aufruf-Posts, und da ist sie ehrlich gesagt besser aufgehoben — man liest sie, bevor man klickt, statt danach. Geprueft gegen discord.js selbst, weil Ueberlaengen und ungueltige Emojis sonst erst beim Posten auffallen: null Eintraege (der eine alte Knopf), eins bis fuenf (Knoepfe mit Emoji und der ID in der Kennung), sechs (zurueck zum Menue), Murks-Emoji (faellt weg, Knopf bleibt), 200 Zeichen langer Name (auf 80 gekuerzt), und die Beschreibungszeile in allen drei Faellen. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- src/bot/client.js | 19 ++++++++++------- src/bot/commands/ticket-setup.js | 6 ++++-- src/bot/tickets.js | 35 +++++++++++++++++++++++++++++++- src/bot/wishes.js | 19 +++++++++++++++-- 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 13297ef..405d1c7 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen. | 🚨 **Raid-Schutz** | Erkennt Beitritts-Wellen und sperrt den Server vorübergehend (Prüfstufe hoch, Einladungen aus). `/raid an\|aus\|status` auch von Hand | | 📋 **Protokoll** | Gelöschte und bearbeitete Nachrichten, Joins/Leaves, Namens- und Rollen-Änderungen in einem privaten Kanal | | 📬 **Modmail** | DM an den Bot → Thread im privaten Staff-Kanal; Antworten im Thread gehen als DM zurück | -| 🎫 **Tickets** | Button öffnet einen privaten Thread (ein offenes Ticket pro User). **Anliegen** als Auswahlmenü — je eigener Name, Einstiegstext und Rolle, die benachrichtigt wird. Das Team kann ein Ticket **übernehmen**, damit klar ist, wer sich kümmert. Schließen = **Transcript per DM** an den Ersteller + Kopie ins Mod-Log, inklusive Anliegen und Bearbeiter | +| 🎫 **Tickets** | Button öffnet einen privaten Thread (ein offenes Ticket pro User). **Anliegen** zur Auswahl — je eigener Name, Einstiegstext und Rolle, die benachrichtigt wird; bis fünf als Knöpfe, darüber als Menü. Das Team kann ein Ticket **übernehmen**, damit klar ist, wer sich kümmert. Schließen = **Transcript per DM** an den Ersteller + Kopie ins Mod-Log, inklusive Anliegen und Bearbeiter | | 👋 **Willkommens-Karten** | Begrüßung als gerendertes Bild (Avatar mit Neon-Ring, Member-Nummer, Brand-Look), konfigurierbar; Fallback aufs Text-Embed | | 🪪 **Auto- & Sticky-Roles** | Start-Rolle für neue Member; Rollen werden bei Leave gesichert und bei Rejoin wiederhergestellt | | 🐛 **`/bug`** | Eingabefenster (was ist passiert, was war erwartet, wie nachstellen) → Gitea-Issue inkl. Screenshot-Upload; Issue geschlossen → DM an den Reporter | diff --git a/src/bot/client.js b/src/bot/client.js index 2df4569..e6b5d45 100644 --- a/src/bot/client.js +++ b/src/bot/client.js @@ -200,12 +200,12 @@ export async function startBot() { return; } - // Ideen einreichen: Knopf oder Auswahl im Aufruf-Post -> Eingabefenster - if ((interaction.isButton() && interaction.customId === 'wunsch_neu') + // Ideen einreichen: Knopf (wunsch_neu / wunsch_pick:3) oder Auswahlmenue + if ((interaction.isButton() && /^wunsch_(neu|pick:\d+)$/.test(interaction.customId)) || (interaction.isStringSelectMenu() && interaction.customId === 'wunsch_pick')) { try { - if (interaction.isButton()) await handleWunschButton(interaction); - else await handleWunschPick(interaction); + if (interaction.isStringSelectMenu()) await handleWunschPick(interaction); + else await handleWunschButton(interaction); } catch (error) { console.error('[voting] Eingabefenster fehlgeschlagen:', error); } @@ -263,12 +263,17 @@ export async function startBot() { return; } - // Ticket: erstellen (Knopf ohne Anliegen, Menue mit), uebernehmen, schliessen - const ticketErstellen = (interaction.isButton() && interaction.customId === 'ticket_create') + // Ticket erstellen. Das Anliegen kommt je nach Zahl der Anliegen aus + // einem Knopf (ticket_pick:3) oder aus dem Auswahlmenue. + const ticketErstellen = (interaction.isButton() + && /^ticket_(create|pick:\d+)$/.test(interaction.customId)) || (interaction.isStringSelectMenu() && interaction.customId === 'ticket_pick'); if (ticketErstellen) { try { - const gewaehlt = interaction.isStringSelectMenu() ? Number(interaction.values[0]) : null; + const roh = interaction.isStringSelectMenu() + ? interaction.values[0] + : interaction.customId.split(':')[1]; + const gewaehlt = Number(roh); await createTicket(interaction, Number.isFinite(gewaehlt) ? gewaehlt : null); } catch (error) { console.error('[ticket] Erstellung fehlgeschlagen:', error); diff --git a/src/bot/commands/ticket-setup.js b/src/bot/commands/ticket-setup.js index 8e044b3..ce4243c 100644 --- a/src/bot/commands/ticket-setup.js +++ b/src/bot/commands/ticket-setup.js @@ -2,7 +2,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js'; import { ticketChannelId, brandColor, brandFooter } from '../../runtime-settings.js'; import { renderTemplate } from '../../templates.js'; -import { ticketPanelComponents } from '../tickets.js'; +import { ticketPanelComponents, anliegenListe } from '../tickets.js'; import { listTicketCategories } from '../../db.js'; export const data = new SlashCommandBuilder() @@ -22,7 +22,9 @@ export async function execute(interaction) { const embed = new EmbedBuilder() .setColor(brandColor()) .setTitle(renderTemplate('ticket.panel_title')) - .setDescription(renderTemplate('ticket.panel_text')) + // Bei Knoepfen ist fuer die Kurzbeschreibungen kein Platz — dann + // stehen sie hier im Text, wo man sie ohnehin vorher liest + .setDescription(renderTemplate('ticket.panel_text') + anliegenListe()) .setFooter({ text: brandFooter('SUPPORT') }); // Ohne Anliegen ein Knopf, mit Anliegen ein Auswahlmenue diff --git a/src/bot/tickets.js b/src/bot/tickets.js index 8aaa4b4..c489aa6 100644 --- a/src/bot/tickets.js +++ b/src/bot/tickets.js @@ -39,7 +39,14 @@ export function alsEmoji(text) { return /\p{Extended_Pictographic}/u.test(zeichen) ? { name: zeichen } : null; } -/** Knopf oder Auswahlmenü fürs Panel — je nachdem, ob es Anliegen gibt */ +/** + * Ab wann ein Auswahlmenü statt Knöpfen. Fünf passen in eine Reihe; bis dahin + * ist ein Knopf besser als ein Menü — ein Klick statt zwei, und man sieht die + * Auswahl, ohne erst aufzuklappen. Darüber wird die Knopfreihe zur Wand. + */ +export const MAX_KNOEPFE = 5; + +/** Knöpfe oder Auswahlmenü fürs Panel — je nachdem, wie viele Anliegen es gibt */ export function ticketPanelComponents() { const kategorien = listTicketCategories(); if (kategorien.length === 0) { @@ -52,6 +59,18 @@ export function ticketPanelComponents() { )]; } + if (kategorien.length <= MAX_KNOEPFE) { + return [new ActionRowBuilder().addComponents(kategorien.map((k) => { + const knopf = new ButtonBuilder() + .setCustomId(`ticket_pick:${k.id}`) + .setStyle(ButtonStyle.Secondary) + .setLabel(k.name.slice(0, 80)); + const emoji = alsEmoji(k.emoji); + if (emoji) knopf.setEmoji(emoji); + return knopf; + }))]; + } + const menu = new StringSelectMenuBuilder() .setCustomId('ticket_pick') .setPlaceholder('Worum geht es?') @@ -66,6 +85,20 @@ export function ticketPanelComponents() { return [new ActionRowBuilder().addComponents(menu)]; } +/** + * Die Kurzbeschreibungen fürs Panel-Embed. Im Menü stehen sie als graue Zeile; + * bei Knöpfen ist dafür kein Platz — dann gehören sie in den Text, wo man sie + * ohnehin liest, bevor man klickt. + */ +export function anliegenListe() { + const kategorien = listTicketCategories(); + if (kategorien.length === 0 || kategorien.length > MAX_KNOEPFE) return ''; + const zeilen = kategorien + .filter((k) => k.beschreibung) + .map((k) => `${k.emoji || '•'} **${k.name}** — ${k.beschreibung}`); + return zeilen.length > 0 ? `\n\n${zeilen.join('\n')}` : ''; +} + /** Knopfleiste im Ticket-Thread — der mittlere Knopf hängt am Bearbeiter */ export function ticketThreadComponents(claimedBy) { const uebernehmen = claimedBy diff --git a/src/bot/wishes.js b/src/bot/wishes.js index 2641413..7063b26 100644 --- a/src/bot/wishes.js +++ b/src/bot/wishes.js @@ -21,7 +21,7 @@ import { bilderSichern, bilderLoeschen, bildOrdner } from './bilder.js'; import { votingChannelId, publicUrl, brandColor, brandFooter } from '../runtime-settings.js'; import { renderTemplate } from '../templates.js'; import { tuning } from '../tuning.js'; -import { alsEmoji } from './tickets.js'; +import { alsEmoji, MAX_KNOEPFE } from './tickets.js'; // Bilder aus den Threads liegen neben der SQLite, wie bei Devlogs und Galerie export const wunschBilderDir = bildOrdner('wish_images'); @@ -42,7 +42,7 @@ export const STATUS_EMOJI = { offen: '💡', geplant: '📌', in_arbeit: '🔨', umgesetzt: '✅', abgelehnt: '🚫', }; -/** Knopf oder Auswahlmenü für den Aufruf-Post — je nachdem, ob es Bereiche gibt */ +/** Knöpfe oder Auswahlmenü für den Aufruf-Post — je nachdem, wie viele Bereiche */ export function wunschPanelComponents() { const bereiche = listWishCategories(); if (bereiche.length === 0) { @@ -54,6 +54,21 @@ export function wunschPanelComponents() { .setEmoji('💡') )]; } + + // Bis fünf Bereiche Knöpfe: ein Klick statt zwei, und die Auswahl steht da, + // ohne dass man erst aufklappt. Siehe MAX_KNOEPFE in tickets.js. + if (bereiche.length <= MAX_KNOEPFE) { + return [new ActionRowBuilder().addComponents(bereiche.map((b) => { + const knopf = new ButtonBuilder() + .setCustomId(`wunsch_pick:${b.id}`) + .setStyle(ButtonStyle.Secondary) + .setLabel(b.name.slice(0, 80)); + const emoji = alsEmoji(b.emoji); + if (emoji) knopf.setEmoji(emoji); + return knopf; + }))]; + } + const menu = new StringSelectMenuBuilder() .setCustomId('wunsch_pick') .setPlaceholder('Worum geht es?')