Skip to content

Commit 28bf9d1

Browse files
committed
Convert addon rendering to plain component data
util/addons.ts was the last file building components with the discord.js builder pattern — the thing this whole refactor started from. Its four render functions now return plain data like everything else: createAddonComponent -> ContainerComponentData createAddonSection -> SectionComponentData createAddonList -> [TextDisplayComponentData, ContainerComponentData] createNavigation -> ActionRowData<MessageActionRowComponentData> The `new ContainerBuilder().addSectionComponents(new SectionBuilder()...)` chain that the original review used as the argument against builders is gone. Two small local helpers (separator, thumbnail) plus the existing row / container / text cover the rest. Verified by rendering both versions of all four functions and diffing the API JSON, normalising the camelCase/snake_case difference between the data interfaces and the builders' output: a full addon page, the same page with a support-server button, a section, a two-addon list, and the navigation menu in both selected and disabled states are all identical, 6/6. The only EmbedBuilder left in src/ is /about, which keeps it deliberately and says why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hDX4g1EjTN7424A5wnptE
1 parent 1291e57 commit 28bf9d1

1 file changed

Lines changed: 73 additions & 62 deletions

File tree

src/util/addons.ts

Lines changed: 73 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, ContainerBuilder, MessageFlags, SectionBuilder, SeparatorBuilder, SeparatorSpacingSize, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextDisplayBuilder, ThumbnailBuilder} from "discord.js";
1+
import {
2+
ButtonStyle, ChatInputCommandInteraction, ComponentType, MessageFlags, SeparatorSpacingSize,
3+
StringSelectMenuInteraction,
4+
type ActionRowData, type ComponentInContainerData, type ContainerComponentData,
5+
type MessageActionRowComponentData, type SectionComponentData, type TextDisplayComponentData
6+
} from "discord.js";
7+
import {container, row, text} from "../framework";
28
import type {BdWebAddon} from "../types";
39

410
import Web from "../util/web";
@@ -72,86 +78,91 @@ export function sortAddons(addons: BdWebAddon[], sortBy: "likes" | "downloads" |
7278
}
7379

7480

75-
export function createAddonComponent(addon: BdWebAddon) {
81+
const separator = (spacing: SeparatorSpacingSize, divider: boolean): ComponentInContainerData =>
82+
({type: ComponentType.Separator, spacing, divider});
7683

77-
const buttons = new ActionRowBuilder<ButtonBuilder>()
78-
.addComponents(
79-
new ButtonBuilder()
80-
.setStyle(ButtonStyle.Link)
81-
.setLabel("View Online")
82-
.setURL(Web.pages[addon.type](addon.name)),
83-
new ButtonBuilder()
84-
.setStyle(ButtonStyle.Link)
85-
.setLabel("Download Now")
86-
.setURL(Web.redirects.download(addon.id.toString())),
87-
);
84+
const thumbnail = (url: string) => ({type: ComponentType.Thumbnail as const, media: {url}});
85+
86+
/** The link buttons every addon carries, plus a support server when there is one. */
87+
function addonLinks(addon: BdWebAddon): MessageActionRowComponentData[] {
88+
const buttons: MessageActionRowComponentData[] = [
89+
{type: ComponentType.Button, style: ButtonStyle.Link, label: "View Online", url: Web.pages[addon.type](addon.name)},
90+
{type: ComponentType.Button, style: ButtonStyle.Link, label: "Download Now", url: Web.redirects.download(addon.id.toString())}
91+
];
8892

8993
if (addon.author.guild?.invite_link) {
90-
buttons.addComponents(
91-
new ButtonBuilder()
92-
.setStyle(ButtonStyle.Link)
93-
.setLabel("Support Server")
94-
.setURL(addon.author.guild.invite_link),
95-
);
94+
buttons.push({type: ComponentType.Button, style: ButtonStyle.Link, label: "Support Server", url: addon.author.guild.invite_link});
9695
}
9796

98-
const page = new ContainerBuilder()
99-
.addSectionComponents(
100-
new SectionBuilder()
101-
.setThumbnailAccessory(
102-
new ThumbnailBuilder().setURL(Web.resources.thumbnail(addon.thumbnail_url))
103-
)
104-
.addTextDisplayComponents(
105-
new TextDisplayBuilder().setContent(`# ${addon.name} v${addon.version}`),
106-
new TextDisplayBuilder().setContent(addon.description ?? "No description provided."),
107-
new TextDisplayBuilder().setContent(addon.tags.map(tag => `\`${tag}\``).join(" ")),
108-
),
109-
)
110-
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(false))
111-
.addTextDisplayComponents(new TextDisplayBuilder().setContent(`👍 ${addon.likes.toLocaleString()} Likes ⬇️ ${addon.downloads.toLocaleString()} Downloads`))
112-
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true))
113-
.addActionRowComponents(buttons)
114-
.addTextDisplayComponents(new TextDisplayBuilder().setContent(`-# Updated ${new Date(addon.latest_release_date).toLocaleDateString()} • Released ${new Date(addon.initial_release_date).toLocaleDateString()}`));
115-
116-
return page;
97+
return buttons;
11798
}
11899

119-
export function createAddonSection(addon: BdWebAddon) {
120100

101+
/** One addon rendered in full, as its own page. */
102+
export function createAddonComponent(addon: BdWebAddon): ContainerComponentData {
103+
const details: TextDisplayComponentData[] = [
104+
{type: ComponentType.TextDisplay, content: `# ${addon.name} v${addon.version}`},
105+
{type: ComponentType.TextDisplay, content: addon.description ?? "No description provided."},
106+
{type: ComponentType.TextDisplay, content: addon.tags.map(tag => `\`${tag}\``).join(" ")}
107+
];
108+
109+
return container([
110+
{
111+
type: ComponentType.Section,
112+
components: details,
113+
accessory: thumbnail(Web.resources.thumbnail(addon.thumbnail_url))
114+
},
115+
separator(SeparatorSpacingSize.Small, false),
116+
text(`👍 ${addon.likes.toLocaleString()} Likes ⬇️ ${addon.downloads.toLocaleString()} Downloads`),
117+
separator(SeparatorSpacingSize.Large, true),
118+
row(...addonLinks(addon)),
119+
text(`-# Updated ${new Date(addon.latest_release_date).toLocaleDateString()} • Released ${new Date(addon.initial_release_date).toLocaleDateString()}`)
120+
]);
121+
}
122+
123+
124+
/** One addon as a compact row within a list. */
125+
export function createAddonSection(addon: BdWebAddon): SectionComponentData {
121126
const links = [
122127
`[View Online](${Web.pages[addon.type](addon.name)})`,
123128
`[Download Now](${Web.redirects.download(addon.id.toString())})`,
124-
addon.author.guild?.invite_link && `[Support Server](${addon.author.guild?.invite_link})`
129+
addon.author.guild?.invite_link && `[Support Server](${addon.author.guild.invite_link})`
125130
].filter(Boolean).join(" • ");
126131

127-
const section = new SectionBuilder()
128-
.setThumbnailAccessory(new ThumbnailBuilder().setURL(Web.resources.thumbnail(addon.thumbnail_url)))
129-
.addTextDisplayComponents(
130-
new TextDisplayBuilder().setContent(`### ${addon.name}`),
131-
new TextDisplayBuilder().setContent(addon.description ?? "No description provided."),
132-
new TextDisplayBuilder().setContent(links),
133-
);
134-
135-
return section;
132+
return {
133+
type: ComponentType.Section,
134+
components: [
135+
{type: ComponentType.TextDisplay, content: `### ${addon.name}`},
136+
{type: ComponentType.TextDisplay, content: addon.description ?? "No description provided."},
137+
{type: ComponentType.TextDisplay, content: links}
138+
],
139+
accessory: thumbnail(Web.resources.thumbnail(addon.thumbnail_url))
140+
};
136141
}
137142

138-
export function createAddonList(title: string, addons: BdWebAddon[]) {
139-
const page = new ContainerBuilder();
143+
144+
export function createAddonList(title: string, addons: BdWebAddon[]): [TextDisplayComponentData, ContainerComponentData] {
145+
const body: ComponentInContainerData[] = [];
140146
for (const [index, addon] of addons.entries()) {
141-
page.addSectionComponents(createAddonSection(addon));
142-
if (index < addons.length - 1) page.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true));
147+
body.push(createAddonSection(addon));
148+
if (index < addons.length - 1) body.push(separator(SeparatorSpacingSize.Large, true));
143149
}
144-
return [new TextDisplayBuilder().setContent(`## ${title}`), page];
150+
151+
return [{type: ComponentType.TextDisplay, content: `## ${title}`}, container(body)];
145152
}
146153

147-
export function createNavigation(addons: BdWebAddon[], selectedIndex = 0, disabled = false) {
148-
const navigation = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
149-
new StringSelectMenuBuilder().setCustomId(`addons-navigation`).addOptions(
150-
...addons.map((addon, index) => new StringSelectMenuOptionBuilder().setLabel(`${index + 1}. ${addon.name}`).setValue(addon.name).setDefault(index === selectedIndex))
151-
)
152-
.setDisabled(disabled)
153-
);
154-
return navigation;
154+
155+
export function createNavigation(addons: BdWebAddon[], selectedIndex = 0, disabled = false): ActionRowData<MessageActionRowComponentData> {
156+
return row({
157+
type: ComponentType.StringSelect,
158+
customId: "addons-navigation",
159+
disabled,
160+
options: addons.map((addon, index) => ({
161+
"label": `${index + 1}. ${addon.name}`,
162+
"value": addon.name,
163+
"default": index === selectedIndex
164+
}))
165+
});
155166
}
156167

157168

0 commit comments

Comments
 (0)