diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..ec7d07a --- /dev/null +++ b/check.sh @@ -0,0 +1 @@ +tsc --noEmit diff --git a/src/commands/admin/listing.ts b/src/commands/admin/listing.ts new file mode 100644 index 0000000..9ded877 --- /dev/null +++ b/src/commands/admin/listing.ts @@ -0,0 +1,77 @@ +import { createCommand } from "@/lib/utils"; +import { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type BaseGuildTextChannel, + PermissionFlagsBits, + MessageFlags +} from "discord.js"; +import { inventoryService } from "@/modules/inventory/inventory.service"; +import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; + +export const listing = createCommand({ + data: new SlashCommandBuilder() + .setName("listing") + .setDescription("Post an item listing in the channel for users to buy") + .addNumberOption(option => + option.setName("itemid") + .setDescription("The ID of the item to list") + .setRequired(true) + ) + .addChannelOption(option => + option.setName("channel") + .setDescription("The channel to post the listing in (defaults to current)") + .setRequired(false) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + const itemId = interaction.options.getNumber("itemid", true); + const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel; + + if (!targetChannel || !targetChannel.isSendable()) { + await interaction.editReply({ content: "", embeds: [createErrorEmbed("Target channel is invalid or not sendable.")] }); + return; + } + + const item = await inventoryService.getItem(itemId); + if (!item) { + await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item with ID ${itemId} not found.`)] }); + return; + } + + if (!item.price) { + await interaction.editReply({ content: "", embeds: [createWarningEmbed(`Item "${item.name}" is not for sale (no price set).`)] }); + return; + } + + const embed = new EmbedBuilder() + .setTitle(`Shop: ${item.name}`) + .setDescription(item.description || "No description available.") + .addFields({ name: "Price", value: `${item.price} 🪙`, inline: true }) + .setColor("Green") + .setThumbnail(item.iconUrl || null) + .setImage(item.imageUrl || null) + .setFooter({ text: "Click the button below to purchase instantly." }); + + const buyButton = new ButtonBuilder() + .setCustomId(`shop_buy_${item.id}`) + .setLabel(`Buy for ${item.price} 🪙`) + .setStyle(ButtonStyle.Success) + .setEmoji("🛒"); + + const actionRow = new ActionRowBuilder().addComponents(buyButton); + + try { + await targetChannel.send({ embeds: [embed], components: [actionRow] }); + await interaction.editReply({ content: `✅ Listing for **${item.name}** posted in ${targetChannel}.` }); + } catch (error) { + console.error("Failed to send listing message:", error); + await interaction.editReply({ content: "", embeds: [createErrorEmbed("Failed to post the listing.")] }); + } + } +}); diff --git a/src/commands/economy/sell.ts b/src/commands/economy/sell.ts deleted file mode 100644 index da717d5..0000000 --- a/src/commands/economy/sell.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { createCommand } from "@/lib/utils"; -import { - SlashCommandBuilder, - EmbedBuilder, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - ComponentType, - type BaseGuildTextChannel, - type ButtonInteraction, - PermissionFlagsBits, - MessageFlags -} from "discord.js"; -import { userService } from "@/modules/user/user.service"; -import { inventoryService } from "@/modules/inventory/inventory.service"; -import type { items } from "@db/schema"; -import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; - -export const sell = createCommand({ - data: new SlashCommandBuilder() - .setName("sell") - .setDescription("Post an item for sale in the current channel so regular users can buy it") - .addNumberOption(option => - option.setName("itemid") - .setDescription("The ID of the item to sell") - .setRequired(true) - ) - .addChannelOption(option => - option.setName("channel") - .setDescription("The channel to post the item in") - .setRequired(false) - ) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - execute: async (interaction) => { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - - const itemId = interaction.options.getNumber("itemid", true); - const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel; - - if (!targetChannel || !targetChannel.isSendable()) { - await interaction.editReply({ content: "", embeds: [createErrorEmbed("Target channel is invalid or not sendable.")] }); - return; - } - - const item = await inventoryService.getItem(itemId); - if (!item) { - await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item with ID ${itemId} not found.`)] }); - return; - } - - if (!item.price) { - await interaction.editReply({ content: "", embeds: [createWarningEmbed(`Item "${item.name}" is not for sale (no price set).`)] }); - return; - } - - const embed = new EmbedBuilder() - .setTitle(`Item for sale: ${item.name}`) - .setDescription(item.description || "No description available.") - .addFields({ name: "Price", value: `${item.price} 🪙`, inline: true }) - .setColor("Yellow") - .setThumbnail(item.iconUrl || null) - .setImage(item.imageUrl || null); - - const buyButton = new ButtonBuilder() - .setCustomId("buy") - .setLabel("Buy") - .setStyle(ButtonStyle.Success); - - const actionRow = new ActionRowBuilder().addComponents(buyButton); - - try { - const message = await targetChannel.send({ embeds: [embed], components: [actionRow] }); - await interaction.editReply({ content: `Item posted in ${targetChannel}.` }); - - // Create a collector on the specific message - const collector = message.createMessageComponentCollector({ - componentType: ComponentType.Button, - filter: (i) => i.customId === "buy", - }); - - collector.on("collect", async (i) => { - await handleBuyInteraction(i, item); - }); - - } catch (error) { - console.error("Failed to send sell message:", error); - await interaction.editReply({ content: "", embeds: [createErrorEmbed("Failed to post the item for sale.")] }); - } - } -}); - -async function handleBuyInteraction(interaction: ButtonInteraction, item: typeof items.$inferSelect) { - try { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - - const userId = interaction.user.id; - const user = await userService.getUserById(userId); - - if (!user) { - await interaction.editReply({ content: "", embeds: [createErrorEmbed("User profile not found.")] }); - return; - } - - if ((user.balance ?? 0n) < (item.price ?? 0n)) { - await interaction.editReply({ content: "", embeds: [createWarningEmbed(`You don't have enough money! You need ${item.price} 🪙.`)] }); - return; - } - - const result = await inventoryService.buyItem(userId, item.id, 1n); - - if (!result.success) { - await interaction.editReply({ content: "", embeds: [createErrorEmbed("Transaction failed. Please try again.")] }); - return; - } - - await interaction.editReply({ content: `Successfully bought **${item.name}** for ${item.price} 🪙!` }); - } catch (error) { - console.error("Error processing purchase:", error); - if (interaction.deferred || interaction.replied) { - await interaction.editReply({ content: "", embeds: [createErrorEmbed("An error occurred while processing your purchase.")] }); - } else { - await interaction.reply({ embeds: [createErrorEmbed("An error occurred while processing your purchase.")], flags: MessageFlags.Ephemeral }); - } - } -} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 4abc82b..5277510 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -13,6 +13,10 @@ const event: Event = { await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction)); return; } + if (interaction.customId.startsWith("shop_buy_") && interaction.isButton()) { + await import("@/modules/economy/shop.interaction").then(m => m.handleShopInteraction(interaction)); + return; + } } if (!interaction.isChatInputCommand()) return; diff --git a/src/modules/economy/shop.interaction.ts b/src/modules/economy/shop.interaction.ts new file mode 100644 index 0000000..2af78dc --- /dev/null +++ b/src/modules/economy/shop.interaction.ts @@ -0,0 +1,40 @@ +import { ButtonInteraction, MessageFlags } from "discord.js"; +import { inventoryService } from "@/modules/inventory/inventory.service"; +import { userService } from "@/modules/user/user.service"; +import { createErrorEmbed, createWarningEmbed } from "@/lib/embeds"; + +export async function handleShopInteraction(interaction: ButtonInteraction) { + if (!interaction.customId.startsWith("shop_buy_")) return; + + try { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + const itemId = parseInt(interaction.customId.replace("shop_buy_", "")); + if (isNaN(itemId)) { + await interaction.editReply({ embeds: [createErrorEmbed("Invalid Item ID.")] }); + return; + } + + const item = await inventoryService.getItem(itemId); + if (!item || !item.price) { + await interaction.editReply({ embeds: [createErrorEmbed("Item not found or not for sale.")] }); + return; + } + + const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username); + + // Double check balance here too, although service handles it, we want a nice message + if ((user.balance ?? 0n) < item.price) { + await interaction.editReply({ embeds: [createWarningEmbed(`You need ${item.price} 🪙 to buy this item. You have ${user.balance} 🪙.`)] }); + return; + } + + const result = await inventoryService.buyItem(user.id, item.id, 1n); + + await interaction.editReply({ content: `✅ **Success!** You bought **${item.name}** for ${item.price} 🪙.` }); + + } catch (error: any) { + console.error("Shop Purchase Error:", error); + await interaction.editReply({ embeds: [createErrorEmbed(error.message || "An error occurred while processing your purchase.")] }); + } +}