feat(inventory): rewrite command with CV2 pagination and detail view
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,83 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder } from "discord.js";
|
import { SlashCommandBuilder, MessageFlags, ComponentType } from "discord.js";
|
||||||
import { inventoryService } from "@shared/modules/inventory/inventory.service";
|
import { inventoryService } from "@shared/modules/inventory/inventory.service";
|
||||||
import { userService } from "@shared/modules/user/user.service";
|
import { userService } from "@shared/modules/user/user.service";
|
||||||
import { createWarningEmbed } from "@lib/embeds";
|
import { createWarningEmbed, createErrorEmbed } from "@lib/embeds";
|
||||||
import { getInventoryEmbed } from "@/modules/inventory/inventory.view";
|
import {
|
||||||
|
getInventoryListMessage,
|
||||||
|
getEmptyInventoryMessage,
|
||||||
|
getItemDetailMessage,
|
||||||
|
getDiscardConfirmMessage,
|
||||||
|
sortInventoryItems,
|
||||||
|
ITEMS_PER_PAGE,
|
||||||
|
type InventoryEntry,
|
||||||
|
} from "@/modules/inventory/inventory.view";
|
||||||
|
import { getLootboxResultMessage } from "@/modules/inventory/inventory.view";
|
||||||
|
import {
|
||||||
|
parseInventoryCustomId,
|
||||||
|
isInventoryInteraction,
|
||||||
|
executeItemUse,
|
||||||
|
} from "@/modules/inventory/inventory.interaction";
|
||||||
|
import { UserError } from "@shared/lib/errors";
|
||||||
|
|
||||||
export const inventory = createCommand({
|
export const inventory = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("inventory")
|
.setName("inventory")
|
||||||
.setDescription("View your or another user's inventory")
|
.setDescription("View your or another user's inventory")
|
||||||
.addUserOption(option =>
|
.addSubcommand(sub =>
|
||||||
option.setName("user")
|
sub.setName("list")
|
||||||
.setDescription("User to view")
|
.setDescription("View your or another user's inventory")
|
||||||
.setRequired(false)
|
.addUserOption(option =>
|
||||||
|
option.setName("user")
|
||||||
|
.setDescription("User to view")
|
||||||
|
.setRequired(false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName("view")
|
||||||
|
.setDescription("View details of a specific item")
|
||||||
|
.addNumberOption(option =>
|
||||||
|
option.setName("item")
|
||||||
|
.setDescription("The item to view")
|
||||||
|
.setRequired(true)
|
||||||
|
.setAutocomplete(true)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const viewerId = interaction.user.id;
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
if (subcommand === "view") {
|
||||||
|
// Direct item detail view
|
||||||
|
const itemId = interaction.options.getNumber("item", true);
|
||||||
|
const user = await userService.getOrCreateUser(viewerId, interaction.user.username);
|
||||||
|
if (!user) {
|
||||||
|
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await inventoryService.getInventory(user.id.toString());
|
||||||
|
const entry = entries.find((e: any) => e.item.id === itemId);
|
||||||
|
if (!entry) {
|
||||||
|
await interaction.editReply({ embeds: [createWarningEmbed("Item not found in your inventory.", "Not Found")] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerId = user.id.toString();
|
||||||
|
let currentPage = 0;
|
||||||
|
let selectedItemId: number | null = itemId;
|
||||||
|
|
||||||
|
const response = await interaction.editReply(
|
||||||
|
getItemDetailMessage(entry as InventoryEntry, viewerId, ownerId) as any
|
||||||
|
);
|
||||||
|
|
||||||
|
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "list" subcommand
|
||||||
const targetUser = interaction.options.getUser("user") || interaction.user;
|
const targetUser = interaction.options.getUser("user") || interaction.user;
|
||||||
|
|
||||||
if (targetUser.bot) {
|
if (targetUser.bot) {
|
||||||
@@ -30,15 +91,209 @@ export const inventory = createCommand({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = await inventoryService.getInventory(user.id.toString());
|
const ownerId = user.id.toString();
|
||||||
|
const entries = await inventoryService.getInventory(ownerId);
|
||||||
|
|
||||||
if (!items || items.length === 0) {
|
if (!entries || entries.length === 0) {
|
||||||
await interaction.editReply({ embeds: [createWarningEmbed("Inventory is empty.", `${user.username}'s Inventory`)] });
|
await interaction.editReply(getEmptyInventoryMessage(user.username) as any);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = getInventoryEmbed(items, user.username);
|
let currentPage = 0;
|
||||||
|
let selectedItemId: number | null = null;
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
const response = await interaction.editReply(
|
||||||
}
|
getInventoryListMessage(entries as InventoryEntry[], user.username, currentPage, viewerId, ownerId) as any
|
||||||
|
);
|
||||||
|
|
||||||
|
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
|
||||||
|
},
|
||||||
|
autocomplete: async (interaction) => {
|
||||||
|
const focusedValue = interaction.options.getFocused();
|
||||||
|
const userId = interaction.user.id;
|
||||||
|
const results = await inventoryService.getAutocompleteItems(userId, focusedValue);
|
||||||
|
await interaction.respond(results);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function setupCollector(
|
||||||
|
interaction: any,
|
||||||
|
response: any,
|
||||||
|
viewerId: string,
|
||||||
|
ownerId: string,
|
||||||
|
username: string,
|
||||||
|
initialPage: number,
|
||||||
|
initialItemId: number | null,
|
||||||
|
) {
|
||||||
|
let currentPage = initialPage;
|
||||||
|
let selectedItemId = initialItemId;
|
||||||
|
|
||||||
|
const collector = response.createMessageComponentCollector({
|
||||||
|
time: 120_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("collect", async (i: any) => {
|
||||||
|
if (i.user.id !== viewerId) return;
|
||||||
|
|
||||||
|
const parsed = parseInventoryCustomId(i.customId);
|
||||||
|
if (!parsed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await i.deferUpdate();
|
||||||
|
|
||||||
|
// Re-fetch inventory for fresh data
|
||||||
|
const entries = await inventoryService.getInventory(ownerId);
|
||||||
|
const sorted = sortInventoryItems(entries as InventoryEntry[]);
|
||||||
|
|
||||||
|
switch (parsed.action) {
|
||||||
|
case "select": {
|
||||||
|
const itemId = parseInt(i.values[0]);
|
||||||
|
const entry = sorted.find(e => e.item.id === itemId);
|
||||||
|
if (!entry) break;
|
||||||
|
selectedItemId = itemId;
|
||||||
|
await interaction.editReply(
|
||||||
|
getItemDetailMessage(entry, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "prev": {
|
||||||
|
currentPage = Math.max(0, currentPage - 1);
|
||||||
|
selectedItemId = null;
|
||||||
|
await interaction.editReply(
|
||||||
|
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "next": {
|
||||||
|
currentPage = currentPage + 1;
|
||||||
|
selectedItemId = null;
|
||||||
|
await interaction.editReply(
|
||||||
|
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "back": {
|
||||||
|
selectedItemId = null;
|
||||||
|
if (sorted.length === 0) {
|
||||||
|
await interaction.editReply(getEmptyInventoryMessage(username));
|
||||||
|
} else {
|
||||||
|
await interaction.editReply(
|
||||||
|
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "use": {
|
||||||
|
if (viewerId !== ownerId || !selectedItemId) break;
|
||||||
|
try {
|
||||||
|
const result = await executeItemUse(i, viewerId, selectedItemId);
|
||||||
|
const message = getLootboxResultMessage(result.results, result.item);
|
||||||
|
await interaction.editReply(message as any);
|
||||||
|
|
||||||
|
// After showing result, wait briefly then return to detail or list
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const freshEntries = await inventoryService.getInventory(ownerId);
|
||||||
|
const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]);
|
||||||
|
const freshEntry = freshSorted.find(e => e.item.id === selectedItemId);
|
||||||
|
|
||||||
|
if (freshEntry) {
|
||||||
|
await interaction.editReply(
|
||||||
|
getItemDetailMessage(freshEntry, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectedItemId = null;
|
||||||
|
if (freshSorted.length === 0) {
|
||||||
|
await interaction.editReply(getEmptyInventoryMessage(username));
|
||||||
|
} else {
|
||||||
|
await interaction.editReply(
|
||||||
|
getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UserError) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed(error.message)],
|
||||||
|
components: [],
|
||||||
|
flags: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "discard": {
|
||||||
|
if (viewerId !== ownerId || !selectedItemId) break;
|
||||||
|
const entry = sorted.find(e => e.item.id === selectedItemId);
|
||||||
|
if (!entry) break;
|
||||||
|
await interaction.editReply(
|
||||||
|
getDiscardConfirmMessage(entry, viewerId)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "discard_confirm": {
|
||||||
|
if (viewerId !== ownerId || !selectedItemId) break;
|
||||||
|
try {
|
||||||
|
await inventoryService.removeItem(ownerId, selectedItemId, 1n);
|
||||||
|
|
||||||
|
const freshEntries = await inventoryService.getInventory(ownerId);
|
||||||
|
const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]);
|
||||||
|
const freshEntry = freshSorted.find(e => e.item.id === selectedItemId);
|
||||||
|
|
||||||
|
if (freshEntry) {
|
||||||
|
await interaction.editReply(
|
||||||
|
getItemDetailMessage(freshEntry, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectedItemId = null;
|
||||||
|
if (freshSorted.length === 0) {
|
||||||
|
await interaction.editReply(getEmptyInventoryMessage(username));
|
||||||
|
} else {
|
||||||
|
await interaction.editReply(
|
||||||
|
getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UserError) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed(error.message)],
|
||||||
|
components: [],
|
||||||
|
flags: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "discard_cancel": {
|
||||||
|
if (!selectedItemId) break;
|
||||||
|
const entry = sorted.find(e => e.item.id === selectedItemId);
|
||||||
|
if (!entry) break;
|
||||||
|
await interaction.editReply(
|
||||||
|
getItemDetailMessage(entry, viewerId, ownerId)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Inventory interaction error:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("end", () => {
|
||||||
|
interaction.editReply({ components: [] }).catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user