Files
aurorabot/docs/superpowers/plans/2026-03-28-inventory-display-redesign.md
syntaxbullet ba8afd144e docs: add inventory display redesign implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:01:09 +01:00

875 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Inventory Display Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Redesign the `/inventory` command into a polished Components V2 experience with rarity emojis, paginated list, item detail view with artwork, and inline item actions (use/discard).
**Architecture:** Rewrite `inventory.view.ts` to produce CV2 containers for list and detail views. Rewrite `inventory.ts` command to manage pagination and view state with a collector. Add `inventory.interaction.ts` for interaction routing. Extend `RARITY_CONFIG` with square emojis.
**Tech Stack:** discord.js Components V2 (ContainerBuilder, TextDisplayBuilder, SectionBuilder, MediaGalleryBuilder, ActionRowBuilder, ButtonBuilder, StringSelectMenuBuilder), Drizzle ORM, Bun test runner.
---
## File Structure
| File | Action | Responsibility |
|------|--------|----------------|
| `shared/lib/rarity.ts` | Modify | Add `squareEmoji` field to `RARITY_CONFIG` |
| `bot/modules/inventory/inventory.view.ts` | Rewrite | CV2 list message builder, CV2 detail message builder (keep `getLootboxResultMessage` untouched) |
| `bot/modules/inventory/inventory.interaction.ts` | Create | Handle all inventory interactions (select, pagination, back, use, discard, confirm) |
| `bot/commands/inventory/inventory.ts` | Rewrite | Command definition with `view` subcommand, pagination collector, autocomplete |
---
### Task 1: Add squareEmoji to RARITY_CONFIG
**Files:**
- Modify: `shared/lib/rarity.ts`
- [ ] **Step 1: Update the RARITY_CONFIG type and entries**
In `shared/lib/rarity.ts`, update the type signature and add `squareEmoji` to each entry:
```typescript
/**
* Shared Rarity Configuration
* Provides the canonical rarity display config (colors, emoji, labels)
* used by lootbox pull results and shop loot table views.
*/
export const RARITY_CONFIG: Record<string, { color: number; emoji: string; squareEmoji: string; label: string }> = {
C: { color: 0x95A5A6, emoji: "📦", squareEmoji: "🟤", label: "Common" },
R: { color: 0x3498DB, emoji: "📦", squareEmoji: "🔵", label: "Rare" },
SR: { color: 0x9B59B6, emoji: "✨", squareEmoji: "🟣", label: "Super Rare" },
SSR: { color: 0xF1C40F, emoji: "🌟", squareEmoji: "🟡", label: "SSR" },
CURRENCY: { color: 0x2ECC71, emoji: "💰", squareEmoji: "💰", label: "Currency" },
XP: { color: 0x1ABC9C, emoji: "🔮", squareEmoji: "🔮", label: "Experience" },
NOTHING: { color: 0x636363, emoji: "💨", squareEmoji: "💨", label: "Empty" },
};
export function getRarityConfig(rarity: string): { color: number; emoji: string; squareEmoji: string; label: string } {
return RARITY_CONFIG[rarity] ?? (RARITY_CONFIG["C"]!);
}
```
- [ ] **Step 2: Verify nothing is broken**
Run: `bun test`
Expected: All existing tests pass (lootbox and other rarity consumers still work since they access `emoji`, not `squareEmoji`).
- [ ] **Step 3: Commit**
```bash
git add shared/lib/rarity.ts
git commit -m "feat(inventory): add squareEmoji to RARITY_CONFIG"
```
---
### Task 2: Build the inventory list view (CV2)
**Files:**
- Rewrite: `bot/modules/inventory/inventory.view.ts`
This task rewrites `getInventoryEmbed``getInventoryListMessage` and adds `getItemDetailMessage`. The existing `getLootboxResultMessage` function stays untouched.
- [ ] **Step 1: Define constants and types at the top of inventory.view.ts**
Replace the existing `InventoryEntry` interface and add constants. Keep all existing imports and add the new ones needed:
```typescript
import {
EmbedBuilder,
AttachmentBuilder,
ContainerBuilder,
SectionBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
MediaGalleryItemBuilder,
ThumbnailBuilder,
SeparatorBuilder,
SeparatorSpacingSize,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
MessageFlags,
} from "discord.js";
import { resolveAssetUrl, isLocalAssetUrl } from "@shared/lib/assets";
import { getRarityConfig, defaultName, stripQuery } from "@shared/lib/rarity";
import { ItemType } from "@shared/lib/constants";
import type { ItemUsageData } from "@shared/lib/types";
import { join } from "path";
import { existsSync } from "fs";
export const ITEMS_PER_PAGE = 5;
const RARITY_SORT_ORDER: Record<string, number> = {
SSR: 0,
SR: 1,
R: 2,
C: 3,
};
export interface InventoryItem {
id: number;
name: string;
description: string | null;
rarity: string | null;
type: string;
price: bigint | null;
iconUrl: string;
imageUrl: string;
usageData: unknown;
}
export interface InventoryEntry {
quantity: bigint | null;
item: InventoryItem;
}
```
- [ ] **Step 2: Add the sortInventoryItems helper**
```typescript
export function sortInventoryItems(entries: InventoryEntry[]): InventoryEntry[] {
return [...entries].sort((a, b) => {
const rarityA = RARITY_SORT_ORDER[a.item.rarity ?? "C"] ?? 3;
const rarityB = RARITY_SORT_ORDER[b.item.rarity ?? "C"] ?? 3;
if (rarityA !== rarityB) return rarityA - rarityB;
return a.item.name.localeCompare(b.item.name);
});
}
```
- [ ] **Step 3: Implement getInventoryListMessage**
```typescript
export function getInventoryListMessage(
entries: InventoryEntry[],
username: string,
page: number,
viewerId: string,
ownerId: string,
) {
const sorted = sortInventoryItems(entries);
const totalPages = Math.max(1, Math.ceil(sorted.length / ITEMS_PER_PAGE));
const safePage = Math.min(page, totalPages - 1);
const pageItems = sorted.slice(safePage * ITEMS_PER_PAGE, (safePage + 1) * ITEMS_PER_PAGE);
// Accent color from highest-rarity item on page
const highestRarity = pageItems[0]?.item.rarity ?? "C";
const accentColor = getRarityConfig(highestRarity).color;
const container = new ContainerBuilder()
.setAccentColor(accentColor)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# 📦 ${username}'s Inventory`),
new TextDisplayBuilder().setContent(`-# ${sorted.length} item${sorted.length !== 1 ? "s" : ""} total`)
);
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Item rows
const lines = pageItems.map((entry) => {
const rc = getRarityConfig(entry.item.rarity ?? "C");
return `${rc.squareEmoji} **${entry.item.name}** — ${rc.label} · ${entry.item.type} · ×${entry.quantity}`;
});
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(lines.join("\n"))
);
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Select menu with current page items
const selectMenu = new StringSelectMenuBuilder()
.setCustomId(`inv_select_${viewerId}`)
.setPlaceholder("Select an item for details");
for (const entry of pageItems) {
const rc = getRarityConfig(entry.item.rarity ?? "C");
selectMenu.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel(entry.item.name)
.setDescription(`${rc.label} · ${entry.item.type}`)
.setValue(entry.item.id.toString())
);
}
container.addActionRowComponents(
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
);
// Pagination buttons
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_prev_${viewerId}`)
.setLabel("◀ Previous")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage <= 0),
new ButtonBuilder()
.setCustomId(`inv_page_${viewerId}`)
.setLabel(`Page ${safePage + 1}/${totalPages}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true),
new ButtonBuilder()
.setCustomId(`inv_next_${viewerId}`)
.setLabel("Next ▶")
.setStyle(ButtonStyle.Secondary)
.setDisabled(safePage >= totalPages - 1),
);
container.addActionRowComponents(navRow);
return {
components: [container] as any,
files: [] as AttachmentBuilder[],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
```
- [ ] **Step 4: Implement getEmptyInventoryMessage**
```typescript
export function getEmptyInventoryMessage(username: string) {
const container = new ContainerBuilder()
.setAccentColor(0x95A5A6)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# 📦 ${username}'s Inventory`),
new TextDisplayBuilder().setContent("*No items yet. Visit the shop or complete quests to earn items!*")
);
return {
components: [container] as any,
files: [],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
```
- [ ] **Step 5: Implement getItemDetailMessage**
```typescript
export function getItemDetailMessage(
entry: InventoryEntry,
viewerId: string,
ownerId: string,
) {
const { item } = entry;
const rc = getRarityConfig(item.rarity ?? "C");
const files: AttachmentBuilder[] = [];
const container = new ContainerBuilder().setAccentColor(rc.color);
// Header section with thumbnail
const section = new SectionBuilder().addTextDisplayComponents(
new TextDisplayBuilder().setContent(`${rc.squareEmoji} **${item.name}**`),
new TextDisplayBuilder().setContent(`-# ${rc.label} · ${item.type}`)
);
// Resolve icon thumbnail
const iconUrl = resolveItemUrl(item.iconUrl, files);
if (iconUrl) {
section.setThumbnailAccessory(new ThumbnailBuilder().setURL(iconUrl));
}
container.addSectionComponents(section);
// Artwork via MediaGallery
const imageUrl = resolveItemUrl(item.imageUrl, files);
if (imageUrl && item.imageUrl !== item.iconUrl) {
container.addMediaGalleryComponents(
new MediaGalleryBuilder().addItems(
new MediaGalleryItemBuilder().setURL(imageUrl)
)
);
}
// Description
if (item.description) {
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(item.description)
);
}
container.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
// Stats row
const priceText = item.price ? `${item.price} 🪙` : "Not tradeable";
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Owned: **×${entry.quantity}** · Value: **${priceText}**`
)
);
// Action buttons
const isOwner = viewerId === ownerId;
const usageData = item.usageData as ItemUsageData | null;
const isUsable = isOwner && item.type === ItemType.CONSUMABLE &&
usageData?.effects && usageData.effects.length > 0;
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_back_${viewerId}`)
.setLabel("◀ Back")
.setStyle(ButtonStyle.Primary)
);
if (isUsable) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_use_${viewerId}`)
.setLabel("🧪 Use")
.setStyle(ButtonStyle.Success)
);
}
if (isOwner) {
actionRow.addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_${viewerId}`)
.setLabel("🗑 Discard")
.setStyle(ButtonStyle.Danger)
);
}
container.addActionRowComponents(actionRow);
return {
components: [container] as any,
files,
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
```
- [ ] **Step 6: Implement getDiscardConfirmMessage and the resolveItemUrl helper**
```typescript
export function getDiscardConfirmMessage(entry: InventoryEntry, viewerId: string) {
const rc = getRarityConfig(entry.item.rarity ?? "C");
const container = new ContainerBuilder()
.setAccentColor(0xED4245)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Are you sure you want to discard 1× **${entry.item.name}**?`
)
)
.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`inv_discard_confirm_${viewerId}`)
.setLabel("Confirm")
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`inv_discard_cancel_${viewerId}`)
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary)
)
);
return {
components: [container] as any,
files: [],
flags: MessageFlags.IsComponentsV2,
embeds: [],
};
}
/**
* Resolves an item URL (icon or image) for use in CV2 components.
* Handles both local assets and remote URLs.
* Pushes AttachmentBuilders to `files` array for local assets.
*/
function resolveItemUrl(url: string | null | undefined, files: AttachmentBuilder[]): string | null {
if (!url) return null;
if (isLocalAssetUrl(url)) {
const filePath = join(process.cwd(), "bot/assets/graphics", stripQuery(url).replace(/^\/?assets\//, ""));
if (existsSync(filePath)) {
const fileName = defaultName(url);
if (!files.find(f => f.name === fileName)) {
files.push(new AttachmentBuilder(filePath, { name: fileName }));
}
return `attachment://${fileName}`;
}
return null;
}
return resolveAssetUrl(url);
}
```
- [ ] **Step 7: Verify the file compiles**
Run: `bunx tsc --noEmit bot/modules/inventory/inventory.view.ts`
Expected: No type errors. (Note: `getLootboxResultMessage` remains unchanged below all the new code.)
- [ ] **Step 8: Commit**
```bash
git add bot/modules/inventory/inventory.view.ts
git commit -m "feat(inventory): rewrite inventory view with CV2 list and detail builders"
```
---
### Task 3: Create the inventory interaction handler
**Files:**
- Create: `bot/modules/inventory/inventory.interaction.ts`
- [ ] **Step 1: Create the interaction handler file**
```typescript
import type { StringSelectMenuInteraction, ButtonInteraction, MessageFlags } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { getLootboxResultMessage } from "./inventory.view";
import type { ItemUsageData } from "@shared/lib/types";
import { getGuildConfig } from "@shared/lib/config";
export interface InventoryState {
ownerId: string;
viewerId: string;
page: number;
selectedItemId: number | null;
}
/**
* Extracts the viewer user ID from an inventory custom ID.
* Custom IDs follow the format: inv_{action}_{viewerId}
*/
export function parseInventoryCustomId(customId: string): { action: string; viewerId: string } | null {
const match = customId.match(/^inv_(\w+?)_(\d+)$/);
if (!match) return null;
return { action: match[1], viewerId: match[2] };
}
/**
* Checks if a custom ID belongs to the inventory system.
*/
export function isInventoryInteraction(customId: string): boolean {
return customId.startsWith("inv_");
}
/**
* Handles the "Use" button — executes item effects.
* Returns the result messages array from inventoryService.useItem,
* plus handles role-based effects that require the guild member.
*/
export async function executeItemUse(
interaction: ButtonInteraction,
userId: string,
itemId: number,
): Promise<{ results: any[]; usageData: ItemUsageData | null; item: any }> {
const result = await inventoryService.useItem(userId, itemId);
// Handle role effects (same logic as /use command)
const usageData = result.usageData;
if (usageData) {
const guildConfig = await getGuildConfig(interaction.guildId!);
const colorRoles = guildConfig.colorRoles ?? [];
for (const effect of usageData.effects) {
if (effect.type === "TEMP_ROLE" || effect.type === "COLOR_ROLE") {
try {
const member = await interaction.guild?.members.fetch(userId);
if (member) {
if (effect.type === "TEMP_ROLE") {
await member.roles.add(effect.roleId);
} else if (effect.type === "COLOR_ROLE") {
const rolesToRemove = colorRoles.filter((r: string) => member.roles.cache.has(r));
if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove);
await member.roles.add(effect.roleId);
}
}
} catch (e) {
console.error("Failed to assign role in inventory use:", e);
result.results.push("⚠️ Failed to assign role (Check bot permissions)");
}
}
}
}
return result;
}
```
- [ ] **Step 2: Verify the file compiles**
Run: `bunx tsc --noEmit bot/modules/inventory/inventory.interaction.ts`
Expected: No type errors.
- [ ] **Step 3: Commit**
```bash
git add bot/modules/inventory/inventory.interaction.ts
git commit -m "feat(inventory): add inventory interaction handler utilities"
```
---
### Task 4: Rewrite the inventory command
**Files:**
- Rewrite: `bot/commands/inventory/inventory.ts`
- [ ] **Step 1: Rewrite the command with subcommands, collector, and interaction routing**
```typescript
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags, ComponentType } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { createWarningEmbed, createErrorEmbed } from "@lib/embeds";
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({
data: new SlashCommandBuilder()
.setName("inventory")
.setDescription("View your or another user's inventory")
.addSubcommand(sub =>
sub.setName("list")
.setDescription("View your or another user's inventory")
.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) => {
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)
);
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
return;
}
// "list" subcommand
const targetUser = interaction.options.getUser("user") || interaction.user;
if (targetUser.bot) {
await interaction.editReply({ embeds: [createWarningEmbed("Bots do not have inventories.", "Inventory Check")] });
return;
}
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
if (!user) {
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
return;
}
const ownerId = user.id.toString();
const entries = await inventoryService.getInventory(ownerId);
if (!entries || entries.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(user.username));
return;
}
let currentPage = 0;
let selectedItemId: number | null = null;
const response = await interaction.editReply(
getInventoryListMessage(entries as InventoryEntry[], user.username, currentPage, viewerId, ownerId)
);
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(() => {});
});
}
```
- [ ] **Step 2: Verify the file compiles**
Run: `bunx tsc --noEmit bot/commands/inventory/inventory.ts`
Expected: No type errors.
- [ ] **Step 3: Commit**
```bash
git add bot/commands/inventory/inventory.ts
git commit -m "feat(inventory): rewrite command with CV2 pagination and detail view"
```
---
### Task 5: Integration testing and verification
**Files:**
- All modified files
- [ ] **Step 1: Run the full test suite**
Run: `bun test`
Expected: All tests pass. The inventory service tests should still pass since we didn't change the service.
- [ ] **Step 2: Verify TypeScript compiles cleanly**
Run: `bunx tsc --noEmit`
Expected: No type errors across the entire project.
- [ ] **Step 3: Verify the bot starts**
Run: `bun --watch bot/index.ts` (start and verify no startup errors, then stop)
Expected: Bot initializes and registers commands without errors.
- [ ] **Step 4: Final commit if any fixes were needed**
```bash
git add -A
git commit -m "fix(inventory): address integration issues from inventory redesign"
```
(Only if fixes were needed in the previous steps.)