refactor: mejorar la legibilidad y consistencia del código en _helpers.ts
This commit is contained in:
@@ -1,67 +1,103 @@
|
|||||||
import { prisma } from '../../../core/database/prisma';
|
import { prisma } from "../../../core/database/prisma";
|
||||||
import type { GameArea } from '@prisma/client';
|
import type { GameArea } from "@prisma/client";
|
||||||
import type { ItemProps } from '../../../game/economy/types';
|
import type { ItemProps } from "../../../game/economy/types";
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
TextBasedChannel,
|
TextBasedChannel,
|
||||||
MessageComponentInteraction,
|
MessageComponentInteraction,
|
||||||
StringSelectMenuInteraction,
|
StringSelectMenuInteraction,
|
||||||
ButtonInteraction,
|
ButtonInteraction,
|
||||||
ModalSubmitInteraction
|
ModalSubmitInteraction,
|
||||||
} from 'discord.js';
|
} from "discord.js";
|
||||||
import { MessageFlags } from 'discord.js';
|
import { MessageFlags } from "discord.js";
|
||||||
import { ButtonStyle, ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
import {
|
||||||
|
ButtonStyle,
|
||||||
|
ComponentType,
|
||||||
|
TextInputStyle,
|
||||||
|
} from "discord-api-types/v10";
|
||||||
|
|
||||||
export function parseItemProps(json: unknown): ItemProps {
|
export function parseItemProps(json: unknown): ItemProps {
|
||||||
if (!json || typeof json !== 'object') return {};
|
if (!json || typeof json !== "object") return {};
|
||||||
return json as ItemProps;
|
return json as ItemProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveArea(guildId: string, areaKey: string) {
|
export async function resolveArea(guildId: string, areaKey: string) {
|
||||||
const area = await prisma.gameArea.findFirst({ where: { key: areaKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
const area = await prisma.gameArea.findFirst({
|
||||||
|
where: { key: areaKey, OR: [{ guildId }, { guildId: null }] },
|
||||||
|
orderBy: [{ guildId: "desc" }],
|
||||||
|
});
|
||||||
return area;
|
return area;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolvedAreaInfo {
|
export interface ResolvedAreaInfo {
|
||||||
area: GameArea | null;
|
area: GameArea | null;
|
||||||
source: 'guild' | 'global' | 'none';
|
source: "guild" | "global" | "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveGuildAreaWithFallback(guildId: string, areaKey: string): Promise<ResolvedAreaInfo> {
|
export async function resolveGuildAreaWithFallback(
|
||||||
const guildArea = await prisma.gameArea.findFirst({ where: { key: areaKey, guildId } });
|
guildId: string,
|
||||||
|
areaKey: string
|
||||||
|
): Promise<ResolvedAreaInfo> {
|
||||||
|
const guildArea = await prisma.gameArea.findFirst({
|
||||||
|
where: { key: areaKey, guildId },
|
||||||
|
});
|
||||||
if (guildArea) {
|
if (guildArea) {
|
||||||
return { area: guildArea, source: 'guild' };
|
return { area: guildArea, source: "guild" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalArea = await prisma.gameArea.findFirst({ where: { key: areaKey, guildId: null } });
|
const globalArea = await prisma.gameArea.findFirst({
|
||||||
|
where: { key: areaKey, guildId: null },
|
||||||
|
});
|
||||||
if (globalArea) {
|
if (globalArea) {
|
||||||
return { area: globalArea, source: 'global' };
|
return { area: globalArea, source: "global" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { area: null, source: 'none' };
|
return { area: null, source: "none" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveAreaByType(guildId: string, type: string): Promise<ResolvedAreaInfo> {
|
export async function resolveAreaByType(
|
||||||
const guildArea = await prisma.gameArea.findFirst({ where: { type, guildId }, orderBy: [{ createdAt: 'asc' }] });
|
guildId: string,
|
||||||
|
type: string
|
||||||
|
): Promise<ResolvedAreaInfo> {
|
||||||
|
const guildArea = await prisma.gameArea.findFirst({
|
||||||
|
where: { type, guildId },
|
||||||
|
orderBy: [{ createdAt: "asc" }],
|
||||||
|
});
|
||||||
if (guildArea) {
|
if (guildArea) {
|
||||||
return { area: guildArea, source: 'guild' };
|
return { area: guildArea, source: "guild" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalArea = await prisma.gameArea.findFirst({ where: { type, guildId: null }, orderBy: [{ createdAt: 'asc' }] });
|
const globalArea = await prisma.gameArea.findFirst({
|
||||||
|
where: { type, guildId: null },
|
||||||
|
orderBy: [{ createdAt: "asc" }],
|
||||||
|
});
|
||||||
if (globalArea) {
|
if (globalArea) {
|
||||||
return { area: globalArea, source: 'global' };
|
return { area: globalArea, source: "global" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { area: null, source: 'none' };
|
return { area: null, source: "none" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDefaultLevel(userId: string, guildId: string, areaId: string): Promise<number> {
|
export async function getDefaultLevel(
|
||||||
const prog = await prisma.playerProgress.findUnique({ where: { userId_guildId_areaId: { userId, guildId, areaId } } });
|
userId: string,
|
||||||
|
guildId: string,
|
||||||
|
areaId: string
|
||||||
|
): Promise<number> {
|
||||||
|
const prog = await prisma.playerProgress.findUnique({
|
||||||
|
where: { userId_guildId_areaId: { userId, guildId, areaId } },
|
||||||
|
});
|
||||||
return Math.max(1, prog?.highestLevel ?? 1);
|
return Math.max(1, prog?.highestLevel ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findBestToolKey(userId: string, guildId: string, toolType: string): Promise<string | null> {
|
export async function findBestToolKey(
|
||||||
const inv = await prisma.inventoryEntry.findMany({ where: { userId, guildId, quantity: { gt: 0 } }, include: { item: true } });
|
userId: string,
|
||||||
|
guildId: string,
|
||||||
|
toolType: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
const inv = await prisma.inventoryEntry.findMany({
|
||||||
|
where: { userId, guildId, quantity: { gt: 0 } },
|
||||||
|
include: { item: true },
|
||||||
|
});
|
||||||
let best: { key: string; tier: number } | null = null;
|
let best: { key: string; tier: number } | null = null;
|
||||||
for (const e of inv) {
|
for (const e of inv) {
|
||||||
const it = e.item;
|
const it = e.item;
|
||||||
@@ -80,10 +116,12 @@ export interface ParsedGameArgs {
|
|||||||
areaOverride: string | null;
|
areaOverride: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AREA_OVERRIDE_PREFIX = 'area:';
|
const AREA_OVERRIDE_PREFIX = "area:";
|
||||||
|
|
||||||
export function parseGameArgs(args: string[]): ParsedGameArgs {
|
export function parseGameArgs(args: string[]): ParsedGameArgs {
|
||||||
const tokens = args.filter((arg): arg is string => typeof arg === 'string' && arg.trim().length > 0);
|
const tokens = args.filter(
|
||||||
|
(arg): arg is string => typeof arg === "string" && arg.trim().length > 0
|
||||||
|
);
|
||||||
|
|
||||||
let levelArg: number | null = null;
|
let levelArg: number | null = null;
|
||||||
let providedTool: string | null = null;
|
let providedTool: string | null = null;
|
||||||
@@ -109,9 +147,12 @@ export function parseGameArgs(args: string[]): ParsedGameArgs {
|
|||||||
return { levelArg, providedTool, areaOverride };
|
return { levelArg, providedTool, areaOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_ITEM_ICON = '📦';
|
const DEFAULT_ITEM_ICON = "📦";
|
||||||
|
|
||||||
export function resolveItemIcon(icon?: string | null, fallback = DEFAULT_ITEM_ICON) {
|
export function resolveItemIcon(
|
||||||
|
icon?: string | null,
|
||||||
|
fallback = DEFAULT_ITEM_ICON
|
||||||
|
) {
|
||||||
const trimmed = icon?.trim();
|
const trimmed = icon?.trim();
|
||||||
return trimmed && trimmed.length > 0 ? trimmed : fallback;
|
return trimmed && trimmed.length > 0 ? trimmed : fallback;
|
||||||
}
|
}
|
||||||
@@ -122,15 +163,24 @@ export function formatItemLabel(
|
|||||||
): string {
|
): string {
|
||||||
const fallbackIcon = options.fallbackIcon ?? DEFAULT_ITEM_ICON;
|
const fallbackIcon = options.fallbackIcon ?? DEFAULT_ITEM_ICON;
|
||||||
const icon = resolveItemIcon(item.icon, fallbackIcon);
|
const icon = resolveItemIcon(item.icon, fallbackIcon);
|
||||||
const label = (item.name ?? '').trim() || item.key;
|
const label = (item.name ?? "").trim() || item.key;
|
||||||
const content = `${icon ? `${icon} ` : ''}${label}`.trim();
|
const content = `${icon ? `${icon} ` : ""}${label}`.trim();
|
||||||
return options.bold ? `**${content}**` : content;
|
return options.bold ? `**${content}**` : content;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ItemBasicInfo = { key: string; name: string | null; icon: string | null };
|
export type ItemBasicInfo = {
|
||||||
|
key: string;
|
||||||
|
name: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export async function fetchItemBasics(guildId: string, keys: string[]): Promise<Map<string, ItemBasicInfo>> {
|
export async function fetchItemBasics(
|
||||||
const uniqueKeys = Array.from(new Set(keys.filter((key): key is string => Boolean(key && key.trim()))));
|
guildId: string,
|
||||||
|
keys: string[]
|
||||||
|
): Promise<Map<string, ItemBasicInfo>> {
|
||||||
|
const uniqueKeys = Array.from(
|
||||||
|
new Set(keys.filter((key): key is string => Boolean(key && key.trim())))
|
||||||
|
);
|
||||||
if (uniqueKeys.length === 0) return new Map();
|
if (uniqueKeys.length === 0) return new Map();
|
||||||
|
|
||||||
const rows = await prisma.economyItem.findMany({
|
const rows = await prisma.economyItem.findMany({
|
||||||
@@ -138,7 +188,7 @@ export async function fetchItemBasics(guildId: string, keys: string[]): Promise<
|
|||||||
key: { in: uniqueKeys },
|
key: { in: uniqueKeys },
|
||||||
OR: [{ guildId }, { guildId: null }],
|
OR: [{ guildId }, { guildId: null }],
|
||||||
},
|
},
|
||||||
orderBy: [{ key: 'asc' }, { guildId: 'desc' }],
|
orderBy: [{ key: "asc" }, { guildId: "desc" }],
|
||||||
select: { key: true, name: true, icon: true, guildId: true },
|
select: { key: true, name: true, icon: true, guildId: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,7 +231,7 @@ export interface KeyPickerConfig<T> {
|
|||||||
export interface KeyPickerResult<T> {
|
export interface KeyPickerResult<T> {
|
||||||
entry: T | null;
|
entry: T | null;
|
||||||
panelMessage: Message | null;
|
panelMessage: Message | null;
|
||||||
reason: 'selected' | 'empty' | 'cancelled' | 'timeout';
|
reason: "selected" | "empty" | "cancelled" | "timeout";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function promptKeySelection<T>(
|
export async function promptKeySelection<T>(
|
||||||
@@ -193,9 +243,14 @@ export async function promptKeySelection<T>(
|
|||||||
|
|
||||||
const baseOptions = config.entries.map((entry) => {
|
const baseOptions = config.entries.map((entry) => {
|
||||||
const option = config.getOption(entry);
|
const option = config.getOption(entry);
|
||||||
const searchText = [option.label, option.description, option.value, ...(option.keywords ?? [])]
|
const searchText = [
|
||||||
|
option.label,
|
||||||
|
option.description,
|
||||||
|
option.value,
|
||||||
|
...(option.keywords ?? []),
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
return { entry, option, searchText };
|
return { entry, option, searchText };
|
||||||
});
|
});
|
||||||
@@ -203,7 +258,7 @@ export async function promptKeySelection<T>(
|
|||||||
if (baseOptions.length === 0) {
|
if (baseOptions.length === 0) {
|
||||||
const emptyPanel = {
|
const emptyPanel = {
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFFA500,
|
accent_color: 0xffa500,
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
@@ -217,14 +272,14 @@ export async function promptKeySelection<T>(
|
|||||||
reply: { messageReference: message.id },
|
reply: { messageReference: message.id },
|
||||||
components: [emptyPanel],
|
components: [emptyPanel],
|
||||||
});
|
});
|
||||||
return { entry: null, panelMessage: null, reason: 'empty' };
|
return { entry: null, panelMessage: null, reason: "empty" };
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = '';
|
let filter = "";
|
||||||
let page = 0;
|
let page = 0;
|
||||||
const pageSize = 25;
|
const pageSize = 25;
|
||||||
const accentColor = config.accentColor ?? 0x5865F2;
|
const accentColor = config.accentColor ?? 0x5865f2;
|
||||||
const placeholder = config.placeholder ?? 'Selecciona una opción…';
|
const placeholder = config.placeholder ?? "Selecciona una opción…";
|
||||||
|
|
||||||
const buildComponents = () => {
|
const buildComponents = () => {
|
||||||
const normalizedFilter = filter.trim().toLowerCase();
|
const normalizedFilter = filter.trim().toLowerCase();
|
||||||
@@ -238,10 +293,12 @@ export async function promptKeySelection<T>(
|
|||||||
const start = safePage * pageSize;
|
const start = safePage * pageSize;
|
||||||
const slice = filtered.slice(start, start + pageSize);
|
const slice = filtered.slice(start, start + pageSize);
|
||||||
|
|
||||||
const pageLabel = `Página ${totalFiltered === 0 ? 0 : safePage + 1}/${totalPages}`;
|
const pageLabel = `Página ${
|
||||||
|
totalFiltered === 0 ? 0 : safePage + 1
|
||||||
|
}/${totalPages}`;
|
||||||
const statsLine = `Total: **${baseOptions.length}** • Coincidencias: **${totalFiltered}**\n${pageLabel}`;
|
const statsLine = `Total: **${baseOptions.length}** • Coincidencias: **${totalFiltered}**\n${pageLabel}`;
|
||||||
const filterLine = filter ? `\nFiltro activo: \`${filter}\`` : '';
|
const filterLine = filter ? `\nFiltro activo: \`${filter}\`` : "";
|
||||||
const hintLine = config.filterHint ? `\n${config.filterHint}` : '';
|
const hintLine = config.filterHint ? `\n${config.filterHint}` : "";
|
||||||
|
|
||||||
const display = {
|
const display = {
|
||||||
type: 17,
|
type: 17,
|
||||||
@@ -256,9 +313,10 @@ export async function promptKeySelection<T>(
|
|||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: totalFiltered === 0
|
content:
|
||||||
? 'No hay resultados para el filtro actual. Ajusta el filtro o limpia la búsqueda.'
|
totalFiltered === 0
|
||||||
: 'Selecciona una opción del menú desplegable para continuar.',
|
? "No hay resultados para el filtro actual. Ajusta el filtro o limpia la búsqueda."
|
||||||
|
: "Selecciona una opción del menú desplegable para continuar.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -273,9 +331,9 @@ export async function promptKeySelection<T>(
|
|||||||
if (selectDisabled) {
|
if (selectDisabled) {
|
||||||
options = [
|
options = [
|
||||||
{
|
{
|
||||||
label: 'Sin resultados',
|
label: "Sin resultados",
|
||||||
value: `${config.customIdPrefix}_empty`,
|
value: `${config.customIdPrefix}_empty`,
|
||||||
description: 'Ajusta el filtro para ver opciones.',
|
description: "Ajusta el filtro para ver opciones.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -299,34 +357,34 @@ export async function promptKeySelection<T>(
|
|||||||
{
|
{
|
||||||
type: 2,
|
type: 2,
|
||||||
style: ButtonStyle.Secondary,
|
style: ButtonStyle.Secondary,
|
||||||
label: '◀️',
|
label: "◀️",
|
||||||
custom_id: `${config.customIdPrefix}_prev`,
|
custom_id: `${config.customIdPrefix}_prev`,
|
||||||
disabled: safePage <= 0 || totalFiltered === 0,
|
disabled: safePage <= 0 || totalFiltered === 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 2,
|
type: 2,
|
||||||
style: ButtonStyle.Secondary,
|
style: ButtonStyle.Secondary,
|
||||||
label: '▶️',
|
label: "▶️",
|
||||||
custom_id: `${config.customIdPrefix}_next`,
|
custom_id: `${config.customIdPrefix}_next`,
|
||||||
disabled: safePage >= totalPages - 1 || totalFiltered === 0,
|
disabled: safePage >= totalPages - 1 || totalFiltered === 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 2,
|
type: 2,
|
||||||
style: ButtonStyle.Primary,
|
style: ButtonStyle.Primary,
|
||||||
label: '🔎 Filtro',
|
label: "🔎 Filtro",
|
||||||
custom_id: `${config.customIdPrefix}_filter`,
|
custom_id: `${config.customIdPrefix}_filter`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 2,
|
type: 2,
|
||||||
style: ButtonStyle.Secondary,
|
style: ButtonStyle.Secondary,
|
||||||
label: 'Limpiar',
|
label: "Limpiar",
|
||||||
custom_id: `${config.customIdPrefix}_clear`,
|
custom_id: `${config.customIdPrefix}_clear`,
|
||||||
disabled: filter.length === 0,
|
disabled: filter.length === 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 2,
|
type: 2,
|
||||||
style: ButtonStyle.Danger,
|
style: ButtonStyle.Danger,
|
||||||
label: 'Cancelar',
|
label: "Cancelar",
|
||||||
custom_id: `${config.customIdPrefix}_cancel`,
|
custom_id: `${config.customIdPrefix}_cancel`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -345,7 +403,10 @@ export async function promptKeySelection<T>(
|
|||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
|
||||||
const result = await new Promise<KeyPickerResult<T>>((resolve) => {
|
const result = await new Promise<KeyPickerResult<T>>((resolve) => {
|
||||||
const finish = (entry: T | null, reason: 'selected' | 'cancelled' | 'timeout') => {
|
const finish = (
|
||||||
|
entry: T | null,
|
||||||
|
reason: "selected" | "cancelled" | "timeout"
|
||||||
|
) => {
|
||||||
if (resolved) return;
|
if (resolved) return;
|
||||||
resolved = true;
|
resolved = true;
|
||||||
resolve({ entry, panelMessage, reason });
|
resolve({ entry, panelMessage, reason });
|
||||||
@@ -353,158 +414,193 @@ export async function promptKeySelection<T>(
|
|||||||
|
|
||||||
const collector = panelMessage.createMessageComponentCollector({
|
const collector = panelMessage.createMessageComponentCollector({
|
||||||
time: 5 * 60_000,
|
time: 5 * 60_000,
|
||||||
filter: (i: MessageComponentInteraction) => i.user.id === userId && i.customId.startsWith(config.customIdPrefix),
|
filter: (i: MessageComponentInteraction) =>
|
||||||
|
i.user.id === userId && i.customId.startsWith(config.customIdPrefix),
|
||||||
});
|
});
|
||||||
|
|
||||||
collector.on('collect', async (interaction: MessageComponentInteraction) => {
|
collector.on(
|
||||||
try {
|
"collect",
|
||||||
if (interaction.customId === `${config.customIdPrefix}_select` && interaction.isStringSelectMenu()) {
|
async (interaction: MessageComponentInteraction) => {
|
||||||
const select = interaction as StringSelectMenuInteraction;
|
try {
|
||||||
const value = select.values?.[0];
|
if (
|
||||||
const selected = baseOptions.find((opt) => opt.option.value === value);
|
interaction.customId === `${config.customIdPrefix}_select` &&
|
||||||
if (!selected) {
|
interaction.isStringSelectMenu()
|
||||||
await select.reply({ content: '❌ Opción no válida.', flags: MessageFlags.Ephemeral });
|
) {
|
||||||
|
const select = interaction as StringSelectMenuInteraction;
|
||||||
|
const value = select.values?.[0];
|
||||||
|
const selected = baseOptions.find(
|
||||||
|
(opt) => opt.option.value === value
|
||||||
|
);
|
||||||
|
if (!selected) {
|
||||||
|
await select.reply({
|
||||||
|
content: "❌ Opción no válida.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await select.update({
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 17,
|
||||||
|
accent_color: accentColor,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: `⏳ Cargando **${selected.option.label}**…`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
if (!select.deferred && !select.replied) {
|
||||||
|
try {
|
||||||
|
await select.deferUpdate();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(selected.entry, "selected");
|
||||||
|
collector.stop("selected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (
|
||||||
await select.update({
|
interaction.customId === `${config.customIdPrefix}_prev` &&
|
||||||
components: [
|
interaction.isButton()
|
||||||
{
|
) {
|
||||||
type: 17,
|
if (page > 0) page -= 1;
|
||||||
accent_color: accentColor,
|
await interaction.update({ components: buildComponents() });
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: `⏳ Cargando **${selected.option.label}**…`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
if (!select.deferred && !select.replied) {
|
|
||||||
try { await select.deferUpdate(); } catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(selected.entry, 'selected');
|
|
||||||
collector.stop('selected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId === `${config.customIdPrefix}_prev` && interaction.isButton()) {
|
|
||||||
if (page > 0) page -= 1;
|
|
||||||
await interaction.update({ components: buildComponents() });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId === `${config.customIdPrefix}_next` && interaction.isButton()) {
|
|
||||||
page += 1;
|
|
||||||
await interaction.update({ components: buildComponents() });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId === `${config.customIdPrefix}_clear` && interaction.isButton()) {
|
|
||||||
filter = '';
|
|
||||||
page = 0;
|
|
||||||
await interaction.update({ components: buildComponents() });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId === `${config.customIdPrefix}_cancel` && interaction.isButton()) {
|
|
||||||
try {
|
|
||||||
await interaction.update({
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 17,
|
|
||||||
accent_color: 0xFF0000,
|
|
||||||
components: [
|
|
||||||
{ type: 10, content: '❌ Selección cancelada.' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
if (!interaction.deferred && !interaction.replied) {
|
|
||||||
try { await interaction.deferUpdate(); } catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(null, 'cancelled');
|
|
||||||
collector.stop('cancelled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId === `${config.customIdPrefix}_filter` && interaction.isButton()) {
|
|
||||||
const modal = {
|
|
||||||
title: 'Filtrar lista',
|
|
||||||
customId: `${config.customIdPrefix}_filter_modal`,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: ComponentType.Label,
|
|
||||||
label: 'Texto a buscar',
|
|
||||||
component: {
|
|
||||||
type: ComponentType.TextInput,
|
|
||||||
customId: 'query',
|
|
||||||
style: TextInputStyle.Short,
|
|
||||||
required: false,
|
|
||||||
value: filter,
|
|
||||||
placeholder: 'Nombre, key, categoría…',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
await (interaction as ButtonInteraction).showModal(modal);
|
|
||||||
let submitted: ModalSubmitInteraction | undefined;
|
|
||||||
try {
|
|
||||||
submitted = await interaction.awaitModalSubmit({
|
|
||||||
time: 120_000,
|
|
||||||
filter: (sub) => sub.user.id === userId && sub.customId === `${config.customIdPrefix}_filter_modal`,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (
|
||||||
const value = submitted.components.getTextInputValue('query')?.trim() ?? '';
|
interaction.customId === `${config.customIdPrefix}_next` &&
|
||||||
filter = value;
|
interaction.isButton()
|
||||||
|
) {
|
||||||
|
page += 1;
|
||||||
|
await interaction.update({ components: buildComponents() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
interaction.customId === `${config.customIdPrefix}_clear` &&
|
||||||
|
interaction.isButton()
|
||||||
|
) {
|
||||||
|
filter = "";
|
||||||
page = 0;
|
page = 0;
|
||||||
await submitted.deferUpdate();
|
await interaction.update({ components: buildComponents() });
|
||||||
await panelMessage.edit({ components: buildComponents() });
|
return;
|
||||||
} catch {
|
}
|
||||||
// ignore errors updating filter
|
|
||||||
|
if (
|
||||||
|
interaction.customId === `${config.customIdPrefix}_cancel` &&
|
||||||
|
interaction.isButton()
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await interaction.update({
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xff0000,
|
||||||
|
components: [
|
||||||
|
{ type: 10, content: "❌ Selección cancelada." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
if (!interaction.deferred && !interaction.replied) {
|
||||||
|
try {
|
||||||
|
await interaction.deferUpdate();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(null, "cancelled");
|
||||||
|
collector.stop("cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
interaction.customId === `${config.customIdPrefix}_filter` &&
|
||||||
|
interaction.isButton()
|
||||||
|
) {
|
||||||
|
const modal = {
|
||||||
|
title: "Filtrar lista",
|
||||||
|
customId: `${config.customIdPrefix}_filter_modal`,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Label,
|
||||||
|
label: "Texto a buscar",
|
||||||
|
component: {
|
||||||
|
type: ComponentType.TextInput,
|
||||||
|
customId: "query",
|
||||||
|
style: TextInputStyle.Short,
|
||||||
|
required: false,
|
||||||
|
value: filter,
|
||||||
|
placeholder: "Nombre, key, categoría…",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await (interaction as ButtonInteraction).showModal(modal);
|
||||||
|
let submitted: ModalSubmitInteraction | undefined;
|
||||||
|
try {
|
||||||
|
submitted = await interaction.awaitModalSubmit({
|
||||||
|
time: 120_000,
|
||||||
|
filter: (sub) =>
|
||||||
|
sub.user.id === userId &&
|
||||||
|
sub.customId === `${config.customIdPrefix}_filter_modal`,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value =
|
||||||
|
submitted.components.getTextInputValue("query")?.trim() ?? "";
|
||||||
|
filter = value;
|
||||||
|
page = 0;
|
||||||
|
await submitted.deferUpdate();
|
||||||
|
await panelMessage.edit({ components: buildComponents() });
|
||||||
|
} catch {
|
||||||
|
// ignore errors updating filter
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!interaction.deferred && !interaction.replied) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "❌ Error procesando la selección.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!interaction.deferred && !interaction.replied) {
|
|
||||||
await interaction.reply({ content: '❌ Error procesando la selección.', flags: MessageFlags.Ephemeral });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
collector.on('end', async (_collected, reason) => {
|
collector.on("end", async (_collected, reason) => {
|
||||||
if (resolved) return;
|
if (resolved) return;
|
||||||
resolved = true;
|
resolved = true;
|
||||||
if (reason !== 'selected' && reason !== 'cancelled') {
|
if (reason !== "selected" && reason !== "cancelled") {
|
||||||
const expiredPanel = {
|
const expiredPanel = {
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFFA500,
|
accent_color: 0xffa500,
|
||||||
components: [
|
components: [{ type: 10, content: "⏰ Selección expirada." }],
|
||||||
{ type: 10, content: '⏰ Selección expirada.' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await panelMessage.edit({ components: [expiredPanel] });
|
await panelMessage.edit({ components: [expiredPanel] });
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mappedReason: 'selected' | 'cancelled' | 'timeout';
|
let mappedReason: "selected" | "cancelled" | "timeout";
|
||||||
if (reason === 'selected') mappedReason = 'selected';
|
if (reason === "selected") mappedReason = "selected";
|
||||||
else if (reason === 'cancelled') mappedReason = 'cancelled';
|
else if (reason === "cancelled") mappedReason = "cancelled";
|
||||||
else mappedReason = 'timeout';
|
else mappedReason = "timeout";
|
||||||
|
|
||||||
resolve({ entry: null, panelMessage, reason: mappedReason });
|
resolve({ entry: null, panelMessage, reason: mappedReason });
|
||||||
});
|
});
|
||||||
@@ -513,7 +609,11 @@ export async function promptKeySelection<T>(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendDisplayReply(message: Message, display: any, extraComponents: any[] = []) {
|
export function sendDisplayReply(
|
||||||
|
message: Message,
|
||||||
|
display: any,
|
||||||
|
extraComponents: any[] = []
|
||||||
|
) {
|
||||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||||
return (channel.send as any)({
|
return (channel.send as any)({
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
@@ -521,4 +621,3 @@ export function sendDisplayReply(message: Message, display: any, extraComponents
|
|||||||
components: [display, ...extraComponents],
|
components: [display, ...extraComponents],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user