forked from syntaxbullet/AuroraBot-discord
feat: Introduce a comprehensive feedback system with a slash command, interactive UI, and configuration.
This commit is contained in:
29
src/commands/feedback/feedback.ts
Normal file
29
src/commands/feedback/feedback.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
100
src/modules/feedback/feedback.interaction.ts
Normal file
100
src/modules/feedback/feedback.interaction.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
23
src/modules/feedback/feedback.types.ts
Normal file
23
src/modules/feedback/feedback.types.ts
Normal 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;
|
||||||
117
src/modules/feedback/feedback.view.ts
Normal file
117
src/modules/feedback/feedback.view.ts
Normal 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];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user