refactor: centralize custom interaction IDs into constants

Replace all hardcoded custom ID strings with module-level constants.
Each module now has *_CUSTOM_IDS in its types file, using functions
for dynamic IDs and PREFIX for startsWith matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-02 11:36:35 +02:00
parent 70d59a091a
commit 3c256ba0b2
27 changed files with 238 additions and 132 deletions

View File

@@ -11,6 +11,7 @@ import {
getCancelledEmbed getCancelledEmbed
} from "@/modules/moderation/prune.view"; } from "@/modules/moderation/prune.view";
import { withCommandErrorHandling } from "@lib/commandUtils"; import { withCommandErrorHandling } from "@lib/commandUtils";
import { PRUNE_CUSTOM_IDS } from "@modules/moderation/prune.types";
export const prune = createCommand({ export const prune = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -83,7 +84,7 @@ export const prune = createCommand({
time: 30000 time: 30000
}); });
if (confirmation.customId === "cancel_prune") { if (confirmation.customId === PRUNE_CUSTOM_IDS.CANCEL) {
await confirmation.update({ await confirmation.update({
embeds: [getCancelledEmbed()], embeds: [getCancelledEmbed()],
components: [] components: []

View File

@@ -2,11 +2,12 @@ import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags } from "discord.js"; import { SlashCommandBuilder, MessageFlags } from "discord.js";
import { questService } from "@shared/modules/quest/quest.service"; import { questService } from "@shared/modules/quest/quest.service";
import { createSuccessEmbed } from "@lib/embeds"; import { createSuccessEmbed } from "@lib/embeds";
import { import {
getQuestListComponents, getQuestListComponents,
getAvailableQuestsComponents, getAvailableQuestsComponents,
getQuestActionRows getQuestActionRows
} from "@/modules/quest/quest.view"; } from "@/modules/quest/quest.view";
import { QUEST_CUSTOM_IDS } from "@modules/quest/quest.types";
export const quests = createCommand({ export const quests = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -56,19 +57,19 @@ export const quests = createCommand({
if (i.user.id !== interaction.user.id) return; if (i.user.id !== interaction.user.id) return;
try { try {
if (i.customId === "quest_view_active") { if (i.customId === QUEST_CUSTOM_IDS.VIEW_ACTIVE) {
await i.deferUpdate(); await i.deferUpdate();
await updateView('active', 0); await updateView('active', 0);
} else if (i.customId === "quest_view_available") { } else if (i.customId === QUEST_CUSTOM_IDS.VIEW_AVAILABLE) {
await i.deferUpdate(); await i.deferUpdate();
await updateView('available', 0); await updateView('available', 0);
} else if (i.customId === "quest_page_prev") { } else if (i.customId === QUEST_CUSTOM_IDS.PAGE_PREV) {
await i.deferUpdate(); await i.deferUpdate();
await updateView(currentView, Math.max(0, currentPage - 1)); await updateView(currentView, Math.max(0, currentPage - 1));
} else if (i.customId === "quest_page_next") { } else if (i.customId === QUEST_CUSTOM_IDS.PAGE_NEXT) {
await i.deferUpdate(); await i.deferUpdate();
await updateView(currentView, currentPage + 1); await updateView(currentView, currentPage + 1);
} else if (i.customId.startsWith("quest_accept:")) { } else if (i.customId.startsWith(QUEST_CUSTOM_IDS.ACCEPT_PREFIX)) {
const questIdStr = i.customId.split(":")[1]; const questIdStr = i.customId.split(":")[1];
if (!questIdStr) return; if (!questIdStr) return;
const questId = parseInt(questIdStr); const questId = parseInt(questIdStr);

View File

@@ -1,11 +1,14 @@
import { ButtonInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from "discord.js"; import { ButtonInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from "discord.js";
import { TRADE_CUSTOM_IDS } from "@modules/trade/trade.types";
import { SHOP_CUSTOM_IDS, LOOTDROP_CUSTOM_IDS } from "@modules/economy/economy.types";
import { ITEM_WIZARD_CUSTOM_IDS } from "@modules/admin/item_wizard.types";
import { TRIVIA_CUSTOM_IDS } from "@modules/trivia/trivia.types";
import { ENROLLMENT_CUSTOM_IDS } from "@modules/user/user.types";
import { FEEDBACK_CUSTOM_IDS } from "@modules/feedback/feedback.types";
// Union type for all component interactions // Union type for all component interactions
type ComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction; type ComponentInteraction = ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction;
// Type for the handler function that modules export
type InteractionHandler = (interaction: ComponentInteraction) => Promise<void>;
// Type for the dynamically imported module containing the handler // Type for the dynamically imported module containing the handler
interface InteractionModule { interface InteractionModule {
[key: string]: (...args: any[]) => Promise<void> | any; [key: string]: (...args: any[]) => Promise<void> | any;
@@ -21,45 +24,45 @@ interface InteractionRoute {
export const interactionRoutes: InteractionRoute[] = [ export const interactionRoutes: InteractionRoute[] = [
// --- TRADE MODULE --- // --- TRADE MODULE ---
{ {
predicate: (i) => i.customId.startsWith("trade_") || i.customId === "amount", predicate: (i) => i.customId.startsWith(TRADE_CUSTOM_IDS.PREFIX) || i.customId === TRADE_CUSTOM_IDS.MONEY_AMOUNT_FIELD,
handler: () => import("@/modules/trade/trade.interaction"), handler: () => import("@/modules/trade/trade.interaction"),
method: 'handleTradeInteraction' method: 'handleTradeInteraction'
}, },
// --- ECONOMY MODULE --- // --- ECONOMY MODULE ---
{ {
predicate: (i) => i.isButton() && i.customId.startsWith("shop_buy_"), predicate: (i) => i.isButton() && i.customId.startsWith(SHOP_CUSTOM_IDS.BUY_PREFIX),
handler: () => import("@/modules/economy/shop.interaction"), handler: () => import("@/modules/economy/shop.interaction"),
method: 'handleShopInteraction' method: 'handleShopInteraction'
}, },
{ {
predicate: (i) => i.isButton() && i.customId.startsWith("lootdrop_"), predicate: (i) => i.isButton() && i.customId.startsWith(LOOTDROP_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/economy/lootdrop.interaction"), handler: () => import("@/modules/economy/lootdrop.interaction"),
method: 'handleLootdropInteraction' method: 'handleLootdropInteraction'
}, },
{ {
predicate: (i) => i.isButton() && i.customId.startsWith("trivia_"), predicate: (i) => i.isButton() && i.customId.startsWith(TRIVIA_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/trivia/trivia.interaction"), handler: () => import("@/modules/trivia/trivia.interaction"),
method: 'handleTriviaInteraction' method: 'handleTriviaInteraction'
}, },
// --- ADMIN MODULE --- // --- ADMIN MODULE ---
{ {
predicate: (i) => i.customId.startsWith("createitem_"), predicate: (i) => i.customId.startsWith(ITEM_WIZARD_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/admin/item_wizard"), handler: () => import("@/modules/admin/item_wizard"),
method: 'handleItemWizardInteraction' method: 'handleItemWizardInteraction'
}, },
// --- USER MODULE --- // --- USER MODULE ---
{ {
predicate: (i) => i.isButton() && i.customId === "enrollment", predicate: (i) => i.isButton() && i.customId === ENROLLMENT_CUSTOM_IDS.ENROLL,
handler: () => import("@/modules/user/enrollment.interaction"), handler: () => import("@/modules/user/enrollment.interaction"),
method: 'handleEnrollmentInteraction' method: 'handleEnrollmentInteraction'
}, },
// --- FEEDBACK MODULE --- // --- FEEDBACK MODULE ---
{ {
predicate: (i) => i.customId.startsWith("feedback_"), predicate: (i) => i.customId.startsWith(FEEDBACK_CUSTOM_IDS.PREFIX),
handler: () => import("@/modules/feedback/feedback.interaction"), handler: () => import("@/modules/feedback/feedback.interaction"),
method: 'handleFeedbackInteraction' method: 'handleFeedbackInteraction'
} }

View File

@@ -3,7 +3,7 @@ import { items } from "@db/schema";
import { DrizzleClient } from "@shared/db/DrizzleClient"; import { DrizzleClient } from "@shared/db/DrizzleClient";
import type { ItemUsageData, ItemEffect } from "@shared/lib/types"; import type { ItemUsageData, ItemEffect } from "@shared/lib/types";
import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view"; import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view";
import type { DraftItem } from "./item_wizard.types"; import { ITEM_WIZARD_CUSTOM_IDS, type DraftItem } from "./item_wizard.types";
import { ItemType, EffectType } from "@shared/lib/constants"; import { ItemType, EffectType } from "@shared/lib/constants";
// --- Types --- // --- Types ---
@@ -41,13 +41,13 @@ export const renderWizard = (userId: string, isDraft = true) => {
export const handleItemWizardInteraction = async (interaction: Interaction) => { export const handleItemWizardInteraction = async (interaction: Interaction) => {
// Only handle createitem interactions // Only handle createitem interactions
if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit()) return; if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit()) return;
if (!interaction.customId.startsWith("createitem_")) return; if (!interaction.customId.startsWith(ITEM_WIZARD_CUSTOM_IDS.PREFIX)) return;
const userId = interaction.user.id; const userId = interaction.user.id;
let draft = draftSession.get(userId); let draft = draftSession.get(userId);
// Special case for Cancel - doesn't need draft checks usually, but we want to clear it // Special case for Cancel - doesn't need draft checks usually, but we want to clear it
if (interaction.customId === "createitem_cancel") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.CANCEL) {
draftSession.delete(userId); draftSession.delete(userId);
if (interaction.isMessageComponent()) { if (interaction.isMessageComponent()) {
await interaction.update({ content: "❌ Item creation cancelled.", embeds: [], components: [] }); await interaction.update({ content: "❌ Item creation cancelled.", embeds: [], components: [] });
@@ -59,7 +59,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
if (!draft) { if (!draft) {
if (interaction.isMessageComponent()) { if (interaction.isMessageComponent()) {
// Create one implicitly to prevent crashes, or warn user // Create one implicitly to prevent crashes, or warn user
if (interaction.customId === "createitem_start") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.START) {
// Allow start // Allow start
} else { } else {
await interaction.reply({ content: "⚠️ Session expired. Please run `/createitem` again.", ephemeral: true }); await interaction.reply({ content: "⚠️ Session expired. Please run `/createitem` again.", ephemeral: true });
@@ -81,7 +81,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
// --- Routing --- // --- Routing ---
// 1. Details Modal // 1. Details Modal
if (interaction.customId === "createitem_details") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.DETAILS) {
if (!interaction.isButton()) return; if (!interaction.isButton()) return;
const modal = getDetailsModal(draft); const modal = getDetailsModal(draft);
await interaction.showModal(modal); await interaction.showModal(modal);
@@ -89,7 +89,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
} }
// 2. Economy Modal // 2. Economy Modal
if (interaction.customId === "createitem_economy") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.ECONOMY) {
if (!interaction.isButton()) return; if (!interaction.isButton()) return;
const modal = getEconomyModal(draft); const modal = getEconomyModal(draft);
await interaction.showModal(modal); await interaction.showModal(modal);
@@ -97,7 +97,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
} }
// 3. Visuals Modal // 3. Visuals Modal
if (interaction.customId === "createitem_visuals") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.VISUALS) {
if (!interaction.isButton()) return; if (!interaction.isButton()) return;
const modal = getVisualsModal(draft); const modal = getVisualsModal(draft);
await interaction.showModal(modal); await interaction.showModal(modal);
@@ -105,14 +105,14 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
} }
// 4. Type Toggle (Start Select Menu) // 4. Type Toggle (Start Select Menu)
if (interaction.customId === "createitem_type_toggle") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.TYPE_TOGGLE) {
if (!interaction.isButton()) return; if (!interaction.isButton()) return;
const { components } = getItemTypeSelection(); const { components } = getItemTypeSelection();
await interaction.update({ components }); // Temporary view await interaction.update({ components }); // Temporary view
return; return;
} }
if (interaction.customId === "createitem_select_type") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.SELECT_TYPE) {
if (!interaction.isStringSelectMenu()) return; if (!interaction.isStringSelectMenu()) return;
const selected = interaction.values[0]; const selected = interaction.values[0];
if (selected) { if (selected) {
@@ -125,14 +125,14 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
} }
// 5. Add Effect Flow // 5. Add Effect Flow
if (interaction.customId === "createitem_addeffect_start") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.ADD_EFFECT_START) {
if (!interaction.isButton()) return; if (!interaction.isButton()) return;
const { components } = getEffectTypeSelection(); const { components } = getEffectTypeSelection();
await interaction.update({ components }); await interaction.update({ components });
return; return;
} }
if (interaction.customId === "createitem_select_effect_type") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.SELECT_EFFECT_TYPE) {
if (!interaction.isStringSelectMenu()) return; if (!interaction.isStringSelectMenu()) return;
const effectType = interaction.values[0]; const effectType = interaction.values[0];
if (!effectType) return; if (!effectType) return;
@@ -149,7 +149,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
} }
// Toggle Consume // Toggle Consume
if (interaction.customId === "createitem_toggle_consume") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.TOGGLE_CONSUME) {
if (!interaction.isButton()) return; if (!interaction.isButton()) return;
draft.usageData.consume = !draft.usageData.consume; draft.usageData.consume = !draft.usageData.consume;
const payload = renderWizard(userId); const payload = renderWizard(userId);
@@ -159,43 +159,43 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
// 6. Handle Modal Submits // 6. Handle Modal Submits
if (interaction.isModalSubmit()) { if (interaction.isModalSubmit()) {
if (interaction.customId === "createitem_modal_details") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_DETAILS) {
draft.name = interaction.fields.getTextInputValue("name"); draft.name = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_NAME);
draft.description = interaction.fields.getTextInputValue("desc"); draft.description = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_DESC);
draft.rarity = interaction.fields.getTextInputValue("rarity"); draft.rarity = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_RARITY);
} }
else if (interaction.customId === "createitem_modal_economy") { else if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_ECONOMY) {
const price = parseInt(interaction.fields.getTextInputValue("price")); const price = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_PRICE));
draft.price = isNaN(price) || price === 0 ? null : price; draft.price = isNaN(price) || price === 0 ? null : price;
} }
else if (interaction.customId === "createitem_modal_visuals") { else if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_VISUALS) {
draft.iconUrl = interaction.fields.getTextInputValue("icon"); draft.iconUrl = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_ICON);
draft.imageUrl = interaction.fields.getTextInputValue("image"); draft.imageUrl = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_IMAGE);
} }
else if (interaction.customId === "createitem_modal_effect") { else if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.MODAL_EFFECT) {
const type = draft.pendingEffectType; const type = draft.pendingEffectType;
if (type) { if (type) {
let effect: ItemEffect | null = null; let effect: ItemEffect | null = null;
if (type === EffectType.ADD_XP || type === EffectType.ADD_BALANCE) { if (type === EffectType.ADD_XP || type === EffectType.ADD_BALANCE) {
const amount = parseInt(interaction.fields.getTextInputValue("amount")); const amount = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_AMOUNT));
if (!isNaN(amount)) effect = { type: type as any, amount }; if (!isNaN(amount)) effect = { type: type as any, amount };
} }
else if (type === EffectType.REPLY_MESSAGE) { else if (type === EffectType.REPLY_MESSAGE) {
effect = { type: EffectType.REPLY_MESSAGE, message: interaction.fields.getTextInputValue("message") }; effect = { type: EffectType.REPLY_MESSAGE, message: interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_MESSAGE) };
} }
else if (type === EffectType.XP_BOOST) { else if (type === EffectType.XP_BOOST) {
const multiplier = parseFloat(interaction.fields.getTextInputValue("multiplier")); const multiplier = parseFloat(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_MULTIPLIER));
const duration = parseInt(interaction.fields.getTextInputValue("duration")); const duration = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION));
if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: EffectType.XP_BOOST, multiplier, durationSeconds: duration }; if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: EffectType.XP_BOOST, multiplier, durationSeconds: duration };
} }
else if (type === EffectType.TEMP_ROLE) { else if (type === EffectType.TEMP_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id"); const roleId = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID);
const duration = parseInt(interaction.fields.getTextInputValue("duration")); const duration = parseInt(interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION));
if (roleId && !isNaN(duration)) effect = { type: EffectType.TEMP_ROLE, roleId: roleId, durationSeconds: duration }; if (roleId && !isNaN(duration)) effect = { type: EffectType.TEMP_ROLE, roleId: roleId, durationSeconds: duration };
} }
else if (type === EffectType.COLOR_ROLE) { else if (type === EffectType.COLOR_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id"); const roleId = interaction.fields.getTextInputValue(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID);
if (roleId) effect = { type: EffectType.COLOR_ROLE, roleId: roleId }; if (roleId) effect = { type: EffectType.COLOR_ROLE, roleId: roleId };
} }
@@ -214,7 +214,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
} }
// 7. Save // 7. Save
if (interaction.customId === "createitem_save") { if (interaction.customId === ITEM_WIZARD_CUSTOM_IDS.SAVE) {
if (!interaction.isButton()) return; if (!interaction.isButton()) return;
await interaction.deferUpdate(); // Prepare to save await interaction.deferUpdate(); // Prepare to save

View File

@@ -1,5 +1,36 @@
import type { ItemUsageData } from "@shared/lib/types"; import type { ItemUsageData } from "@shared/lib/types";
export const ITEM_WIZARD_CUSTOM_IDS = {
PREFIX: "createitem_",
START: "createitem_start",
DETAILS: "createitem_details",
ECONOMY: "createitem_economy",
VISUALS: "createitem_visuals",
TYPE_TOGGLE: "createitem_type_toggle",
SELECT_TYPE: "createitem_select_type",
ADD_EFFECT_START: "createitem_addeffect_start",
SELECT_EFFECT_TYPE: "createitem_select_effect_type",
TOGGLE_CONSUME: "createitem_toggle_consume",
SAVE: "createitem_save",
CANCEL: "createitem_cancel",
MODAL_DETAILS: "createitem_modal_details",
MODAL_ECONOMY: "createitem_modal_economy",
MODAL_VISUALS: "createitem_modal_visuals",
MODAL_EFFECT: "createitem_modal_effect",
// Modal field IDs
FIELD_NAME: "name",
FIELD_DESC: "desc",
FIELD_RARITY: "rarity",
FIELD_PRICE: "price",
FIELD_ICON: "icon",
FIELD_IMAGE: "image",
FIELD_AMOUNT: "amount",
FIELD_MESSAGE: "message",
FIELD_MULTIPLIER: "multiplier",
FIELD_DURATION: "duration",
FIELD_ROLE_ID: "role_id",
} as const;
export interface DraftItem { export interface DraftItem {
name: string; name: string;
description: string; description: string;

View File

@@ -9,7 +9,7 @@ import {
type MessageActionRowComponentBuilder type MessageActionRowComponentBuilder
} from "discord.js"; } from "discord.js";
import { createBaseEmbed } from "@lib/embeds"; import { createBaseEmbed } from "@lib/embeds";
import type { DraftItem } from "./item_wizard.types"; import { ITEM_WIZARD_CUSTOM_IDS, type DraftItem } from "./item_wizard.types";
import { ItemType } from "@shared/lib/constants"; import { ItemType } from "@shared/lib/constants";
const getItemTypeOptions = () => [ const getItemTypeOptions = () => [
@@ -51,18 +51,18 @@ export const getItemWizardEmbed = (draft: DraftItem) => {
// Components // Components
const row1 = new ActionRowBuilder<MessageActionRowComponentBuilder>() const row1 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents( .addComponents(
new ButtonBuilder().setCustomId("createitem_details").setLabel("Edit Details").setStyle(ButtonStyle.Secondary).setEmoji("📝"), new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.DETAILS).setLabel("Edit Details").setStyle(ButtonStyle.Secondary).setEmoji("📝"),
new ButtonBuilder().setCustomId("createitem_economy").setLabel("Edit Economy").setStyle(ButtonStyle.Secondary).setEmoji("💰"), new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.ECONOMY).setLabel("Edit Economy").setStyle(ButtonStyle.Secondary).setEmoji("💰"),
new ButtonBuilder().setCustomId("createitem_visuals").setLabel("Edit Visuals").setStyle(ButtonStyle.Secondary).setEmoji("🖼️"), new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.VISUALS).setLabel("Edit Visuals").setStyle(ButtonStyle.Secondary).setEmoji("🖼️"),
new ButtonBuilder().setCustomId("createitem_type_toggle").setLabel("Change Type").setStyle(ButtonStyle.Secondary).setEmoji("🔄"), new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.TYPE_TOGGLE).setLabel("Change Type").setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
); );
const row2 = new ActionRowBuilder<MessageActionRowComponentBuilder>() const row2 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents( .addComponents(
new ButtonBuilder().setCustomId("createitem_addeffect_start").setLabel("Add Effect").setStyle(ButtonStyle.Primary).setEmoji("✨"), new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.ADD_EFFECT_START).setLabel("Add Effect").setStyle(ButtonStyle.Primary).setEmoji("✨"),
new ButtonBuilder().setCustomId("createitem_toggle_consume").setLabel(`Consume: ${draft.usageData.consume ? "ON" : "OFF"}`).setStyle(ButtonStyle.Secondary).setEmoji("🔄"), new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.TOGGLE_CONSUME).setLabel(`Consume: ${draft.usageData.consume ? "ON" : "OFF"}`).setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
new ButtonBuilder().setCustomId("createitem_save").setLabel("Save Item").setStyle(ButtonStyle.Success).setEmoji("💾"), new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.SAVE).setLabel("Save Item").setStyle(ButtonStyle.Success).setEmoji("💾"),
new ButtonBuilder().setCustomId("createitem_cancel").setLabel("Cancel").setStyle(ButtonStyle.Danger).setEmoji("✖️") new ButtonBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.CANCEL).setLabel("Cancel").setStyle(ButtonStyle.Danger).setEmoji("✖️")
); );
return { embeds: [embed], components: [row1, row2] }; return { embeds: [embed], components: [row1, row2] };
@@ -70,65 +70,65 @@ export const getItemWizardEmbed = (draft: DraftItem) => {
export const getItemTypeSelection = () => { export const getItemTypeSelection = () => {
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents( const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder().setCustomId("createitem_select_type").setPlaceholder("Select Item Type").addOptions(getItemTypeOptions()) new StringSelectMenuBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.SELECT_TYPE).setPlaceholder("Select Item Type").addOptions(getItemTypeOptions())
); );
return { components: [row] }; return { components: [row] };
}; };
export const getEffectTypeSelection = () => { export const getEffectTypeSelection = () => {
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents( const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder().setCustomId("createitem_select_effect_type").setPlaceholder("Select Effect Type").addOptions(getEffectTypeOptions()) new StringSelectMenuBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.SELECT_EFFECT_TYPE).setPlaceholder("Select Effect Type").addOptions(getEffectTypeOptions())
); );
return { components: [row] }; return { components: [row] };
}; };
export const getDetailsModal = (current: DraftItem) => { export const getDetailsModal = (current: DraftItem) => {
const modal = new ModalBuilder().setCustomId("createitem_modal_details").setTitle("Edit Details"); const modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_DETAILS).setTitle("Edit Details");
modal.addComponents( modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("name").setLabel("Name").setValue(current.name).setStyle(TextInputStyle.Short).setRequired(true)), new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_NAME).setLabel("Name").setValue(current.name).setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("desc").setLabel("Description").setValue(current.description).setStyle(TextInputStyle.Paragraph).setRequired(false)), new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_DESC).setLabel("Description").setValue(current.description).setStyle(TextInputStyle.Paragraph).setRequired(false)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("rarity").setLabel("Rarity").setValue(current.rarity).setStyle(TextInputStyle.Short).setPlaceholder("C, R, SR, SSR").setRequired(true)) new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_RARITY).setLabel("Rarity").setValue(current.rarity).setStyle(TextInputStyle.Short).setPlaceholder("C, R, SR, SSR").setRequired(true))
); );
return modal; return modal;
}; };
export const getEconomyModal = (current: DraftItem) => { export const getEconomyModal = (current: DraftItem) => {
const modal = new ModalBuilder().setCustomId("createitem_modal_economy").setTitle("Edit Economy"); const modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_ECONOMY).setTitle("Edit Economy");
modal.addComponents( modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("price").setLabel("Price (0 for not for sale)").setValue(current.price?.toString() || "0").setStyle(TextInputStyle.Short).setRequired(true)) new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_PRICE).setLabel("Price (0 for not for sale)").setValue(current.price?.toString() || "0").setStyle(TextInputStyle.Short).setRequired(true))
); );
return modal; return modal;
}; };
export const getVisualsModal = (current: DraftItem) => { export const getVisualsModal = (current: DraftItem) => {
const modal = new ModalBuilder().setCustomId("createitem_modal_visuals").setTitle("Edit Visuals"); const modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_VISUALS).setTitle("Edit Visuals");
modal.addComponents( modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("icon").setLabel("Icon URL (Emoji or Link)").setValue(current.iconUrl).setStyle(TextInputStyle.Short).setRequired(false)), new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_ICON).setLabel("Icon URL (Emoji or Link)").setValue(current.iconUrl).setStyle(TextInputStyle.Short).setRequired(false)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("image").setLabel("Image URL").setValue(current.imageUrl).setStyle(TextInputStyle.Short).setRequired(false)) new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_IMAGE).setLabel("Image URL").setValue(current.imageUrl).setStyle(TextInputStyle.Short).setRequired(false))
); );
return modal; return modal;
}; };
export const getEffectConfigModal = (effectType: string) => { export const getEffectConfigModal = (effectType: string) => {
let modal = new ModalBuilder().setCustomId("createitem_modal_effect").setTitle(`Config ${effectType}`); let modal = new ModalBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.MODAL_EFFECT).setTitle(`Config ${effectType}`);
if (effectType === "ADD_XP" || effectType === "ADD_BALANCE") { if (effectType === "ADD_XP" || effectType === "ADD_BALANCE") {
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("amount").setLabel("Amount").setStyle(TextInputStyle.Short).setRequired(true).setPlaceholder("100"))); modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_AMOUNT).setLabel("Amount").setStyle(TextInputStyle.Short).setRequired(true).setPlaceholder("100")));
} else if (effectType === "REPLY_MESSAGE") { } else if (effectType === "REPLY_MESSAGE") {
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("message").setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true))); modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_MESSAGE).setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true)));
} else if (effectType === "XP_BOOST") { } else if (effectType === "XP_BOOST") {
modal.addComponents( modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("multiplier").setLabel("Multiplier (e.g. 1.5)").setStyle(TextInputStyle.Short).setRequired(true)), new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_MULTIPLIER).setLabel("Multiplier (e.g. 1.5)").setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600")) new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION).setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
); );
} else if (effectType === "TEMP_ROLE") { } else if (effectType === "TEMP_ROLE") {
modal.addComponents( modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)), new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID).setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)),
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600")) new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_DURATION).setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
); );
} else if (effectType === "COLOR_ROLE") { } else if (effectType === "COLOR_ROLE") {
modal.addComponents( modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)) new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId(ITEM_WIZARD_CUSTOM_IDS.FIELD_ROLE_ID).setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true))
); );
} }
return modal; return modal;

View File

@@ -0,0 +1,10 @@
export const LOOTDROP_CUSTOM_IDS = {
PREFIX: "lootdrop_",
CLAIM: "lootdrop_claim",
CLAIM_DISABLED: "lootdrop_claim_disabled",
} as const;
export const SHOP_CUSTOM_IDS = {
BUY_PREFIX: "shop_buy_",
BUY: (itemId: number) => `shop_buy_${itemId}`,
} as const;

View File

@@ -3,9 +3,10 @@ import { lootdropService } from "@shared/modules/economy/lootdrop.service";
import { UserError } from "@shared/lib/errors"; import { UserError } from "@shared/lib/errors";
import { getLootdropClaimedMessage } from "./lootdrop.view"; import { getLootdropClaimedMessage } from "./lootdrop.view";
import { terminalService } from "@modules/system/terminal.service"; import { terminalService } from "@modules/system/terminal.service";
import { LOOTDROP_CUSTOM_IDS } from "./economy.types";
export async function handleLootdropInteraction(interaction: ButtonInteraction) { export async function handleLootdropInteraction(interaction: ButtonInteraction) {
if (interaction.customId === "lootdrop_claim") { if (interaction.customId === LOOTDROP_CUSTOM_IDS.CLAIM) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
const result = await lootdropService.tryClaim(interaction.message.id, interaction.user.id, interaction.user.username); const result = await lootdropService.tryClaim(interaction.message.id, interaction.user.id, interaction.user.username);

View File

@@ -1,12 +1,13 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import { generateLootdropCard, generateClaimedLootdropCard } from "@/graphics/lootdrop"; import { generateLootdropCard, generateClaimedLootdropCard } from "@/graphics/lootdrop";
import { LOOTDROP_CUSTOM_IDS } from "./economy.types";
export async function getLootdropMessage(reward: number, currency: string) { export async function getLootdropMessage(reward: number, currency: string) {
const cardBuffer = await generateLootdropCard(reward, currency); const cardBuffer = await generateLootdropCard(reward, currency);
const attachment = new AttachmentBuilder(cardBuffer, { name: "lootdrop.png" }); const attachment = new AttachmentBuilder(cardBuffer, { name: "lootdrop.png" });
const claimButton = new ButtonBuilder() const claimButton = new ButtonBuilder()
.setCustomId("lootdrop_claim") .setCustomId(LOOTDROP_CUSTOM_IDS.CLAIM)
.setLabel("CLAIM REWARD") .setLabel("CLAIM REWARD")
.setStyle(ButtonStyle.Secondary) // Changed to Secondary to fit the darker theme better? Or keep Success? Let's try Secondary with custom emoji .setStyle(ButtonStyle.Secondary) // Changed to Secondary to fit the darker theme better? Or keep Success? Let's try Secondary with custom emoji
.setEmoji("🌠"); .setEmoji("🌠");
@@ -28,7 +29,7 @@ export async function getLootdropClaimedMessage(userId: string, username: string
const newRow = new ActionRowBuilder<ButtonBuilder>() const newRow = new ActionRowBuilder<ButtonBuilder>()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId("lootdrop_claim_disabled") .setCustomId(LOOTDROP_CUSTOM_IDS.CLAIM_DISABLED)
.setLabel("CLAIMED") .setLabel("CLAIMED")
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
.setEmoji("✅") .setEmoji("✅")

View File

@@ -2,13 +2,14 @@ import { ButtonInteraction, MessageFlags } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service"; import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service"; import { userService } from "@shared/modules/user/user.service";
import { UserError } from "@shared/lib/errors"; import { UserError } from "@shared/lib/errors";
import { SHOP_CUSTOM_IDS } from "./economy.types";
export async function handleShopInteraction(interaction: ButtonInteraction) { export async function handleShopInteraction(interaction: ButtonInteraction) {
if (!interaction.customId.startsWith("shop_buy_")) return; if (!interaction.customId.startsWith(SHOP_CUSTOM_IDS.BUY_PREFIX)) return;
await interaction.deferReply({ flags: MessageFlags.Ephemeral }); await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const itemId = parseInt(interaction.customId.replace("shop_buy_", "")); const itemId = parseInt(interaction.customId.replace(SHOP_CUSTOM_IDS.BUY_PREFIX, ""));
if (isNaN(itemId)) { if (isNaN(itemId)) {
throw new UserError("Invalid Item ID."); throw new UserError("Invalid Item ID.");
} }

View File

@@ -19,6 +19,7 @@ import { existsSync } from "fs";
import { LootType, EffectType } from "@shared/lib/constants"; import { LootType, EffectType } from "@shared/lib/constants";
import type { LootTableItem } from "@shared/lib/types"; import type { LootTableItem } from "@shared/lib/types";
import { getRarityConfig, defaultName, stripQuery } from "@shared/lib/rarity"; import { getRarityConfig, defaultName, stripQuery } from "@shared/lib/rarity";
import { SHOP_CUSTOM_IDS } from "./economy.types";
export function getShopListingMessage( export function getShopListingMessage(
item: { item: {
@@ -100,7 +101,7 @@ export function getShopListingMessage(
// Create buy button (used in either main or loot container) // Create buy button (used in either main or loot container)
const buyButton = new ButtonBuilder() const buyButton = new ButtonBuilder()
.setCustomId(`shop_buy_${item.id}`) .setCustomId(SHOP_CUSTOM_IDS.BUY(item.id))
.setLabel(`Purchase for ${item.price} 🪙`) .setLabel(`Purchase for ${item.price} 🪙`)
.setStyle(ButtonStyle.Success) .setStyle(ButtonStyle.Success)
.setEmoji("🛒"); .setEmoji("🛒");

View File

@@ -8,7 +8,7 @@ import { UserError } from "@shared/lib/errors";
export const handleFeedbackInteraction = async (interaction: Interaction) => { export const handleFeedbackInteraction = async (interaction: Interaction) => {
// Handle select menu for choosing feedback type // Handle select menu for choosing feedback type
if (interaction.isStringSelectMenu() && interaction.customId === "feedback_select_type") { if (interaction.isStringSelectMenu() && interaction.customId === FEEDBACK_CUSTOM_IDS.SELECT_TYPE) {
const feedbackType = interaction.values[0] as FeedbackType; const feedbackType = interaction.values[0] as FeedbackType;
if (!feedbackType) { if (!feedbackType) {

View File

@@ -16,8 +16,10 @@ export const FEEDBACK_TYPE_LABELS: Record<FeedbackType, string> = {
}; };
export const FEEDBACK_CUSTOM_IDS = { export const FEEDBACK_CUSTOM_IDS = {
PREFIX: "feedback_",
SELECT_TYPE: "feedback_select_type",
MODAL: "feedback_modal", MODAL: "feedback_modal",
TYPE_FIELD: "feedback_type", TYPE_FIELD: "feedback_type",
TITLE_FIELD: "feedback_title", TITLE_FIELD: "feedback_title",
DESCRIPTION_FIELD: "feedback_description" DESCRIPTION_FIELD: "feedback_description",
} as const; } as const;

View File

@@ -14,7 +14,7 @@ import { FEEDBACK_TYPE_LABELS, FEEDBACK_CUSTOM_IDS, type FeedbackData, type Feed
export function getFeedbackTypeMenu() { export function getFeedbackTypeMenu() {
const select = new StringSelectMenuBuilder() const select = new StringSelectMenuBuilder()
.setCustomId("feedback_select_type") .setCustomId(FEEDBACK_CUSTOM_IDS.SELECT_TYPE)
.setPlaceholder("Choose feedback type") .setPlaceholder("Choose feedback type")
.addOptions([ .addOptions([
{ {

View File

@@ -3,6 +3,7 @@ import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { getLootboxResultMessage } from "./inventory.view"; import { getLootboxResultMessage } from "./inventory.view";
import type { ItemUsageData } from "@shared/lib/types"; import type { ItemUsageData } from "@shared/lib/types";
import { getGuildConfig } from "@shared/lib/config"; import { getGuildConfig } from "@shared/lib/config";
import { INVENTORY_CUSTOM_IDS } from "./inventory.types";
export interface InventoryState { export interface InventoryState {
ownerId: string; ownerId: string;
@@ -25,7 +26,7 @@ export function parseInventoryCustomId(customId: string): { action: string; view
* Checks if a custom ID belongs to the inventory system. * Checks if a custom ID belongs to the inventory system.
*/ */
export function isInventoryInteraction(customId: string): boolean { export function isInventoryInteraction(customId: string): boolean {
return customId.startsWith("inv_"); return customId.startsWith(INVENTORY_CUSTOM_IDS.PREFIX);
} }
/** /**

View File

@@ -0,0 +1,13 @@
export const INVENTORY_CUSTOM_IDS = {
PREFIX: "inv_",
SELECT: (viewerId: string) => `inv_select_${viewerId}`,
PREV: (viewerId: string) => `inv_prev_${viewerId}`,
PAGE: (viewerId: string) => `inv_page_${viewerId}`,
NEXT: (viewerId: string) => `inv_next_${viewerId}`,
BACK: (viewerId: string) => `inv_back_${viewerId}`,
USE: (viewerId: string) => `inv_use_${viewerId}`,
DISCARD: (viewerId: string) => `inv_discard_${viewerId}`,
DISCARD_CONFIRM: (viewerId: string) => `inv_discard_confirm_${viewerId}`,
DISCARD_CANCEL: (viewerId: string) => `inv_discard_cancel_${viewerId}`,
USE_BACK: (viewerId: string) => `inv_use_back_${viewerId}`,
} as const;

View File

@@ -22,6 +22,7 @@ import { ItemType } from "@shared/lib/constants";
import type { ItemUsageData } from "@shared/lib/types"; import type { ItemUsageData } from "@shared/lib/types";
import { join } from "path"; import { join } from "path";
import { existsSync } from "fs"; import { existsSync } from "fs";
import { INVENTORY_CUSTOM_IDS } from "./inventory.types";
export const ITEMS_PER_PAGE = 5; export const ITEMS_PER_PAGE = 5;
@@ -101,7 +102,7 @@ export function getInventoryListMessage(
// Select menu with current page items // Select menu with current page items
const selectMenu = new StringSelectMenuBuilder() const selectMenu = new StringSelectMenuBuilder()
.setCustomId(`inv_select_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.SELECT(viewerId))
.setPlaceholder("Select an item for details"); .setPlaceholder("Select an item for details");
for (const entry of pageItems) { for (const entry of pageItems) {
@@ -121,17 +122,17 @@ export function getInventoryListMessage(
// Pagination buttons // Pagination buttons
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents( const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_prev_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.PREV(viewerId))
.setLabel("◀ Previous") .setLabel("◀ Previous")
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
.setDisabled(safePage <= 0), .setDisabled(safePage <= 0),
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_page_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.PAGE(viewerId))
.setLabel(`Page ${safePage + 1}/${totalPages}`) .setLabel(`Page ${safePage + 1}/${totalPages}`)
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
.setDisabled(true), .setDisabled(true),
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_next_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.NEXT(viewerId))
.setLabel("Next ▶") .setLabel("Next ▶")
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
.setDisabled(safePage >= totalPages - 1), .setDisabled(safePage >= totalPages - 1),
@@ -225,7 +226,7 @@ export function getItemDetailMessage(
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents( const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_back_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.BACK(viewerId))
.setLabel("◀ Back") .setLabel("◀ Back")
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
); );
@@ -233,7 +234,7 @@ export function getItemDetailMessage(
if (isUsable) { if (isUsable) {
actionRow.addComponents( actionRow.addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_use_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.USE(viewerId))
.setLabel("🧪 Use") .setLabel("🧪 Use")
.setStyle(ButtonStyle.Success) .setStyle(ButtonStyle.Success)
); );
@@ -242,7 +243,7 @@ export function getItemDetailMessage(
if (isOwner) { if (isOwner) {
actionRow.addComponents( actionRow.addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_discard_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.DISCARD(viewerId))
.setLabel("🗑 Discard") .setLabel("🗑 Discard")
.setStyle(ButtonStyle.Danger) .setStyle(ButtonStyle.Danger)
); );
@@ -271,11 +272,11 @@ export function getDiscardConfirmMessage(entry: InventoryEntry, viewerId: string
.addActionRowComponents( .addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents( new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_discard_confirm_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.DISCARD_CONFIRM(viewerId))
.setLabel("Confirm") .setLabel("Confirm")
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_discard_cancel_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.DISCARD_CANCEL(viewerId))
.setLabel("Cancel") .setLabel("Cancel")
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
) )
@@ -296,7 +297,7 @@ export function getDiscardConfirmMessage(entry: InventoryEntry, viewerId: string
export function appendUseBackButton(message: any, viewerId: string): any { export function appendUseBackButton(message: any, viewerId: string): any {
const backRow = new ActionRowBuilder<ButtonBuilder>().addComponents( const backRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inv_use_back_${viewerId}`) .setCustomId(INVENTORY_CUSTOM_IDS.USE_BACK(viewerId))
.setLabel("◀ Back to Inventory") .setLabel("◀ Back to Inventory")
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
); );

View File

@@ -1,3 +1,8 @@
export const PRUNE_CUSTOM_IDS = {
CONFIRM: "confirm_prune",
CANCEL: "cancel_prune",
} as const;
export interface PruneOptions { export interface PruneOptions {
amount?: number; amount?: number;
userId?: string; userId?: string;

View File

@@ -1,5 +1,5 @@
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, Colors } from "discord.js"; import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, Colors } from "discord.js";
import type { PruneResult, PruneProgress } from "./prune.types"; import { PRUNE_CUSTOM_IDS, type PruneResult, type PruneProgress } from "./prune.types";
/** /**
* Creates a confirmation message for prune operations * Creates a confirmation message for prune operations
@@ -25,12 +25,12 @@ export function getConfirmationMessage(
.setTimestamp(); .setTimestamp();
const confirmButton = new ButtonBuilder() const confirmButton = new ButtonBuilder()
.setCustomId("confirm_prune") .setCustomId(PRUNE_CUSTOM_IDS.CONFIRM)
.setLabel("Confirm") .setLabel("Confirm")
.setStyle(ButtonStyle.Danger); .setStyle(ButtonStyle.Danger);
const cancelButton = new ButtonBuilder() const cancelButton = new ButtonBuilder()
.setCustomId("cancel_prune") .setCustomId(PRUNE_CUSTOM_IDS.CANCEL)
.setLabel("Cancel") .setLabel("Cancel")
.setStyle(ButtonStyle.Secondary); .setStyle(ButtonStyle.Secondary);

View File

@@ -0,0 +1,8 @@
export const QUEST_CUSTOM_IDS = {
ACCEPT_PREFIX: "quest_accept:",
ACCEPT: (questId: number) => `quest_accept:${questId}`,
PAGE_PREV: "quest_page_prev",
PAGE_NEXT: "quest_page_next",
VIEW_ACTIVE: "quest_view_active",
VIEW_AVAILABLE: "quest_view_available",
} as const;

View File

@@ -8,6 +8,7 @@ import {
SeparatorSpacingSize, SeparatorSpacingSize,
MessageFlags MessageFlags
} from "discord.js"; } from "discord.js";
import { QUEST_CUSTOM_IDS } from "./quest.types";
/** /**
* Quest entry with quest details and progress * Quest entry with quest details and progress
@@ -169,7 +170,7 @@ export function getAvailableQuestsComponents(availableQuests: AvailableQuest[],
container.addActionRowComponents( container.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents( new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`quest_accept:${quest.id}`) .setCustomId(QUEST_CUSTOM_IDS.ACCEPT(quest.id))
.setLabel("Accept Quest") .setLabel("Accept Quest")
.setStyle(ButtonStyle.Success) .setStyle(ButtonStyle.Success)
.setEmoji("✅") .setEmoji("✅")
@@ -191,12 +192,12 @@ export function getQuestActionRows(viewType: 'active' | 'available', totalItems:
if (totalPages > 1) { if (totalPages > 1) {
rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents( rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId("quest_page_prev") .setCustomId(QUEST_CUSTOM_IDS.PAGE_PREV)
.setLabel("◀ Prev") .setLabel("◀ Prev")
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
.setDisabled(page <= 0), .setDisabled(page <= 0),
new ButtonBuilder() new ButtonBuilder()
.setCustomId("quest_page_next") .setCustomId(QUEST_CUSTOM_IDS.PAGE_NEXT)
.setLabel("Next ▶") .setLabel("Next ▶")
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Secondary)
.setDisabled(page >= totalPages - 1) .setDisabled(page >= totalPages - 1)
@@ -206,12 +207,12 @@ export function getQuestActionRows(viewType: 'active' | 'available', totalItems:
// Tab navigation row // Tab navigation row
rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents( rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId("quest_view_active") .setCustomId(QUEST_CUSTOM_IDS.VIEW_ACTIVE)
.setLabel("📜 Active") .setLabel("📜 Active")
.setStyle(viewType === 'active' ? ButtonStyle.Primary : ButtonStyle.Secondary) .setStyle(viewType === 'active' ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setDisabled(viewType === 'active'), .setDisabled(viewType === 'active'),
new ButtonBuilder() new ButtonBuilder()
.setCustomId("quest_view_available") .setCustomId(QUEST_CUSTOM_IDS.VIEW_AVAILABLE)
.setLabel("🗺️ Available") .setLabel("🗺️ Available")
.setStyle(viewType === 'available' ? ButtonStyle.Primary : ButtonStyle.Secondary) .setStyle(viewType === 'available' ? ButtonStyle.Primary : ButtonStyle.Secondary)
.setDisabled(viewType === 'available') .setDisabled(viewType === 'available')

View File

@@ -12,6 +12,7 @@ import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds"; import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds";
import { UserError } from "@shared/lib/errors"; import { UserError } from "@shared/lib/errors";
import { getTradeDashboard, getTradeMoneyModal, getItemSelectMenu, getTradeCompletedEmbed } from "./trade.view"; import { getTradeDashboard, getTradeMoneyModal, getItemSelectMenu, getTradeCompletedEmbed } from "./trade.view";
import { TRADE_CUSTOM_IDS } from "./trade.types";
@@ -23,25 +24,25 @@ export async function handleTradeInteraction(interaction: Interaction) {
if (!threadId) return; if (!threadId) return;
if (customId === 'trade_cancel') { if (customId === TRADE_CUSTOM_IDS.CANCEL) {
await handleCancel(interaction, threadId); await handleCancel(interaction, threadId);
} else if (customId === 'trade_lock') { } else if (customId === TRADE_CUSTOM_IDS.LOCK) {
await handleLock(interaction, threadId); await handleLock(interaction, threadId);
} else if (customId === 'trade_confirm') { } else if (customId === TRADE_CUSTOM_IDS.CONFIRM) {
// Confirm logic is handled implicitly by both locking or explicitly if needed. // Confirm logic is handled implicitly by both locking or explicitly if needed.
// For now, locking both triggers execution, so no separate confirm handler is actively used // For now, locking both triggers execution, so no separate confirm handler is actively used
// unless we re-introduce a specific button. keeping basic handler stub if needed. // unless we re-introduce a specific button. keeping basic handler stub if needed.
} else if (customId === 'trade_add_money') { } else if (customId === TRADE_CUSTOM_IDS.ADD_MONEY) {
await handleAddMoneyClick(interaction); await handleAddMoneyClick(interaction);
} else if (customId === 'trade_money_modal') { } else if (customId === TRADE_CUSTOM_IDS.MONEY_MODAL) {
await handleMoneySubmit(interaction as ModalSubmitInteraction, threadId); await handleMoneySubmit(interaction as ModalSubmitInteraction, threadId);
} else if (customId === 'trade_add_item') { } else if (customId === TRADE_CUSTOM_IDS.ADD_ITEM) {
await handleAddItemClick(interaction as ButtonInteraction, threadId); await handleAddItemClick(interaction as ButtonInteraction, threadId);
} else if (customId === 'trade_select_item') { } else if (customId === TRADE_CUSTOM_IDS.SELECT_ITEM) {
await handleItemSelect(interaction as StringSelectMenuInteraction, threadId); await handleItemSelect(interaction as StringSelectMenuInteraction, threadId);
} else if (customId === 'trade_remove_item') { } else if (customId === TRADE_CUSTOM_IDS.REMOVE_ITEM) {
await handleRemoveItemClick(interaction as ButtonInteraction, threadId); await handleRemoveItemClick(interaction as ButtonInteraction, threadId);
} else if (customId === 'trade_remove_item_select') { } else if (customId === TRADE_CUSTOM_IDS.REMOVE_ITEM_SELECT) {
await handleRemoveItemSelect(interaction as StringSelectMenuInteraction, threadId); await handleRemoveItemSelect(interaction as StringSelectMenuInteraction, threadId);
} }
} }
@@ -82,7 +83,7 @@ async function handleAddMoneyClick(interaction: Interaction) {
} }
async function handleMoneySubmit(interaction: ModalSubmitInteraction, threadId: string) { async function handleMoneySubmit(interaction: ModalSubmitInteraction, threadId: string) {
const amountStr = interaction.fields.getTextInputValue('amount'); const amountStr = interaction.fields.getTextInputValue(TRADE_CUSTOM_IDS.MONEY_AMOUNT_FIELD);
const amount = BigInt(amountStr); const amount = BigInt(amountStr);
if (amount < 0n) throw new UserError("Amount must be positive"); if (amount < 0n) throw new UserError("Amount must be positive");
@@ -107,7 +108,7 @@ async function handleAddItemClick(interaction: ButtonInteraction, threadId: stri
description: `Rarity: ${entry.item.rarity} ` description: `Rarity: ${entry.item.rarity} `
})); }));
const { components } = getItemSelectMenu(options, 'trade_select_item', 'Select an item to add'); const { components } = getItemSelectMenu(options, TRADE_CUSTOM_IDS.SELECT_ITEM, 'Select an item to add');
await interaction.reply({ content: "Select an item to add:", components, ephemeral: true }); await interaction.reply({ content: "Select an item to add:", components, ephemeral: true });
} }
@@ -142,7 +143,7 @@ async function handleRemoveItemClick(interaction: ButtonInteraction, threadId: s
value: i.id.toString(), value: i.id.toString(),
})); }));
const { components } = getItemSelectMenu(options, 'trade_remove_item_select', 'Select an item to remove'); const { components } = getItemSelectMenu(options, TRADE_CUSTOM_IDS.REMOVE_ITEM_SELECT, 'Select an item to remove');
await interaction.reply({ content: "Select an item to remove:", components, ephemeral: true }); await interaction.reply({ content: "Select an item to remove:", components, ephemeral: true });
} }

View File

@@ -1,3 +1,16 @@
export const TRADE_CUSTOM_IDS = {
PREFIX: "trade_",
ADD_ITEM: "trade_add_item",
ADD_MONEY: "trade_add_money",
REMOVE_ITEM: "trade_remove_item",
LOCK: "trade_lock",
CANCEL: "trade_cancel",
CONFIRM: "trade_confirm",
MONEY_MODAL: "trade_money_modal",
MONEY_AMOUNT_FIELD: "amount",
SELECT_ITEM: "trade_select_item",
REMOVE_ITEM_SELECT: "trade_remove_item_select",
} as const;
export interface TradeItem { export interface TradeItem {
id: number; id: number;

View File

@@ -1,6 +1,6 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
import { createBaseEmbed } from "@lib/embeds"; import { createBaseEmbed } from "@lib/embeds";
import type { TradeSession, TradeParticipant } from "./trade.types"; import { TRADE_CUSTOM_IDS, type TradeSession, type TradeParticipant } from "./trade.types";
const EMBED_COLOR = 0xFFD700; // Gold const EMBED_COLOR = 0xFFD700; // Gold
@@ -34,11 +34,11 @@ export function getTradeDashboard(session: TradeSession) {
const row = new ActionRowBuilder<ButtonBuilder>() const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents( .addComponents(
new ButtonBuilder().setCustomId('trade_add_item').setLabel('Add Item').setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.ADD_ITEM).setLabel('Add Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('trade_add_money').setLabel('Add Money').setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.ADD_MONEY).setLabel('Add Money').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId('trade_remove_item').setLabel('Remove Item').setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.REMOVE_ITEM).setLabel('Remove Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('trade_lock').setLabel('Lock / Unlock').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.LOCK).setLabel('Lock / Unlock').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId('trade_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger), new ButtonBuilder().setCustomId(TRADE_CUSTOM_IDS.CANCEL).setLabel('Cancel').setStyle(ButtonStyle.Danger),
); );
return { embeds: [embed], components: [row] }; return { embeds: [embed], components: [row] };
@@ -57,11 +57,11 @@ export function getTradeCompletedEmbed(session: TradeSession) {
export function getTradeMoneyModal() { export function getTradeMoneyModal() {
const modal = new ModalBuilder() const modal = new ModalBuilder()
.setCustomId('trade_money_modal') .setCustomId(TRADE_CUSTOM_IDS.MONEY_MODAL)
.setTitle('Add Money'); .setTitle('Add Money');
const input = new TextInputBuilder() const input = new TextInputBuilder()
.setCustomId('amount') .setCustomId(TRADE_CUSTOM_IDS.MONEY_AMOUNT_FIELD)
.setLabel("Amount to trade") .setLabel("Amount to trade")
.setStyle(TextInputStyle.Short) .setStyle(TextInputStyle.Short)
.setPlaceholder("100") .setPlaceholder("100")

View File

@@ -0,0 +1,7 @@
export const TRIVIA_CUSTOM_IDS = {
PREFIX: "trivia_",
ANSWER: (sessionId: string, index: number) => `trivia_answer_${sessionId}_${index}`,
GIVE_UP: (sessionId: string) => `trivia_giveup_${sessionId}`,
RESULT: (index: number) => `trivia_result_${index}`,
TIMEOUT: (index: number) => `trivia_timeout_${index}`,
} as const;

View File

@@ -1,5 +1,6 @@
import { MessageFlags } from "discord.js"; import { MessageFlags } from "discord.js";
import type { TriviaSession, TriviaResult } from "@shared/modules/trivia/trivia.service"; import type { TriviaSession, TriviaResult } from "@shared/modules/trivia/trivia.service";
import { TRIVIA_CUSTOM_IDS } from "./trivia.types";
/** /**
* Get color based on difficulty level * Get color based on difficulty level
@@ -97,14 +98,14 @@ export function getTriviaQuestionView(session: TriviaSession, username: string):
components: [ components: [
{ {
type: 2, // Button type: 2, // Button
custom_id: `trivia_answer_${sessionId}_${trueIndex}`, custom_id: TRIVIA_CUSTOM_IDS.ANSWER(sessionId, trueIndex),
label: 'True', label: 'True',
style: 3, // Success style: 3, // Success
emoji: { name: '✅' } emoji: { name: '✅' }
}, },
{ {
type: 2, // Button type: 2, // Button
custom_id: `trivia_answer_${sessionId}_${falseIndex}`, custom_id: TRIVIA_CUSTOM_IDS.ANSWER(sessionId, falseIndex),
label: 'False', label: 'False',
style: 4, // Danger style: 4, // Danger
emoji: { name: '❌' } emoji: { name: '❌' }
@@ -129,7 +130,7 @@ export function getTriviaQuestionView(session: TriviaSession, username: string):
buttonRow.components.push({ buttonRow.components.push({
type: 2, // Button type: 2, // Button
custom_id: `trivia_answer_${sessionId}_${i}`, custom_id: TRIVIA_CUSTOM_IDS.ANSWER(sessionId, i),
label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`, label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`,
style: 2, // Secondary style: 2, // Secondary
emoji: { name: emoji } emoji: { name: emoji }
@@ -145,7 +146,7 @@ export function getTriviaQuestionView(session: TriviaSession, username: string):
components: [ components: [
{ {
type: 2, // Button type: 2, // Button
custom_id: `trivia_giveup_${sessionId}`, custom_id: TRIVIA_CUSTOM_IDS.GIVE_UP(sessionId),
label: 'Give Up', label: 'Give Up',
style: 4, // Danger style: 4, // Danger
emoji: { name: '🏳️' } emoji: { name: '🏳️' }
@@ -245,7 +246,7 @@ export function getTriviaResultView(
buttonRow.components.push({ buttonRow.components.push({
type: 2, // Button type: 2, // Button
custom_id: `trivia_result_${i}`, custom_id: TRIVIA_CUSTOM_IDS.RESULT(i),
label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`, label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`,
style: isCorrect ? 3 : wasUserAnswer ? 4 : 2, // Success : Danger : Secondary style: isCorrect ? 3 : wasUserAnswer ? 4 : 2, // Success : Danger : Secondary
emoji: { name: isCorrect ? '✅' : wasUserAnswer ? '❌' : emoji }, emoji: { name: isCorrect ? '✅' : wasUserAnswer ? '❌' : emoji },
@@ -318,7 +319,7 @@ export function getTriviaTimeoutView(
buttonRow.components.push({ buttonRow.components.push({
type: 2, // Button type: 2, // Button
custom_id: `trivia_timeout_${i}`, custom_id: TRIVIA_CUSTOM_IDS.TIMEOUT(i),
label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`, label: `${label}: ${answer.substring(0, 30)}${answer.length > 30 ? '...' : ''}`,
style: isCorrect ? 3 : 2, // Success : Secondary style: isCorrect ? 3 : 2, // Success : Secondary
emoji: { name: isCorrect ? '✅' : emoji }, emoji: { name: isCorrect ? '✅' : emoji },

View File

@@ -0,0 +1,3 @@
export const ENROLLMENT_CUSTOM_IDS = {
ENROLL: "enrollment",
} as const;