2 Commits

20 changed files with 387 additions and 311 deletions

View File

@@ -1,5 +1,6 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from "discord.js"; import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { createBaseEmbed } from "@lib/embeds";
import { configManager } from "@/lib/configManager"; import { configManager } from "@/lib/configManager";
import { config, reloadConfig } from "@/lib/config"; import { config, reloadConfig } from "@/lib/config";
import { AuroraClient } from "@/lib/BotClient"; import { AuroraClient } from "@/lib/BotClient";
@@ -45,9 +46,7 @@ export const features = createCommand({
const overrides = Object.entries(config.commands) const overrides = Object.entries(config.commands)
.map(([name, enabled]) => `• **${name}**: ${enabled ? "✅ Enabled (Override)" : "❌ Disabled"}`); .map(([name, enabled]) => `• **${name}**: ${enabled ? "✅ Enabled (Override)" : "❌ Disabled"}`);
const embed = new EmbedBuilder() const embed = createBaseEmbed("Command Features", undefined, "Blue");
.setTitle("Command Features")
.setColor("Blue");
// Add fields for each category // Add fields for each category
const sortedCategories = [...categories.keys()].sort(); const sortedCategories = [...categories.keys()].sort();

View File

@@ -1,7 +1,6 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { import {
SlashCommandBuilder, SlashCommandBuilder,
EmbedBuilder,
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
ButtonStyle, ButtonStyle,
@@ -10,7 +9,7 @@ import {
MessageFlags MessageFlags
} from "discord.js"; } from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service"; import { inventoryService } from "@/modules/inventory/inventory.service";
import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds"; import { createSuccessEmbed, createErrorEmbed, createBaseEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors"; import { UserError } from "@/lib/errors";
import { items } from "@/db/schema"; import { items } from "@/db/schema";
import { ilike, isNotNull, and } from "drizzle-orm"; import { ilike, isNotNull, and } from "drizzle-orm";
@@ -54,11 +53,8 @@ export const listing = createCommand({
return; return;
} }
const embed = new EmbedBuilder() const embed = createBaseEmbed(`Shop: ${item.name}`, item.description || "No description available.", "Green")
.setTitle(`Shop: ${item.name}`)
.setDescription(item.description || "No description available.")
.addFields({ name: "Price", value: `${item.price} 🪙`, inline: true }) .addFields({ name: "Price", value: `${item.price} 🪙`, inline: true })
.setColor("Green")
.setThumbnail(item.iconUrl || null) .setThumbnail(item.iconUrl || null)
.setImage(item.imageUrl || null) .setImage(item.imageUrl || null)
.setFooter({ text: "Click the button below to purchase instantly." }); .setFooter({ text: "Click the button below to purchase instantly." });

View File

@@ -1,6 +1,7 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder } from "discord.js";
import { userService } from "@/modules/user/user.service"; import { userService } from "@/modules/user/user.service";
import { createBaseEmbed } from "@lib/embeds";
export const balance = createCommand({ export const balance = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -22,10 +23,8 @@ export const balance = createCommand({
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username); const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
const embed = new EmbedBuilder() const embed = createBaseEmbed(undefined, `**Balance**: ${user.balance || 0n} AU`, "Yellow")
.setAuthor({ name: targetUser.username, iconURL: targetUser.displayAvatarURL() }) .setAuthor({ name: targetUser.username, iconURL: targetUser.displayAvatarURL() });
.setDescription(`**Balance**: ${user.balance || 0n} AU`)
.setColor("Yellow");
await interaction.editReply({ embeds: [embed] }); await interaction.editReply({ embeds: [embed] });
} }

View File

@@ -1,8 +1,8 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder } from "discord.js";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { createErrorEmbed } from "@lib/embeds"; import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors"; import { UserError } from "@/lib/errors";
export const daily = createCommand({ export const daily = createCommand({
@@ -13,16 +13,13 @@ export const daily = createCommand({
try { try {
const result = await economyService.claimDaily(interaction.user.id); const result = await economyService.claimDaily(interaction.user.id);
const embed = new EmbedBuilder() const embed = createSuccessEmbed(`You claimed ** ${result.amount}** Astral Units!${result.isWeekly ? `\n🎉 **Weekly Bonus!** +${result.weeklyBonus} extra!` : ''}`, "💰 Daily Reward Claimed!")
.setTitle("💰 Daily Reward Claimed!")
.setDescription(`You claimed ** ${result.amount}** Astral Units!${result.isWeekly ? `\n🎉 **Weekly Bonus!** +${result.weeklyBonus} extra!` : ''}`)
.addFields( .addFields(
{ name: "Streak", value: `🔥 ${result.streak} days`, inline: true }, { name: "Streak", value: `🔥 ${result.streak} days`, inline: true },
{ name: "Weekly Progress", value: `${"🟩".repeat(result.streak % 7 || 7)}${"⬜".repeat(7 - (result.streak % 7 || 7))} (${result.streak % 7 || 7}/7)`, inline: true }, { name: "Weekly Progress", value: `${"🟩".repeat(result.streak % 7 || 7)}${"⬜".repeat(7 - (result.streak % 7 || 7))} (${result.streak % 7 || 7}/7)`, inline: true },
{ name: "Next Reward", value: `<t:${Math.floor(result.nextReadyAt.getTime() / 1000)}:R> `, inline: true } { name: "Next Reward", value: `<t:${Math.floor(result.nextReadyAt.getTime() / 1000)}:R> `, inline: true }
) )
.setColor("Gold") .setColor("Gold");
.setTimestamp();
await interaction.reply({ embeds: [embed] }); await interaction.reply({ embeds: [embed] });

View File

@@ -1,10 +1,10 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"; import { SlashCommandBuilder, MessageFlags } from "discord.js";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { userService } from "@/modules/user/user.service"; import { userService } from "@/modules/user/user.service";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { createErrorEmbed } from "@lib/embeds"; import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors"; import { UserError } from "@/lib/errors";
export const pay = createCommand({ export const pay = createCommand({
@@ -49,12 +49,7 @@ export const pay = createCommand({
await interaction.deferReply(); await interaction.deferReply();
await economyService.transfer(senderId, receiverId, amount); await economyService.transfer(senderId, receiverId, amount);
const embed = new EmbedBuilder() const embed = createSuccessEmbed(`Successfully sent ** ${amount}** Astral Units to <@${targetUser.id}>.`, "💸 Transfer Successful");
.setTitle("💸 Transfer Successful")
.setDescription(`Successfully sent ** ${amount}** Astral Units to <@${targetUser.id}>.`)
.setColor("Green")
.setTimestamp();
await interaction.editReply({ embeds: [embed], content: `<@${receiverId}>` }); await interaction.editReply({ embeds: [embed], content: `<@${receiverId}>` });
} catch (error: any) { } catch (error: any) {

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ThreadAutoArchiveDuration, MessageFlags } from "discord.js"; import { SlashCommandBuilder, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ThreadAutoArchiveDuration, MessageFlags } from "discord.js";
import { TradeService } from "@/modules/trade/trade.service"; import { TradeService } from "@/modules/trade/trade.service";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; import { createErrorEmbed, createWarningEmbed, createBaseEmbed } from "@lib/embeds";
export const trade = createCommand({ export const trade = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -64,10 +64,7 @@ export const trade = createCommand({
); );
// Send Dashboard to Thread // Send Dashboard to Thread
const embed = new EmbedBuilder() const embed = createBaseEmbed("🤝 Trading Session", `Trade started between ${interaction.user} and ${targetUser}.\nUse the controls below to build your offer.`, 0xFFD700)
.setTitle("🤝 Trading Session")
.setDescription(`Trade started between ${interaction.user} and ${targetUser}.\nUse the controls below to build your offer.`)
.setColor(0xFFD700)
.addFields( .addFields(
{ name: interaction.user.username, value: "*Empty Offer*", inline: true }, { name: interaction.user.username, value: "*Empty Offer*", inline: true },
{ name: targetUser.username, value: "*Empty Offer*", inline: true } { name: targetUser.username, value: "*Empty Offer*", inline: true }

View File

@@ -1,8 +1,8 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder } from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service"; import { inventoryService } from "@/modules/inventory/inventory.service";
import { userService } from "@/modules/user/user.service"; import { userService } from "@/modules/user/user.service";
import { createWarningEmbed } from "@lib/embeds"; import { createWarningEmbed, createBaseEmbed } from "@lib/embeds";
export const inventory = createCommand({ export const inventory = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -35,11 +35,7 @@ export const inventory = createCommand({
return `**${entry.item.name}** x${entry.quantity}`; return `**${entry.item.name}** x${entry.quantity}`;
}).join("\n"); }).join("\n");
const embed = new EmbedBuilder() const embed = createBaseEmbed(`${user.username}'s Inventory`, description, "Blue");
.setTitle(`${user.username}'s Inventory`)
.setDescription(description)
.setColor("Blue")
.setTimestamp();
await interaction.editReply({ embeds: [embed] }); await interaction.editReply({ embeds: [embed] });
} }

View File

@@ -1,9 +1,9 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder } from "discord.js";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { users } from "@/db/schema"; import { users } from "@/db/schema";
import { desc } from "drizzle-orm"; import { desc } from "drizzle-orm";
import { createWarningEmbed } from "@lib/embeds"; import { createWarningEmbed, createBaseEmbed } from "@lib/embeds";
export const leaderboard = createCommand({ export const leaderboard = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -40,11 +40,7 @@ export const leaderboard = createCommand({
return `${medal} **${user.username}** — ${value}`; return `${medal} **${user.username}** — ${value}`;
}).join("\n"); }).join("\n");
const embed = new EmbedBuilder() const embed = createBaseEmbed(isXp ? "🏆 XP Leaderboard" : "💰 Richest Players", description, "Gold");
.setTitle(isXp ? "🏆 XP Leaderboard" : "💰 Richest Players")
.setDescription(description)
.setColor("Gold")
.setTimestamp();
await interaction.editReply({ embeds: [embed] }); await interaction.editReply({ embeds: [embed] });
} }

View File

@@ -1,7 +1,7 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"; import { SlashCommandBuilder, MessageFlags } from "discord.js";
import { questService } from "@/modules/quest/quest.service"; import { questService } from "@/modules/quest/quest.service";
import { createWarningEmbed } from "@lib/embeds"; import { createWarningEmbed, createBaseEmbed } from "@lib/embeds";
export const quests = createCommand({ export const quests = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -17,10 +17,7 @@ export const quests = createCommand({
return; return;
} }
const embed = new EmbedBuilder() const embed = createBaseEmbed("📜 Quest Log", undefined, "Blue");
.setTitle("📜 Quest Log")
.setColor("Blue")
.setTimestamp();
userQuests.forEach(entry => { userQuests.forEach(entry => {
const status = entry.completedAt ? "✅ Completed" : "In Progress"; const status = entry.completedAt ? "✅ Completed" : "In Progress";

View File

@@ -1,4 +1,4 @@
import { EmbedBuilder, Colors } from "discord.js"; import { Colors, type ColorResolvable, EmbedBuilder } from "discord.js";
/** /**
* Creates a standardized error embed. * Creates a standardized error embed.
@@ -55,3 +55,21 @@ export function createInfoEmbed(message: string, title: string = "Info"): EmbedB
.setColor(Colors.Blue) .setColor(Colors.Blue)
.setTimestamp(); .setTimestamp();
} }
/**
* Creates a standardized base embed with common configuration.
* @param title Optional title for the embed.
* @param description Optional description for the embed.
* @param color Optional color for the embed.
* @returns An EmbedBuilder instance with base configuration.
*/
export function createBaseEmbed(title?: string, description?: string, color?: ColorResolvable): EmbedBuilder {
const embed = new EmbedBuilder()
.setTimestamp();
if (title) embed.setTitle(title);
if (description) embed.setDescription(description);
if (color) embed.setColor(color);
return embed;
}

View File

@@ -1,51 +1,17 @@
import { import { type Interaction } from "discord.js";
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
EmbedBuilder,
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 { 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,45 +32,8 @@ export const renderWizard = (userId: string, isDraft = true) => {
draftSession.set(userId, draft); draftSession.set(userId, draft);
} }
const embed = new EmbedBuilder() const { embeds, components } = getItemWizardEmbed(draft);
.setTitle(`🛠️ Item Creator: ${draft.name}`) return { embeds, components };
.setColor("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] };
}; };
// --- Handler --- // --- Handler ---
@@ -153,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;
} }
@@ -166,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;
} }
@@ -177,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;
} }
@@ -189,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;
} }
@@ -211,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
@@ -228,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;
} }

View 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;
}

View 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;
};

View File

@@ -1,6 +1,7 @@
import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, EmbedBuilder, ButtonStyle } from "discord.js"; import { ButtonInteraction } from "discord.js";
import { lootdropService } from "./lootdrop.service"; import { lootdropService } from "./lootdrop.service";
import { createErrorEmbed } 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,23 +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 = new EmbedBuilder(originalEmbed.data) const { embeds, components } = getLootdropClaimedMessage(
.setDescription(`✅ Claimed by <@${interaction.user.id}> for **${result.amount} ${result.currency}**!`) originalEmbed.title || "💰 LOOTDROP!",
.setColor("#00FF00"); 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({

View File

@@ -1,8 +1,10 @@
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 { lootdrops } from "@/db/schema"; import { lootdrops } from "@/db/schema";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { eq, and, isNull, lt } from "drizzle-orm"; import { eq, and, isNull, lt } from "drizzle-orm";
@@ -91,23 +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 = new EmbedBuilder() const { embeds, components } = getLootdropMessage(reward, currency);
.setTitle("💰 LOOTDROP!")
.setDescription(`A lootdrop has appeared! Click the button below to claim **${reward} ${currency}**!`)
.setColor("#FFD700")
.setTimestamp();
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({

View 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] };
}

View File

@@ -1,24 +1,18 @@
import { import {
type Interaction,
ButtonInteraction, ButtonInteraction,
ModalSubmitInteraction, ModalSubmitInteraction,
StringSelectMenuInteraction, StringSelectMenuInteraction,
type Interaction,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
StringSelectMenuBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ThreadChannel, ThreadChannel,
TextChannel, TextChannel,
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 } 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,15 +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 = new EmbedBuilder() const embed = getTradeCompletedEmbed(session);
.setTitle("✅ Trade Completed")
.setColor("Green")
.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
@@ -223,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
); );
@@ -246,33 +207,8 @@ export async function updateTradeDashboard(interaction: Interaction, threadId: s
} }
// Build Status Embed // Build Status Embed
const embed = new EmbedBuilder() const { embeds, components } = getTradeDashboard(session);
.setTitle("🤝 Trading Session") await updateDashboardMessage(interaction, { embeds, components });
.setColor(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),
);
await updateDashboardMessage(interaction, { embeds: [embed], components: [row] });
} }
async function updateDashboardMessage(interaction: Interaction, payload: any) { async function updateDashboardMessage(interaction: Interaction, payload: any) {
@@ -300,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 {
@@ -322,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) {

View 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] };
}

View File

@@ -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
}); });
} }

View 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.`
};
}