From 8dc4ef4ff40957eb3e2a69fef2fc0f983edee70f Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Fri, 18 Sep 2026 19:58:48 +0300 Subject: [PATCH] feat: add a modal to showcase deletion and author in logs --- src/features/showcase/create-showcase.ts | 3 +- src/features/showcase/delete-showcase.ts | 150 +++++++++++++++++++---- src/features/showcase/util.ts | 28 +++++ 3 files changed, 159 insertions(+), 22 deletions(-) diff --git a/src/features/showcase/create-showcase.ts b/src/features/showcase/create-showcase.ts index 85e766e..108edc4 100644 --- a/src/features/showcase/create-showcase.ts +++ b/src/features/showcase/create-showcase.ts @@ -20,7 +20,7 @@ import { } from '@/common/interactions/modal-interaction.js'; import { logToChannel } from '@/util/channel-logging.js'; import { customId } from '@/util/custom-id.js'; -import { deleteShowcase } from './delete-showcase.js'; +import { deleteShowcase, deleteShowcaseModal } from './delete-showcase.js'; import { editShowcaseInteraction } from './edit-showcase.js'; import { buildShowcaseModal, createShowcaseMessageContent } from './util.js'; import { SERVER_CHANNELS } from '@/constants/channels.js'; @@ -157,5 +157,6 @@ const modalHandler: ModalSubmitInteraction = { }; registerModalSubmitInteraction(modalHandler); +registerModalSubmitInteraction(deleteShowcaseModal); registerButtonSubmitInteraction(deleteShowcase); registerButtonSubmitInteraction(editShowcaseInteraction); diff --git a/src/features/showcase/delete-showcase.ts b/src/features/showcase/delete-showcase.ts index 2385adc..b2f634a 100644 --- a/src/features/showcase/delete-showcase.ts +++ b/src/features/showcase/delete-showcase.ts @@ -1,9 +1,55 @@ -import { ChannelType, Colors, EmbedBuilder, MessageFlags } from 'discord.js'; +import { + ChannelType, + Colors, + EmbedBuilder, + MessageFlags, + User, +} from 'discord.js'; import type { ButtonSubmitInteraction } from '@/common/interactions/button-interaction.js'; +import type { ModalSubmitInteraction } from '@/common/interactions/modal-interaction.js'; import { logToChannel } from '@/util/channel-logging.js'; -import { parseCustomId } from '@/util/custom-id.js'; +import { customId, parseCustomId } from '@/util/custom-id.js'; import { isUserInServer, isUserModerator } from '@/util/member.js'; import { SERVER_CHANNELS } from '@/constants/channels.js'; +import { buildDeleteShowcaseModal, parseShowcaseMessage } from './util.js'; + +const logShowcaseDeletion = async ({ + projectName, + authorId, + interactionUser, + reason, +}: { + projectName: string; + authorId: string; + interactionUser: User; + reason?: string; +}): Promise => { + const embed = new EmbedBuilder() + .setTitle('Showcase Deleted') + .setDescription( + [ + `**Project Name:** ${projectName}`, + `**Author:** <@${authorId}>`, + `**Deleted By:** <@${interactionUser.id}>`, + reason ? `**Reason:** ${reason}` : undefined, + ] + .filter((line) => line !== undefined) + .join('\n') + ) + .setColor(Colors.Red) + .setAuthor({ + name: interactionUser.tag, + iconURL: interactionUser.displayAvatarURL(), + }); + + await logToChannel({ + channel: SERVER_CHANNELS.showcaseLogs, + content: { + type: 'embed', + embed, + }, + }); +}; export const deleteShowcase: ButtonSubmitInteraction = { commandName: 'delete_showcase', @@ -42,7 +88,75 @@ export const deleteShowcase: ButtonSubmitInteraction = { }); return; } - if (forumPost === null) { + + const message = await forumPost.fetchStarterMessage(); + if (!message) { + await interaction.reply({ + content: '❌ Could not find the showcase message to delete.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + if ( + interactionUser.id !== ownerId && + isUserModerator(interaction.member, interaction) + ) { + const modal = buildDeleteShowcaseModal({ + id: customId('delete_showcase_modal', forumPost.id), + }); + await interaction.showModal(modal); + return; + } + + const projectName = forumPost.name; + const { authorId } = parseShowcaseMessage(message.content); + await interaction.channel?.delete(); + await logShowcaseDeletion({ projectName, authorId, interactionUser }); + await interaction.reply({ + content: '✅ Showcase post has been deleted.', + flags: MessageFlags.Ephemeral, + }); + } catch (error) { + console.error('Error deleting showcase:', error); + await interaction.reply({ + content: '❌ An error occurred while trying to delete the showcase.', + flags: MessageFlags.Ephemeral, + }); + } + }, +}; + +export const deleteShowcaseModal: ModalSubmitInteraction = { + commandName: 'delete_showcase_modal', + handler: async (interaction) => { + const interactionUser = interaction.user; + const [, forumPostId] = parseCustomId(interaction.customId); + const deleteReason = + interaction.fields.getTextInputValue('deleteReason') || undefined; + + if (!interaction.member || !isUserInServer(interaction.member)) { + await interaction.reply({ + content: '❌ This command can only be used by server members.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (!isUserModerator(interaction.member, interaction)) { + await interaction.reply({ + content: '❌ You do not have permission to delete this showcase.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + try { + const forumPost = + interaction.channel?.type === ChannelType.PublicThread + ? interaction.channel + : null; + if (forumPost === null || forumPost.id !== forumPostId) { await interaction.reply({ content: '❌ This command can only be used in a forum post.', flags: MessageFlags.Ephemeral, @@ -60,25 +174,21 @@ export const deleteShowcase: ButtonSubmitInteraction = { } const projectName = forumPost.name; + const { authorId } = parseShowcaseMessage(message.content); + await interaction.reply({ + content: '✅ Showcase deleted successfully.', + flags: MessageFlags.Ephemeral, + }); await interaction.channel?.delete(); - await logToChannel({ - channel: SERVER_CHANNELS.showcaseLogs, - content: { - type: 'embed', - embed: new EmbedBuilder() - .setTitle('Showcase Deleted') - .setDescription( - `**Project Name:** ${projectName}\n**Deleted By:** <@${interactionUser.id}>` - ) - .setColor(Colors.Red) - .setAuthor({ - name: interactionUser.tag, - iconURL: interactionUser.displayAvatarURL(), - }), - }, + + void logShowcaseDeletion({ + projectName, + authorId, + interactionUser, + reason: deleteReason, }); } catch (error) { - console.error('Error deleting showcase:', error); + console.error('Error deleting showcase via modal:', error); await interaction.reply({ content: '❌ An error occurred while trying to delete the showcase.', flags: MessageFlags.Ephemeral, @@ -86,5 +196,3 @@ export const deleteShowcase: ButtonSubmitInteraction = { } }, }; - -// Registration is in create-showcase.ts diff --git a/src/features/showcase/util.ts b/src/features/showcase/util.ts index 571e468..417829b 100644 --- a/src/features/showcase/util.ts +++ b/src/features/showcase/util.ts @@ -8,6 +8,8 @@ import { StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle, + ActionRowBuilder, + type ModalActionRowComponentBuilder, } from 'discord.js'; export type ShowcaseMessageData = { @@ -162,3 +164,29 @@ export const resolveTagNames = ( (id) => availableTags.find((tag) => tag.id === id)?.name ?? id ); }; + +export type BuildDeleteShowcaseModalOptions = { + id: string; +}; + +export const buildDeleteShowcaseModal = ({ + id, +}: BuildDeleteShowcaseModalOptions): ModalBuilder => { + return new ModalBuilder() + .setCustomId(id) + .setTitle('Delete Showcase') + .addLabelComponents((label) => + label + .setLabel('Reason for deletion (optional)') + .setDescription( + 'Provide a reason for deleting this showcase (optional)' + ) + .setTextInputComponent((textInput) => + textInput + .setCustomId('deleteReason') + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(1000) + .setRequired(false) + ) + ); +};