feat: Introduce a comprehensive feedback system with a slash command, interactive UI, and configuration.

This commit is contained in:
syntaxbullet
2025-12-24 20:16:47 +01:00
parent cddd8cdf57
commit 42d2313933
6 changed files with 276 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder } from "discord.js";
import { config } from "@/lib/config";
import { createErrorEmbed } from "@/lib/embeds";
import { getFeedbackTypeMenu } from "@/modules/feedback/feedback.view";
export const feedback = createCommand({
data: new SlashCommandBuilder()
.setName("feedback")
.setDescription("Submit feedback, feature requests, or bug reports"),
execute: async (interaction) => {
// Check if feedback channel is configured
if (!config.feedbackChannelId) {
await interaction.reply({
embeds: [createErrorEmbed("Feedback system is not configured. Please contact an administrator.")],
ephemeral: true
});
return;
}
// Show feedback type selection menu
const menu = getFeedbackTypeMenu();
await interaction.reply({
content: "## 🌟 Share Your Thoughts\n\nThank you for helping improve Aurora! Please select the type of feedback you'd like to submit:",
...menu,
ephemeral: true
});
}
});

View File

@@ -51,6 +51,7 @@ export interface GameConfigType {
colorRoles: string[]; colorRoles: string[];
welcomeChannelId?: string; welcomeChannelId?: string;
welcomeMessage?: string; welcomeMessage?: string;
feedbackChannelId?: string;
terminal?: { terminal?: {
channelId: string; channelId: string;
messageId: string; messageId: string;
@@ -119,6 +120,7 @@ const configSchema = z.object({
colorRoles: z.array(z.string()).default([]), colorRoles: z.array(z.string()).default([]),
welcomeChannelId: z.string().optional(), welcomeChannelId: z.string().optional(),
welcomeMessage: z.string().optional(), welcomeMessage: z.string().optional(),
feedbackChannelId: z.string().optional(),
terminal: z.object({ terminal: z.object({
channelId: z.string(), channelId: z.string(),
messageId: z.string() messageId: z.string()

View File

@@ -33,5 +33,10 @@ export const interactionRoutes: InteractionRoute[] = [
predicate: (i) => i.isButton() && i.customId === "enrollment", predicate: (i) => i.isButton() && i.customId === "enrollment",
handler: () => import("@/modules/user/enrollment.interaction"), handler: () => import("@/modules/user/enrollment.interaction"),
method: 'handleEnrollmentInteraction' method: 'handleEnrollmentInteraction'
},
{
predicate: (i) => i.customId.startsWith("feedback_"),
handler: () => import("@/modules/feedback/feedback.interaction"),
method: 'handleFeedbackInteraction'
} }
]; ];

View File

@@ -0,0 +1,100 @@
import type { Interaction } from "discord.js";
import { TextChannel, MessageFlags } from "discord.js";
import { config } from "@/lib/config";
import { AuroraClient } from "@/lib/BotClient";
import { buildFeedbackMessage, getFeedbackModal } from "./feedback.view";
import { FEEDBACK_CUSTOM_IDS, type FeedbackType, type FeedbackData } from "./feedback.types";
import { createErrorEmbed, createSuccessEmbed } from "@/lib/embeds";
export const handleFeedbackInteraction = async (interaction: Interaction) => {
// Handle select menu for choosing feedback type
if (interaction.isStringSelectMenu() && interaction.customId === "feedback_select_type") {
const feedbackType = interaction.values[0] as FeedbackType;
if (!feedbackType) {
await interaction.reply({
embeds: [createErrorEmbed("Invalid feedback type selected.")],
ephemeral: true
});
return;
}
const modal = getFeedbackModal(feedbackType);
await interaction.showModal(modal);
return;
}
// Handle modal submission
if (interaction.isModalSubmit() && interaction.customId.startsWith(FEEDBACK_CUSTOM_IDS.MODAL)) {
// Extract feedback type from customId (format: feedback_modal_FEATURE_REQUEST)
const feedbackType = interaction.customId.split("_")[2] as FeedbackType;
if (!config.feedbackChannelId) {
await interaction.reply({
embeds: [createErrorEmbed("Feedback channel is not configured. Please contact an administrator.")],
ephemeral: true
});
return;
}
try {
// Parse modal inputs
const title = interaction.fields.getTextInputValue(FEEDBACK_CUSTOM_IDS.TITLE_FIELD);
const description = interaction.fields.getTextInputValue(FEEDBACK_CUSTOM_IDS.DESCRIPTION_FIELD);
// Build feedback data
const feedbackData: FeedbackData = {
type: feedbackType,
title,
description,
userId: interaction.user.id,
username: interaction.user.username,
timestamp: new Date()
};
// Get feedback channel
const channel = await AuroraClient.channels.fetch(config.feedbackChannelId).catch(() => null) as TextChannel | null;
if (!channel) {
await interaction.reply({
embeds: [createErrorEmbed("Feedback channel not found. Please contact an administrator.")],
ephemeral: true
});
return;
}
// Build and send beautiful message
const containers = buildFeedbackMessage(feedbackData);
const feedbackMessage = await channel.send({
components: containers as any,
flags: MessageFlags.IsComponentsV2
});
// Add reaction votes
await feedbackMessage.react("👍");
await feedbackMessage.react("👎");
// Confirm to user
await interaction.reply({
embeds: [createSuccessEmbed("Your feedback has been submitted successfully! Thank you for helping improve Aurora.", "✨ Feedback Submitted")],
ephemeral: true
});
} catch (error: any) {
console.error("Error submitting feedback:", error);
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
embeds: [createErrorEmbed("An error occurred while submitting your feedback. Please try again later.")],
ephemeral: true
});
} else {
await interaction.followUp({
embeds: [createErrorEmbed("An error occurred while submitting your feedback. Please try again later.")],
ephemeral: true
});
}
}
}
};

View File

@@ -0,0 +1,23 @@
export type FeedbackType = "FEATURE_REQUEST" | "BUG_REPORT" | "GENERAL";
export interface FeedbackData {
type: FeedbackType;
title: string;
description: string;
userId: string;
username: string;
timestamp: Date;
}
export const FEEDBACK_TYPE_LABELS: Record<FeedbackType, string> = {
FEATURE_REQUEST: "💡 Feature Request",
BUG_REPORT: "🐛 Bug Report",
GENERAL: "💬 General Feedback"
};
export const FEEDBACK_CUSTOM_IDS = {
MODAL: "feedback_modal",
TYPE_FIELD: "feedback_type",
TITLE_FIELD: "feedback_title",
DESCRIPTION_FIELD: "feedback_description"
} as const;

View File

@@ -0,0 +1,117 @@
import {
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder,
StringSelectMenuBuilder,
ActionRowBuilder as MessageActionRowBuilder,
ContainerBuilder,
TextDisplayBuilder,
ButtonBuilder,
ButtonStyle
} from "discord.js";
import { FEEDBACK_TYPE_LABELS, FEEDBACK_CUSTOM_IDS, type FeedbackData, type FeedbackType } from "./feedback.types";
export function getFeedbackTypeMenu() {
const select = new StringSelectMenuBuilder()
.setCustomId("feedback_select_type")
.setPlaceholder("Choose feedback type")
.addOptions([
{
label: "💡 Feature Request",
description: "Suggest a new feature or improvement",
value: "FEATURE_REQUEST"
},
{
label: "🐛 Bug Report",
description: "Report a bug or issue",
value: "BUG_REPORT"
},
{
label: "💬 General Feedback",
description: "Share your thoughts or suggestions",
value: "GENERAL"
}
]);
const row = new MessageActionRowBuilder<StringSelectMenuBuilder>().addComponents(select);
return { components: [row] };
}
export function getFeedbackModal(feedbackType: FeedbackType) {
const modal = new ModalBuilder()
.setCustomId(`${FEEDBACK_CUSTOM_IDS.MODAL}_${feedbackType}`)
.setTitle(FEEDBACK_TYPE_LABELS[feedbackType]);
// Title Input
const titleInput = new TextInputBuilder()
.setCustomId(FEEDBACK_CUSTOM_IDS.TITLE_FIELD)
.setLabel("Title")
.setStyle(TextInputStyle.Short)
.setPlaceholder("Brief summary of your feedback")
.setRequired(true)
.setMaxLength(100);
const titleRow = new ActionRowBuilder<TextInputBuilder>().addComponents(titleInput);
// Description Input
const descriptionInput = new TextInputBuilder()
.setCustomId(FEEDBACK_CUSTOM_IDS.DESCRIPTION_FIELD)
.setLabel("Description")
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder("Provide detailed information about your feedback")
.setRequired(true)
.setMaxLength(1000);
const descriptionRow = new ActionRowBuilder<TextInputBuilder>().addComponents(descriptionInput);
modal.addComponents(titleRow, descriptionRow);
return modal;
}
export function buildFeedbackMessage(feedback: FeedbackData) {
// Define colors/themes for each feedback type
const themes = {
FEATURE_REQUEST: {
icon: "💡",
color: "Blue",
title: "FEATURE REQUEST",
description: "A new starlight suggestion has been received"
},
BUG_REPORT: {
icon: "🐛",
color: "Red",
title: "BUG REPORT",
description: "A cosmic anomaly has been detected"
},
GENERAL: {
icon: "💬",
color: "Gray",
title: "GENERAL FEEDBACK",
description: "A message from the cosmos"
}
};
const theme = themes[feedback.type];
const timestamp = Math.floor(feedback.timestamp.getTime() / 1000);
// Header Container
const headerContainer = new ContainerBuilder()
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# ${theme.icon} ${theme.title}`),
new TextDisplayBuilder().setContent(`*${theme.description}*`)
);
// Content Container
const contentContainer = new ContainerBuilder()
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`## ${feedback.title}`),
new TextDisplayBuilder().setContent(`> ${feedback.description.split('\n').join('\n> ')}`),
new TextDisplayBuilder().setContent(
`**Submitted by:** <@${feedback.userId}>\n**Time:** <t:${timestamp}:F> (<t:${timestamp}:R>)`
)
);
return [headerContainer, contentContainer];
}