Bis fuenf Eintraege Knoepfe statt Auswahlmenue
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
+12
-7
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+34
-1
@@ -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
|
||||
|
||||
+17
-2
@@ -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?')
|
||||
|
||||
Reference in New Issue
Block a user