fix: add pagination to quest list to stay within Discord component limits
All checks were successful
Deploy to Production / test (push) Successful in 45s
All checks were successful
Deploy to Production / test (push) Successful in 45s
The available quests view was exceeding Discord's 40-component container limit when many quests existed, causing an API error. Paginate both active and available quest views at 7 quests per page with prev/next navigation buttons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,16 +16,24 @@ export const quests = createCommand({
|
||||
const response = await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const userId = interaction.user.id;
|
||||
let currentView: 'active' | 'available' = 'active';
|
||||
let currentPage = 0;
|
||||
|
||||
const updateView = async (viewType: 'active' | 'available', page: number = 0) => {
|
||||
currentView = viewType;
|
||||
currentPage = page;
|
||||
|
||||
const updateView = async (viewType: 'active' | 'available') => {
|
||||
const userQuests = await questService.getUserQuests(userId);
|
||||
const availableQuests = await questService.getAvailableQuests(userId);
|
||||
|
||||
const containers = viewType === 'active'
|
||||
? getQuestListComponents(userQuests)
|
||||
: getAvailableQuestsComponents(availableQuests);
|
||||
const activeQuests = userQuests.filter(entry => entry.completedAt === null);
|
||||
const totalItems = viewType === 'active' ? activeQuests.length : availableQuests.length;
|
||||
|
||||
const actionRows = getQuestActionRows(viewType);
|
||||
const containers = viewType === 'active'
|
||||
? getQuestListComponents(userQuests, page)
|
||||
: getAvailableQuestsComponents(availableQuests, page);
|
||||
|
||||
const actionRows = getQuestActionRows(viewType, totalItems, page);
|
||||
|
||||
await interaction.editReply({
|
||||
content: null,
|
||||
@@ -50,10 +58,16 @@ export const quests = createCommand({
|
||||
try {
|
||||
if (i.customId === "quest_view_active") {
|
||||
await i.deferUpdate();
|
||||
await updateView('active');
|
||||
await updateView('active', 0);
|
||||
} else if (i.customId === "quest_view_available") {
|
||||
await i.deferUpdate();
|
||||
await updateView('available');
|
||||
await updateView('available', 0);
|
||||
} else if (i.customId === "quest_page_prev") {
|
||||
await i.deferUpdate();
|
||||
await updateView(currentView, Math.max(0, currentPage - 1));
|
||||
} else if (i.customId === "quest_page_next") {
|
||||
await i.deferUpdate();
|
||||
await updateView(currentView, currentPage + 1);
|
||||
} else if (i.customId.startsWith("quest_accept:")) {
|
||||
const questIdStr = i.customId.split(":")[1];
|
||||
if (!questIdStr) return;
|
||||
@@ -65,7 +79,8 @@ export const quests = createCommand({
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
await updateView('active');
|
||||
// Stay on current view/page but refresh (accepted quest disappears from available)
|
||||
await updateView(currentView, currentPage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Quest interaction error:", error);
|
||||
|
||||
@@ -43,6 +43,9 @@ const COLORS = {
|
||||
COMPLETED: 0xf1c40f // Gold - completed
|
||||
};
|
||||
|
||||
// Max quests per page (2 header + 1 page indicator + 7×5 components = 38, Discord max is 40 per container)
|
||||
const QUESTS_PER_PAGE = 7;
|
||||
|
||||
/**
|
||||
* Formats quest rewards object into a human-readable string
|
||||
*/
|
||||
@@ -70,15 +73,22 @@ function renderProgressBar(current: number, total: number, size: number = 10): s
|
||||
/**
|
||||
* Creates Components v2 containers for the quest list (active quests only)
|
||||
*/
|
||||
export function getQuestListComponents(userQuests: QuestEntry[]): ContainerBuilder[] {
|
||||
export function getQuestListComponents(userQuests: QuestEntry[], page: number = 0): ContainerBuilder[] {
|
||||
// Filter to only show in-progress quests (not completed)
|
||||
const activeQuests = userQuests.filter(entry => entry.completedAt === null);
|
||||
const totalPages = Math.max(1, Math.ceil(activeQuests.length / QUESTS_PER_PAGE));
|
||||
const safePage = Math.min(page, totalPages - 1);
|
||||
const pageQuests = activeQuests.slice(safePage * QUESTS_PER_PAGE, (safePage + 1) * QUESTS_PER_PAGE);
|
||||
|
||||
const container = new ContainerBuilder()
|
||||
.setAccentColor(COLORS.ACTIVE)
|
||||
.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("# 📜 Quest Log"),
|
||||
new TextDisplayBuilder().setContent("-# Your active quests")
|
||||
new TextDisplayBuilder().setContent(
|
||||
totalPages > 1
|
||||
? `-# Your active quests — Page ${safePage + 1}/${totalPages}`
|
||||
: "-# Your active quests"
|
||||
)
|
||||
);
|
||||
|
||||
if (activeQuests.length === 0) {
|
||||
@@ -89,7 +99,7 @@ export function getQuestListComponents(userQuests: QuestEntry[]): ContainerBuild
|
||||
return [container];
|
||||
}
|
||||
|
||||
activeQuests.forEach((entry) => {
|
||||
pageQuests.forEach((entry) => {
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
|
||||
|
||||
const rewards = entry.quest.rewards as { xp?: number, balance?: number };
|
||||
@@ -113,12 +123,20 @@ export function getQuestListComponents(userQuests: QuestEntry[]): ContainerBuild
|
||||
/**
|
||||
* Creates Components v2 containers for available quests with inline accept buttons
|
||||
*/
|
||||
export function getAvailableQuestsComponents(availableQuests: AvailableQuest[]): ContainerBuilder[] {
|
||||
export function getAvailableQuestsComponents(availableQuests: AvailableQuest[], page: number = 0): ContainerBuilder[] {
|
||||
const totalPages = Math.max(1, Math.ceil(availableQuests.length / QUESTS_PER_PAGE));
|
||||
const safePage = Math.min(page, totalPages - 1);
|
||||
const pageQuests = availableQuests.slice(safePage * QUESTS_PER_PAGE, (safePage + 1) * QUESTS_PER_PAGE);
|
||||
|
||||
const container = new ContainerBuilder()
|
||||
.setAccentColor(COLORS.AVAILABLE)
|
||||
.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("# 🗺️ Available Quests"),
|
||||
new TextDisplayBuilder().setContent("-# Quests you can accept")
|
||||
new TextDisplayBuilder().setContent(
|
||||
totalPages > 1
|
||||
? `-# Quests you can accept — Page ${safePage + 1}/${totalPages}`
|
||||
: "-# Quests you can accept"
|
||||
)
|
||||
);
|
||||
|
||||
if (availableQuests.length === 0) {
|
||||
@@ -129,10 +147,7 @@ export function getAvailableQuestsComponents(availableQuests: AvailableQuest[]):
|
||||
return [container];
|
||||
}
|
||||
|
||||
// Limit to 10 quests (5 action rows max with 2 added for navigation)
|
||||
const questsToShow = availableQuests.slice(0, 10);
|
||||
|
||||
questsToShow.forEach((quest) => {
|
||||
pageQuests.forEach((quest) => {
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
|
||||
|
||||
const rewards = quest.rewards as { xp?: number, balance?: number };
|
||||
@@ -163,11 +178,30 @@ export function getAvailableQuestsComponents(availableQuests: AvailableQuest[]):
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns action rows for navigation only
|
||||
* Returns action rows for navigation and pagination
|
||||
*/
|
||||
export function getQuestActionRows(viewType: 'active' | 'available'): ActionRowBuilder<ButtonBuilder>[] {
|
||||
// Navigation row
|
||||
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
export function getQuestActionRows(viewType: 'active' | 'available', totalItems: number, page: number): ActionRowBuilder<ButtonBuilder>[] {
|
||||
const totalPages = Math.max(1, Math.ceil(totalItems / QUESTS_PER_PAGE));
|
||||
const rows: ActionRowBuilder<ButtonBuilder>[] = [];
|
||||
|
||||
// Pagination row (only if more than one page)
|
||||
if (totalPages > 1) {
|
||||
rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("quest_page_prev")
|
||||
.setLabel("◀ Prev")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(page <= 0),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("quest_page_next")
|
||||
.setLabel("Next ▶")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(page >= totalPages - 1)
|
||||
));
|
||||
}
|
||||
|
||||
// Tab navigation row
|
||||
rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("quest_view_active")
|
||||
.setLabel("📜 Active")
|
||||
@@ -178,9 +212,9 @@ export function getQuestActionRows(viewType: 'active' | 'available'): ActionRowB
|
||||
.setLabel("🗺️ Available")
|
||||
.setStyle(viewType === 'available' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
||||
.setDisabled(viewType === 'available')
|
||||
);
|
||||
));
|
||||
|
||||
return [navRow];
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user