chrxmaticc-framework 1.2.1 → 1.3.0
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/core/ChrxComponentsV3.js +652 -0
- package/core/ChrxEmbedBuilder.js +127 -0
- package/core/ChrxModalBuilder.js +151 -0
- package/index.js +35 -9
- package/package.json +1 -1
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ChrxComponentsV3.js
|
|
3
|
+
* Components V3 — every Discord component simplified into clean Chrx builders.
|
|
4
|
+
*
|
|
5
|
+
* Includes:
|
|
6
|
+
* - ChrxButton
|
|
7
|
+
* - ChrxSelectMenu
|
|
8
|
+
* - ChrxPagination
|
|
9
|
+
* - ChrxContextMenu
|
|
10
|
+
* - ChrxMenu
|
|
11
|
+
* - ChrxForm
|
|
12
|
+
* - ChrxConfirm
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
ButtonBuilder,
|
|
17
|
+
ButtonStyle,
|
|
18
|
+
ActionRowBuilder,
|
|
19
|
+
StringSelectMenuBuilder,
|
|
20
|
+
StringSelectMenuOptionBuilder,
|
|
21
|
+
ContextMenuCommandBuilder,
|
|
22
|
+
ApplicationCommandType,
|
|
23
|
+
EmbedBuilder,
|
|
24
|
+
ComponentType,
|
|
25
|
+
} = require("discord.js");
|
|
26
|
+
|
|
27
|
+
// ── Style map ─────────────────────────────────────────────────────────────
|
|
28
|
+
const BUTTON_STYLE_MAP = {
|
|
29
|
+
primary: ButtonStyle.Primary,
|
|
30
|
+
secondary: ButtonStyle.Secondary,
|
|
31
|
+
success: ButtonStyle.Success,
|
|
32
|
+
danger: ButtonStyle.Danger,
|
|
33
|
+
link: ButtonStyle.Link,
|
|
34
|
+
blurple: ButtonStyle.Primary,
|
|
35
|
+
grey: ButtonStyle.Secondary,
|
|
36
|
+
gray: ButtonStyle.Secondary,
|
|
37
|
+
green: ButtonStyle.Success,
|
|
38
|
+
red: ButtonStyle.Danger,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// ChrxButton
|
|
43
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
class ChrxButton {
|
|
46
|
+
/**
|
|
47
|
+
* @param {object} options
|
|
48
|
+
* @param {string} options.label Button label
|
|
49
|
+
* @param {string} [options.style] "primary"|"secondary"|"success"|"danger"|"link" (default: primary)
|
|
50
|
+
* @param {string} [options.id] Custom ID (not needed for link buttons)
|
|
51
|
+
* @param {string} [options.url] URL for link buttons
|
|
52
|
+
* @param {string} [options.emoji] Emoji for button
|
|
53
|
+
* @param {boolean} [options.disabled] Disable the button
|
|
54
|
+
* @param {Function} [options.onClick] Handler (interaction) => void
|
|
55
|
+
*/
|
|
56
|
+
constructor(options = {}) {
|
|
57
|
+
if (!options.label) throw new Error("[ChrxButton] label is required.");
|
|
58
|
+
|
|
59
|
+
this._options = options;
|
|
60
|
+
this._handler = options.onClick ?? null;
|
|
61
|
+
|
|
62
|
+
const btn = new ButtonBuilder().setLabel(options.label);
|
|
63
|
+
|
|
64
|
+
if (options.style === "link" || options.url) {
|
|
65
|
+
btn.setStyle(ButtonStyle.Link).setURL(options.url);
|
|
66
|
+
} else {
|
|
67
|
+
btn.setStyle(BUTTON_STYLE_MAP[options.style ?? "primary"] ?? ButtonStyle.Primary);
|
|
68
|
+
btn.setCustomId(options.id ?? `chrx-btn-${Math.random().toString(36).slice(2, 9)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (options.emoji) btn.setEmoji(options.emoji);
|
|
72
|
+
if (options.disabled) btn.setDisabled(true);
|
|
73
|
+
|
|
74
|
+
this._button = btn;
|
|
75
|
+
this.customId = options.id;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build into an ActionRow ready to send.
|
|
80
|
+
*/
|
|
81
|
+
toRow() {
|
|
82
|
+
return new ActionRowBuilder().addComponents(this._button);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Send the button with a message and listen for clicks.
|
|
87
|
+
* Auto cleans up the collector after timeout.
|
|
88
|
+
*
|
|
89
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
90
|
+
* @param {object} [options]
|
|
91
|
+
* @param {string} [options.content] Message content
|
|
92
|
+
* @param {object} [options.embed] EmbedBuilder to attach
|
|
93
|
+
* @param {boolean} [options.ephemeral]
|
|
94
|
+
* @param {number} [options.timeout] Collector timeout in ms (default: 60000)
|
|
95
|
+
*/
|
|
96
|
+
async send(interaction, options = {}) {
|
|
97
|
+
const payload = {
|
|
98
|
+
components: [this.toRow()],
|
|
99
|
+
ephemeral: options.ephemeral ?? false,
|
|
100
|
+
};
|
|
101
|
+
if (options.content) payload.content = options.content;
|
|
102
|
+
if (options.embed) payload.embeds = [options.embed];
|
|
103
|
+
|
|
104
|
+
const reply = await interaction.reply({ ...payload, fetchReply: true });
|
|
105
|
+
|
|
106
|
+
if (!this._handler) return;
|
|
107
|
+
|
|
108
|
+
const collector = reply.createMessageComponentCollector({
|
|
109
|
+
componentType: ComponentType.Button,
|
|
110
|
+
time: options.timeout ?? 60000,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
collector.on("collect", async (i) => {
|
|
114
|
+
if (i.customId !== (this._options.id ?? this._button.data.custom_id)) return;
|
|
115
|
+
try {
|
|
116
|
+
await this._handler(i);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("[ChrxButton] onClick error:", err);
|
|
119
|
+
i.reply({ content: err.message || "❌ Something went wrong.", ephemeral: true }).catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
collector.on("end", () => {
|
|
124
|
+
reply.edit({ components: [] }).catch(() => {});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
130
|
+
// ChrxSelectMenu
|
|
131
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
132
|
+
|
|
133
|
+
class ChrxSelectMenu {
|
|
134
|
+
/**
|
|
135
|
+
* @param {object} options
|
|
136
|
+
* @param {string} options.id Custom ID
|
|
137
|
+
* @param {string} options.placeholder Placeholder text
|
|
138
|
+
* @param {object[]} options.options Menu options
|
|
139
|
+
* @param {string} options.options[].label
|
|
140
|
+
* @param {string} options.options[].value
|
|
141
|
+
* @param {string} [options.options[].description]
|
|
142
|
+
* @param {string} [options.options[].emoji]
|
|
143
|
+
* @param {boolean} [options.options[].default]
|
|
144
|
+
* @param {number} [options.min] Min selections (default: 1)
|
|
145
|
+
* @param {number} [options.max] Max selections (default: 1)
|
|
146
|
+
* @param {Function} [options.onSelect] Handler (interaction, values) => void
|
|
147
|
+
*/
|
|
148
|
+
constructor(options = {}) {
|
|
149
|
+
if (!options.id) throw new Error("[ChrxSelectMenu] id is required.");
|
|
150
|
+
if (!options.placeholder) throw new Error("[ChrxSelectMenu] placeholder is required.");
|
|
151
|
+
if (!options.options?.length) throw new Error("[ChrxSelectMenu] options are required.");
|
|
152
|
+
|
|
153
|
+
this._options = options;
|
|
154
|
+
this._handler = options.onSelect ?? null;
|
|
155
|
+
|
|
156
|
+
const menu = new StringSelectMenuBuilder()
|
|
157
|
+
.setCustomId(options.id)
|
|
158
|
+
.setPlaceholder(options.placeholder)
|
|
159
|
+
.setMinValues(options.min ?? 1)
|
|
160
|
+
.setMaxValues(options.max ?? 1)
|
|
161
|
+
.addOptions(
|
|
162
|
+
options.options.map((opt) => {
|
|
163
|
+
const o = new StringSelectMenuOptionBuilder()
|
|
164
|
+
.setLabel(opt.label)
|
|
165
|
+
.setValue(opt.value);
|
|
166
|
+
if (opt.description) o.setDescription(opt.description);
|
|
167
|
+
if (opt.emoji) o.setEmoji(opt.emoji);
|
|
168
|
+
if (opt.default) o.setDefault(true);
|
|
169
|
+
return o;
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
this._menu = menu;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
toRow() {
|
|
177
|
+
return new ActionRowBuilder().addComponents(this._menu);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Send the select menu and listen for selections.
|
|
182
|
+
*/
|
|
183
|
+
async send(interaction, options = {}) {
|
|
184
|
+
const payload = {
|
|
185
|
+
components: [this.toRow()],
|
|
186
|
+
ephemeral: options.ephemeral ?? false,
|
|
187
|
+
};
|
|
188
|
+
if (options.content) payload.content = options.content;
|
|
189
|
+
if (options.embed) payload.embeds = [options.embed];
|
|
190
|
+
|
|
191
|
+
const reply = await interaction.reply({ ...payload, fetchReply: true });
|
|
192
|
+
|
|
193
|
+
if (!this._handler) return;
|
|
194
|
+
|
|
195
|
+
const collector = reply.createMessageComponentCollector({
|
|
196
|
+
componentType: ComponentType.StringSelect,
|
|
197
|
+
time: options.timeout ?? 60000,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
collector.on("collect", async (i) => {
|
|
201
|
+
if (i.customId !== this._options.id) return;
|
|
202
|
+
try {
|
|
203
|
+
await this._handler(i, i.values);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error("[ChrxSelectMenu] onSelect error:", err);
|
|
206
|
+
i.reply({ content: err.message || "❌ Something went wrong.", ephemeral: true }).catch(() => {});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
collector.on("end", () => {
|
|
211
|
+
reply.edit({ components: [] }).catch(() => {});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
217
|
+
// ChrxPagination
|
|
218
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
219
|
+
|
|
220
|
+
class ChrxPagination {
|
|
221
|
+
/**
|
|
222
|
+
* Auto paginated embed system with next/prev buttons.
|
|
223
|
+
*
|
|
224
|
+
* @param {object} options
|
|
225
|
+
* @param {EmbedBuilder[]} options.pages Array of EmbedBuilders
|
|
226
|
+
* @param {number} [options.timeout] Collector timeout ms (default: 120000)
|
|
227
|
+
* @param {boolean} [options.ephemeral]
|
|
228
|
+
* @param {boolean} [options.showPageNumber] Show "Page X/Y" in footer (default: true)
|
|
229
|
+
*/
|
|
230
|
+
constructor(options = {}) {
|
|
231
|
+
if (!options.pages?.length) throw new Error("[ChrxPagination] pages array is required.");
|
|
232
|
+
this._options = options;
|
|
233
|
+
this._page = 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_getButtons(page, total) {
|
|
237
|
+
const prev = new ButtonBuilder()
|
|
238
|
+
.setCustomId("chrx-page-prev")
|
|
239
|
+
.setLabel("◀")
|
|
240
|
+
.setStyle(ButtonStyle.Secondary)
|
|
241
|
+
.setDisabled(page === 0);
|
|
242
|
+
|
|
243
|
+
const next = new ButtonBuilder()
|
|
244
|
+
.setCustomId("chrx-page-next")
|
|
245
|
+
.setLabel("▶")
|
|
246
|
+
.setStyle(ButtonStyle.Secondary)
|
|
247
|
+
.setDisabled(page === total - 1);
|
|
248
|
+
|
|
249
|
+
const counter = new ButtonBuilder()
|
|
250
|
+
.setCustomId("chrx-page-counter")
|
|
251
|
+
.setLabel(`${page + 1} / ${total}`)
|
|
252
|
+
.setStyle(ButtonStyle.Primary)
|
|
253
|
+
.setDisabled(true);
|
|
254
|
+
|
|
255
|
+
return new ActionRowBuilder().addComponents(prev, counter, next);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_getPage(index) {
|
|
259
|
+
const page = this._options.pages[index];
|
|
260
|
+
const total = this._options.pages.length;
|
|
261
|
+
if (this._options.showPageNumber !== false) {
|
|
262
|
+
page.setFooter({ text: `Page ${index + 1} of ${total}` });
|
|
263
|
+
}
|
|
264
|
+
return page;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Send the paginated embed.
|
|
269
|
+
*/
|
|
270
|
+
async send(interaction) {
|
|
271
|
+
const total = this._options.pages.length;
|
|
272
|
+
const reply = await interaction.reply({
|
|
273
|
+
embeds: [this._getPage(0)],
|
|
274
|
+
components: total > 1 ? [this._getButtons(0, total)] : [],
|
|
275
|
+
ephemeral: this._options.ephemeral ?? false,
|
|
276
|
+
fetchReply: true,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (total <= 1) return;
|
|
280
|
+
|
|
281
|
+
const collector = reply.createMessageComponentCollector({
|
|
282
|
+
componentType: ComponentType.Button,
|
|
283
|
+
time: this._options.timeout ?? 120000,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
collector.on("collect", async (i) => {
|
|
287
|
+
if (i.user.id !== interaction.user.id) {
|
|
288
|
+
return i.reply({ content: "❌ These controls aren't for you.", ephemeral: true });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (i.customId === "chrx-page-prev") this._page = Math.max(0, this._page - 1);
|
|
292
|
+
if (i.customId === "chrx-page-next") this._page = Math.min(total - 1, this._page + 1);
|
|
293
|
+
|
|
294
|
+
await i.update({
|
|
295
|
+
embeds: [this._getPage(this._page)],
|
|
296
|
+
components: [this._getButtons(this._page, total)],
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
collector.on("end", () => {
|
|
301
|
+
reply.edit({ components: [] }).catch(() => {});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
307
|
+
// ChrxContextMenu
|
|
308
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
309
|
+
|
|
310
|
+
class ChrxContextMenu {
|
|
311
|
+
/**
|
|
312
|
+
* Right click context menu command.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} options
|
|
315
|
+
* @param {string} options.name Context menu name
|
|
316
|
+
* @param {string} options.type "user" | "message"
|
|
317
|
+
* @param {Function} options.run Handler (interaction) => void
|
|
318
|
+
*/
|
|
319
|
+
constructor(options = {}) {
|
|
320
|
+
if (!options.name) throw new Error("[ChrxContextMenu] name is required.");
|
|
321
|
+
if (!options.type) throw new Error("[ChrxContextMenu] type is required. Use 'user' or 'message'.");
|
|
322
|
+
if (!options.run) throw new Error("[ChrxContextMenu] run function is required.");
|
|
323
|
+
|
|
324
|
+
const builder = new ContextMenuCommandBuilder()
|
|
325
|
+
.setName(options.name)
|
|
326
|
+
.setType(
|
|
327
|
+
options.type === "user"
|
|
328
|
+
? ApplicationCommandType.User
|
|
329
|
+
: ApplicationCommandType.Message
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
this.data = builder;
|
|
333
|
+
this.execute = async (interaction) => {
|
|
334
|
+
try {
|
|
335
|
+
await options.run(interaction);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(`[ChrxContextMenu] Error in "${options.name}":`, err);
|
|
338
|
+
const msg = { content: err.message || "❌ Something went wrong.", ephemeral: true };
|
|
339
|
+
if (interaction.replied || interaction.deferred) {
|
|
340
|
+
interaction.followUp(msg).catch(() => {});
|
|
341
|
+
} else {
|
|
342
|
+
interaction.reply(msg).catch(() => {});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
350
|
+
// ChrxMenu
|
|
351
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
352
|
+
|
|
353
|
+
class ChrxMenu {
|
|
354
|
+
/**
|
|
355
|
+
* Full interactive menu — combines embed, buttons and select menus together.
|
|
356
|
+
*
|
|
357
|
+
* @param {object} options
|
|
358
|
+
* @param {object} options.embed Embed options for ChrxEmbedBuilder.build()
|
|
359
|
+
* @param {object[]} [options.buttons] Array of ChrxButton-like option objects
|
|
360
|
+
* @param {object} [options.select] ChrxSelectMenu-like option object
|
|
361
|
+
* @param {number} [options.timeout] Collector timeout ms (default: 60000)
|
|
362
|
+
* @param {boolean} [options.ephemeral]
|
|
363
|
+
*/
|
|
364
|
+
constructor(options = {}) {
|
|
365
|
+
if (!options.embed) throw new Error("[ChrxMenu] embed is required.");
|
|
366
|
+
this._options = options;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async send(interaction) {
|
|
370
|
+
const { EmbedBuilder } = require("discord.js");
|
|
371
|
+
const embed = new EmbedBuilder();
|
|
372
|
+
|
|
373
|
+
if (this._options.embed.title) embed.setTitle(this._options.embed.title);
|
|
374
|
+
if (this._options.embed.desc) embed.setDescription(this._options.embed.desc);
|
|
375
|
+
if (this._options.embed.color) embed.setColor(this._options.embed.color);
|
|
376
|
+
if (this._options.embed.footer) embed.setFooter({ text: this._options.embed.footer });
|
|
377
|
+
if (this._options.embed.thumbnail) embed.setThumbnail(this._options.embed.thumbnail);
|
|
378
|
+
if (this._options.embed.timestamp) embed.setTimestamp();
|
|
379
|
+
|
|
380
|
+
const rows = [];
|
|
381
|
+
|
|
382
|
+
// ── Buttons ───────────────────────────────────────────────────────────
|
|
383
|
+
if (this._options.buttons?.length) {
|
|
384
|
+
const btnRow = new ActionRowBuilder();
|
|
385
|
+
const handlers = {};
|
|
386
|
+
|
|
387
|
+
for (const btnOpt of this._options.buttons) {
|
|
388
|
+
const btn = new ButtonBuilder()
|
|
389
|
+
.setLabel(btnOpt.label)
|
|
390
|
+
.setStyle(BUTTON_STYLE_MAP[btnOpt.style ?? "primary"] ?? ButtonStyle.Primary)
|
|
391
|
+
.setCustomId(btnOpt.id ?? `chrx-menu-btn-${Math.random().toString(36).slice(2, 7)}`);
|
|
392
|
+
|
|
393
|
+
if (btnOpt.emoji) btn.setEmoji(btnOpt.emoji);
|
|
394
|
+
if (btnOpt.disabled) btn.setDisabled(true);
|
|
395
|
+
|
|
396
|
+
handlers[btnOpt.id] = btnOpt.onClick;
|
|
397
|
+
btnRow.addComponents(btn);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
rows.push(btnRow);
|
|
401
|
+
|
|
402
|
+
const reply = await interaction.reply({
|
|
403
|
+
embeds: [embed],
|
|
404
|
+
components: rows,
|
|
405
|
+
ephemeral: this._options.ephemeral ?? false,
|
|
406
|
+
fetchReply: true,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const collector = reply.createMessageComponentCollector({
|
|
410
|
+
time: this._options.timeout ?? 60000,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
collector.on("collect", async (i) => {
|
|
414
|
+
const handler = handlers[i.customId];
|
|
415
|
+
if (handler) {
|
|
416
|
+
try {
|
|
417
|
+
await handler(i);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
i.reply({ content: err.message || "❌ Error.", ephemeral: true }).catch(() => {});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
collector.on("end", () => {
|
|
425
|
+
reply.edit({ components: [] }).catch(() => {});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Select only ───────────────────────────────────────────────────────
|
|
432
|
+
if (this._options.select) {
|
|
433
|
+
const menu = new ChrxSelectMenu(this._options.select);
|
|
434
|
+
rows.push(menu.toRow());
|
|
435
|
+
await interaction.reply({
|
|
436
|
+
embeds: [embed],
|
|
437
|
+
components: rows,
|
|
438
|
+
ephemeral: this._options.ephemeral ?? false,
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Embed only ────────────────────────────────────────────────────────
|
|
444
|
+
await interaction.reply({
|
|
445
|
+
embeds: [embed],
|
|
446
|
+
ephemeral: this._options.ephemeral ?? false,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
452
|
+
// ChrxForm
|
|
453
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
454
|
+
|
|
455
|
+
class ChrxForm {
|
|
456
|
+
/**
|
|
457
|
+
* Combines a modal AND processes the response in one block.
|
|
458
|
+
* No need to set up onModalSubmit separately.
|
|
459
|
+
*
|
|
460
|
+
* @param {object} options
|
|
461
|
+
* @param {string} options.id Modal ID
|
|
462
|
+
* @param {string} options.title Modal title
|
|
463
|
+
* @param {object[]} options.inputs Same as ChrxModalBuilder inputs
|
|
464
|
+
* @param {Function} options.onSubmit (interaction, values) => void
|
|
465
|
+
* values is an object keyed by input id e.g. { answer: "yes" }
|
|
466
|
+
*/
|
|
467
|
+
constructor(options = {}) {
|
|
468
|
+
if (!options.id) throw new Error("[ChrxForm] id is required.");
|
|
469
|
+
if (!options.title) throw new Error("[ChrxForm] title is required.");
|
|
470
|
+
if (!options.inputs?.length) throw new Error("[ChrxForm] inputs are required.");
|
|
471
|
+
if (!options.onSubmit) throw new Error("[ChrxForm] onSubmit is required.");
|
|
472
|
+
|
|
473
|
+
this._options = options;
|
|
474
|
+
this._modal = null;
|
|
475
|
+
|
|
476
|
+
// Build the modal
|
|
477
|
+
const {
|
|
478
|
+
ModalBuilder,
|
|
479
|
+
TextInputBuilder,
|
|
480
|
+
TextInputStyle,
|
|
481
|
+
ActionRowBuilder,
|
|
482
|
+
} = require("discord.js");
|
|
483
|
+
|
|
484
|
+
const STYLE_MAP = {
|
|
485
|
+
short: TextInputStyle.Short,
|
|
486
|
+
paragraph: TextInputStyle.Paragraph,
|
|
487
|
+
long: TextInputStyle.Paragraph,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const modal = new ModalBuilder()
|
|
491
|
+
.setCustomId(options.id)
|
|
492
|
+
.setTitle(options.title);
|
|
493
|
+
|
|
494
|
+
const rows = options.inputs.map((input) => {
|
|
495
|
+
const textInput = new TextInputBuilder()
|
|
496
|
+
.setCustomId(input.id)
|
|
497
|
+
.setLabel(input.label)
|
|
498
|
+
.setStyle(STYLE_MAP[input.type ?? "short"] ?? TextInputStyle.Short);
|
|
499
|
+
|
|
500
|
+
if (input.required) textInput.setRequired(input.required);
|
|
501
|
+
if (input.placeholder) textInput.setPlaceholder(input.placeholder);
|
|
502
|
+
if (input.value) textInput.setValue(input.value);
|
|
503
|
+
if (input.minLength) textInput.setMinLength(input.minLength);
|
|
504
|
+
if (input.maxLength) textInput.setMaxLength(input.maxLength);
|
|
505
|
+
|
|
506
|
+
return new ActionRowBuilder().addComponents(textInput);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
modal.addComponents(...rows);
|
|
510
|
+
this._modal = modal;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Show the form and handle submission automatically.
|
|
515
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
516
|
+
*/
|
|
517
|
+
async show(interaction) {
|
|
518
|
+
await interaction.showModal(this._modal);
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const submitted = await interaction.awaitModalSubmit({
|
|
522
|
+
time: 300000, // 5 minutes to fill out the form
|
|
523
|
+
filter: (i) => i.customId === this._options.id && i.user.id === interaction.user.id,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Build values object keyed by input id
|
|
527
|
+
const values = {};
|
|
528
|
+
for (const input of this._options.inputs) {
|
|
529
|
+
values[input.id] = submitted.fields.getTextInputValue(input.id);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
await this._options.onSubmit(submitted, values);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.error(`[ChrxForm] onSubmit error:`, err);
|
|
536
|
+
const msg = { content: err.message || "❌ Something went wrong.", ephemeral: true };
|
|
537
|
+
if (submitted.replied || submitted.deferred) {
|
|
538
|
+
submitted.followUp(msg).catch(() => {});
|
|
539
|
+
} else {
|
|
540
|
+
submitted.reply(msg).catch(() => {});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
// User didn't submit in time — silently ignore
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
550
|
+
// ChrxConfirm
|
|
551
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
552
|
+
|
|
553
|
+
class ChrxConfirm {
|
|
554
|
+
/**
|
|
555
|
+
* Yes/No confirmation prompt in two lines.
|
|
556
|
+
*
|
|
557
|
+
* @param {object} options
|
|
558
|
+
* @param {string} options.question Question to ask
|
|
559
|
+
* @param {Function} options.onConfirm Called when user clicks Yes
|
|
560
|
+
* @param {Function} [options.onCancel] Called when user clicks No
|
|
561
|
+
* @param {string} [options.confirmLabel] Yes button label (default: "Confirm")
|
|
562
|
+
* @param {string} [options.cancelLabel] No button label (default: "Cancel")
|
|
563
|
+
* @param {string} [options.color] Embed color (default: #5865F2)
|
|
564
|
+
* @param {boolean} [options.ephemeral]
|
|
565
|
+
* @param {number} [options.timeout] ms before auto cancel (default: 30000)
|
|
566
|
+
*/
|
|
567
|
+
constructor(options = {}) {
|
|
568
|
+
if (!options.question) throw new Error("[ChrxConfirm] question is required.");
|
|
569
|
+
if (!options.onConfirm) throw new Error("[ChrxConfirm] onConfirm is required.");
|
|
570
|
+
this._options = options;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Send the confirmation prompt.
|
|
575
|
+
*/
|
|
576
|
+
async send(interaction) {
|
|
577
|
+
const confirmId = `chrx-confirm-yes-${Math.random().toString(36).slice(2, 7)}`;
|
|
578
|
+
const cancelId = `chrx-confirm-no-${Math.random().toString(36).slice(2, 7)}`;
|
|
579
|
+
|
|
580
|
+
const embed = new EmbedBuilder()
|
|
581
|
+
.setColor(this._options.color ?? "#5865F2")
|
|
582
|
+
.setDescription(`❓ ${this._options.question}`);
|
|
583
|
+
|
|
584
|
+
const row = new ActionRowBuilder().addComponents(
|
|
585
|
+
new ButtonBuilder()
|
|
586
|
+
.setCustomId(confirmId)
|
|
587
|
+
.setLabel(this._options.confirmLabel ?? "Confirm")
|
|
588
|
+
.setStyle(ButtonStyle.Success),
|
|
589
|
+
new ButtonBuilder()
|
|
590
|
+
.setCustomId(cancelId)
|
|
591
|
+
.setLabel(this._options.cancelLabel ?? "Cancel")
|
|
592
|
+
.setStyle(ButtonStyle.Danger)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const reply = await interaction.reply({
|
|
596
|
+
embeds: [embed],
|
|
597
|
+
components: [row],
|
|
598
|
+
ephemeral: this._options.ephemeral ?? false,
|
|
599
|
+
fetchReply: true,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const collector = reply.createMessageComponentCollector({
|
|
603
|
+
componentType: ComponentType.Button,
|
|
604
|
+
time: this._options.timeout ?? 30000,
|
|
605
|
+
max: 1,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
collector.on("collect", async (i) => {
|
|
609
|
+
if (i.user.id !== interaction.user.id) {
|
|
610
|
+
return i.reply({ content: "❌ This isn't your confirmation.", ephemeral: true });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
if (i.customId === confirmId) {
|
|
615
|
+
await this._options.onConfirm(i);
|
|
616
|
+
} else {
|
|
617
|
+
if (this._options.onCancel) {
|
|
618
|
+
await this._options.onCancel(i);
|
|
619
|
+
} else {
|
|
620
|
+
await i.update({ embeds: [embed.setDescription("❌ Cancelled.").setColor("#ED4245")], components: [] });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} catch (err) {
|
|
624
|
+
console.error("[ChrxConfirm] Error:", err);
|
|
625
|
+
i.reply({ content: err.message || "❌ Something went wrong.", ephemeral: true }).catch(() => {});
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
collector.on("end", (collected) => {
|
|
630
|
+
if (collected.size === 0) {
|
|
631
|
+
reply.edit({
|
|
632
|
+
embeds: [embed.setDescription("⏱ Confirmation timed out.").setColor("#ED4245")],
|
|
633
|
+
components: [],
|
|
634
|
+
}).catch(() => {});
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
641
|
+
// Exports
|
|
642
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
643
|
+
|
|
644
|
+
module.exports = {
|
|
645
|
+
ChrxButton,
|
|
646
|
+
ChrxSelectMenu,
|
|
647
|
+
ChrxPagination,
|
|
648
|
+
ChrxContextMenu,
|
|
649
|
+
ChrxMenu,
|
|
650
|
+
ChrxForm,
|
|
651
|
+
ChrxConfirm,
|
|
652
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ChrxEmbedBuilder.js
|
|
3
|
+
* Simple and Advanced embed builder.
|
|
4
|
+
*
|
|
5
|
+
* Simple mode → key/value syntax (title, desc, color)
|
|
6
|
+
* AdvancedEmbed → parses raw JSON or HTML-like string into a full embed
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { EmbedBuilder } = require("discord.js");
|
|
10
|
+
|
|
11
|
+
class ChrxEmbedBuilder {
|
|
12
|
+
/**
|
|
13
|
+
* Simple mode — just pass title, desc, color etc.
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {string} [options.title]
|
|
16
|
+
* @param {string} [options.desc]
|
|
17
|
+
* @param {string} [options.color]
|
|
18
|
+
* @param {string} [options.thumbnail]
|
|
19
|
+
* @param {string} [options.image]
|
|
20
|
+
* @param {string} [options.footer]
|
|
21
|
+
* @param {boolean} [options.timestamp]
|
|
22
|
+
* @param {object[]} [options.fields] [{ name, value, inline? }]
|
|
23
|
+
*/
|
|
24
|
+
static build(options = {}) {
|
|
25
|
+
const embed = new EmbedBuilder();
|
|
26
|
+
|
|
27
|
+
if (options.title) embed.setTitle(options.title);
|
|
28
|
+
if (options.desc) embed.setDescription(options.desc);
|
|
29
|
+
if (options.color) embed.setColor(options.color);
|
|
30
|
+
if (options.thumbnail) embed.setThumbnail(options.thumbnail);
|
|
31
|
+
if (options.image) embed.setImage(options.image);
|
|
32
|
+
if (options.footer) embed.setFooter({ text: options.footer });
|
|
33
|
+
if (options.timestamp) embed.setTimestamp();
|
|
34
|
+
if (options.fields) embed.addFields(options.fields);
|
|
35
|
+
|
|
36
|
+
return embed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* AdvancedEmbed — reads raw JSON string or HTML-like string and parses it.
|
|
41
|
+
* Supports both:
|
|
42
|
+
* - JSON: { "title": "Hi", "desc": "ok bro", "color": "#5865F2", ... }
|
|
43
|
+
* - HTML-like: <title>Hi</title><desc>ok bro</desc><color>#5865F2</color>
|
|
44
|
+
*
|
|
45
|
+
* @param {string|object} input Raw JSON string, HTML-like string, or plain object
|
|
46
|
+
* @returns {EmbedBuilder}
|
|
47
|
+
*/
|
|
48
|
+
static advanced(input) {
|
|
49
|
+
let data = {};
|
|
50
|
+
|
|
51
|
+
if (typeof input === "object") {
|
|
52
|
+
// Already a plain object
|
|
53
|
+
data = input;
|
|
54
|
+
|
|
55
|
+
} else if (typeof input === "string") {
|
|
56
|
+
// Try JSON first
|
|
57
|
+
try {
|
|
58
|
+
data = JSON.parse(input);
|
|
59
|
+
} catch {
|
|
60
|
+
// Fall back to HTML-like tag parser
|
|
61
|
+
data = ChrxEmbedBuilder._parseHTML(input);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return ChrxEmbedBuilder.build(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse HTML-like tag syntax into a plain object.
|
|
70
|
+
* Supports: <title> <desc> <color> <thumbnail> <image> <footer> <timestamp> <field>
|
|
71
|
+
*
|
|
72
|
+
* Example input:
|
|
73
|
+
* <title>Hello!</title>
|
|
74
|
+
* <desc>This is my embed</desc>
|
|
75
|
+
* <color>#5865F2</color>
|
|
76
|
+
* <field name="Field 1" value="Value 1" inline="true"/>
|
|
77
|
+
*/
|
|
78
|
+
static _parseHTML(html) {
|
|
79
|
+
const data = {};
|
|
80
|
+
|
|
81
|
+
const extract = (tag) => {
|
|
82
|
+
const match = html.match(new RegExp(`<${tag}[^>]*>(.*?)<\\/${tag}>`, "si"));
|
|
83
|
+
return match ? match[1].trim() : null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
data.title = extract("title") || undefined;
|
|
87
|
+
data.desc = extract("desc") || extract("description") || undefined;
|
|
88
|
+
data.color = extract("color") || undefined;
|
|
89
|
+
data.thumbnail = extract("thumbnail") || undefined;
|
|
90
|
+
data.image = extract("image") || undefined;
|
|
91
|
+
data.footer = extract("footer") || undefined;
|
|
92
|
+
data.timestamp = html.includes("<timestamp") ? true : undefined;
|
|
93
|
+
|
|
94
|
+
// Parse <field name="x" value="y" inline="true"/> tags
|
|
95
|
+
const fieldRegex = /<field\s+name="([^"]+)"\s+value="([^"]+)"(?:\s+inline="(true|false)")?[^/]*\/>/gi;
|
|
96
|
+
const fields = [];
|
|
97
|
+
let fieldMatch;
|
|
98
|
+
while ((fieldMatch = fieldRegex.exec(html)) !== null) {
|
|
99
|
+
fields.push({
|
|
100
|
+
name: fieldMatch[1],
|
|
101
|
+
value: fieldMatch[2],
|
|
102
|
+
inline: fieldMatch[3] === "true",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (fields.length > 0) data.fields = fields;
|
|
106
|
+
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* EmbedTrigger — fires a ChrxEmbedBuilder embed in a reply/send.
|
|
113
|
+
* Drop this anywhere in a ChrxCommandBuilder run() function.
|
|
114
|
+
*
|
|
115
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
116
|
+
* @param {EmbedBuilder} embed
|
|
117
|
+
* @param {object} [options]
|
|
118
|
+
* @param {boolean} [options.ephemeral]
|
|
119
|
+
*/
|
|
120
|
+
async function EmbedTrigger(interaction, embed, options = {}) {
|
|
121
|
+
if (interaction.replied || interaction.deferred) {
|
|
122
|
+
return interaction.followUp({ embeds: [embed], ephemeral: options.ephemeral ?? false });
|
|
123
|
+
}
|
|
124
|
+
return interaction.reply({ embeds: [embed], ephemeral: options.ephemeral ?? false });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { ChrxEmbedBuilder, EmbedTrigger };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ChrxModalBuilder.js
|
|
3
|
+
* Simple modal builder with built in validation and rate limiting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
ModalBuilder,
|
|
8
|
+
TextInputBuilder,
|
|
9
|
+
TextInputStyle,
|
|
10
|
+
ActionRowBuilder,
|
|
11
|
+
} = require("discord.js");
|
|
12
|
+
|
|
13
|
+
// ── Rate limiter ──────────────────────────────────────────────────────────
|
|
14
|
+
// Tracks how many modals a user has triggered in the last minute
|
|
15
|
+
const modalRateLimit = new Map(); // userId -> { count, resetAt }
|
|
16
|
+
const RATE_LIMIT = 50; // max modals per minute per user
|
|
17
|
+
|
|
18
|
+
function checkRateLimit(userId) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const data = modalRateLimit.get(userId);
|
|
21
|
+
|
|
22
|
+
if (!data || now > data.resetAt) {
|
|
23
|
+
modalRateLimit.set(userId, { count: 1, resetAt: now + 60000 });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (data.count >= RATE_LIMIT) return false;
|
|
28
|
+
|
|
29
|
+
data.count++;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Type map ──────────────────────────────────────────────────────────────
|
|
34
|
+
const STYLE_MAP = {
|
|
35
|
+
short: TextInputStyle.Short,
|
|
36
|
+
paragraph: TextInputStyle.Paragraph,
|
|
37
|
+
long: TextInputStyle.Paragraph, // alias
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
class ChrxModalBuilder {
|
|
41
|
+
/**
|
|
42
|
+
* @param {object} options
|
|
43
|
+
* @param {string} options.name Internal name (for reference)
|
|
44
|
+
* @param {string} options.id Modal custom ID
|
|
45
|
+
* @param {string} options.title Modal title (max 45 chars)
|
|
46
|
+
* @param {object[]} options.inputs Input fields (max 5)
|
|
47
|
+
* @param {string} options.inputs[].id Input custom ID
|
|
48
|
+
* @param {string} options.inputs[].label Input label (max 100 chars)
|
|
49
|
+
* @param {string} [options.inputs[].type] "short" | "paragraph" (default: short)
|
|
50
|
+
* @param {boolean} [options.inputs[].required] Required? (default: false)
|
|
51
|
+
* @param {string} [options.inputs[].placeholder] Placeholder text
|
|
52
|
+
* @param {string} [options.inputs[].value] Pre-filled value
|
|
53
|
+
* @param {number} [options.inputs[].minLength]
|
|
54
|
+
* @param {number} [options.inputs[].maxLength]
|
|
55
|
+
*/
|
|
56
|
+
constructor(options = {}) {
|
|
57
|
+
if (!options.id) throw new Error("[ChrxModal] id is required.");
|
|
58
|
+
if (!options.title) throw new Error("[ChrxModal] title is required.");
|
|
59
|
+
if (!options.inputs || options.inputs.length === 0) throw new Error("[ChrxModal] At least one input is required.");
|
|
60
|
+
|
|
61
|
+
// ── Validation ────────────────────────────────────────────────────────
|
|
62
|
+
if (options.title.length > 45) {
|
|
63
|
+
throw new Error(`[ChrxModal] Title exceeds 45 characters (got ${options.title.length}).`);
|
|
64
|
+
}
|
|
65
|
+
if (options.inputs.length > 5) {
|
|
66
|
+
throw new Error(`[ChrxModal] Maximum 5 inputs allowed (got ${options.inputs.length}).`);
|
|
67
|
+
}
|
|
68
|
+
for (const input of options.inputs) {
|
|
69
|
+
if (!input.id) throw new Error("[ChrxModal] Every input must have an id.");
|
|
70
|
+
if (!input.label) throw new Error("[ChrxModal] Every input must have a label.");
|
|
71
|
+
if (input.label.length > 100) {
|
|
72
|
+
throw new Error(`[ChrxModal] Input label "${input.label}" exceeds 100 characters.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this._options = options;
|
|
77
|
+
this.name = options.name ?? options.id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build and return the raw discord.js Modal.
|
|
82
|
+
* @returns {ModalBuilder}
|
|
83
|
+
*/
|
|
84
|
+
build() {
|
|
85
|
+
const modal = new ModalBuilder()
|
|
86
|
+
.setCustomId(this._options.id)
|
|
87
|
+
.setTitle(this._options.title);
|
|
88
|
+
|
|
89
|
+
const rows = this._options.inputs.map((input) => {
|
|
90
|
+
const textInput = new TextInputBuilder()
|
|
91
|
+
.setCustomId(input.id)
|
|
92
|
+
.setLabel(input.label)
|
|
93
|
+
.setStyle(STYLE_MAP[input.type ?? "short"] ?? TextInputStyle.Short);
|
|
94
|
+
|
|
95
|
+
if (input.required) textInput.setRequired(input.required);
|
|
96
|
+
if (input.placeholder) textInput.setPlaceholder(input.placeholder);
|
|
97
|
+
if (input.value) textInput.setValue(input.value);
|
|
98
|
+
if (input.minLength) textInput.setMinLength(input.minLength);
|
|
99
|
+
if (input.maxLength) textInput.setMaxLength(input.maxLength);
|
|
100
|
+
|
|
101
|
+
return new ActionRowBuilder().addComponents(textInput);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
modal.addComponents(...rows);
|
|
105
|
+
return modal;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ModalTrigger — shows a ChrxModalBuilder modal to the user.
|
|
111
|
+
* Handles rate limiting automatically.
|
|
112
|
+
*
|
|
113
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
114
|
+
* @param {ChrxModalBuilder} modal
|
|
115
|
+
*/
|
|
116
|
+
async function ModalTrigger(interaction, modal) {
|
|
117
|
+
if (!checkRateLimit(interaction.user.id)) {
|
|
118
|
+
return interaction.reply({
|
|
119
|
+
content: "⏱ You're triggering too many modals! Slow down.",
|
|
120
|
+
ephemeral: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await interaction.showModal(modal.build());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* onModalSubmit — listen for a modal submission by ID.
|
|
129
|
+
* @param {import("discord.js").Client} client
|
|
130
|
+
* @param {string} modalId
|
|
131
|
+
* @param {Function} handler (interaction) => void
|
|
132
|
+
*/
|
|
133
|
+
function onModalSubmit(client, modalId, handler) {
|
|
134
|
+
client.on("interactionCreate", async (interaction) => {
|
|
135
|
+
if (!interaction.isModalSubmit()) return;
|
|
136
|
+
if (interaction.customId !== modalId) return;
|
|
137
|
+
try {
|
|
138
|
+
await handler(interaction);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(`[ChrxModal] Error in modal "${modalId}":`, err);
|
|
141
|
+
const msg = { content: err.message || "❌ Something went wrong.", ephemeral: true };
|
|
142
|
+
if (interaction.replied || interaction.deferred) {
|
|
143
|
+
interaction.followUp(msg).catch(() => {});
|
|
144
|
+
} else {
|
|
145
|
+
interaction.reply(msg).catch(() => {});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { ChrxModalBuilder, ModalTrigger, onModalSubmit };
|
package/index.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* index.js
|
|
3
|
-
* Main entry point
|
|
4
|
-
* Listen to Project4play / SVJ at SoundCloud NOW!
|
|
2
|
+
* index.js
|
|
3
|
+
* Main entry point — exports everything.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
const { ChrxClient }
|
|
8
|
-
const Database
|
|
9
|
-
const XPSystem
|
|
10
|
-
const AIWrapper
|
|
11
|
-
const MusicManager
|
|
12
|
-
const ChrxCommandBuilder
|
|
6
|
+
const { ChrxClient } = require("./core/Client");
|
|
7
|
+
const Database = require("./core/Database");
|
|
8
|
+
const XPSystem = require("./core/XPSystem");
|
|
9
|
+
const AIWrapper = require("./core/AIWrapper");
|
|
10
|
+
const MusicManager = require("./core/MusicManager");
|
|
11
|
+
const ChrxCommandBuilder = require("./core/ChrxCommandBuilder");
|
|
12
|
+
const { ChrxEmbedBuilder, EmbedTrigger } = require("./core/ChrxEmbedBuilder");
|
|
13
|
+
const { ChrxModalBuilder, ModalTrigger, onModalSubmit } = require("./core/ChrxModalBuilder");
|
|
14
|
+
const {
|
|
15
|
+
ChrxButton,
|
|
16
|
+
ChrxSelectMenu,
|
|
17
|
+
ChrxPagination,
|
|
18
|
+
ChrxContextMenu,
|
|
19
|
+
ChrxMenu,
|
|
20
|
+
ChrxForm,
|
|
21
|
+
ChrxConfirm,
|
|
22
|
+
} = require("./core/ChrxComponentsV3");
|
|
13
23
|
const {
|
|
14
24
|
parseTime,
|
|
15
25
|
formatTime,
|
|
@@ -37,6 +47,22 @@ module.exports = {
|
|
|
37
47
|
ChrxClient,
|
|
38
48
|
ChrxCommandBuilder,
|
|
39
49
|
|
|
50
|
+
// Embed & Modal
|
|
51
|
+
ChrxEmbedBuilder,
|
|
52
|
+
EmbedTrigger,
|
|
53
|
+
ChrxModalBuilder,
|
|
54
|
+
ModalTrigger,
|
|
55
|
+
onModalSubmit,
|
|
56
|
+
|
|
57
|
+
// Components V3
|
|
58
|
+
ChrxButton,
|
|
59
|
+
ChrxSelectMenu,
|
|
60
|
+
ChrxPagination,
|
|
61
|
+
ChrxContextMenu,
|
|
62
|
+
ChrxMenu,
|
|
63
|
+
ChrxForm,
|
|
64
|
+
ChrxConfirm,
|
|
65
|
+
|
|
40
66
|
// Modules
|
|
41
67
|
Database,
|
|
42
68
|
XPSystem,
|