feat: add resource checker prompts and display component types for enhanced component management
This commit is contained in:
41
.github/prompts/resource-checker.prompt.md
vendored
Normal file
41
.github/prompts/resource-checker.prompt.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Prompt: Node.js Resource Checker (Memory & CPU)
|
||||||
|
|
||||||
|
Whenever Copilot generates, modifies, or reviews code in Node.js:
|
||||||
|
|
||||||
|
1. **Memory Monitoring**
|
||||||
|
- Always remind to check memory usage with:
|
||||||
|
```js
|
||||||
|
logger.info(process.memoryUsage());
|
||||||
|
```
|
||||||
|
which returns RSS, Heap Used, Heap Total, External, and Array Buffers.
|
||||||
|
- If `Heap Used` grows continuously without freeing, suggest potential memory leaks (unreleased timers, listeners, or large cached data).
|
||||||
|
- If `RSS` (Resident Set Size) is unusually higher than `Heap Total`, warn that external/native modules might be consuming memory.
|
||||||
|
|
||||||
|
2. **CPU Usage**
|
||||||
|
- Remind to check CPU load using:
|
||||||
|
```bash
|
||||||
|
top -p <pid>
|
||||||
|
```
|
||||||
|
or Node.js profiling (`--inspect`, `clinic flame`).
|
||||||
|
- Flag infinite loops, synchronous blocking calls, or heavy computations running on the main thread as potential CPU overuse.
|
||||||
|
- Encourage offloading heavy tasks to Workers, child processes, or queues when appropriate.
|
||||||
|
|
||||||
|
3. **Best Practices**
|
||||||
|
- Always validate memory and CPU usage after implementing new features.
|
||||||
|
- Suggest garbage collection checks (`global.gc()`) in development when Node is run with `--expose-gc`.
|
||||||
|
- Warn if long-living references (e.g., large Maps, Sets, caches, or listeners) might not be released.
|
||||||
|
- Highlight that CPU and memory efficiency are as important as correctness.
|
||||||
|
|
||||||
|
4. **Output Analysis**
|
||||||
|
- When given memory stats like:
|
||||||
|
```
|
||||||
|
RSS: 186.9MB
|
||||||
|
Heap Used: 67.6MB
|
||||||
|
Heap Total: 71.2MB
|
||||||
|
External: 5.0MB
|
||||||
|
```
|
||||||
|
- Compare `Heap Used` vs `Heap Total`: if close to the limit, risk of OOM.
|
||||||
|
- Compare `RSS` vs `Heap Total`: if RSS is much larger, check for native module or buffer leaks.
|
||||||
|
- If growth is unbounded, warn about potential memory leaks.
|
||||||
|
|
||||||
|
5. **Always remind** to rerun tests with `npx tsc --noEmit` (for type safety) and memory checks together, ensuring both correctness and performance.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,217 +1,411 @@
|
|||||||
|
import {
|
||||||
|
Message,
|
||||||
|
ButtonInteraction,
|
||||||
|
StringSelectMenuInteraction,
|
||||||
|
MessageComponentInteraction,
|
||||||
|
ComponentType,
|
||||||
|
ButtonStyle,
|
||||||
|
APIEmbed
|
||||||
|
} from "discord.js";
|
||||||
import { CommandMessage } from "../../../core/types/commands";
|
import { CommandMessage } from "../../../core/types/commands";
|
||||||
|
import type Amayo from "../../../core/client";
|
||||||
|
import type { JsonValue } from "@prisma/client/runtime/library";
|
||||||
|
|
||||||
|
interface BlockItem {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionRowBuilder {
|
||||||
|
type: ComponentType.ActionRow;
|
||||||
|
components: any[];
|
||||||
|
}
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: "eliminar-embed",
|
name: "eliminar-embed",
|
||||||
type: "message",
|
type: "message",
|
||||||
aliases: ["embed-eliminar", "borrar-embed", "embeddelete"],
|
aliases: ["embed-eliminar", "borrar-embed", "embeddelete"],
|
||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
run: async (message: any, args: string[], client: any) => {
|
description: "Elimina bloques DisplayComponents del servidor",
|
||||||
|
category: "Alianzas",
|
||||||
|
usage: "eliminar-embed [nombre_bloque]",
|
||||||
|
run: async (message: Message, args: string[], client: Amayo): Promise<void> => {
|
||||||
if (!message.member?.permissions.has("Administrator")) {
|
if (!message.member?.permissions.has("Administrator")) {
|
||||||
await message.reply("❌ No tienes permisos de Administrador.");
|
await message.reply("❌ No tienes permisos de Administrador.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener todos los bloques del servidor
|
// If specific block name provided, handle direct deletion
|
||||||
const blocks = await client.prisma.blockV2Config.findMany({
|
if (args.length > 0) {
|
||||||
where: { guildId: message.guildId! },
|
const blockName = args.join(" ").trim();
|
||||||
select: { name: true, id: true }
|
await handleDirectDeletion(message, client, blockName);
|
||||||
});
|
|
||||||
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
const noBlocksEmbed = {
|
|
||||||
color: 0xf04747,
|
|
||||||
title: "🗂️ Panel de Eliminación de Bloques",
|
|
||||||
description: "📭 **No hay bloques disponibles**\n\nNo se encontraron bloques para eliminar en este servidor.\n\nPuedes crear nuevos bloques usando `!blockcreate`.",
|
|
||||||
footer: {
|
|
||||||
text: "Sistema de gestión de bloques • Amayo Bot"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await message.reply({
|
|
||||||
embeds: [noBlocksEmbed]
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear opciones para el select menu
|
// Otherwise, show interactive panel
|
||||||
const selectOptions = blocks.slice(0, 25).map((block: any, index: number) => ({
|
await showDeletionPanel(message, client);
|
||||||
label: block.name,
|
|
||||||
value: block.name,
|
|
||||||
description: `ID: ${block.id}`,
|
|
||||||
emoji: index < 10 ? { name: `${index + 1}️⃣` } : { name: "📄" }
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Crear embed principal de eliminación
|
|
||||||
const deleteEmbed = {
|
|
||||||
color: 0xff6b35,
|
|
||||||
title: "🗑️ Panel de Eliminación de Bloques",
|
|
||||||
description: `📊 **${blocks.length} bloque(s) encontrado(s)**\n\n⚠️ **ADVERTENCIA:** La eliminación es permanente e irreversible.\n\nSelecciona el bloque que deseas eliminar del menú de abajo:`,
|
|
||||||
footer: {
|
|
||||||
text: "Selecciona un bloque para eliminar • Timeout: 5 minutos"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionRow = {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 3, // StringSelect
|
|
||||||
custom_id: "delete_block_select",
|
|
||||||
placeholder: "🗑️ Selecciona un bloque para eliminar...",
|
|
||||||
min_values: 1,
|
|
||||||
max_values: 1,
|
|
||||||
options: selectOptions
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelRow = {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2, // Button
|
|
||||||
style: 4, // Danger
|
|
||||||
label: "❌ Cancelar",
|
|
||||||
custom_id: "cancel_delete"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const panelMessage = await message.reply({
|
|
||||||
embeds: [deleteEmbed],
|
|
||||||
components: [actionRow, cancelRow]
|
|
||||||
});
|
|
||||||
|
|
||||||
const collector = panelMessage.createMessageComponentCollector({
|
|
||||||
time: 300000, // 5 minutos
|
|
||||||
filter: (i: any) => i.user.id === message.author.id
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.on("collect", async (interaction: any) => {
|
|
||||||
if (interaction.customId === "cancel_delete") {
|
|
||||||
const canceledEmbed = {
|
|
||||||
color: 0x36393f,
|
|
||||||
title: "❌ Operación Cancelada",
|
|
||||||
description: "La eliminación de bloques ha sido cancelada.\nNingún bloque fue eliminado.",
|
|
||||||
footer: { text: "Operación cancelada por el usuario" }
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [canceledEmbed],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.stop("cancelled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId === "delete_block_select" && interaction.isStringSelectMenu()) {
|
|
||||||
const selectedBlock = interaction.values[0];
|
|
||||||
|
|
||||||
const confirmationEmbed = {
|
|
||||||
color: 0xf04747,
|
|
||||||
title: "⚠️ CONFIRMAR ELIMINACIÓN",
|
|
||||||
description: `🗑️ **Bloque a eliminar:** \`${selectedBlock}\`\n\n❗ **ESTA ACCIÓN ES IRREVERSIBLE**\n\nUna vez eliminado, no podrás recuperar:\n• Toda la configuración del bloque\n• Los componentes y contenido\n• Las imágenes y colores personalizados\n\n¿Estás seguro de que quieres continuar?`,
|
|
||||||
footer: { text: "⚠️ Acción irreversible - Piénsalo bien" }
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmationRow = {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 4, // Danger
|
|
||||||
label: "🗑️ SÍ, ELIMINAR",
|
|
||||||
custom_id: `confirm_delete_${selectedBlock}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2, // Secondary
|
|
||||||
label: "↩️ Volver Atrás",
|
|
||||||
custom_id: "back_to_selection"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [confirmationEmbed],
|
|
||||||
components: [confirmationRow]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId.startsWith("confirm_delete_")) {
|
|
||||||
const blockName = interaction.customId.replace("confirm_delete_", "");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.prisma.blockV2Config.delete({
|
|
||||||
where: {
|
|
||||||
guildId_name: {
|
|
||||||
guildId: message.guildId!,
|
|
||||||
name: blockName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const successEmbed = {
|
|
||||||
color: 0x57f287,
|
|
||||||
title: "✅ Eliminación Exitosa",
|
|
||||||
description: `🗑️ **Bloque eliminado:** \`${blockName}\`\n\n✨ El bloque ha sido eliminado permanentemente de la base de datos.\n\n📋 Para ver los bloques restantes, usa: \`!embedlist\`\n📝 Para crear un nuevo bloque, usa: \`!blockcreate\``,
|
|
||||||
footer: { text: "Bloque eliminado exitosamente" }
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [successEmbed],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.stop("success");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const errorEmbed = {
|
|
||||||
color: 0xf04747,
|
|
||||||
title: "❌ Error en la Eliminación",
|
|
||||||
description: `🔍 **Bloque no encontrado:** \`${blockName}\`\n\n💭 Posibles causas:\n• El bloque ya fue eliminado\n• Error de conexión con la base de datos\n• El nombre del bloque cambió\n\n🔄 Intenta refrescar la lista con \`!embedlist\``,
|
|
||||||
footer: { text: "Error de eliminación" }
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [errorEmbed],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.stop("error");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.customId === "back_to_selection") {
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [deleteEmbed],
|
|
||||||
components: [actionRow, cancelRow]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.on("end", async (collected: any, reason: string) => {
|
|
||||||
if (reason === "time") {
|
|
||||||
const timeoutEmbed = {
|
|
||||||
color: 0x36393f,
|
|
||||||
title: "⏰ Tiempo Agotado",
|
|
||||||
description: "El panel de eliminación ha expirado por inactividad.\nUsa el comando nuevamente si necesitas eliminar bloques.",
|
|
||||||
footer: { text: "Panel expirado por inactividad" }
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await panelMessage.edit({
|
|
||||||
embeds: [timeoutEmbed],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// Mensaje ya eliminado o error de edición
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleDirectDeletion(
|
||||||
|
message: Message,
|
||||||
|
client: Amayo,
|
||||||
|
blockName: string
|
||||||
|
): Promise<void> {
|
||||||
|
const block = await client.prisma.blockV2Config.findFirst({
|
||||||
|
where: {
|
||||||
|
guildId: message.guildId!,
|
||||||
|
name: blockName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!block) {
|
||||||
|
await message.reply(`❌ No se encontró un bloque llamado \`${blockName}\`.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show confirmation for direct deletion
|
||||||
|
const confirmEmbed: APIEmbed = {
|
||||||
|
color: 0xff6b35,
|
||||||
|
title: "⚠️ Confirmar Eliminación",
|
||||||
|
description: `¿Estás seguro de que quieres eliminar el bloque \`${blockName}\`?\n\n**Esta acción es irreversible.**`,
|
||||||
|
footer: { text: "Confirma la eliminación usando los botones" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmRow: ActionRowBuilder = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "🗑️ Confirmar Eliminación",
|
||||||
|
custom_id: `confirm_delete_${block.id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "❌ Cancelar",
|
||||||
|
custom_id: "cancel_delete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmMessage = await message.reply({
|
||||||
|
embeds: [confirmEmbed],
|
||||||
|
components: [confirmRow]
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleConfirmationInteraction(confirmMessage, message, client, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showDeletionPanel(message: Message, client: Amayo): Promise<void> {
|
||||||
|
const blocks = await fetchBlocks(client, message.guildId!);
|
||||||
|
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
await handleNoBlocks(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteEmbed = createDeletionEmbed(blocks);
|
||||||
|
const actionRow = createBlockSelectRow(blocks);
|
||||||
|
const cancelRow = createCancelRow();
|
||||||
|
|
||||||
|
const panelMessage = await message.reply({
|
||||||
|
embeds: [deleteEmbed],
|
||||||
|
components: [actionRow, cancelRow]
|
||||||
|
});
|
||||||
|
|
||||||
|
await handlePanelInteractions(panelMessage, message, client, blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBlocks(client: Amayo, guildId: string): Promise<BlockItem[]> {
|
||||||
|
return await client.prisma.blockV2Config.findMany({
|
||||||
|
where: { guildId },
|
||||||
|
select: { name: true, id: true },
|
||||||
|
orderBy: { name: 'asc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleNoBlocks(message: Message): Promise<void> {
|
||||||
|
const noBlocksEmbed: APIEmbed = {
|
||||||
|
color: 0xf04747,
|
||||||
|
title: "🗂️ Panel de Eliminación de Bloques",
|
||||||
|
description: "📭 **No hay bloques disponibles**\n\nNo se encontraron bloques para eliminar en este servidor.\n\nPuedes crear nuevos bloques usando `!crear-embed`.",
|
||||||
|
footer: { text: "Sistema de gestión de bloques • Amayo Bot" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await message.reply({
|
||||||
|
embeds: [noBlocksEmbed]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeletionEmbed(blocks: BlockItem[]): APIEmbed {
|
||||||
|
return {
|
||||||
|
color: 0xff6b35,
|
||||||
|
title: "🗑️ Panel de Eliminación de Bloques",
|
||||||
|
description: `📊 **${blocks.length} bloque(s) encontrado(s)**\n\n⚠️ **ADVERTENCIA:** La eliminación es permanente e irreversible.\n\nSelecciona el bloque que deseas eliminar del menú de abajo:`,
|
||||||
|
footer: { text: "Selecciona un bloque para eliminar • Timeout: 5 minutos" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlockSelectRow(blocks: BlockItem[]): ActionRowBuilder {
|
||||||
|
const selectOptions = blocks.slice(0, 25).map((block, index) => ({
|
||||||
|
label: block.name,
|
||||||
|
value: block.id, // Use ID instead of name for better uniqueness
|
||||||
|
description: `ID: ${block.id.slice(-8)}`,
|
||||||
|
emoji: index < 10 ? { name: `${index + 1}️⃣` } : { name: "📄" }
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.StringSelect,
|
||||||
|
custom_id: "delete_block_select",
|
||||||
|
placeholder: "🗑️ Selecciona un bloque para eliminar...",
|
||||||
|
min_values: 1,
|
||||||
|
max_values: 1,
|
||||||
|
options: selectOptions
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCancelRow(): ActionRowBuilder {
|
||||||
|
return {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "❌ Cancelar",
|
||||||
|
custom_id: "cancel_delete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePanelInteractions(
|
||||||
|
panelMessage: Message,
|
||||||
|
originalMessage: Message,
|
||||||
|
client: Amayo,
|
||||||
|
blocks: BlockItem[]
|
||||||
|
): Promise<void> {
|
||||||
|
const collector = panelMessage.createMessageComponentCollector({
|
||||||
|
time: 300000, // 5 minutes
|
||||||
|
filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("collect", async (interaction: MessageComponentInteraction) => {
|
||||||
|
try {
|
||||||
|
if (interaction.isButton() && interaction.customId === "cancel_delete") {
|
||||||
|
await handleCancellation(interaction);
|
||||||
|
collector.stop();
|
||||||
|
} else if (interaction.isStringSelectMenu() && interaction.customId === "delete_block_select") {
|
||||||
|
const selectedBlockId = interaction.values[0];
|
||||||
|
const selectedBlock = blocks.find(b => b.id === selectedBlockId);
|
||||||
|
|
||||||
|
if (selectedBlock) {
|
||||||
|
await handleBlockSelection(interaction, client, selectedBlock);
|
||||||
|
collector.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling deletion interaction:", error);
|
||||||
|
if (!interaction.replied && !interaction.deferred) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "❌ Ocurrió un error al procesar la interacción.",
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("end", async (collected, reason) => {
|
||||||
|
if (reason === "time") {
|
||||||
|
await handlePanelTimeout(panelMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancellation(interaction: ButtonInteraction): Promise<void> {
|
||||||
|
const canceledEmbed: APIEmbed = {
|
||||||
|
color: 0x36393f,
|
||||||
|
title: "❌ Operación Cancelada",
|
||||||
|
description: "La eliminación de bloques ha sido cancelada.\nNingún bloque fue eliminado.",
|
||||||
|
footer: { text: "Operación cancelada por el usuario" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [canceledEmbed],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBlockSelection(
|
||||||
|
interaction: StringSelectMenuInteraction,
|
||||||
|
client: Amayo,
|
||||||
|
selectedBlock: BlockItem
|
||||||
|
): Promise<void> {
|
||||||
|
const confirmEmbed: APIEmbed = {
|
||||||
|
color: 0xff4444,
|
||||||
|
title: "⚠️ Confirmar Eliminación",
|
||||||
|
description: `¿Estás seguro de que quieres **eliminar permanentemente** el bloque?\n\n📄 **Nombre:** \`${selectedBlock.name}\`\n🔑 **ID:** \`${selectedBlock.id}\`\n\n❗ **Esta acción NO se puede deshacer.**`,
|
||||||
|
footer: { text: "Confirma tu decisión usando los botones" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmRow: ActionRowBuilder = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "🗑️ SÍ, ELIMINAR",
|
||||||
|
custom_id: `confirm_delete_${selectedBlock.id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "❌ Cancelar",
|
||||||
|
custom_id: "cancel_delete_final"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [confirmEmbed],
|
||||||
|
components: [confirmRow]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle final confirmation
|
||||||
|
const finalCollector = interaction.message.createMessageComponentCollector({
|
||||||
|
time: 60000, // 1 minute for final confirmation
|
||||||
|
filter: (i: MessageComponentInteraction) => i.user.id === interaction.user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
finalCollector.on("collect", async (finalInteraction: ButtonInteraction) => {
|
||||||
|
try {
|
||||||
|
if (finalInteraction.customId === "cancel_delete_final") {
|
||||||
|
await handleCancellation(finalInteraction);
|
||||||
|
} else if (finalInteraction.customId === `confirm_delete_${selectedBlock.id}`) {
|
||||||
|
await executeBlockDeletion(finalInteraction, client, selectedBlock);
|
||||||
|
}
|
||||||
|
finalCollector.stop();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in final confirmation:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
finalCollector.on("end", async (collected, reason) => {
|
||||||
|
if (reason === "time") {
|
||||||
|
await handleConfirmationTimeout(interaction.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmationInteraction(
|
||||||
|
confirmMessage: Message,
|
||||||
|
originalMessage: Message,
|
||||||
|
client: Amayo,
|
||||||
|
block: any
|
||||||
|
): Promise<void> {
|
||||||
|
const collector = confirmMessage.createMessageComponentCollector({
|
||||||
|
time: 60000, // 1 minute
|
||||||
|
filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("collect", async (interaction: ButtonInteraction) => {
|
||||||
|
try {
|
||||||
|
if (interaction.customId === "cancel_delete") {
|
||||||
|
await handleCancellation(interaction);
|
||||||
|
} else if (interaction.customId === `confirm_delete_${block.id}`) {
|
||||||
|
await executeBlockDeletion(interaction, client, { name: block.name, id: block.id });
|
||||||
|
}
|
||||||
|
collector.stop();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in confirmation interaction:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("end", async (collected, reason) => {
|
||||||
|
if (reason === "time") {
|
||||||
|
await handleConfirmationTimeout(confirmMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeBlockDeletion(
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
client: Amayo,
|
||||||
|
block: BlockItem
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Delete the block from database
|
||||||
|
await client.prisma.blockV2Config.delete({
|
||||||
|
where: { id: block.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
const successEmbed: APIEmbed = {
|
||||||
|
color: 0x57f287,
|
||||||
|
title: "✅ Bloque Eliminado",
|
||||||
|
description: `El bloque \`${block.name}\` ha sido eliminado exitosamente.\n\n🗑️ **Operación completada**\n📄 **Bloque:** \`${block.name}\`\n🔑 **ID:** \`${block.id}\``,
|
||||||
|
footer: { text: "Bloque eliminado permanentemente" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [successEmbed],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting block:", error);
|
||||||
|
|
||||||
|
const errorEmbed: APIEmbed = {
|
||||||
|
color: 0xf04747,
|
||||||
|
title: "❌ Error al Eliminar",
|
||||||
|
description: `No se pudo eliminar el bloque \`${block.name}\`.\n\nPor favor, inténtalo de nuevo más tarde.`,
|
||||||
|
footer: { text: "Error en la eliminación" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [errorEmbed],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePanelTimeout(panelMessage: Message): Promise<void> {
|
||||||
|
const timeoutEmbed: APIEmbed = {
|
||||||
|
color: 0x36393f,
|
||||||
|
title: "⏰ Panel Expirado",
|
||||||
|
description: "El panel de eliminación ha expirado por inactividad.\n\nUsa `!eliminar-embed` para abrir un nuevo panel.",
|
||||||
|
footer: { text: "Panel expirado por inactividad" }
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await panelMessage.edit({
|
||||||
|
embeds: [timeoutEmbed],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Could not edit message on timeout, likely deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmationTimeout(confirmMessage: Message): Promise<void> {
|
||||||
|
const timeoutEmbed: APIEmbed = {
|
||||||
|
color: 0x36393f,
|
||||||
|
title: "⏰ Confirmación Expirada",
|
||||||
|
description: "La confirmación ha expirado por inactividad.\nLa eliminación ha sido cancelada.",
|
||||||
|
footer: { text: "Confirmación expirada" }
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await confirmMessage.edit({
|
||||||
|
embeds: [timeoutEmbed],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Could not edit confirmation message on timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,402 +1,483 @@
|
|||||||
|
import {
|
||||||
|
Message,
|
||||||
|
ButtonInteraction,
|
||||||
|
StringSelectMenuInteraction,
|
||||||
|
MessageComponentInteraction,
|
||||||
|
ComponentType,
|
||||||
|
ButtonStyle,
|
||||||
|
APIButtonComponent,
|
||||||
|
APIStringSelectComponent,
|
||||||
|
APIEmbed
|
||||||
|
} from "discord.js";
|
||||||
import { CommandMessage } from "../../../core/types/commands";
|
import { CommandMessage } from "../../../core/types/commands";
|
||||||
|
import type {
|
||||||
|
BlockConfig,
|
||||||
|
PaginationData
|
||||||
|
} from "../../../core/types/displayComponents";
|
||||||
|
import type Amayo from "../../../core/client";
|
||||||
|
import type { JsonValue } from "@prisma/client/runtime/library";
|
||||||
|
|
||||||
|
interface BlockListItem {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
config: JsonValue; // Use Prisma's JsonValue type
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionRowBuilder {
|
||||||
|
type: ComponentType.ActionRow;
|
||||||
|
components: (APIButtonComponent | APIStringSelectComponent)[];
|
||||||
|
}
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: "lista-embeds",
|
name: "lista-embeds",
|
||||||
type: "message",
|
type: "message",
|
||||||
aliases: ["embeds", "ver-embeds", "embedlist"],
|
aliases: ["embeds", "ver-embeds", "embedlist"],
|
||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
run: async (message: any, args: string[], client: any) => {
|
description: "Muestra todos los bloques DisplayComponents configurados en el servidor",
|
||||||
|
category: "Alianzas",
|
||||||
|
usage: "lista-embeds",
|
||||||
|
run: async (message: Message, args: string[], client: Amayo): Promise<void> => {
|
||||||
|
// Permission check
|
||||||
if (!message.member?.permissions.has("Administrator")) {
|
if (!message.member?.permissions.has("Administrator")) {
|
||||||
return message.reply("❌ No tienes permisos de Administrador.");
|
await message.reply("❌ No tienes permisos de Administrador.");
|
||||||
}
|
|
||||||
|
|
||||||
const blocks = await client.prisma.blockV2Config.findMany({
|
|
||||||
where: { guildId: message.guildId! },
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
config: true
|
|
||||||
},
|
|
||||||
orderBy: { name: 'asc' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
const emptyEmbed = {
|
|
||||||
color: 0x5865f2,
|
|
||||||
title: "📚 Centro de Gestión de Bloques",
|
|
||||||
description: "📭 **No hay bloques disponibles**\n\nEste servidor aún no tiene bloques configurados.\n\n🚀 **¿Quieres empezar?**\n• Usa `!crear-embed <nombre>` para crear tu primer bloque\n• Usa `!editar-embed <nombre>` para editar bloques existentes",
|
|
||||||
footer: { text: "Sistema de gestión de bloques • Amayo Bot" }
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRow = {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 3,
|
|
||||||
label: "📝 Crear Primer Bloque",
|
|
||||||
custom_id: "show_create_help"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const helpMessage = await message.reply({
|
|
||||||
embeds: [emptyEmbed],
|
|
||||||
components: [createRow]
|
|
||||||
});
|
|
||||||
|
|
||||||
const helpCollector = helpMessage.createMessageComponentCollector({
|
|
||||||
time: 60000,
|
|
||||||
filter: (i: any) => i.user.id === message.author.id
|
|
||||||
});
|
|
||||||
|
|
||||||
helpCollector.on("collect", async (interaction: any) => {
|
|
||||||
if (interaction.customId === "show_create_help") {
|
|
||||||
const helpEmbed = {
|
|
||||||
color: 0x57f287,
|
|
||||||
title: "📖 Guía de Creación de Bloques",
|
|
||||||
description: "🔧 **Comandos disponibles:**\n\n• `!crear-embed <nombre>` - Crear nuevo bloque\n• `!editar-embed <nombre>` - Editar bloque existente\n• `!eliminar-embed <nombre>` - Eliminar bloque\n• `!lista-embeds` - Ver todos los bloques\n\n💡 **Tip:** Los bloques permiten crear interfaces modernas e interactivas.",
|
|
||||||
footer: { text: "Guía de comandos de creación" }
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [helpEmbed],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dividir bloques en páginas de 5
|
const blocks = await fetchBlocks(client, message.guildId!);
|
||||||
const itemsPerPage = 5;
|
|
||||||
const totalPages = Math.ceil(blocks.length / itemsPerPage);
|
|
||||||
let currentPage = 0;
|
|
||||||
|
|
||||||
const generateBlockListEmbed = (page: number) => {
|
if (blocks.length === 0) {
|
||||||
const startIndex = page * itemsPerPage;
|
await handleEmptyBlocksList(message);
|
||||||
const endIndex = Math.min(startIndex + itemsPerPage, blocks.length);
|
return;
|
||||||
const pageBlocks = blocks.slice(startIndex, endIndex);
|
}
|
||||||
|
|
||||||
let blockListText = `📊 **Página ${page + 1} de ${totalPages}** (${blocks.length} total)\n\n`;
|
|
||||||
|
|
||||||
pageBlocks.forEach((block: any, index: number) => {
|
|
||||||
const globalIndex = startIndex + index + 1;
|
|
||||||
const componentsCount = Array.isArray(block.config?.components) ? block.config.components.length : 0;
|
|
||||||
const hasImage = block.config?.coverImage ? "🖼️" : "";
|
|
||||||
|
|
||||||
blockListText += `**${globalIndex}.** \`${block.name}\` ${hasImage}\n`;
|
|
||||||
blockListText += ` └ ${componentsCount} componente(s) • ID: ${block.id.slice(-8)}\n\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
color: 0x5865f2,
|
|
||||||
title: "📚 Centro de Gestión de Bloques",
|
|
||||||
description: blockListText,
|
|
||||||
footer: { text: `Página ${page + 1}/${totalPages} • ${blocks.length} bloques total` }
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateActionRows = (page: number) => {
|
|
||||||
const rows = [];
|
|
||||||
|
|
||||||
// Select menu para acciones rápidas
|
|
||||||
const currentPageBlocks = blocks.slice(page * itemsPerPage, (page + 1) * itemsPerPage);
|
|
||||||
if (currentPageBlocks.length > 0) {
|
|
||||||
const selectOptions = currentPageBlocks.map((block: any) => ({
|
|
||||||
label: block.name,
|
|
||||||
value: block.name,
|
|
||||||
description: `${Array.isArray(block.config?.components) ? block.config.components.length : 0} componente(s)`,
|
|
||||||
emoji: { name: "⚙️" }
|
|
||||||
}));
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 3,
|
|
||||||
custom_id: "block_actions_select",
|
|
||||||
placeholder: "⚙️ Selecciona un bloque para gestionar...",
|
|
||||||
options: selectOptions
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Botones de navegación y acciones generales
|
|
||||||
const navigationRow: any = {
|
|
||||||
type: 1,
|
|
||||||
components: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navegación
|
|
||||||
if (totalPages > 1) {
|
|
||||||
navigationRow.components.push({
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: "◀️ Anterior",
|
|
||||||
custom_id: "prev_page",
|
|
||||||
disabled: page === 0
|
|
||||||
});
|
|
||||||
|
|
||||||
navigationRow.components.push({
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: `${page + 1}/${totalPages}`,
|
|
||||||
custom_id: "page_info",
|
|
||||||
disabled: true
|
|
||||||
});
|
|
||||||
|
|
||||||
navigationRow.components.push({
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: "▶️ Siguiente",
|
|
||||||
custom_id: "next_page",
|
|
||||||
disabled: page === totalPages - 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Botón de refrescar
|
|
||||||
navigationRow.components.push({
|
|
||||||
type: 2,
|
|
||||||
style: 1,
|
|
||||||
label: "🔄 Refrescar",
|
|
||||||
custom_id: "refresh_list"
|
|
||||||
});
|
|
||||||
|
|
||||||
rows.push(navigationRow);
|
|
||||||
|
|
||||||
// Acciones principales
|
|
||||||
const actionsRow = {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 3,
|
|
||||||
label: "📝 Crear Nuevo",
|
|
||||||
custom_id: "show_create_commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: "📋 Exportar Lista",
|
|
||||||
custom_id: "export_block_list"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 4,
|
|
||||||
label: "🗑️ Eliminar",
|
|
||||||
custom_id: "show_delete_commands"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
rows.push(actionsRow);
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const pagination = createPagination(blocks, 0, 5);
|
||||||
const panelMessage = await message.reply({
|
const panelMessage = await message.reply({
|
||||||
embeds: [generateBlockListEmbed(currentPage)],
|
embeds: [generateBlockListEmbed(pagination)],
|
||||||
components: generateActionRows(currentPage)
|
components: generateActionRows(pagination)
|
||||||
});
|
});
|
||||||
|
|
||||||
const collector = panelMessage.createMessageComponentCollector({
|
await handleInteractions(panelMessage, message, client, pagination);
|
||||||
time: 600000,
|
|
||||||
filter: (i: any) => i.user.id === message.author.id
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.on("collect", async (interaction: any) => {
|
|
||||||
switch (interaction.customId) {
|
|
||||||
case "prev_page":
|
|
||||||
if (currentPage > 0) {
|
|
||||||
currentPage--;
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [generateBlockListEmbed(currentPage)],
|
|
||||||
components: generateActionRows(currentPage)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "next_page":
|
|
||||||
if (currentPage < totalPages - 1) {
|
|
||||||
currentPage++;
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [generateBlockListEmbed(currentPage)],
|
|
||||||
components: generateActionRows(currentPage)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "refresh_list":
|
|
||||||
// Recargar datos
|
|
||||||
const refreshedBlocks = await client.prisma.blockV2Config.findMany({
|
|
||||||
where: { guildId: message.guildId! },
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
config: true
|
|
||||||
},
|
|
||||||
orderBy: { name: 'asc' }
|
|
||||||
});
|
|
||||||
|
|
||||||
blocks.length = 0;
|
|
||||||
blocks.push(...refreshedBlocks);
|
|
||||||
|
|
||||||
const newTotalPages = Math.ceil(blocks.length / itemsPerPage);
|
|
||||||
if (currentPage >= newTotalPages) {
|
|
||||||
currentPage = Math.max(0, newTotalPages - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [generateBlockListEmbed(currentPage)],
|
|
||||||
components: generateActionRows(currentPage)
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "block_actions_select":
|
|
||||||
if (interaction.isStringSelectMenu()) {
|
|
||||||
const selectedBlock = interaction.values[0];
|
|
||||||
|
|
||||||
const blockActionEmbed = {
|
|
||||||
color: 0xff9500,
|
|
||||||
title: `⚙️ Gestión de Bloque: \`${selectedBlock}\``,
|
|
||||||
description: "Selecciona la acción que deseas realizar con este bloque:",
|
|
||||||
footer: { text: "Acciones disponibles para el bloque seleccionado" }
|
|
||||||
};
|
|
||||||
|
|
||||||
const blockActionsRow = {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 1,
|
|
||||||
label: "✏️ Editar",
|
|
||||||
custom_id: `edit_block_${selectedBlock}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: "👁️ Vista Previa",
|
|
||||||
custom_id: `preview_block_${selectedBlock}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: "📋 Duplicar",
|
|
||||||
custom_id: `duplicate_block_${selectedBlock}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 4,
|
|
||||||
label: "🗑️ Eliminar",
|
|
||||||
custom_id: `delete_block_${selectedBlock}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const backRow = {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: "↩️ Volver a la Lista",
|
|
||||||
custom_id: "back_to_list"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [blockActionEmbed],
|
|
||||||
components: [blockActionsRow, backRow]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "back_to_list":
|
|
||||||
await interaction.update({
|
|
||||||
embeds: [generateBlockListEmbed(currentPage)],
|
|
||||||
components: generateActionRows(currentPage)
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "show_create_commands":
|
|
||||||
await interaction.reply({
|
|
||||||
content: `🔧 **Crear nuevos bloques:**\n\n• \`!crear-embed <nombre>\` - Crear bloque básico\n• \`!editar-embed <nombre>\` - Editor avanzado\n\n💡 **Ejemplo:** \`!crear-embed bienvenida\`\n\n📖 **Guía completa:** Los bloques usan DisplayComponents para crear interfaces modernas e interactivas.`,
|
|
||||||
flags: 64
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "show_delete_commands":
|
|
||||||
await interaction.reply({
|
|
||||||
content: `⚠️ **Eliminar bloques:**\n\n• \`!eliminar-embed\` - Panel interactivo de eliminación\n• \`!eliminar-embed <nombre>\` - Eliminación directa\n\n❗ **Advertencia:** La eliminación es irreversible.`,
|
|
||||||
flags: 64
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "export_block_list":
|
|
||||||
const exportText = blocks.map((block: any, index: number) => {
|
|
||||||
const componentsCount = Array.isArray(block.config?.components) ? block.config.components.length : 0;
|
|
||||||
return `${index + 1}. ${block.name} (${componentsCount} componentes) - ID: ${block.id}`;
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
await interaction.reply({
|
|
||||||
content: `📋 **Lista Exportada:**\n\`\`\`\n${exportText}\`\`\``,
|
|
||||||
flags: 64
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Manejar acciones específicas de bloques
|
|
||||||
if (interaction.customId.startsWith("edit_block_")) {
|
|
||||||
const blockName = interaction.customId.replace("edit_block_", "");
|
|
||||||
await interaction.reply({
|
|
||||||
content: `Usa: \`!editar-embed ${blockName}\``,
|
|
||||||
flags: 64
|
|
||||||
});
|
|
||||||
} else if (interaction.customId.startsWith("delete_block_")) {
|
|
||||||
const blockName = interaction.customId.replace("delete_block_", "");
|
|
||||||
await interaction.reply({
|
|
||||||
content: `Usa: \`!eliminar-embed ${blockName}\` para eliminar este bloque de forma segura.`,
|
|
||||||
flags: 64
|
|
||||||
});
|
|
||||||
} else if (interaction.customId.startsWith("preview_block_")) {
|
|
||||||
const blockName = interaction.customId.replace("preview_block_", "");
|
|
||||||
await interaction.reply({
|
|
||||||
content: `Vista previa de \`${blockName}\` - Funcionalidad en desarrollo`,
|
|
||||||
flags: 64
|
|
||||||
});
|
|
||||||
} else if (interaction.customId.startsWith("duplicate_block_")) {
|
|
||||||
const blockName = interaction.customId.replace("duplicate_block_", "");
|
|
||||||
await interaction.reply({
|
|
||||||
content: `Funcionalidad de duplicación de \`${blockName}\` en desarrollo`,
|
|
||||||
flags: 64
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.on("end", async (collected: any, reason: string) => {
|
|
||||||
if (reason === "time") {
|
|
||||||
const timeoutEmbed = {
|
|
||||||
color: 0x36393f,
|
|
||||||
title: "⏰ Panel Expirado",
|
|
||||||
description: "El panel de gestión ha expirado por inactividad.\n\nUsa `!lista-embeds` para abrir un nuevo panel de gestión.",
|
|
||||||
footer: { text: "Panel expirado por inactividad" }
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await panelMessage.edit({
|
|
||||||
embeds: [timeoutEmbed],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// Mensaje eliminado o error de edición
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function fetchBlocks(client: Amayo, guildId: string): Promise<BlockListItem[]> {
|
||||||
|
return await client.prisma.blockV2Config.findMany({
|
||||||
|
where: { guildId },
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
id: true,
|
||||||
|
config: true
|
||||||
|
},
|
||||||
|
orderBy: { name: 'asc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEmptyBlocksList(message: Message): Promise<void> {
|
||||||
|
const emptyEmbed: APIEmbed = {
|
||||||
|
color: 0x5865f2,
|
||||||
|
title: "📚 Centro de Gestión de Bloques",
|
||||||
|
description: "📭 **No hay bloques disponibles**\n\nEste servidor aún no tiene bloques configurados.\n\n🚀 **¿Quieres empezar?**\n• Usa `!crear-embed <nombre>` para crear tu primer bloque\n• Usa `!editar-embed <nombre>` para editar bloques existentes",
|
||||||
|
footer: { text: "Sistema de gestión de bloques • Amayo Bot" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRow: ActionRowBuilder = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Success,
|
||||||
|
label: "📝 Crear Primer Bloque",
|
||||||
|
custom_id: "show_create_help"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const helpMessage = await message.reply({
|
||||||
|
embeds: [emptyEmbed],
|
||||||
|
components: [createRow]
|
||||||
|
});
|
||||||
|
|
||||||
|
const helpCollector = helpMessage.createMessageComponentCollector({
|
||||||
|
time: 60000,
|
||||||
|
filter: (interaction: MessageComponentInteraction) => interaction.user.id === message.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
helpCollector.on("collect", async (interaction: ButtonInteraction) => {
|
||||||
|
if (interaction.customId === "show_create_help") {
|
||||||
|
const helpEmbed: APIEmbed = {
|
||||||
|
color: 0x57f287,
|
||||||
|
title: "📖 Guía de Creación de Bloques",
|
||||||
|
description: "🔧 **Comandos disponibles:**\n\n• `!crear-embed <nombre>` - Crear nuevo bloque\n• `!editar-embed <nombre>` - Editar bloque existente\n• `!eliminar-embed <nombre>` - Eliminar bloque\n• `!lista-embeds` - Ver todos los bloques\n\n💡 **Tip:** Los bloques permiten crear interfaces modernas e interactivas.",
|
||||||
|
footer: { text: "Guía de comandos de creación" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [helpEmbed],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPagination(blocks: BlockListItem[], currentPage: number, itemsPerPage: number): PaginationData<BlockListItem> {
|
||||||
|
const totalPages = Math.ceil(blocks.length / itemsPerPage);
|
||||||
|
return {
|
||||||
|
items: blocks,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
itemsPerPage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBlockListEmbed(pagination: PaginationData<BlockListItem>): APIEmbed {
|
||||||
|
const { items, currentPage, totalPages, itemsPerPage } = pagination;
|
||||||
|
const startIndex = currentPage * itemsPerPage;
|
||||||
|
const endIndex = Math.min(startIndex + itemsPerPage, items.length);
|
||||||
|
const pageBlocks = items.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
let blockListText = `📊 **Página ${currentPage + 1} de ${totalPages}** (${items.length} total)\n\n`;
|
||||||
|
|
||||||
|
pageBlocks.forEach((block, index) => {
|
||||||
|
const globalIndex = startIndex + index + 1;
|
||||||
|
const config = block.config as BlockConfig;
|
||||||
|
const componentsCount = Array.isArray(config?.components) ? config.components.length : 0;
|
||||||
|
const hasImage = config?.coverImage ? "🖼️" : "";
|
||||||
|
|
||||||
|
blockListText += `**${globalIndex}.** \`${block.name}\` ${hasImage}\n`;
|
||||||
|
blockListText += ` └ ${componentsCount} componente(s) • ID: ${block.id.slice(-8)}\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: 0x5865f2,
|
||||||
|
title: "📚 Centro de Gestión de Bloques",
|
||||||
|
description: blockListText,
|
||||||
|
footer: { text: `Página ${currentPage + 1}/${totalPages} • ${items.length} bloques total` }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateActionRows(pagination: PaginationData<BlockListItem>): ActionRowBuilder[] {
|
||||||
|
const rows: ActionRowBuilder[] = [];
|
||||||
|
const { items, currentPage, totalPages, itemsPerPage } = pagination;
|
||||||
|
|
||||||
|
// Select menu for quick actions on current page blocks
|
||||||
|
const currentPageBlocks = items.slice(currentPage * itemsPerPage, (currentPage + 1) * itemsPerPage);
|
||||||
|
if (currentPageBlocks.length > 0) {
|
||||||
|
const selectOptions = currentPageBlocks.map((block) => {
|
||||||
|
const config = block.config as BlockConfig;
|
||||||
|
const componentsCount = Array.isArray(config?.components) ? config.components.length : 0;
|
||||||
|
return {
|
||||||
|
label: block.name,
|
||||||
|
value: block.name,
|
||||||
|
description: `${componentsCount} componente(s)`,
|
||||||
|
emoji: { name: "⚙️" }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.StringSelect,
|
||||||
|
custom_id: "block_actions_select",
|
||||||
|
placeholder: "⚙️ Selecciona un bloque para gestionar...",
|
||||||
|
options: selectOptions
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation buttons
|
||||||
|
const navigationComponents: APIButtonComponent[] = [];
|
||||||
|
|
||||||
|
if (totalPages > 1) {
|
||||||
|
navigationComponents.push(
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "◀️ Anterior",
|
||||||
|
custom_id: "prev_page",
|
||||||
|
disabled: currentPage === 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: `${currentPage + 1}/${totalPages}`,
|
||||||
|
custom_id: "page_info",
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "▶️ Siguiente",
|
||||||
|
custom_id: "next_page",
|
||||||
|
disabled: currentPage === totalPages - 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationComponents.push({
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
label: "🔄 Refrescar",
|
||||||
|
custom_id: "refresh_list"
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: navigationComponents
|
||||||
|
});
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
rows.push({
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Success,
|
||||||
|
label: "📝 Crear Nuevo",
|
||||||
|
custom_id: "show_create_commands"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📋 Exportar Lista",
|
||||||
|
custom_id: "export_block_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "🗑️ Eliminar",
|
||||||
|
custom_id: "show_delete_commands"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleInteractions(
|
||||||
|
panelMessage: Message,
|
||||||
|
originalMessage: Message,
|
||||||
|
client: Amayo,
|
||||||
|
pagination: PaginationData<BlockListItem>
|
||||||
|
): Promise<void> {
|
||||||
|
const collector = panelMessage.createMessageComponentCollector({
|
||||||
|
time: 600000,
|
||||||
|
filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("collect", async (interaction: MessageComponentInteraction) => {
|
||||||
|
try {
|
||||||
|
if (interaction.isButton()) {
|
||||||
|
await handleButtonInteraction(interaction, client, pagination, originalMessage.guildId!);
|
||||||
|
} else if (interaction.isStringSelectMenu()) {
|
||||||
|
await handleSelectMenuInteraction(interaction);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling interaction:", error);
|
||||||
|
if (!interaction.replied && !interaction.deferred) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "❌ Ocurrió un error al procesar la interacción.",
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("end", async (collected, reason) => {
|
||||||
|
if (reason === "time") {
|
||||||
|
await handleCollectorTimeout(panelMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleButtonInteraction(
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
client: Amayo,
|
||||||
|
pagination: PaginationData<BlockListItem>,
|
||||||
|
guildId: string
|
||||||
|
): Promise<void> {
|
||||||
|
switch (interaction.customId) {
|
||||||
|
case "prev_page":
|
||||||
|
if (pagination.currentPage > 0) {
|
||||||
|
pagination.currentPage--;
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [generateBlockListEmbed(pagination)],
|
||||||
|
components: generateActionRows(pagination)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "next_page":
|
||||||
|
if (pagination.currentPage < pagination.totalPages - 1) {
|
||||||
|
pagination.currentPage++;
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [generateBlockListEmbed(pagination)],
|
||||||
|
components: generateActionRows(pagination)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "refresh_list":
|
||||||
|
const refreshedBlocks = await fetchBlocks(client, guildId);
|
||||||
|
pagination.items = refreshedBlocks;
|
||||||
|
pagination.totalPages = Math.ceil(refreshedBlocks.length / pagination.itemsPerPage);
|
||||||
|
|
||||||
|
if (pagination.currentPage >= pagination.totalPages) {
|
||||||
|
pagination.currentPage = Math.max(0, pagination.totalPages - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [generateBlockListEmbed(pagination)],
|
||||||
|
components: generateActionRows(pagination)
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "show_create_commands":
|
||||||
|
await interaction.reply({
|
||||||
|
content: `🔧 **Crear nuevos bloques:**\n\n• \`!crear-embed <nombre>\` - Crear bloque básico\n• \`!editar-embed <nombre>\` - Editor avanzado\n\n💡 **Ejemplo:** \`!crear-embed bienvenida\`\n\n📖 **Guía completa:** Los bloques usan DisplayComponents para crear interfaces modernas e interactivas.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "show_delete_commands":
|
||||||
|
await interaction.reply({
|
||||||
|
content: `⚠️ **Eliminar bloques:**\n\n• \`!eliminar-embed\` - Panel interactivo de eliminación\n• \`!eliminar-embed <nombre>\` - Eliminación directa\n\n❗ **Advertencia:** La eliminación es irreversible.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "export_block_list":
|
||||||
|
const exportText = pagination.items.map((block, index) => {
|
||||||
|
const config = block.config as BlockConfig;
|
||||||
|
const componentsCount = Array.isArray(config?.components) ? config.components.length : 0;
|
||||||
|
return `${index + 1}. ${block.name} (${componentsCount} componentes) - ID: ${block.id}`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `📋 **Lista Exportada:**\n\`\`\`\n${exportText}\`\`\``,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "back_to_list":
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [generateBlockListEmbed(pagination)],
|
||||||
|
components: generateActionRows(pagination)
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
await handleSpecificBlockActions(interaction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSelectMenuInteraction(interaction: StringSelectMenuInteraction): Promise<void> {
|
||||||
|
if (interaction.customId === "block_actions_select") {
|
||||||
|
const selectedBlock = interaction.values[0];
|
||||||
|
|
||||||
|
const blockActionEmbed: APIEmbed = {
|
||||||
|
color: 0xff9500,
|
||||||
|
title: `⚙️ Gestión de Bloque: \`${selectedBlock}\``,
|
||||||
|
description: "Selecciona la acción que deseas realizar con este bloque:",
|
||||||
|
footer: { text: "Acciones disponibles para el bloque seleccionado" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockActionsRow: ActionRowBuilder = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
label: "✏️ Editar",
|
||||||
|
custom_id: `edit_block_${selectedBlock}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "👁️ Vista Previa",
|
||||||
|
custom_id: `preview_block_${selectedBlock}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📋 Duplicar",
|
||||||
|
custom_id: `duplicate_block_${selectedBlock}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "🗑️ Eliminar",
|
||||||
|
custom_id: `delete_block_${selectedBlock}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const backRow: ActionRowBuilder = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "↩️ Volver a la Lista",
|
||||||
|
custom_id: "back_to_list"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [blockActionEmbed],
|
||||||
|
components: [blockActionsRow, backRow]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSpecificBlockActions(interaction: ButtonInteraction): Promise<void> {
|
||||||
|
const { customId } = interaction;
|
||||||
|
|
||||||
|
if (customId.startsWith("edit_block_")) {
|
||||||
|
const blockName = customId.replace("edit_block_", "");
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Usa: \`!editar-embed ${blockName}\``,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else if (customId.startsWith("delete_block_")) {
|
||||||
|
const blockName = customId.replace("delete_block_", "");
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Usa: \`!eliminar-embed ${blockName}\` para eliminar este bloque de forma segura.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else if (customId.startsWith("preview_block_")) {
|
||||||
|
const blockName = customId.replace("preview_block_", "");
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Vista previa de \`${blockName}\` - Funcionalidad en desarrollo`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else if (customId.startsWith("duplicate_block_")) {
|
||||||
|
const blockName = customId.replace("duplicate_block_", "");
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Funcionalidad de duplicación de \`${blockName}\` en desarrollo`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCollectorTimeout(panelMessage: Message): Promise<void> {
|
||||||
|
const timeoutEmbed: APIEmbed = {
|
||||||
|
color: 0x36393f,
|
||||||
|
title: "⏰ Panel Expirado",
|
||||||
|
description: "El panel de gestión ha expirado por inactividad.\n\nUsa `!lista-embeds` para abrir un nuevo panel de gestión.",
|
||||||
|
footer: { text: "Panel expirado por inactividad" }
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await panelMessage.edit({
|
||||||
|
embeds: [timeoutEmbed],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Message was deleted or other edit error - ignore
|
||||||
|
console.log("Could not edit message on timeout, likely deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
|
import {
|
||||||
|
Message,
|
||||||
|
ButtonInteraction,
|
||||||
|
MessageComponentInteraction,
|
||||||
|
ComponentType,
|
||||||
|
ButtonStyle,
|
||||||
|
MessageFlags
|
||||||
|
} from "discord.js";
|
||||||
import { CommandMessage } from "../../../core/types/commands";
|
import { CommandMessage } from "../../../core/types/commands";
|
||||||
|
import type {
|
||||||
|
DisplayComponentContainer
|
||||||
|
} from "../../../core/types/displayComponents";
|
||||||
|
import type Amayo from "../../../core/client";
|
||||||
|
|
||||||
|
interface ActionRowBuilder {
|
||||||
|
type: ComponentType.ActionRow;
|
||||||
|
components: any[]; // Discord.js API components
|
||||||
|
}
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: "displaydemo",
|
name: "displaydemo",
|
||||||
@@ -8,401 +25,450 @@ export const command: CommandMessage = {
|
|||||||
description: "Demostración de DisplayComponents con accesorios y acciones.",
|
description: "Demostración de DisplayComponents con accesorios y acciones.",
|
||||||
category: "Alianzas",
|
category: "Alianzas",
|
||||||
usage: "displaydemo",
|
usage: "displaydemo",
|
||||||
run: async (message, args, client) => {
|
run: async (message: Message, _args: string[], _client: Amayo): Promise<void> => {
|
||||||
if (!message.member?.permissions.has("Administrator")) {
|
if (!message.member?.permissions.has("Administrator")) {
|
||||||
await message.reply("❌ No tienes permisos de Administrador.");
|
await message.reply("❌ No tienes permisos de Administrador.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 DEMOSTRACIÓN COMPLETA DE DISPLAYCOMPONENTS CON ACCESORIOS
|
const mainPanel = createMainPanel(message);
|
||||||
|
const actionRow = createActionRow();
|
||||||
// Panel principal con accessory de thumbnail
|
|
||||||
const mainPanel = {
|
|
||||||
type: 17, // Container
|
|
||||||
accent_color: 0x5865f2,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10, // TextDisplay
|
|
||||||
content: "🎨 **Demostración de DisplayComponents Avanzados**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14, // Separator
|
|
||||||
divider: true,
|
|
||||||
spacing: 2
|
|
||||||
},
|
|
||||||
// Sección con accessory de botón
|
|
||||||
{
|
|
||||||
type: 9, // Section
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🔘 **Sección con Botón Accesorio**\n\nEste texto aparece junto a un botón como accesorio. Los accesorios permiten añadir elementos interactivos sin ocupar una fila completa."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2, // Button
|
|
||||||
style: 1, // Primary
|
|
||||||
label: "Acción Rápida",
|
|
||||||
custom_id: "quick_action",
|
|
||||||
emoji: { name: "⚡" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: true,
|
|
||||||
spacing: 1
|
|
||||||
},
|
|
||||||
// Sección con accessory de thumbnail
|
|
||||||
{
|
|
||||||
type: 9, // Section
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🖼️ **Sección con Thumbnail**\n\nAquí se muestra texto con una imagen en miniatura como accesorio. Perfecto para mostrar íconos de servidores, avatares o logotipos."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 11, // Thumbnail
|
|
||||||
media: {
|
|
||||||
url: message.guild?.iconURL({ forceStatic: false }) || "https://cdn.discordapp.com/embed/avatars/0.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: true,
|
|
||||||
spacing: 1
|
|
||||||
},
|
|
||||||
// Sección con accessory de link button
|
|
||||||
{
|
|
||||||
type: 9, // Section
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🔗 **Sección con Botón de Enlace**\n\nEste tipo de accesorio permite enlaces externos directos sin necesidad de interacciones complejas."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2, // Button
|
|
||||||
style: 5, // Link
|
|
||||||
label: "Ir a Discord",
|
|
||||||
url: "https://discord.com",
|
|
||||||
emoji: { name: "🚀" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fila de botones normales para interacción
|
|
||||||
const actionRow = {
|
|
||||||
type: 1, // ActionRow
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 3, // Success
|
|
||||||
label: "✨ Más Ejemplos",
|
|
||||||
custom_id: "show_more_examples"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2, // Secondary
|
|
||||||
label: "🔄 Cambiar Estilos",
|
|
||||||
custom_id: "change_styles"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 4, // Danger
|
|
||||||
label: "❌ Cerrar",
|
|
||||||
custom_id: "close_demo"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const demoMessage = await message.reply({
|
const demoMessage = await message.reply({
|
||||||
// Activar Display Components V2 y mantener SuppressEmbeds
|
// Enable Display Components V2 and suppress embeds
|
||||||
flags: (32768 | 4096),
|
flags: MessageFlags.SuppressEmbeds | 32768,
|
||||||
components: [mainPanel, actionRow]
|
components: [mainPanel, actionRow]
|
||||||
});
|
});
|
||||||
|
|
||||||
const collector = demoMessage.createMessageComponentCollector({
|
await handleDemoInteractions(demoMessage, message);
|
||||||
time: 300000, // 5 minutos
|
},
|
||||||
filter: (i: any) => i.user.id === message.author.id
|
};
|
||||||
});
|
|
||||||
|
|
||||||
collector.on("collect", async (interaction: any) => {
|
function createMainPanel(message: Message): DisplayComponentContainer {
|
||||||
|
return {
|
||||||
|
type: 17, // Container
|
||||||
|
accent_color: 0x5865f2,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10, // TextDisplay
|
||||||
|
content: "🎨 **Demostración de DisplayComponents Avanzados**"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14, // Separator
|
||||||
|
divider: true,
|
||||||
|
spacing: 2
|
||||||
|
},
|
||||||
|
// Section with button accessory
|
||||||
|
{
|
||||||
|
type: 9, // Section
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🔘 **Sección con Botón Accesorio**\n\nEste texto aparece junto a un botón como accesorio. Los accesorios permiten añadir elementos interactivos sin ocupar una fila completa."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 2, // Button
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
label: "Acción Rápida",
|
||||||
|
custom_id: "quick_action",
|
||||||
|
emoji: { name: "⚡" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: true,
|
||||||
|
spacing: 1
|
||||||
|
},
|
||||||
|
// Section with thumbnail accessory
|
||||||
|
{
|
||||||
|
type: 9, // Section
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🖼️ **Sección con Thumbnail**\n\nAquí se muestra texto con una imagen en miniatura como accesorio. Perfecto para mostrar íconos de servidores, avatares o logotipos."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 11, // Thumbnail
|
||||||
|
media: {
|
||||||
|
url: message.guild?.iconURL({ forceStatic: false }) || "https://cdn.discordapp.com/embed/avatars/0.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: true,
|
||||||
|
spacing: 1
|
||||||
|
},
|
||||||
|
// Section with link button accessory
|
||||||
|
{
|
||||||
|
type: 9, // Section
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🔗 **Sección con Botón de Enlace**\n\nEste tipo de accesorio permite enlaces externos directos sin necesidad de interacciones complejas."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 2, // Button
|
||||||
|
style: ButtonStyle.Link,
|
||||||
|
label: "Ir a Discord",
|
||||||
|
url: "https://discord.com",
|
||||||
|
emoji: { name: "🚀" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createActionRow(): ActionRowBuilder {
|
||||||
|
return {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Success,
|
||||||
|
label: "✨ Más Ejemplos",
|
||||||
|
custom_id: "show_more_examples"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "🔄 Cambiar Estilos",
|
||||||
|
custom_id: "change_styles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "❌ Cerrar",
|
||||||
|
custom_id: "close_demo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDemoInteractions(demoMessage: Message, originalMessage: Message): Promise<void> {
|
||||||
|
const collector = demoMessage.createMessageComponentCollector({
|
||||||
|
time: 300000, // 5 minutes
|
||||||
|
filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("collect", async (interaction: ButtonInteraction) => {
|
||||||
|
try {
|
||||||
switch (interaction.customId) {
|
switch (interaction.customId) {
|
||||||
case "quick_action":
|
case "quick_action":
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: "⚡ **Acción Rápida Ejecutada!**\n\nEste botón estaba como accesorio en una sección.",
|
content: "⚡ **Acción Rápida Ejecutada!**\n\nEste botón estaba como accesorio en una sección.",
|
||||||
flags: 64 // Ephemeral
|
ephemeral: true
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "show_more_examples":
|
case "show_more_examples":
|
||||||
// Panel con múltiples ejemplos de accesorios
|
await handleMoreExamples(interaction, originalMessage);
|
||||||
const examplesPanel = {
|
|
||||||
type: 17,
|
|
||||||
accent_color: 0xff9500,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🎯 **Más Ejemplos de Accesorios**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: true,
|
|
||||||
spacing: 2
|
|
||||||
},
|
|
||||||
// Ejemplo con avatar del usuario
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: `👤 **Perfil de ${message.author.username}**\n\nEjemplo usando tu avatar como thumbnail accesorio.`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 11,
|
|
||||||
media: {
|
|
||||||
url: message.author.displayAvatarURL({ forceStatic: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: false,
|
|
||||||
spacing: 1
|
|
||||||
},
|
|
||||||
// Ejemplo con botón de estilo diferente
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🎨 **Botones con Diferentes Estilos**\n\nLos accesorios pueden tener distintos estilos y emojis personalizados."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2,
|
|
||||||
style: 4, // Danger
|
|
||||||
label: "Peligro",
|
|
||||||
custom_id: "danger_button",
|
|
||||||
emoji: { name: "⚠️" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: false,
|
|
||||||
spacing: 1
|
|
||||||
},
|
|
||||||
// Imagen como accessory
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🖼️ **Imágenes Personalizadas**\n\nTambién puedes usar imágenes personalizadas, íconos de servidores invitados, etc."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 11,
|
|
||||||
media: {
|
|
||||||
url: "https://cdn.discordapp.com/attachments/123/456/discord-logo.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
components: [examplesPanel, {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2, // Secondary
|
|
||||||
label: "↩️ Volver",
|
|
||||||
custom_id: "back_to_main"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "change_styles":
|
case "change_styles":
|
||||||
// Panel mostrando diferentes combinaciones de estilos
|
await handleStylesDemo(interaction);
|
||||||
const stylesPanel = {
|
|
||||||
type: 17,
|
|
||||||
accent_color: 0x57f287,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🎨 **Galería de Estilos**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: true,
|
|
||||||
spacing: 2
|
|
||||||
},
|
|
||||||
// Botón Primary como accesorio
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🔵 **Botón Primary (Azul)**\nEstilo: 1 - Para acciones principales"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2,
|
|
||||||
style: 1, // Primary
|
|
||||||
label: "Principal",
|
|
||||||
custom_id: "style_primary"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Botón Secondary como accesorio
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "⚫ **Botón Secondary (Gris)**\nEstilo: 2 - Para acciones secundarias"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2,
|
|
||||||
style: 2, // Secondary
|
|
||||||
label: "Secundario",
|
|
||||||
custom_id: "style_secondary"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Botón Success como accesorio
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🟢 **Botón Success (Verde)**\nEstilo: 3 - Para confirmar acciones"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2,
|
|
||||||
style: 3, // Success
|
|
||||||
label: "Confirmar",
|
|
||||||
custom_id: "style_success"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Botón Danger como accesorio
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "🔴 **Botón Danger (Rojo)**\nEstilo: 4 - Para acciones destructivas"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2,
|
|
||||||
style: 4, // Danger
|
|
||||||
label: "Eliminar",
|
|
||||||
custom_id: "style_danger"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
components: [stylesPanel, {
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
style: 2,
|
|
||||||
label: "↩️ Volver",
|
|
||||||
custom_id: "back_to_main"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "danger_button":
|
|
||||||
case "style_primary":
|
|
||||||
case "style_secondary":
|
|
||||||
case "style_success":
|
|
||||||
case "style_danger":
|
|
||||||
await interaction.reply({
|
|
||||||
content: `🎯 **Botón ${interaction.customId.replace('style_', '').replace('_', ' ')} activado!**\n\nEste botón era un accesorio de una sección.`,
|
|
||||||
flags: 64 // Ephemeral
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "back_to_main":
|
case "back_to_main":
|
||||||
|
const mainPanel = createMainPanel(originalMessage);
|
||||||
|
const actionRow = createActionRow();
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
components: [mainPanel, actionRow]
|
components: [mainPanel, actionRow]
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "close_demo":
|
case "close_demo":
|
||||||
const closedPanel = {
|
await handleCloseDemo(interaction);
|
||||||
type: 17,
|
|
||||||
accent_color: 0x36393f,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "✅ **Demostración Finalizada**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: true,
|
|
||||||
spacing: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "Gracias por probar DisplayComponents con accesorios!\n\n💡 **Recuerda:** Los accesorios son ideales para:\n• Botones de acción rápida\n• Thumbnails e íconos\n• Enlaces externos\n• Elementos decorativos"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await interaction.update({
|
|
||||||
components: [closedPanel]
|
|
||||||
});
|
|
||||||
collector.stop();
|
collector.stop();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
await handleStyleButtons(interaction);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error("Error handling demo interaction:", error);
|
||||||
|
if (!interaction.replied && !interaction.deferred) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "❌ Ocurrió un error al procesar la interacción.",
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
collector.on("end", async (collected: any, reason: string) => {
|
collector.on("end", async (collected, reason) => {
|
||||||
if (reason === "time") {
|
if (reason === "time") {
|
||||||
try {
|
await handleDemoTimeout(demoMessage);
|
||||||
const timeoutPanel = {
|
}
|
||||||
type: 17,
|
});
|
||||||
accent_color: 0x36393f,
|
}
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "⏰ **Demostración Expirada**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 14,
|
|
||||||
divider: true,
|
|
||||||
spacing: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
content: "La demostración ha expirado por inactividad.\nUsa `!displaydemo` nuevamente para verla."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await demoMessage.edit({
|
async function handleMoreExamples(interaction: ButtonInteraction, originalMessage: Message): Promise<void> {
|
||||||
components: [timeoutPanel]
|
const examplesPanel: DisplayComponentContainer = {
|
||||||
});
|
type: 17,
|
||||||
} catch (error) {
|
accent_color: 0xff9500,
|
||||||
// Mensaje eliminado o error de edición
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🎯 **Más Ejemplos de Accesorios**"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: true,
|
||||||
|
spacing: 2
|
||||||
|
},
|
||||||
|
// Example with user avatar
|
||||||
|
{
|
||||||
|
type: 9,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: `👤 **Perfil de ${originalMessage.author.username}**\n\nEjemplo usando tu avatar como thumbnail accesorio.`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 11,
|
||||||
|
media: {
|
||||||
|
url: originalMessage.author.displayAvatarURL({ forceStatic: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: false,
|
||||||
|
spacing: 1
|
||||||
|
},
|
||||||
|
// Example with different button style
|
||||||
|
{
|
||||||
|
type: 9,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🎨 **Botones con Diferentes Estilos**\n\nLos accesorios pueden tener distintos estilos y emojis personalizados."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "Peligro",
|
||||||
|
custom_id: "danger_button",
|
||||||
|
emoji: { name: "⚠️" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: false,
|
||||||
|
spacing: 1
|
||||||
|
},
|
||||||
|
// Custom image as accessory
|
||||||
|
{
|
||||||
|
type: 9,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🖼️ **Imágenes Personalizadas**\n\nTambién puedes usar imágenes personalizadas, íconos de servidores invitados, etc."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 11,
|
||||||
|
media: {
|
||||||
|
url: "https://cdn.discordapp.com/attachments/1234/5678/discord-logo.png"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const backRow: ActionRowBuilder = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "↩️ Volver",
|
||||||
|
custom_id: "back_to_main"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
components: [examplesPanel, backRow]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStylesDemo(interaction: ButtonInteraction): Promise<void> {
|
||||||
|
const stylesPanel: DisplayComponentContainer = {
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0x57f287,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🎨 **Galería de Estilos**"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: true,
|
||||||
|
spacing: 2
|
||||||
|
},
|
||||||
|
// Primary button as accessory
|
||||||
|
{
|
||||||
|
type: 9,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🔵 **Botón Primary (Azul)**\nEstilo: 1 - Para acciones principales"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
label: "Principal",
|
||||||
|
custom_id: "style_primary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Secondary button as accessory
|
||||||
|
{
|
||||||
|
type: 9,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "⚫ **Botón Secondary (Gris)**\nEstilo: 2 - Para acciones secundarias"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "Secundario",
|
||||||
|
custom_id: "style_secondary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Success button as accessory
|
||||||
|
{
|
||||||
|
type: 9,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🟢 **Botón Success (Verde)**\nEstilo: 3 - Para confirmar acciones"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Success,
|
||||||
|
label: "Confirmar",
|
||||||
|
custom_id: "style_success"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Danger button as accessory
|
||||||
|
{
|
||||||
|
type: 9,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "🔴 **Botón Danger (Rojo)**\nEstilo: 4 - Para acciones destructivas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "Eliminar",
|
||||||
|
custom_id: "style_danger"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const backRow: ActionRowBuilder = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "↩️ Volver",
|
||||||
|
custom_id: "back_to_main"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
components: [stylesPanel, backRow]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStyleButtons(interaction: ButtonInteraction): Promise<void> {
|
||||||
|
const styleMap: Record<string, string> = {
|
||||||
|
"danger_button": "Peligro",
|
||||||
|
"style_primary": "Primary",
|
||||||
|
"style_secondary": "Secondary",
|
||||||
|
"style_success": "Success",
|
||||||
|
"style_danger": "Danger"
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleName = styleMap[interaction.customId];
|
||||||
|
if (styleName) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: `🎯 **Botón ${styleName} activado!**\n\nEste botón era un accesorio de una sección.`,
|
||||||
|
ephemeral: true
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
async function handleCloseDemo(interaction: ButtonInteraction): Promise<void> {
|
||||||
|
const closedPanel: DisplayComponentContainer = {
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0x36393f,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "✅ **Demostración Finalizada**"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: true,
|
||||||
|
spacing: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "Gracias por probar DisplayComponents con accesorios!\n\n💡 **Recuerda:** Los accesorios son ideales para:\n• Botones de acción rápida\n• Thumbnails e íconos\n• Enlaces externos\n• Elementos decorativos"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
components: [closedPanel]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDemoTimeout(demoMessage: Message): Promise<void> {
|
||||||
|
try {
|
||||||
|
const timeoutPanel: DisplayComponentContainer = {
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0x36393f,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "⏰ **Demostración Expirada**"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 14,
|
||||||
|
divider: true,
|
||||||
|
spacing: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: "La demostración ha expirado por inactividad.\nUsa `!displaydemo` nuevamente para verla."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await demoMessage.edit({
|
||||||
|
components: [timeoutPanel]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Message was deleted or other edit error - ignore
|
||||||
|
console.log("Could not edit demo message on timeout, likely deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
408
src/core/types/displayComponentEditor.ts
Normal file
408
src/core/types/displayComponentEditor.ts
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import {
|
||||||
|
ButtonStyle,
|
||||||
|
ComponentType,
|
||||||
|
APIButtonComponent,
|
||||||
|
APISelectMenuComponent,
|
||||||
|
GuildMember,
|
||||||
|
Guild
|
||||||
|
} from 'discord.js';
|
||||||
|
import type {
|
||||||
|
DisplayComponent,
|
||||||
|
DisplayComponentContainer,
|
||||||
|
BlockConfig
|
||||||
|
} from './displayComponents';
|
||||||
|
import { replaceVars, isValidUrlOrVariable } from '../lib/vars';
|
||||||
|
|
||||||
|
// Editor-specific component types (how we store configuration while editing)
|
||||||
|
export interface EditorTextDisplay {
|
||||||
|
type: 10; // TextDisplay
|
||||||
|
content: string;
|
||||||
|
thumbnail?: string | null; // optional image URL
|
||||||
|
linkButton?: LinkButton | null; // optional link button accessory
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorSeparator {
|
||||||
|
type: 14; // Separator
|
||||||
|
divider?: boolean;
|
||||||
|
spacing?: number; // 1-3 typical
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorImage {
|
||||||
|
type: 12; // Image/Media
|
||||||
|
url: string; // single image URL (later rendered as items: [{ media: { url } }])
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EditorComponent = EditorTextDisplay | EditorSeparator | EditorImage;
|
||||||
|
|
||||||
|
// Block state for editing/creating
|
||||||
|
export interface BlockState {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
color?: number;
|
||||||
|
coverImage?: string;
|
||||||
|
components: EditorComponent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emoji input types
|
||||||
|
export interface CustomEmoji {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
animated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnicodeEmoji {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EmojiInput = CustomEmoji | UnicodeEmoji;
|
||||||
|
|
||||||
|
// Link button configuration
|
||||||
|
export interface LinkButton {
|
||||||
|
url: string;
|
||||||
|
label?: string;
|
||||||
|
emoji?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action row builders
|
||||||
|
export interface EditorActionRow {
|
||||||
|
type: ComponentType.ActionRow;
|
||||||
|
components: APIButtonComponent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectActionRow {
|
||||||
|
type: ComponentType.ActionRow;
|
||||||
|
components: APISelectMenuComponent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
export class DisplayComponentUtils {
|
||||||
|
/**
|
||||||
|
* Validates if a URL is valid or a system variable
|
||||||
|
*/
|
||||||
|
static isValidUrl(url: string): boolean {
|
||||||
|
return isValidUrlOrVariable(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and cleans content for Discord
|
||||||
|
*/
|
||||||
|
static validateContent(content: string): string {
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return "Sin contenido";
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = content.trim();
|
||||||
|
if (cleaned.length === 0) {
|
||||||
|
return "Sin contenido";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate if exceeds Discord limit (4000 characters)
|
||||||
|
if (cleaned.length > 4000) {
|
||||||
|
return cleaned.substring(0, 3997) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses emoji input (unicode or custom <a:name:id> / <:name:id>)
|
||||||
|
*/
|
||||||
|
static parseEmojiInput(input?: string): EmojiInput | null {
|
||||||
|
if (!input) return null;
|
||||||
|
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^<(a?):(\w+):(\d+)>$/);
|
||||||
|
if (match) {
|
||||||
|
const animated = match[1] === 'a';
|
||||||
|
const name = match[2];
|
||||||
|
const id = match[3];
|
||||||
|
return { id, name, animated };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume unicode if not custom emoji format
|
||||||
|
return { name: trimmed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a link accessory for Display Components
|
||||||
|
*/
|
||||||
|
static async buildLinkAccessory(
|
||||||
|
link: LinkButton,
|
||||||
|
member: GuildMember,
|
||||||
|
guild: Guild
|
||||||
|
): Promise<any | null> {
|
||||||
|
if (!link || !link.url) return null;
|
||||||
|
|
||||||
|
const processedUrl = await replaceVars(link.url, member, guild);
|
||||||
|
if (!this.isValidUrl(processedUrl)) return null;
|
||||||
|
|
||||||
|
const accessory: any = {
|
||||||
|
type: 2, // Button
|
||||||
|
style: ButtonStyle.Link,
|
||||||
|
url: processedUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
if (link.label && typeof link.label === 'string' && link.label.trim().length > 0) {
|
||||||
|
accessory.label = link.label.trim().slice(0, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link.emoji && typeof link.emoji === 'string') {
|
||||||
|
const parsed = this.parseEmojiInput(link.emoji);
|
||||||
|
if (parsed) accessory.emoji = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have at least label or emoji
|
||||||
|
if (!accessory.label && !accessory.emoji) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders preview of a block
|
||||||
|
*/
|
||||||
|
static async renderPreview(
|
||||||
|
blockState: BlockState,
|
||||||
|
member: GuildMember,
|
||||||
|
guild: Guild
|
||||||
|
): Promise<DisplayComponentContainer> {
|
||||||
|
const previewComponents: DisplayComponent[] = [];
|
||||||
|
|
||||||
|
// Add cover image first if exists
|
||||||
|
if (blockState.coverImage && this.isValidUrl(blockState.coverImage)) {
|
||||||
|
const processedCoverUrl = await replaceVars(blockState.coverImage, member, guild);
|
||||||
|
if (this.isValidUrl(processedCoverUrl)) {
|
||||||
|
previewComponents.push({
|
||||||
|
type: 12,
|
||||||
|
items: [{ media: { url: processedCoverUrl } }]
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add title after cover - VALIDATE CONTENT
|
||||||
|
const processedTitle = await replaceVars(blockState.title ?? "Sin título", member, guild);
|
||||||
|
previewComponents.push({
|
||||||
|
type: 10,
|
||||||
|
content: this.validateContent(processedTitle)
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
// Process components in order
|
||||||
|
for (const c of blockState.components) {
|
||||||
|
if (c.type === 10) {
|
||||||
|
const processedThumbnail = c.thumbnail ?
|
||||||
|
await replaceVars(c.thumbnail, member, guild) : null;
|
||||||
|
const processedContent = await replaceVars(c.content || "Sin contenido", member, guild);
|
||||||
|
const validatedContent = this.validateContent(processedContent);
|
||||||
|
|
||||||
|
// Build accessory by priority: linkButton > thumbnail
|
||||||
|
let accessory: any = null;
|
||||||
|
if (c.linkButton) {
|
||||||
|
const built = await this.buildLinkAccessory(c.linkButton, member, guild);
|
||||||
|
if (built) accessory = built;
|
||||||
|
}
|
||||||
|
if (!accessory && processedThumbnail && this.isValidUrl(processedThumbnail)) {
|
||||||
|
accessory = {
|
||||||
|
type: 11,
|
||||||
|
media: { url: processedThumbnail }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessory) {
|
||||||
|
previewComponents.push({
|
||||||
|
type: 9,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: validatedContent
|
||||||
|
} as any],
|
||||||
|
accessory
|
||||||
|
} as any);
|
||||||
|
} else {
|
||||||
|
// No valid accessory
|
||||||
|
previewComponents.push({
|
||||||
|
type: 10,
|
||||||
|
content: validatedContent
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
} else if (c.type === 14) {
|
||||||
|
// Separator
|
||||||
|
previewComponents.push({
|
||||||
|
type: 14,
|
||||||
|
divider: c.divider ?? true,
|
||||||
|
spacing: c.spacing ?? 1
|
||||||
|
} as any);
|
||||||
|
} else if (c.type === 12) {
|
||||||
|
// Image
|
||||||
|
const processedImageUrl = await replaceVars(c.url, member, guild);
|
||||||
|
if (this.isValidUrl(processedImageUrl)) {
|
||||||
|
previewComponents.push({
|
||||||
|
type: 12,
|
||||||
|
items: [{ media: { url: processedImageUrl } }]
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 17,
|
||||||
|
accent_color: blockState.color || 0x5865f2,
|
||||||
|
components: previewComponents
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates editor button rows
|
||||||
|
*/
|
||||||
|
static createEditorButtons(disabled = false): EditorActionRow[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📝 Título",
|
||||||
|
disabled,
|
||||||
|
custom_id: "edit_title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📄 Descripción",
|
||||||
|
disabled,
|
||||||
|
custom_id: "edit_description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "🎨 Color",
|
||||||
|
disabled,
|
||||||
|
custom_id: "edit_color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "➕ Contenido",
|
||||||
|
disabled,
|
||||||
|
custom_id: "add_content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "➖ Separador",
|
||||||
|
disabled,
|
||||||
|
custom_id: "add_separator"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "🖼️ Imagen",
|
||||||
|
disabled,
|
||||||
|
custom_id: "add_image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "🖼️ Portada",
|
||||||
|
disabled,
|
||||||
|
custom_id: "cover_image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📎 Thumbnail",
|
||||||
|
disabled,
|
||||||
|
custom_id: "edit_thumbnail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "🔗 Crear Botón Link",
|
||||||
|
disabled,
|
||||||
|
custom_id: "edit_link_button"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
label: "🔄 Mover",
|
||||||
|
disabled,
|
||||||
|
custom_id: "move_block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "🎯 Variables",
|
||||||
|
disabled,
|
||||||
|
custom_id: "show_variables"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📋 Duplicar",
|
||||||
|
disabled,
|
||||||
|
custom_id: "duplicate_block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📊 Vista Raw",
|
||||||
|
disabled,
|
||||||
|
custom_id: "show_raw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📥 Importar",
|
||||||
|
disabled,
|
||||||
|
custom_id: "import_json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: "📤 Exportar",
|
||||||
|
disabled,
|
||||||
|
custom_id: "export_json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Success,
|
||||||
|
label: "💾 Guardar",
|
||||||
|
disabled,
|
||||||
|
custom_id: "save_block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "❌ Cancelar",
|
||||||
|
disabled,
|
||||||
|
custom_id: "cancel_block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: "🗑️ Eliminar",
|
||||||
|
disabled,
|
||||||
|
custom_id: "delete_block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/core/types/displayComponents.ts
Normal file
101
src/core/types/displayComponents.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type {
|
||||||
|
ButtonStyle,
|
||||||
|
APIEmbed,
|
||||||
|
ComponentType
|
||||||
|
} from 'discord.js';
|
||||||
|
|
||||||
|
// Display Components V2 Types
|
||||||
|
export interface DisplayComponentContainer {
|
||||||
|
type: 17; // Container type
|
||||||
|
accent_color?: number;
|
||||||
|
components: DisplayComponent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisplayComponentSection {
|
||||||
|
type: 9; // Section type
|
||||||
|
components: DisplayComponent[];
|
||||||
|
accessory?: DisplayComponentAccessory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisplayComponentText {
|
||||||
|
type: 10; // TextDisplay type
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisplayComponentSeparator {
|
||||||
|
type: 14; // Separator type
|
||||||
|
divider?: boolean;
|
||||||
|
spacing?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisplayComponentThumbnail {
|
||||||
|
type: 11; // Thumbnail type
|
||||||
|
media: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisplayComponentButton {
|
||||||
|
type: 2; // Button type
|
||||||
|
style: ButtonStyle;
|
||||||
|
label: string;
|
||||||
|
custom_id?: string;
|
||||||
|
url?: string;
|
||||||
|
emoji?: {
|
||||||
|
name: string;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DisplayComponent =
|
||||||
|
| DisplayComponentContainer
|
||||||
|
| DisplayComponentSection
|
||||||
|
| DisplayComponentText
|
||||||
|
| DisplayComponentSeparator
|
||||||
|
| DisplayComponentThumbnail;
|
||||||
|
|
||||||
|
export type DisplayComponentAccessory =
|
||||||
|
| DisplayComponentButton
|
||||||
|
| DisplayComponentThumbnail;
|
||||||
|
|
||||||
|
// Block configuration types - compatible with Prisma JsonValue
|
||||||
|
export interface BlockConfig {
|
||||||
|
components?: any[]; // Use any[] to be compatible with JsonValue
|
||||||
|
coverImage?: string;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Block {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
guildId: string;
|
||||||
|
config: BlockConfig;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination helpers
|
||||||
|
export interface PaginationData<T> {
|
||||||
|
items: T[];
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
itemsPerPage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationControls {
|
||||||
|
hasPrevious: boolean;
|
||||||
|
hasNext: boolean;
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component constants for runtime use
|
||||||
|
export const COMPONENT_TYPES = {
|
||||||
|
CONTAINER: 17,
|
||||||
|
SECTION: 9,
|
||||||
|
TEXT_DISPLAY: 10,
|
||||||
|
SEPARATOR: 14,
|
||||||
|
THUMBNAIL: 11,
|
||||||
|
BUTTON: 2
|
||||||
|
} as const;
|
||||||
Reference in New Issue
Block a user