refactor: Extract UI component creation into new view files for lootdrop, trade, item wizard, and enrollment.
This commit is contained in:
@@ -1,51 +1,17 @@
|
|||||||
import {
|
import { type Interaction } from "discord.js";
|
||||||
ActionRowBuilder,
|
|
||||||
ButtonBuilder,
|
|
||||||
ButtonStyle,
|
|
||||||
ModalBuilder,
|
|
||||||
StringSelectMenuBuilder,
|
|
||||||
TextInputBuilder,
|
|
||||||
TextInputStyle,
|
|
||||||
type Interaction,
|
|
||||||
type MessageActionRowComponentBuilder
|
|
||||||
} from "discord.js";
|
|
||||||
import { items } from "@/db/schema";
|
import { items } from "@/db/schema";
|
||||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||||
import type { ItemUsageData, ItemEffect } from "@/lib/types";
|
import type { ItemUsageData, ItemEffect } from "@/lib/types";
|
||||||
import { createBaseEmbed } from "@lib/embeds";
|
import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view";
|
||||||
|
import type { DraftItem } from "./item_wizard.types";
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
export interface DraftItem {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
rarity: string;
|
|
||||||
type: string;
|
|
||||||
price: number | null;
|
|
||||||
iconUrl: string;
|
|
||||||
imageUrl: string;
|
|
||||||
usageData: ItemUsageData;
|
|
||||||
// Temporary state for effect adding flow
|
|
||||||
pendingEffectType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const draftSession = new Map<string, DraftItem>();
|
const draftSession = new Map<string, DraftItem>();
|
||||||
|
|
||||||
const getItemTypeOptions = () => [
|
|
||||||
{ label: "Material", value: "MATERIAL", description: "Used for crafting or trading" },
|
|
||||||
{ label: "Consumable", value: "CONSUMABLE", description: "Can be used to gain effects" },
|
|
||||||
{ label: "Equipment", value: "EQUIPMENT", description: "Can be equipped (Not yet implemented)" },
|
|
||||||
{ label: "Quest Item", value: "QUEST", description: "Required for quests" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getEffectTypeOptions = () => [
|
|
||||||
{ label: "Add XP", value: "ADD_XP", description: "Gives XP to the user" },
|
|
||||||
{ label: "Add Balance", value: "ADD_BALANCE", description: "Gives currency to the user" },
|
|
||||||
{ label: "Reply Message", value: "REPLY_MESSAGE", description: "Bot replies with a message" },
|
|
||||||
{ label: "XP Boost", value: "XP_BOOST", description: "Temporarily boosts XP gain" },
|
|
||||||
{ label: "Temp Role", value: "TEMP_ROLE", description: "Gives a temporary role" },
|
|
||||||
{ label: "Color Role", value: "COLOR_ROLE", description: "Equips a permanent color role (swaps)" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- Render ---
|
// --- Render ---
|
||||||
export const renderWizard = (userId: string, isDraft = true) => {
|
export const renderWizard = (userId: string, isDraft = true) => {
|
||||||
@@ -66,43 +32,8 @@ export const renderWizard = (userId: string, isDraft = true) => {
|
|||||||
draftSession.set(userId, draft);
|
draftSession.set(userId, draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = createBaseEmbed(`🛠️ Item Creator: ${draft.name}`, undefined, "Blue")
|
const { embeds, components } = getItemWizardEmbed(draft);
|
||||||
.addFields(
|
return { embeds, components };
|
||||||
{ name: "General", value: `**Type:** ${draft.type}\n**Rarity:** ${draft.rarity}\n**Desc:** ${draft.description}`, inline: true },
|
|
||||||
{ name: "Economy", value: `**Price:** ${draft.price ? `${draft.price} 🪙` : "Not for sale"}`, inline: true },
|
|
||||||
{ name: "Visuals", value: `**Icon:** ${draft.iconUrl ? "✅ Set" : "❌"}\n**Image:** ${draft.imageUrl ? "✅ Set" : "❌"}`, inline: true },
|
|
||||||
{ name: "Usage", value: `**Consume:** ${draft.usageData.consume ? "✅ Yes" : "❌ No"}`, inline: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Effects Display
|
|
||||||
if (draft.usageData.effects.length > 0) {
|
|
||||||
const effecto = draft.usageData.effects.map((e, i) => `${i + 1}. **${e.type}**: ${JSON.stringify(e)}`).join("\n");
|
|
||||||
embed.addFields({ name: "Usage Effects", value: effecto.substring(0, 1024) });
|
|
||||||
} else {
|
|
||||||
embed.addFields({ name: "Usage Effects", value: "None" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draft.imageUrl) embed.setImage(draft.imageUrl);
|
|
||||||
if (draft.iconUrl) embed.setThumbnail(draft.iconUrl);
|
|
||||||
|
|
||||||
// Components
|
|
||||||
const row1 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
|
|
||||||
.addComponents(
|
|
||||||
new ButtonBuilder().setCustomId("createitem_details").setLabel("Edit Details").setStyle(ButtonStyle.Secondary).setEmoji("📝"),
|
|
||||||
new ButtonBuilder().setCustomId("createitem_economy").setLabel("Edit Economy").setStyle(ButtonStyle.Secondary).setEmoji("💰"),
|
|
||||||
new ButtonBuilder().setCustomId("createitem_visuals").setLabel("Edit Visuals").setStyle(ButtonStyle.Secondary).setEmoji("🖼️"),
|
|
||||||
new ButtonBuilder().setCustomId("createitem_type_toggle").setLabel("Change Type").setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const row2 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
|
|
||||||
.addComponents(
|
|
||||||
new ButtonBuilder().setCustomId("createitem_addeffect_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("createitem_save").setLabel("Save Item").setStyle(ButtonStyle.Success).setEmoji("💾"),
|
|
||||||
new ButtonBuilder().setCustomId("createitem_cancel").setLabel("Cancel").setStyle(ButtonStyle.Danger).setEmoji("✖️")
|
|
||||||
);
|
|
||||||
|
|
||||||
return { embeds: [embed], components: [row1, row2] };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Handler ---
|
// --- Handler ---
|
||||||
@@ -151,12 +82,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
|
|||||||
// 1. Details Modal
|
// 1. Details Modal
|
||||||
if (interaction.customId === "createitem_details") {
|
if (interaction.customId === "createitem_details") {
|
||||||
if (!interaction.isButton()) return;
|
if (!interaction.isButton()) return;
|
||||||
const modal = new ModalBuilder().setCustomId("createitem_modal_details").setTitle("Edit Details");
|
const modal = getDetailsModal(draft);
|
||||||
modal.addComponents(
|
|
||||||
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("name").setLabel("Name").setValue(draft.name).setStyle(TextInputStyle.Short).setRequired(true)),
|
|
||||||
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("desc").setLabel("Description").setValue(draft.description).setStyle(TextInputStyle.Paragraph).setRequired(false)),
|
|
||||||
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("rarity").setLabel("Rarity").setValue(draft.rarity).setStyle(TextInputStyle.Short).setPlaceholder("Common, Rare, Legendary...").setRequired(true))
|
|
||||||
);
|
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,10 +90,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
|
|||||||
// 2. Economy Modal
|
// 2. Economy Modal
|
||||||
if (interaction.customId === "createitem_economy") {
|
if (interaction.customId === "createitem_economy") {
|
||||||
if (!interaction.isButton()) return;
|
if (!interaction.isButton()) return;
|
||||||
const modal = new ModalBuilder().setCustomId("createitem_modal_economy").setTitle("Edit Economy");
|
const modal = getEconomyModal(draft);
|
||||||
modal.addComponents(
|
|
||||||
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("price").setLabel("Price (0 for not for sale)").setValue(draft.price?.toString() || "0").setStyle(TextInputStyle.Short).setRequired(true))
|
|
||||||
);
|
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -175,11 +98,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
|
|||||||
// 3. Visuals Modal
|
// 3. Visuals Modal
|
||||||
if (interaction.customId === "createitem_visuals") {
|
if (interaction.customId === "createitem_visuals") {
|
||||||
if (!interaction.isButton()) return;
|
if (!interaction.isButton()) return;
|
||||||
const modal = new ModalBuilder().setCustomId("createitem_modal_visuals").setTitle("Edit Visuals");
|
const modal = getVisualsModal(draft);
|
||||||
modal.addComponents(
|
|
||||||
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("icon").setLabel("Icon URL (Emoji or Link)").setValue(draft.iconUrl).setStyle(TextInputStyle.Short).setRequired(false)),
|
|
||||||
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("image").setLabel("Image URL").setValue(draft.imageUrl).setStyle(TextInputStyle.Short).setRequired(false))
|
|
||||||
);
|
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -187,10 +106,8 @@ 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 === "createitem_type_toggle") {
|
||||||
if (!interaction.isButton()) return;
|
if (!interaction.isButton()) return;
|
||||||
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
const { components } = getItemTypeSelection();
|
||||||
new StringSelectMenuBuilder().setCustomId("createitem_select_type").setPlaceholder("Select Item Type").addOptions(getItemTypeOptions())
|
await interaction.update({ components }); // Temporary view
|
||||||
);
|
|
||||||
await interaction.update({ components: [row as any] }); // Temporary view
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,16 +126,15 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
|
|||||||
// 5. Add Effect Flow
|
// 5. Add Effect Flow
|
||||||
if (interaction.customId === "createitem_addeffect_start") {
|
if (interaction.customId === "createitem_addeffect_start") {
|
||||||
if (!interaction.isButton()) return;
|
if (!interaction.isButton()) return;
|
||||||
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
const { components } = getEffectTypeSelection();
|
||||||
new StringSelectMenuBuilder().setCustomId("createitem_select_effect_type").setPlaceholder("Select Effect Type").addOptions(getEffectTypeOptions())
|
await interaction.update({ components });
|
||||||
);
|
|
||||||
await interaction.update({ components: [row as any] });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.customId === "createitem_select_effect_type") {
|
if (interaction.customId === "createitem_select_effect_type") {
|
||||||
if (!interaction.isStringSelectMenu()) return;
|
if (!interaction.isStringSelectMenu()) return;
|
||||||
const effectType = interaction.values[0];
|
const effectType = interaction.values[0];
|
||||||
|
if (!effectType) return;
|
||||||
draft.pendingEffectType = effectType;
|
draft.pendingEffectType = effectType;
|
||||||
|
|
||||||
// Immediately show modal for data collection
|
// Immediately show modal for data collection
|
||||||
@@ -226,28 +142,7 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
|
|||||||
// But we shouldn't update the message AND show modal. We must pick one.
|
// But we shouldn't update the message AND show modal. We must pick one.
|
||||||
// We will show modal. The message remains in "Select Effect" state until modal submit re-renders it.
|
// We will show modal. The message remains in "Select Effect" state until modal submit re-renders it.
|
||||||
|
|
||||||
let modal = new ModalBuilder().setCustomId("createitem_modal_effect").setTitle(`Config ${effectType}`);
|
const modal = getEffectConfigModal(effectType);
|
||||||
|
|
||||||
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")));
|
|
||||||
} else if (effectType === "REPLY_MESSAGE") {
|
|
||||||
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("message").setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true)));
|
|
||||||
} else if (effectType === "XP_BOOST") {
|
|
||||||
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("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
|
|
||||||
);
|
|
||||||
} else if (effectType === "TEMP_ROLE") {
|
|
||||||
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("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
|
|
||||||
);
|
|
||||||
} else if (effectType === "COLOR_ROLE") {
|
|
||||||
modal.addComponents(
|
|
||||||
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/modules/admin/item_wizard.types.ts
Normal file
14
src/modules/admin/item_wizard.types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ItemUsageData } from "@/lib/types";
|
||||||
|
|
||||||
|
export interface DraftItem {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
rarity: string;
|
||||||
|
type: string;
|
||||||
|
price: number | null;
|
||||||
|
iconUrl: string;
|
||||||
|
imageUrl: string;
|
||||||
|
usageData: ItemUsageData;
|
||||||
|
// Temporary state for effect adding flow
|
||||||
|
pendingEffectType?: string;
|
||||||
|
}
|
||||||
134
src/modules/admin/item_wizard.view.ts
Normal file
134
src/modules/admin/item_wizard.view.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
ModalBuilder,
|
||||||
|
StringSelectMenuBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
type MessageActionRowComponentBuilder
|
||||||
|
} from "discord.js";
|
||||||
|
import { createBaseEmbed } from "@lib/embeds";
|
||||||
|
import type { DraftItem } from "./item_wizard.types";
|
||||||
|
|
||||||
|
const getItemTypeOptions = () => [
|
||||||
|
{ label: "Material", value: "MATERIAL", description: "Used for crafting or trading" },
|
||||||
|
{ label: "Consumable", value: "CONSUMABLE", description: "Can be used to gain effects" },
|
||||||
|
{ label: "Equipment", value: "EQUIPMENT", description: "Can be equipped (Not yet implemented)" },
|
||||||
|
{ label: "Quest Item", value: "QUEST", description: "Required for quests" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getEffectTypeOptions = () => [
|
||||||
|
{ label: "Add XP", value: "ADD_XP", description: "Gives XP to the user" },
|
||||||
|
{ label: "Add Balance", value: "ADD_BALANCE", description: "Gives currency to the user" },
|
||||||
|
{ label: "Reply Message", value: "REPLY_MESSAGE", description: "Bot replies with a message" },
|
||||||
|
{ label: "XP Boost", value: "XP_BOOST", description: "Temporarily boosts XP gain" },
|
||||||
|
{ label: "Temp Role", value: "TEMP_ROLE", description: "Gives a temporary role" },
|
||||||
|
{ label: "Color Role", value: "COLOR_ROLE", description: "Equips a permanent color role (swaps)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getItemWizardEmbed = (draft: DraftItem) => {
|
||||||
|
const embed = createBaseEmbed(`🛠️ Item Creator: ${draft.name}`, undefined, "Blue")
|
||||||
|
.addFields(
|
||||||
|
{ name: "General", value: `**Type:** ${draft.type}\n**Rarity:** ${draft.rarity}\n**Desc:** ${draft.description}`, inline: true },
|
||||||
|
{ name: "Economy", value: `**Price:** ${draft.price ? `${draft.price} 🪙` : "Not for sale"}`, inline: true },
|
||||||
|
{ name: "Visuals", value: `**Icon:** ${draft.iconUrl ? "✅ Set" : "❌"}\n**Image:** ${draft.imageUrl ? "✅ Set" : "❌"}`, inline: true },
|
||||||
|
{ name: "Usage", value: `**Consume:** ${draft.usageData.consume ? "✅ Yes" : "❌ No"}`, inline: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effects Display
|
||||||
|
if (draft.usageData.effects.length > 0) {
|
||||||
|
const effecto = draft.usageData.effects.map((e, i) => `${i + 1}. **${e.type}**: ${JSON.stringify(e)}`).join("\n");
|
||||||
|
embed.addFields({ name: "Usage Effects", value: effecto.substring(0, 1024) });
|
||||||
|
} else {
|
||||||
|
embed.addFields({ name: "Usage Effects", value: "None" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draft.imageUrl) embed.setImage(draft.imageUrl);
|
||||||
|
if (draft.iconUrl) embed.setThumbnail(draft.iconUrl);
|
||||||
|
|
||||||
|
// Components
|
||||||
|
const row1 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("createitem_details").setLabel("Edit Details").setStyle(ButtonStyle.Secondary).setEmoji("📝"),
|
||||||
|
new ButtonBuilder().setCustomId("createitem_economy").setLabel("Edit Economy").setStyle(ButtonStyle.Secondary).setEmoji("💰"),
|
||||||
|
new ButtonBuilder().setCustomId("createitem_visuals").setLabel("Edit Visuals").setStyle(ButtonStyle.Secondary).setEmoji("🖼️"),
|
||||||
|
new ButtonBuilder().setCustomId("createitem_type_toggle").setLabel("Change Type").setStyle(ButtonStyle.Secondary).setEmoji("🔄"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const row2 = new ActionRowBuilder<MessageActionRowComponentBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("createitem_addeffect_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("createitem_save").setLabel("Save Item").setStyle(ButtonStyle.Success).setEmoji("💾"),
|
||||||
|
new ButtonBuilder().setCustomId("createitem_cancel").setLabel("Cancel").setStyle(ButtonStyle.Danger).setEmoji("✖️")
|
||||||
|
);
|
||||||
|
|
||||||
|
return { embeds: [embed], components: [row1, row2] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getItemTypeSelection = () => {
|
||||||
|
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||||
|
new StringSelectMenuBuilder().setCustomId("createitem_select_type").setPlaceholder("Select Item Type").addOptions(getItemTypeOptions())
|
||||||
|
);
|
||||||
|
return { components: [row] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEffectTypeSelection = () => {
|
||||||
|
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||||
|
new StringSelectMenuBuilder().setCustomId("createitem_select_effect_type").setPlaceholder("Select Effect Type").addOptions(getEffectTypeOptions())
|
||||||
|
);
|
||||||
|
return { components: [row] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDetailsModal = (current: DraftItem) => {
|
||||||
|
const modal = new ModalBuilder().setCustomId("createitem_modal_details").setTitle("Edit Details");
|
||||||
|
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("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("Common, Rare, Legendary...").setRequired(true))
|
||||||
|
);
|
||||||
|
return modal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEconomyModal = (current: DraftItem) => {
|
||||||
|
const modal = new ModalBuilder().setCustomId("createitem_modal_economy").setTitle("Edit Economy");
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
return modal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getVisualsModal = (current: DraftItem) => {
|
||||||
|
const modal = new ModalBuilder().setCustomId("createitem_modal_visuals").setTitle("Edit Visuals");
|
||||||
|
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("image").setLabel("Image URL").setValue(current.imageUrl).setStyle(TextInputStyle.Short).setRequired(false))
|
||||||
|
);
|
||||||
|
return modal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEffectConfigModal = (effectType: string) => {
|
||||||
|
let modal = new ModalBuilder().setCustomId("createitem_modal_effect").setTitle(`Config ${effectType}`);
|
||||||
|
|
||||||
|
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")));
|
||||||
|
} else if (effectType === "REPLY_MESSAGE") {
|
||||||
|
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("message").setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true)));
|
||||||
|
} else if (effectType === "XP_BOOST") {
|
||||||
|
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("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
|
||||||
|
);
|
||||||
|
} else if (effectType === "TEMP_ROLE") {
|
||||||
|
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("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
|
||||||
|
);
|
||||||
|
} else if (effectType === "COLOR_ROLE") {
|
||||||
|
modal.addComponents(
|
||||||
|
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return modal;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle } from "discord.js";
|
import { ButtonInteraction } from "discord.js";
|
||||||
import { lootdropService } from "./lootdrop.service";
|
import { lootdropService } from "./lootdrop.service";
|
||||||
import { createErrorEmbed, createSuccessEmbed, createBaseEmbed } from "@/lib/embeds";
|
import { createErrorEmbed } from "@/lib/embeds";
|
||||||
|
import { getLootdropClaimedMessage } from "./lootdrop.view";
|
||||||
|
|
||||||
export async function handleLootdropInteraction(interaction: ButtonInteraction) {
|
export async function handleLootdropInteraction(interaction: ButtonInteraction) {
|
||||||
if (interaction.customId === "lootdrop_claim") {
|
if (interaction.customId === "lootdrop_claim") {
|
||||||
@@ -17,21 +18,14 @@ export async function handleLootdropInteraction(interaction: ButtonInteraction)
|
|||||||
const originalEmbed = interaction.message.embeds[0];
|
const originalEmbed = interaction.message.embeds[0];
|
||||||
if (!originalEmbed) return;
|
if (!originalEmbed) return;
|
||||||
|
|
||||||
const newEmbed = createBaseEmbed(originalEmbed.title || "💰 LOOTDROP!", `✅ Claimed by <@${interaction.user.id}> for **${result.amount} ${result.currency}**!`, "#00FF00");
|
const { embeds, components } = getLootdropClaimedMessage(
|
||||||
|
originalEmbed.title || "💰 LOOTDROP!",
|
||||||
|
interaction.user.id,
|
||||||
|
result.amount || 0,
|
||||||
|
result.currency || "Coins"
|
||||||
|
);
|
||||||
|
|
||||||
// Disable button
|
await interaction.message.edit({ embeds, components });
|
||||||
// We reconstruct the button using builders for safety
|
|
||||||
const newRow = new ActionRowBuilder<ButtonBuilder>()
|
|
||||||
.addComponents(
|
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId("lootdrop_claim_disabled")
|
|
||||||
.setLabel("CLAIMED")
|
|
||||||
.setStyle(ButtonStyle.Secondary)
|
|
||||||
.setEmoji("✅")
|
|
||||||
.setDisabled(true)
|
|
||||||
);
|
|
||||||
|
|
||||||
await interaction.message.edit({ embeds: [newEmbed], components: [newRow] });
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import { Message, TextChannel, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } from "discord.js";
|
import { Message, TextChannel } from "discord.js";
|
||||||
|
import { getLootdropMessage } from "./lootdrop.view";
|
||||||
import { config } from "@/lib/config";
|
import { config } from "@/lib/config";
|
||||||
import { economyService } from "./economy.service";
|
import { economyService } from "./economy.service";
|
||||||
import { createBaseEmbed } from "@lib/embeds";
|
|
||||||
|
|
||||||
import { lootdrops } from "@/db/schema";
|
import { lootdrops } from "@/db/schema";
|
||||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||||
@@ -92,19 +93,10 @@ class LootdropService {
|
|||||||
const reward = Math.floor(Math.random() * (max - min + 1)) + min;
|
const reward = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
const currency = config.lootdrop.reward.currency;
|
const currency = config.lootdrop.reward.currency;
|
||||||
|
|
||||||
const embed = createBaseEmbed("💰 LOOTDROP!", `A lootdrop has appeared! Click the button below to claim **${reward} ${currency}**!`, "#FFD700");
|
const { embeds, components } = getLootdropMessage(reward, currency);
|
||||||
|
|
||||||
const claimButton = new ButtonBuilder()
|
|
||||||
.setCustomId("lootdrop_claim")
|
|
||||||
.setLabel("CLAIM REWARD")
|
|
||||||
.setStyle(ButtonStyle.Success)
|
|
||||||
.setEmoji("💸");
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
|
||||||
.addComponents(claimButton);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const message = await channel.send({ embeds: [embed], components: [row] });
|
const message = await channel.send({ embeds, components });
|
||||||
|
|
||||||
// Persist to DB
|
// Persist to DB
|
||||||
await DrizzleClient.insert(lootdrops).values({
|
await DrizzleClient.insert(lootdrops).values({
|
||||||
|
|||||||
41
src/modules/economy/lootdrop.view.ts
Normal file
41
src/modules/economy/lootdrop.view.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||||
|
import { createBaseEmbed } from "@lib/embeds";
|
||||||
|
|
||||||
|
export function getLootdropMessage(reward: number, currency: string) {
|
||||||
|
const embed = createBaseEmbed(
|
||||||
|
"💰 LOOTDROP!",
|
||||||
|
`A lootdrop has appeared! Click the button below to claim **${reward} ${currency}**!`,
|
||||||
|
"#FFD700"
|
||||||
|
);
|
||||||
|
|
||||||
|
const claimButton = new ButtonBuilder()
|
||||||
|
.setCustomId("lootdrop_claim")
|
||||||
|
.setLabel("CLAIM REWARD")
|
||||||
|
.setStyle(ButtonStyle.Success)
|
||||||
|
.setEmoji("💸");
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.addComponents(claimButton);
|
||||||
|
|
||||||
|
return { embeds: [embed], components: [row] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLootdropClaimedMessage(originalTitle: string, userId: string, amount: number, currency: string) {
|
||||||
|
const newEmbed = createBaseEmbed(
|
||||||
|
originalTitle || "💰 LOOTDROP!",
|
||||||
|
`✅ Claimed by <@${userId}> for **${amount} ${currency}**!`,
|
||||||
|
"#00FF00"
|
||||||
|
);
|
||||||
|
|
||||||
|
const newRow = new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("lootdrop_claim_disabled")
|
||||||
|
.setLabel("CLAIMED")
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setEmoji("✅")
|
||||||
|
.setDisabled(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { embeds: [newEmbed], components: [newRow] };
|
||||||
|
}
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
|
type Interaction,
|
||||||
ButtonInteraction,
|
ButtonInteraction,
|
||||||
ModalSubmitInteraction,
|
ModalSubmitInteraction,
|
||||||
StringSelectMenuInteraction,
|
StringSelectMenuInteraction,
|
||||||
type Interaction,
|
|
||||||
ActionRowBuilder,
|
|
||||||
ButtonBuilder,
|
|
||||||
ButtonStyle,
|
|
||||||
StringSelectMenuBuilder,
|
|
||||||
ModalBuilder,
|
|
||||||
TextInputBuilder,
|
|
||||||
TextInputStyle,
|
|
||||||
ThreadChannel,
|
ThreadChannel,
|
||||||
TextChannel,
|
TextChannel,
|
||||||
EmbedBuilder
|
EmbedBuilder
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { TradeService } from "./trade.service";
|
import { TradeService } from "./trade.service";
|
||||||
import { inventoryService } from "@/modules/inventory/inventory.service";
|
import { inventoryService } from "@/modules/inventory/inventory.service";
|
||||||
import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed, createBaseEmbed } from "@lib/embeds";
|
import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds";
|
||||||
|
import { getTradeDashboard, getTradeMoneyModal, getItemSelectMenu, getTradeCompletedEmbed } from "./trade.view";
|
||||||
|
|
||||||
|
|
||||||
const EMBED_COLOR = 0xFFD700; // Gold
|
|
||||||
|
|
||||||
export async function handleTradeInteraction(interaction: Interaction) {
|
export async function handleTradeInteraction(interaction: Interaction) {
|
||||||
if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit()) return;
|
if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit()) return;
|
||||||
@@ -91,20 +85,7 @@ async function handleLock(interaction: ButtonInteraction | StringSelectMenuInter
|
|||||||
|
|
||||||
async function handleAddMoneyClick(interaction: Interaction) {
|
async function handleAddMoneyClick(interaction: Interaction) {
|
||||||
if (!interaction.isButton()) return;
|
if (!interaction.isButton()) return;
|
||||||
const modal = new ModalBuilder()
|
const modal = getTradeMoneyModal();
|
||||||
.setCustomId('trade_money_modal')
|
|
||||||
.setTitle('Add Money');
|
|
||||||
|
|
||||||
const input = new TextInputBuilder()
|
|
||||||
.setCustomId('amount')
|
|
||||||
.setLabel("Amount to trade")
|
|
||||||
.setStyle(TextInputStyle.Short)
|
|
||||||
.setPlaceholder("100")
|
|
||||||
.setRequired(true);
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<TextInputBuilder>().addComponents(input);
|
|
||||||
modal.addComponents(row);
|
|
||||||
|
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,17 +112,11 @@ async function handleAddItemClick(interaction: ButtonInteraction, threadId: stri
|
|||||||
const options = inventory.slice(0, 25).map(entry => ({
|
const options = inventory.slice(0, 25).map(entry => ({
|
||||||
label: `${entry.item.name} (${entry.quantity})`,
|
label: `${entry.item.name} (${entry.quantity})`,
|
||||||
value: entry.item.id.toString(),
|
value: entry.item.id.toString(),
|
||||||
description: `Rarity: ${entry.item.rarity}`
|
description: `Rarity: ${entry.item.rarity} `
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const select = new StringSelectMenuBuilder()
|
const { components } = getItemSelectMenu(options, 'trade_select_item', 'Select an item to add');
|
||||||
.setCustomId('trade_select_item')
|
await interaction.reply({ content: "Select an item to add:", components, ephemeral: true });
|
||||||
.setPlaceholder('Select an item to add')
|
|
||||||
.addOptions(options);
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(select);
|
|
||||||
|
|
||||||
await interaction.reply({ content: "Select an item to add:", components: [row], ephemeral: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleItemSelect(interaction: StringSelectMenuInteraction, threadId: string) {
|
async function handleItemSelect(interaction: StringSelectMenuInteraction, threadId: string) {
|
||||||
@@ -175,14 +150,8 @@ async function handleRemoveItemClick(interaction: ButtonInteraction, threadId: s
|
|||||||
value: i.id.toString(),
|
value: i.id.toString(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const select = new StringSelectMenuBuilder()
|
const { components } = getItemSelectMenu(options, 'trade_remove_item_select', 'Select an item to remove');
|
||||||
.setCustomId('trade_remove_item_select')
|
await interaction.reply({ content: "Select an item to remove:", components, ephemeral: true });
|
||||||
.setPlaceholder('Select an item to remove')
|
|
||||||
.addOptions(options);
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(select);
|
|
||||||
|
|
||||||
await interaction.reply({ content: "Select an item to remove:", components: [row], ephemeral: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveItemSelect(interaction: StringSelectMenuInteraction, threadId: string) {
|
async function handleRemoveItemSelect(interaction: StringSelectMenuInteraction, threadId: string) {
|
||||||
@@ -207,13 +176,7 @@ export async function updateTradeDashboard(interaction: Interaction, threadId: s
|
|||||||
// Execute Trade
|
// Execute Trade
|
||||||
try {
|
try {
|
||||||
await TradeService.executeTrade(threadId);
|
await TradeService.executeTrade(threadId);
|
||||||
const embed = createBaseEmbed("✅ Trade Completed", undefined, "Green")
|
const embed = getTradeCompletedEmbed(session);
|
||||||
.addFields(
|
|
||||||
{ name: session.userA.username, value: formatOffer(session.userA), inline: true },
|
|
||||||
{ name: session.userB.username, value: formatOffer(session.userB), inline: true }
|
|
||||||
)
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await updateDashboardMessage(interaction, { embeds: [embed], components: [] });
|
await updateDashboardMessage(interaction, { embeds: [embed], components: [] });
|
||||||
|
|
||||||
// Notify and Schedule Cleanup
|
// Notify and Schedule Cleanup
|
||||||
@@ -221,7 +184,7 @@ export async function updateTradeDashboard(interaction: Interaction, threadId: s
|
|||||||
const successEmbed = createSuccessEmbed("Trade executed successfully. Items and funds have been transferred.", "Trade Complete");
|
const successEmbed = createSuccessEmbed("Trade executed successfully. Items and funds have been transferred.", "Trade Complete");
|
||||||
await scheduleThreadCleanup(
|
await scheduleThreadCleanup(
|
||||||
interaction.channel,
|
interaction.channel,
|
||||||
`🎉 Trade successful! <@${session.userA.id}> <@${session.userB.id}>\nThis thread will be deleted in 10 seconds.`,
|
`🎉 Trade successful! < @${session.userA.id}> <@${session.userB.id}>\nThis thread will be deleted in 10 seconds.`,
|
||||||
10000,
|
10000,
|
||||||
successEmbed
|
successEmbed
|
||||||
);
|
);
|
||||||
@@ -244,31 +207,8 @@ export async function updateTradeDashboard(interaction: Interaction, threadId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build Status Embed
|
// Build Status Embed
|
||||||
const embed = createBaseEmbed("🤝 Trading Session", undefined, EMBED_COLOR)
|
const { embeds, components } = getTradeDashboard(session);
|
||||||
.addFields(
|
await updateDashboardMessage(interaction, { embeds, components });
|
||||||
{
|
|
||||||
name: `${session.userA.username} ${session.userA.locked ? '✅ (Ready)' : '✏️ (Editing)'}`,
|
|
||||||
value: formatOffer(session.userA),
|
|
||||||
inline: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${session.userB.username} ${session.userB.locked ? '✅ (Ready)' : '✏️ (Editing)'}`,
|
|
||||||
value: formatOffer(session.userB),
|
|
||||||
inline: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.setFooter({ text: "Both parties must click Lock to confirm trade." });
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
|
||||||
.addComponents(
|
|
||||||
new ButtonBuilder().setCustomId('trade_add_item').setLabel('Add Item').setStyle(ButtonStyle.Secondary),
|
|
||||||
new ButtonBuilder().setCustomId('trade_add_money').setLabel('Add Money').setStyle(ButtonStyle.Success),
|
|
||||||
new ButtonBuilder().setCustomId('trade_remove_item').setLabel('Remove Item').setStyle(ButtonStyle.Secondary),
|
|
||||||
new ButtonBuilder().setCustomId('trade_lock').setLabel('Lock / Unlock').setStyle(ButtonStyle.Primary),
|
|
||||||
new ButtonBuilder().setCustomId('trade_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger),
|
|
||||||
);
|
|
||||||
|
|
||||||
await updateDashboardMessage(interaction, { embeds: [embed], components: [row] });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDashboardMessage(interaction: Interaction, payload: any) {
|
async function updateDashboardMessage(interaction: Interaction, payload: any) {
|
||||||
@@ -296,17 +236,7 @@ async function updateDashboardMessage(interaction: Interaction, payload: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOffer(participant: any) {
|
|
||||||
let text = "";
|
|
||||||
if (participant.offer.money > 0n) {
|
|
||||||
text += `💰 ${participant.offer.money} 🪙\n`;
|
|
||||||
}
|
|
||||||
if (participant.offer.items.length > 0) {
|
|
||||||
text += participant.offer.items.map((i: any) => `- ${i.name} (x${i.quantity})`).join("\n");
|
|
||||||
}
|
|
||||||
if (text === "") text = "*Empty Offer*";
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scheduleThreadCleanup(channel: ThreadChannel | TextChannel, message: string, delayMs: number = 10000, embed?: EmbedBuilder) {
|
async function scheduleThreadCleanup(channel: ThreadChannel | TextChannel, message: string, delayMs: number = 10000, embed?: EmbedBuilder) {
|
||||||
try {
|
try {
|
||||||
@@ -318,7 +248,7 @@ async function scheduleThreadCleanup(channel: ThreadChannel | TextChannel, messa
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
if (channel.isThread()) {
|
if (channel.isThread()) {
|
||||||
console.log(`Deleting thread: ${channel.id}`);
|
console.log(`Deleting thread: ${channel.id} `);
|
||||||
await channel.delete("Trade Session Ended");
|
await channel.delete("Trade Session Ended");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
85
src/modules/trade/trade.view.ts
Normal file
85
src/modules/trade/trade.view.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
|
||||||
|
import { createBaseEmbed } from "@lib/embeds";
|
||||||
|
import type { TradeSession, TradeParticipant } from "./trade.types";
|
||||||
|
|
||||||
|
const EMBED_COLOR = 0xFFD700; // Gold
|
||||||
|
|
||||||
|
function formatOffer(participant: TradeParticipant) {
|
||||||
|
let text = "";
|
||||||
|
if (participant.offer.money > 0n) {
|
||||||
|
text += `💰 ${participant.offer.money} 🪙\n`;
|
||||||
|
}
|
||||||
|
if (participant.offer.items.length > 0) {
|
||||||
|
text += participant.offer.items.map((i) => `- ${i.name} (x${i.quantity})`).join("\n");
|
||||||
|
}
|
||||||
|
if (text === "") text = "*Empty Offer*";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTradeDashboard(session: TradeSession) {
|
||||||
|
const embed = createBaseEmbed("🤝 Trading Session", undefined, EMBED_COLOR)
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: `${session.userA.username} ${session.userA.locked ? '✅ (Ready)' : '✏️ (Editing)'}`,
|
||||||
|
value: formatOffer(session.userA),
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${session.userB.username} ${session.userB.locked ? '✅ (Ready)' : '✏️ (Editing)'}`,
|
||||||
|
value: formatOffer(session.userB),
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setFooter({ text: "Both parties must click Lock to confirm trade." });
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder().setCustomId('trade_add_item').setLabel('Add Item').setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder().setCustomId('trade_add_money').setLabel('Add Money').setStyle(ButtonStyle.Success),
|
||||||
|
new ButtonBuilder().setCustomId('trade_remove_item').setLabel('Remove Item').setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder().setCustomId('trade_lock').setLabel('Lock / Unlock').setStyle(ButtonStyle.Primary),
|
||||||
|
new ButtonBuilder().setCustomId('trade_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { embeds: [embed], components: [row] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTradeCompletedEmbed(session: TradeSession) {
|
||||||
|
const embed = createBaseEmbed("✅ Trade Completed", undefined, "Green")
|
||||||
|
.addFields(
|
||||||
|
{ name: session.userA.username, value: formatOffer(session.userA), inline: true },
|
||||||
|
{ name: session.userB.username, value: formatOffer(session.userB), inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTradeMoneyModal() {
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId('trade_money_modal')
|
||||||
|
.setTitle('Add Money');
|
||||||
|
|
||||||
|
const input = new TextInputBuilder()
|
||||||
|
.setCustomId('amount')
|
||||||
|
.setLabel("Amount to trade")
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder("100")
|
||||||
|
.setRequired(true);
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder<TextInputBuilder>().addComponents(input);
|
||||||
|
modal.addComponents(row);
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getItemSelectMenu(items: { label: string, value: string, description?: string }[], customId: string, placeholder: string) {
|
||||||
|
const select = new StringSelectMenuBuilder()
|
||||||
|
.setCustomId(customId)
|
||||||
|
.setPlaceholder(placeholder)
|
||||||
|
.addOptions(items);
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(select);
|
||||||
|
|
||||||
|
return { components: [row] };
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ButtonInteraction, MessageFlags } from "discord.js";
|
import { ButtonInteraction, MessageFlags } from "discord.js";
|
||||||
import { config } from "@/lib/config";
|
import { config } from "@/lib/config";
|
||||||
import { createErrorEmbed } from "@/lib/embeds";
|
import { getEnrollmentErrorEmbed, getEnrollmentSuccessMessage } from "./enrollment.view";
|
||||||
import { classService } from "@modules/class/class.service";
|
import { classService } from "@modules/class/class.service";
|
||||||
import { userService } from "@modules/user/user.service";
|
import { userService } from "@modules/user/user.service";
|
||||||
import { sendWebhookMessage } from "@/lib/webhookUtils";
|
import { sendWebhookMessage } from "@/lib/webhookUtils";
|
||||||
@@ -15,7 +15,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
|
|||||||
|
|
||||||
if (!studentRole || !visitorRole) {
|
if (!studentRole || !visitorRole) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [createErrorEmbed("No student or visitor role configured for enrollment.", "Configuration Error")],
|
...getEnrollmentErrorEmbed("No student or visitor role configured for enrollment.", "Configuration Error"),
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -28,7 +28,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
|
|||||||
// Check DB enrollment
|
// Check DB enrollment
|
||||||
if (user.class) {
|
if (user.class) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [createErrorEmbed("You are already enrolled in a class.", "Enrollment Failed")],
|
...getEnrollmentErrorEmbed("You are already enrolled in a class.", "Enrollment Failed"),
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -39,7 +39,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
|
|||||||
// Check Discord role enrollment (Double safety)
|
// Check Discord role enrollment (Double safety)
|
||||||
if (member.roles.cache.has(studentRole)) {
|
if (member.roles.cache.has(studentRole)) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [createErrorEmbed("You already have the student role.", "Enrollment Failed")],
|
...getEnrollmentErrorEmbed("You already have the student role.", "Enrollment Failed"),
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -51,7 +51,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
|
|||||||
|
|
||||||
if (validClasses.length === 0) {
|
if (validClasses.length === 0) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [createErrorEmbed("No classes with specified roles found in database.", "Configuration Error")],
|
...getEnrollmentErrorEmbed("No classes with specified roles found in database.", "Configuration Error"),
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -65,7 +65,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
|
|||||||
const classRole = interaction.guild.roles.cache.get(classRoleId);
|
const classRole = interaction.guild.roles.cache.get(classRoleId);
|
||||||
if (!classRole) {
|
if (!classRole) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [createErrorEmbed(`The configured role ID \`${classRoleId}\` for class **${selectedClass.name}** does not exist in this server.`, "Configuration Error")],
|
...getEnrollmentErrorEmbed(`The configured role ID \`${classRoleId}\` for class **${selectedClass.name}** does not exist in this server.`, "Configuration Error"),
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -81,7 +81,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
|
|||||||
await classService.assignClass(user.id.toString(), selectedClass.id);
|
await classService.assignClass(user.id.toString(), selectedClass.id);
|
||||||
|
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: `🎉 You have been successfully enrolled! You received the **${classRole.name}** role.`,
|
...getEnrollmentSuccessMessage(classRole.name),
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Enrollment error:", error);
|
console.error("Enrollment error:", error);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [createErrorEmbed("An unexpected error occurred during enrollment. Please contact an administrator.", "System Error")],
|
...getEnrollmentErrorEmbed("An unexpected error occurred during enrollment. Please contact an administrator.", "System Error"),
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/modules/user/enrollment.view.ts
Normal file
12
src/modules/user/enrollment.view.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createErrorEmbed } from "@/lib/embeds";
|
||||||
|
|
||||||
|
export function getEnrollmentErrorEmbed(message: string, title: string = "Enrollment Failed") {
|
||||||
|
const embed = createErrorEmbed(message, title);
|
||||||
|
return { embeds: [embed] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnrollmentSuccessMessage(roleName: string) {
|
||||||
|
return {
|
||||||
|
content: `🎉 You have been successfully enrolled! You received the **${roleName}** role.`
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user