diff --git a/src/commands/feedback/feedback.ts b/src/commands/feedback/feedback.ts new file mode 100644 index 0000000..4cfbbe8 --- /dev/null +++ b/src/commands/feedback/feedback.ts @@ -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 + }); + } +}); diff --git a/src/lib/config.ts b/src/lib/config.ts index c6b28b8..aad2b41 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -51,6 +51,7 @@ export interface GameConfigType { colorRoles: string[]; welcomeChannelId?: string; welcomeMessage?: string; + feedbackChannelId?: string; terminal?: { channelId: string; messageId: string; @@ -119,6 +120,7 @@ const configSchema = z.object({ colorRoles: z.array(z.string()).default([]), welcomeChannelId: z.string().optional(), welcomeMessage: z.string().optional(), + feedbackChannelId: z.string().optional(), terminal: z.object({ channelId: z.string(), messageId: z.string() diff --git a/src/lib/interaction.routes.ts b/src/lib/interaction.routes.ts index 15e13da..0e7f536 100644 --- a/src/lib/interaction.routes.ts +++ b/src/lib/interaction.routes.ts @@ -33,5 +33,10 @@ export const interactionRoutes: InteractionRoute[] = [ predicate: (i) => i.isButton() && i.customId === "enrollment", handler: () => import("@/modules/user/enrollment.interaction"), method: 'handleEnrollmentInteraction' + }, + { + predicate: (i) => i.customId.startsWith("feedback_"), + handler: () => import("@/modules/feedback/feedback.interaction"), + method: 'handleFeedbackInteraction' } ]; diff --git a/src/modules/feedback/feedback.interaction.ts b/src/modules/feedback/feedback.interaction.ts new file mode 100644 index 0000000..9f50009 --- /dev/null +++ b/src/modules/feedback/feedback.interaction.ts @@ -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 + }); + } + } + } +}; diff --git a/src/modules/feedback/feedback.types.ts b/src/modules/feedback/feedback.types.ts new file mode 100644 index 0000000..31beec3 --- /dev/null +++ b/src/modules/feedback/feedback.types.ts @@ -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 = { + 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; diff --git a/src/modules/feedback/feedback.view.ts b/src/modules/feedback/feedback.view.ts new file mode 100644 index 0000000..a105064 --- /dev/null +++ b/src/modules/feedback/feedback.view.ts @@ -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().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().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().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:** ()` + ) + ); + + return [headerContainer, contentContainer]; +}