@kyvrixon/utils 1.0.12 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kyvrixon/utils",
3
3
  "main": "./src/index.ts",
4
- "version": "1.0.12",
4
+ "version": "1.4.1",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "license": "MIT",
@@ -11,6 +11,7 @@
11
11
  "engines": {
12
12
  "bun": "1.3.11"
13
13
  },
14
+ "engineStrict": true,
14
15
  "repository": {
15
16
  "url": "https://github.com/kyvrixon/utils"
16
17
  },
@@ -20,18 +21,22 @@
20
21
  },
21
22
  "types": "./src/index.ts",
22
23
  "devDependencies": {
23
- "@biomejs/biome": "2.4.9",
24
+ "@biomejs/biome": "2.4.10",
24
25
  "@types/bun": "1.3.11",
25
26
  "typescript": "6.0.2"
26
27
  },
27
28
  "dependencies": {
28
29
  "chalk": "5.6.2",
29
- "discord.js": "14.25.1"
30
+ "discord.js": "14.26.0"
30
31
  },
31
32
  "exports": {
32
33
  ".": {
33
34
  "types": "./src/index.ts",
34
35
  "import": "./src/index.ts"
36
+ },
37
+ "./*": {
38
+ "types": "./src/lib/*/index.ts",
39
+ "import": "./src/lib/*/index.ts"
35
40
  }
36
41
  }
37
42
  }
@@ -7,11 +7,21 @@ import type {
7
7
  SlashCommandSubcommandsOnlyBuilder,
8
8
  } from "discord.js";
9
9
 
10
+ export interface DiscordCommandMetadata extends Record<string, unknown> {}
11
+
10
12
  /**
11
13
  * Wraps a discord.js slash command with typed `execute` and optional `autocomplete` handlers.
12
- * @typeParam C - The bot's `Client` type.
14
+ * @typeParam C - The bot's `Client` type. Inferred from args[0] in `method`.
15
+ *
16
+ * @example To extend the `metadata` types
17
+ * declare module "@kyvrixon/utils" {
18
+ * interface DiscordCommandMetadata {
19
+ * cooldown?: number;
20
+ * category?: string;
21
+ * }
22
+ * }
13
23
  */
14
- export class DiscordCommand<C extends Client<boolean>> {
24
+ export class DiscordCommand<C extends Client> {
15
25
  public readonly data:
16
26
  | SlashCommandBuilder
17
27
  | SlashCommandOptionsOnlyBuilder
@@ -24,13 +34,16 @@ export class DiscordCommand<C extends Client<boolean>> {
24
34
  client: C,
25
35
  interaction: AutocompleteInteraction,
26
36
  ) => Promise<void>;
37
+ public metadata: DiscordCommandMetadata;
27
38
 
28
39
  constructor(ops: {
29
40
  data: DiscordCommand<C>["data"];
41
+ metadata: DiscordCommand<C>["metadata"];
30
42
  execute: DiscordCommand<C>["execute"];
31
43
  autocomplete?: DiscordCommand<C>["autocomplete"];
32
44
  }) {
33
45
  this.data = ops.data;
46
+ this.metadata = ops.metadata;
34
47
  this.execute = ops.execute;
35
48
  this.autocomplete = ops.autocomplete;
36
49
  }
@@ -10,32 +10,29 @@ import type { Client, ClientEvents, RestEvents } from "discord.js";
10
10
  * }
11
11
  * }
12
12
  */
13
- // biome-ignore lint/suspicious/noEmptyInterface: Its fine
13
+ // biome-ignore lint/suspicious/noEmptyInterface: It's fine
14
14
  export interface DiscordEventCustomType {}
15
15
 
16
16
  /** Maps an event type discriminant to its corresponding event map. */
17
- type EventMap<T extends "client" | "rest" | "custom"> = T extends "client"
18
- ? ClientEvents
19
- : T extends "rest"
20
- ? RestEvents
21
- : keyof DiscordEventCustomType extends never
22
- ? { "sooo.. there's no custom events..": [] }
23
- : DiscordEventCustomType;
17
+ export type EventMap<T extends "client" | "rest" | "custom"> =
18
+ T extends "client"
19
+ ? ClientEvents
20
+ : T extends "rest"
21
+ ? RestEvents
22
+ : keyof DiscordEventCustomType extends never
23
+ ? { "sooo.. there's no custom events..": [] }
24
+ : DiscordEventCustomType;
24
25
 
25
26
  /** Resolves the argument tuple for a given event type + key pair. */
26
- type EventArgs<
27
+ export type EventArgs<
27
28
  T extends "client" | "rest" | "custom",
28
29
  K extends keyof EventMap<T>,
29
30
  > = Extract<EventMap<T>[K], any[]>;
30
31
 
31
32
  /**
32
33
  * Wraps a discord.js event handler. Supports `"client"`, `"rest"`, and `"custom"` event types.
33
- * @typeParam V - The bot's `Client` type.
34
- * @typeParam T - The event source discriminant.
35
- * @typeParam K - The event name within the chosen source.
36
34
  */
37
35
  export class DiscordEvent<
38
- V extends Client,
39
36
  T extends "client" | "rest" | "custom",
40
37
  K extends keyof EventMap<T> = keyof EventMap<T>,
41
38
  > {
@@ -43,7 +40,7 @@ export class DiscordEvent<
43
40
  public readonly name: K;
44
41
  public readonly once: boolean;
45
42
  public readonly method: (
46
- client: V,
43
+ client: Client<true>,
47
44
  ...args: EventArgs<T, K>
48
45
  ) => void | Promise<void>;
49
46
 
@@ -51,7 +48,7 @@ export class DiscordEvent<
51
48
  type: T;
52
49
  name: K;
53
50
  once: boolean;
54
- method: DiscordEvent<V, T, K>["method"];
51
+ method: DiscordEvent<T, K>["method"];
55
52
  }) {
56
53
  this.type = opts.type;
57
54
  this.name = opts.name;
@@ -8,10 +8,12 @@ import {
8
8
  type ChatInputCommandInteraction,
9
9
  ComponentType,
10
10
  ContainerBuilder,
11
+ EmbedBuilder,
11
12
  FileBuilder,
12
13
  LabelBuilder,
13
14
  MediaGalleryBuilder,
14
15
  type MentionableSelectMenuBuilder,
16
+ type Message,
15
17
  ModalBuilder,
16
18
  type RoleSelectMenuBuilder,
17
19
  SectionBuilder,
@@ -33,8 +35,8 @@ export type MessageActionRow = ActionRowBuilder<
33
35
  | MentionableSelectMenuBuilder
34
36
  >;
35
37
 
36
- const BUTTONS_SYMBOL: unique symbol = Symbol("pagination-buttons");
37
- const DATA_SYMBOL: unique symbol = Symbol("pagination-data");
38
+ export const BUTTONS_SYMBOL: unique symbol = Symbol("pagination-buttons");
39
+ export const DATA_SYMBOL: unique symbol = Symbol("pagination-data");
38
40
 
39
41
  /** Valid component types that can appear in a pagination page layout. */
40
42
  export type PaginationInput =
@@ -58,42 +60,76 @@ type InternalComponent =
58
60
  | { type: "gallery"; component: MediaGalleryBuilder }
59
61
  | { type: "actionrow"; component: MessageActionRow };
60
62
 
61
- export interface PaginationOptions {
63
+ /** Shared options for all pagination modes. */
64
+ export interface PaginationBaseOptions {
62
65
  /** Number of list entries shown per page (default: 5). */
63
66
  entriesPerPage?: number;
64
67
  /** Key-value pairs replaced in rendered content. */
65
68
  replacements?: Record<string, string>;
66
- /** Per-page container styling overrides. */
67
- styling?: Array<{ accent_color?: number; spoiler?: boolean }>;
68
69
  /** Whether the pagination message is ephemeral. */
69
70
  ephemeral?: boolean;
70
71
  }
71
72
 
72
73
  /**
73
- * Discord Components V2 paginator. Renders a `ContainerBuilder`-based layout
74
- * with Prev/Next/page-jump buttons. Collector expires after 60 seconds.
74
+ * Options for **container** mode (Components V2).
75
+ * Uses a `ContainerBuilder`-based layout with the `IsComponentsV2` message flag.
76
+ */
77
+ export interface PaginationContainerOptions extends PaginationBaseOptions {
78
+ /** Selects container mode. */
79
+ type: "container";
80
+ /** Single layout template using sentinels `DiscordPagination.DATA` and `DiscordPagination.BUTTONS`. */
81
+ layout: PaginationInput[];
82
+ /** Container accent color. */
83
+ accentColor?: number;
84
+ /** Whether the container is a spoiler. */
85
+ spoiler?: boolean;
86
+ }
87
+
88
+ /**
89
+ * Options for **embed** mode.
90
+ * Uses a standard `EmbedBuilder` with an `ActionRow` for navigation buttons.
91
+ * The embed's `description` and `footer` are reserved for page data and the page counter.
92
+ */
93
+ export interface PaginationEmbedOptions extends PaginationBaseOptions {
94
+ /** Selects embed mode. */
95
+ type: "embed";
96
+ /** EmbedBuilder template. Description and footer are overwritten per page. */
97
+ embed: EmbedBuilder;
98
+ }
99
+
100
+ /** Discriminated union of all pagination option types. Use the `type` field to select a mode. */
101
+ export type PaginationOptions =
102
+ | PaginationContainerOptions
103
+ | PaginationEmbedOptions;
104
+
105
+ /**
106
+ * Discord paginator supporting both **Components V2** (`ContainerBuilder`) and
107
+ * **Embed** (`EmbedBuilder`) modes.
75
108
  *
76
- * Use `DiscordPagination.BUTTONS` and `DiscordPagination.DATA` as sentinel
77
- * values in the structure array to position the navigation row and list data.
109
+ * @example Container mode
110
+ * ```ts
111
+ * const pagination = new DiscordPagination(entries, {
112
+ * type: "container",
113
+ * layout: [
114
+ * "# Leaderboard",
115
+ * new SeparatorBuilder(),
116
+ * DiscordPagination.DATA,
117
+ * new SeparatorBuilder(),
118
+ * DiscordPagination.BUTTONS,
119
+ * ],
120
+ * entriesPerPage: 5,
121
+ * accentColor: 0x5865f2,
122
+ * });
123
+ * ```
78
124
  *
79
- * @example
80
- * const pagination = new DiscordPagination(
81
- * entries,
82
- * [
83
- * [
84
- * "# Leaderboard",
85
- * new SeparatorBuilder(),
86
- * DiscordPagination.DATA,
87
- * new SeparatorBuilder(),
88
- * DiscordPagination.BUTTONS,
89
- * ],
90
- * ],
91
- * {
92
- * entriesPerPage: 5,
93
- * ephemeral: false,
94
- * styling: [{ accent_color: 0x5865f2 }],
95
- * },
96
- * );
125
+ * @example Embed mode
126
+ * ```ts
127
+ * const pagination = new DiscordPagination(entries, {
128
+ * type: "embed",
129
+ * embed: new EmbedBuilder().setTitle("Leaderboard").setColor(0x5865f2),
130
+ * entriesPerPage: 5,
131
+ * });
132
+ * ```
97
133
  */
98
134
  export class DiscordPagination {
99
135
  /** Sentinel — marks where the pagination buttons should render. */
@@ -102,29 +138,30 @@ export class DiscordPagination {
102
138
  static readonly DATA: typeof DATA_SYMBOL = DATA_SYMBOL;
103
139
 
104
140
  private readonly list: string[];
105
- private readonly structure: Array<Array<InternalComponent>>;
106
141
  private readonly entriesPerPage: number;
107
142
  private readonly replacements?: Record<string, string>;
108
- private readonly styling?: PaginationOptions["styling"];
109
143
  private readonly ephemeral: boolean;
110
144
  private readonly prefix: string;
111
145
  private readonly totalPages: number;
146
+ private readonly mode: "container" | "embed";
112
147
 
148
+ // Container mode
149
+ private readonly layout?: InternalComponent[];
150
+ private readonly accentColor?: number;
151
+ private readonly spoiler?: boolean;
152
+
153
+ // Embed mode
154
+ private readonly embedTemplate?: EmbedBuilder;
155
+
156
+ // Runtime state
113
157
  private currentIndex = 0;
114
158
  private ended = false;
115
- private interaction!: ButtonInteraction | ChatInputCommandInteraction;
116
-
117
- constructor(
118
- list: string[],
119
- structure: Array<Array<PaginationInput>>,
120
- options: PaginationOptions = {},
121
- ) {
122
- const {
123
- entriesPerPage = 5,
124
- replacements,
125
- styling,
126
- ephemeral = false,
127
- } = options;
159
+ private isMessage = false;
160
+ private interaction?: ButtonInteraction | ChatInputCommandInteraction;
161
+ private replyMessage?: Message;
162
+
163
+ constructor(list: string[], options: PaginationOptions) {
164
+ const { entriesPerPage = 5, replacements, ephemeral = false } = options;
128
165
 
129
166
  if (entriesPerPage <= 0)
130
167
  throw new Error("entriesPerPage must be greater than 0");
@@ -132,64 +169,74 @@ export class DiscordPagination {
132
169
  this.list = list;
133
170
  this.entriesPerPage = entriesPerPage;
134
171
  this.replacements = replacements;
135
- this.styling = styling;
136
172
  this.ephemeral = ephemeral;
137
173
  this.prefix = `~PAGINATION_${randomUUIDv7()}_`;
138
174
  this.totalPages = Math.ceil(list.length / entriesPerPage);
139
- this.structure = this.expandStructure(
140
- structure.map((page) => page.map((input) => this.normalize(input))),
141
- );
175
+
176
+ this.mode = options.type;
177
+
178
+ if (options.type === "container") {
179
+ this.layout = options.layout.map((input) => this.normalize(input));
180
+ this.accentColor = options.accentColor;
181
+ this.spoiler = options.spoiler;
182
+ } else {
183
+ this.embedTemplate = options.embed;
184
+ }
142
185
  }
143
186
 
144
187
  /**
145
188
  * Sends the paginated message and starts the button collector.
146
- * @param interaction - The interaction to reply to.
189
+ * @param target - The interaction or message to reply to.
147
190
  */
148
191
  public async send(
149
- interaction: ButtonInteraction | ChatInputCommandInteraction,
192
+ target: ButtonInteraction | ChatInputCommandInteraction | Message,
150
193
  ): Promise<void> {
151
- this.interaction = interaction;
194
+ this.isMessage = !("deferReply" in target);
195
+ const userId = this.isMessage
196
+ ? (target as Message).author.id
197
+ : (target as ButtonInteraction | ChatInputCommandInteraction).user.id;
152
198
 
153
199
  if (!this.list.length) {
154
- await interaction
155
- .reply({
156
- allowedMentions: { parse: [], repliedUser: false },
157
- components: [
158
- new ContainerBuilder().addTextDisplayComponents(
159
- new TextDisplayBuilder().setContent("No data to show"),
160
- ),
161
- ],
162
- flags: this.ephemeral
163
- ? ["Ephemeral", "IsComponentsV2"]
164
- : ["IsComponentsV2"],
165
- })
166
- .catch(() => {});
200
+ await this.sendEmpty(target);
167
201
  return;
168
202
  }
169
203
 
170
- if (!interaction.replied && !interaction.deferred) {
171
- await interaction
172
- .deferReply({
173
- withResponse: true,
174
- flags: this.ephemeral ? ["Ephemeral"] : [],
175
- })
176
- .catch(() => null);
177
- }
204
+ if (this.isMessage) {
205
+ this.replyMessage = await (target as Message).reply(this.buildPayload());
206
+ } else {
207
+ const interaction = target as
208
+ | ButtonInteraction
209
+ | ChatInputCommandInteraction;
210
+ this.interaction = interaction;
211
+
212
+ if (!interaction.replied && !interaction.deferred) {
213
+ const response = await interaction
214
+ .deferReply({
215
+ withResponse: true,
216
+ flags: this.ephemeral ? ["Ephemeral"] : [],
217
+ })
218
+ .catch(() => null);
219
+ this.replyMessage =
220
+ response?.resource?.message ??
221
+ (await interaction.fetchReply().catch(() => undefined));
222
+ } else {
223
+ this.replyMessage = await interaction
224
+ .fetchReply()
225
+ .catch(() => undefined);
226
+ }
178
227
 
179
- const channel = interaction.channel;
180
- if (!channel || !("createMessageComponentCollector" in channel)) {
181
- throw new Error("Invalid channel type");
228
+ await this.render();
182
229
  }
183
230
 
184
- await this.render();
231
+ if (!this.replyMessage) return;
185
232
 
186
- const collector = channel.createMessageComponentCollector({
233
+ const collector = this.replyMessage.createMessageComponentCollector({
187
234
  componentType: ComponentType.Button,
188
- time: 60000,
235
+ time: 60_000,
189
236
  });
190
237
 
191
238
  collector.on("collect", async (btn) => {
192
- if (btn.user.id !== interaction.user.id) {
239
+ if (btn.user.id !== userId) {
193
240
  return void btn.deferUpdate();
194
241
  }
195
242
  if (!btn.customId.startsWith(this.prefix)) return;
@@ -223,6 +270,66 @@ export class DiscordPagination {
223
270
  });
224
271
  }
225
272
 
273
+ private async sendEmpty(
274
+ target: ButtonInteraction | ChatInputCommandInteraction | Message,
275
+ ): Promise<void> {
276
+ if (this.mode === "embed" && !this.embedTemplate)
277
+ throw new Error(
278
+ "[@kyvrixon/utils]: Pagination: embedTemplate is in a corrupted state",
279
+ );
280
+
281
+ const allowedMentions = { parse: [] as const, repliedUser: false };
282
+
283
+ if (this.isMessage) {
284
+ const payload =
285
+ this.mode === "container"
286
+ ? {
287
+ components: [
288
+ new ContainerBuilder().addTextDisplayComponents(
289
+ new TextDisplayBuilder().setContent("No data to show"),
290
+ ),
291
+ ],
292
+ flags: ["IsComponentsV2"] as const,
293
+ allowedMentions,
294
+ }
295
+ : {
296
+ embeds: [
297
+ new EmbedBuilder(this.embedTemplate?.toJSON()).setDescription(
298
+ "No data to show",
299
+ ),
300
+ ],
301
+ allowedMentions,
302
+ };
303
+ await (target as Message).reply(payload).catch(() => {});
304
+ } else {
305
+ const payload =
306
+ this.mode === "container"
307
+ ? {
308
+ components: [
309
+ new ContainerBuilder().addTextDisplayComponents(
310
+ new TextDisplayBuilder().setContent("No data to show"),
311
+ ),
312
+ ],
313
+ flags: this.ephemeral
314
+ ? (["Ephemeral", "IsComponentsV2"] as const)
315
+ : (["IsComponentsV2"] as const),
316
+ allowedMentions,
317
+ }
318
+ : {
319
+ embeds: [
320
+ new EmbedBuilder(this.embedTemplate?.toJSON()).setDescription(
321
+ "No data to show",
322
+ ),
323
+ ],
324
+ flags: this.ephemeral ? (["Ephemeral"] as const) : ([] as const),
325
+ allowedMentions,
326
+ };
327
+ await (target as ButtonInteraction | ChatInputCommandInteraction)
328
+ .reply(payload)
329
+ .catch(() => {});
330
+ }
331
+ }
332
+
226
333
  private normalize(input: PaginationInput): InternalComponent {
227
334
  if (input === BUTTONS_SYMBOL) return { type: "buttons" };
228
335
  if (input === DATA_SYMBOL) return { type: "data" };
@@ -243,27 +350,95 @@ export class DiscordPagination {
243
350
  return { type: "actionrow", component: input };
244
351
  }
245
352
 
246
- private expandStructure(
247
- structure: Array<Array<InternalComponent>>,
248
- ): Array<Array<InternalComponent>> {
249
- const lastPage = structure[structure.length - 1];
250
- if (!lastPage) throw new Error("Structure must have at least one page");
251
-
252
- while (structure.length < this.totalPages) {
253
- structure.push(this.clonePage(lastPage));
353
+ private buildPayload() {
354
+ if (this.mode === "embed") {
355
+ return {
356
+ embeds: [this.generateEmbed()],
357
+ components: [this.getPaginationRow()],
358
+ allowedMentions: { parse: [] as const, repliedUser: false },
359
+ };
254
360
  }
255
361
 
256
- return structure;
362
+ return {
363
+ components: [this.generateContainer()],
364
+ flags: ["IsComponentsV2"] as const,
365
+ allowedMentions: { parse: [] as const, repliedUser: false },
366
+ };
257
367
  }
258
368
 
259
- private clonePage(page: Array<InternalComponent>): Array<InternalComponent> {
260
- return page.map((comp) =>
261
- comp.type === "display"
262
- ? {
263
- type: "display" as const,
264
- component: new TextDisplayBuilder(comp.component.toJSON()),
369
+ private generateContainer(): ContainerBuilder {
370
+ if (!this.layout)
371
+ throw new Error(
372
+ "[@kyvrixon/utils]: Pagination: layout is in a corrupted state",
373
+ );
374
+
375
+ const page = Math.floor(this.currentIndex / this.entriesPerPage);
376
+
377
+ return new ContainerBuilder({
378
+ components: this.layout.map((comp) => {
379
+ switch (comp.type) {
380
+ case "buttons":
381
+ return this.getPaginationRow().toJSON();
382
+
383
+ case "data": {
384
+ const content = this.list
385
+ .slice(
386
+ page * this.entriesPerPage,
387
+ (page + 1) * this.entriesPerPage,
388
+ )
389
+ .join("\n");
390
+ return new TextDisplayBuilder()
391
+ .setContent(this.applyReplacements(content))
392
+ .toJSON();
265
393
  }
266
- : comp,
394
+
395
+ default:
396
+ return comp.component.toJSON();
397
+ }
398
+ }),
399
+ accent_color: this.accentColor,
400
+ spoiler: this.spoiler,
401
+ });
402
+ }
403
+
404
+ private generateEmbed(): EmbedBuilder {
405
+ if (!this.embedTemplate)
406
+ throw new Error(
407
+ "[@kyvrixon/utils]: Pagination: embedTemplate is in a corrupted state",
408
+ );
409
+
410
+ const page = Math.floor(this.currentIndex / this.entriesPerPage);
411
+ const content = this.list
412
+ .slice(page * this.entriesPerPage, (page + 1) * this.entriesPerPage)
413
+ .join("\n");
414
+
415
+ return new EmbedBuilder(this.embedTemplate.toJSON())
416
+ .setDescription(this.applyReplacements(content))
417
+ .setFooter({ text: `Page ${page + 1}/${this.totalPages}` });
418
+ }
419
+
420
+ private getPaginationRow(): ActionRowBuilder<ButtonBuilder> {
421
+ return new ActionRowBuilder<ButtonBuilder>().addComponents(
422
+ new ButtonBuilder()
423
+ .setCustomId(`${this.prefix}back`)
424
+ .setLabel("Prev")
425
+ .setStyle(ButtonStyle.Secondary)
426
+ .setDisabled(this.ended || this.currentIndex === 0),
427
+ new ButtonBuilder()
428
+ .setCustomId(`${this.prefix}info`)
429
+ .setLabel(
430
+ `${Math.floor(this.currentIndex / this.entriesPerPage) + 1}/${this.totalPages}`,
431
+ )
432
+ .setStyle(ButtonStyle.Secondary)
433
+ .setDisabled(this.ended || this.totalPages === 1 || this.isMessage),
434
+ new ButtonBuilder()
435
+ .setCustomId(`${this.prefix}forward`)
436
+ .setLabel("Next")
437
+ .setStyle(ButtonStyle.Secondary)
438
+ .setDisabled(
439
+ this.ended ||
440
+ this.currentIndex + this.entriesPerPage >= this.list.length,
441
+ ),
267
442
  );
268
443
  }
269
444
 
@@ -321,69 +496,6 @@ export class DiscordPagination {
321
496
  this.currentIndex = (pageNumber - 1) * this.entriesPerPage;
322
497
  }
323
498
 
324
- private generateContainer(page: number): ContainerBuilder {
325
- const pageStructure = this.structure[page];
326
- if (!pageStructure) throw new Error(`Page ${page} structure not found`);
327
-
328
- return new ContainerBuilder({
329
- components: pageStructure.map((comp) => {
330
- switch (comp.type) {
331
- case "buttons":
332
- return this.getPaginationRow().toJSON();
333
-
334
- case "data": {
335
- const content = this.list
336
- .slice(
337
- page * this.entriesPerPage,
338
- (page + 1) * this.entriesPerPage,
339
- )
340
- .join("\n");
341
- return new TextDisplayBuilder()
342
- .setContent(this.applyReplacements(content))
343
- .toJSON();
344
- }
345
-
346
- // !! Default clause applies to the below as well
347
- // case "display":
348
- // case "section":
349
- // case "separator":
350
- // case "file":
351
- // case "gallery":
352
- // case "actionrow":
353
- default:
354
- return comp.component.toJSON();
355
- }
356
- }),
357
- accent_color: this.styling?.[page]?.accent_color,
358
- spoiler: this.styling?.[page]?.spoiler,
359
- });
360
- }
361
-
362
- private getPaginationRow(): ActionRowBuilder<ButtonBuilder> {
363
- return new ActionRowBuilder<ButtonBuilder>().addComponents(
364
- new ButtonBuilder()
365
- .setCustomId(`${this.prefix}back`)
366
- .setLabel("Prev")
367
- .setStyle(ButtonStyle.Secondary)
368
- .setDisabled(this.ended || this.currentIndex === 0),
369
- new ButtonBuilder()
370
- .setCustomId(`${this.prefix}info`)
371
- .setLabel(
372
- `${Math.floor(this.currentIndex / this.entriesPerPage) + 1}/${this.totalPages}`,
373
- )
374
- .setStyle(ButtonStyle.Secondary)
375
- .setDisabled(this.ended || this.totalPages === 1),
376
- new ButtonBuilder()
377
- .setCustomId(`${this.prefix}forward`)
378
- .setLabel("Next")
379
- .setStyle(ButtonStyle.Secondary)
380
- .setDisabled(
381
- this.ended ||
382
- this.currentIndex + this.entriesPerPage >= this.list.length,
383
- ),
384
- );
385
- }
386
-
387
499
  private applyReplacements(content: string): string {
388
500
  if (!this.replacements) return content;
389
501
  return Object.entries(this.replacements).reduce(
@@ -394,15 +506,13 @@ export class DiscordPagination {
394
506
 
395
507
  private async render(): Promise<void> {
396
508
  try {
397
- await this.interaction.editReply({
398
- components: [
399
- this.generateContainer(
400
- Math.floor(this.currentIndex / this.entriesPerPage),
401
- ),
402
- ],
403
- flags: ["IsComponentsV2"],
404
- allowedMentions: { parse: [], repliedUser: false },
405
- });
509
+ const payload = this.buildPayload();
510
+
511
+ if (this.isMessage && this.replyMessage) {
512
+ await this.replyMessage.edit(payload);
513
+ } else if (this.interaction) {
514
+ await this.interaction.editReply(payload);
515
+ }
406
516
  } catch (error) {
407
517
  const e = error as Error;
408
518
  if (!e.message.includes("Unknown Message")) {
@@ -0,0 +1,3 @@
1
+ export * from "./Command";
2
+ export * from "./createPagination";
3
+ export * from "./Event";
@@ -0,0 +1 @@
1
+ export * from "./LoggerModule";