Goal: Build a Quality Control system where designs are submitted, reviewed, and approved or denied.
All submissions go to a private QC channel.
Only people with QC role can approve or deny.
Buttons update the message and lock after decision.
A thread is created automatically for feedback.
Users submit designs using /qc command with image and channel.
Bot sends submission to QC channel with buttons.
// Buttons: Approve or Deny
if (interaction.customId === "qc_approve") {
// approve logic
}
if (interaction.customId === "qc_deny") {
// deny logic
}
Disable buttons after approval/denial so it can't be changed.
const { ContainerBuilder, SeparatorSpacingSize, ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags, MessageActivityType, Emoji } = require("discord.js");
const { execute } = require("../modals/say");
const QC_ROLE_ID = "";
module.export = {
customIDs: ['qc_approve', 'qc_deny'],
async execute(interaction) {
const member = interaction.member;
if (!member.roles.cache.has(QC_ROLE_ID)) {
return interaction.reply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent("You do not have permission.")
)
],
flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral,
});
}
const approved = interaction.customId === "qc_approve";
const original = interaction.message;
const attachment = original.components
.flatMap((c) => c.components ?? [])
.find((c) => c.type === "media_gallery" || c.data?.type === 11);
const imageUrl =
original.attachments?.first()?.url ??
original.embeds?.[0]?.image?.url ??
null;
const originalText = original.components?.[0]?.components
?.find((c) => c.data?.content?.includes("Designer"))
?.data?.content ?? "";
const designerMatch = originalText.match(/\*\*Designer:\*\* <@(\d+)>/);
const channelMatch = originalText.match(
/\*\*Intended Channel:\*\* <#(\d+)>/
);
const color = approved ? 0x2ecc71 : 0xe74c3c;
const statusText = approved ? 'Approved' : 'Denied';
const emoji = approved ? "✅" : "❌";
const updatedContainer = new ContainerBuilder()
.addTextDisplayComponents(
(t) =>
t.setContent(`## ${emoji} QC Submission`),
(t) =>
t.setContent(
[
`**Designer:** <@${designerId}>`,
`**Intended Channel:** <#${channelId}>`,
`**Status:** ${statusText}`,
`**Actioned by:** <@${interaction.user.id}>`,
].join("\n")
)
)
.addMediaGalleryComponents((g) =>
g.addItems((i) =>
i.setURL(
original.components
.find((c) => c.data?.components?.find(
(i) => i.data?.type === 11
))
?.data?.components
?.find((i) => i.data?.type === 11)
?.data?.items?.[0]?.media.url ?? image
)
)
);
const disabledRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("qc_approve")
.setLabel("approve")
.setStyle(ButtonStyle.Success)
.setDisabled(true),
new ButtonBuilder()
.setCustomId("qc_deny")
.setLabel("Deny")
.setStyle(ButtonStyle.Danger)
.setDisabled(true)
);
return interaction.update({
components: [updatedContainer, disabledRow],
flags: MessageFlags.IsComponentsV2,
});
}
}
const { SlashCommandBuilder } = require("@discordjs/builders");
const {
ContainerBuilder,
SeparatorSpacingSize,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
MessageFlags,
ChannelType,
} = require("discord.js");
const QC_CHANNEL_ID = "";
const QC_ROLE_ID = ""; // who to ping, who can accept / deny
const DESIGNER_ROLE_ID = "";
module.exports = {
dev: true, // visible only to guilds in config.json
guilds: [''], // whitelist by guild ID
roles: [''], // requires user to have listed role IDs
users: [''], // whitelist by user ID
cooldown: 0, // cooldown time in seconds
userPerms: [''], // required user permissions
clientPerms: [''], // required bot permissions
alias: '', // Single Alias
aliases: [''], // Mulitple Aliases
defer: false, // defer without being ephemeral | true or false
cache: true, // the next time you run this command it will jump straight to the bottom without delay. | true or false
data: new SlashCommandBuilder()
.setName('qc') // Name of your slash command
.setDescription('quality control command')
.addAttachmentOption((opt) =>
opt
.setName("image")
.setDescription("the design image to submit")
.setRequired(true)
)
.addChannelOption((opt) =>
opt
.setName("channel")
.setDescription("order channel")
.setRequired(true)
),
async execute(interaction) {
// Command logic here
const image = interaction.options.getAttachment("image");
const channel = interaction.options.getChannel("channel");
const designer = interaction.user;
const qcChannel =
await interaction.client.channels.fetch(QC_CHANNEL_ID);
const container = new ContainerBuilder()
.addTextDisplayComponents(
(t) => t.setContent("## Quality Control"),
(t) =>
t.setContent(
[
`**Designer** <@${designer.id}>`,
`**Intended Channel:** <#${channel.id}>`,
`**Status** Pending`,
`\n<@&${QC_ROLE_ID}>`,
].join('\n')
)
)
.addSeparatorComponents((s) =>
s.setSpacing(SeparatorSpacingSize.Small)
)
.addMediaGallery((g) =>
g.addItems((i) => i.setUrl(image.url))
);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("qc_approve")
.setLabel("Approve")
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId("qc_deny")
.setLabel("Deny")
.setStyle(ButtonStyle.Danger)
);
const qcMessage = await qcChannel.send({
components: [container, row],
flags: MessageFlags.IsComponentsV2,
});
await qcMessage.startThread({
name: `QC - ${designer.username}`,
autoArchiveDuration: 1440,
type: ChannelType.PublicThread,
});
return interaction.editReply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent("Your design was submitted for QC.")
),
],
flags: MessageFlags.IsComponentsV2,
})
},
};