# devcodes-djs

The essential discord.js v14 toolkit — prefix commands, slash commands, context menus, modals, select menus, embed presets, button paginator, event handler, and full Components V2 container support. One install, everything you need.

## Install

```bash
npm install devcodes-djs
```

> **Peer requirement:** `discord.js ^14.0.0`  
> Container components require `discord.js >=14.18.0`

---

## Event Handler

Organize all your client events in one clean, chainable place instead of scattered `client.on()` calls.

```js
const { EventHandler } = require('devcodes-djs');

const events = new EventHandler()
  .once('ready', (client) => console.log(`Logged in as ${client.user.tag}`))
  .on('guildMemberAdd', (member) => {
    member.guild.systemChannel?.send(`👋 Welcome ${member}!`);
  })
  .on('messageDelete', (message) => {
    console.log(`Message deleted in #${message.channel.name}`);
  });

events.attach(client); // binds all listeners at once
```

---

## Modals

Build and handle multi-field modal (popup form) interactions.

```js
const { buildModal, ModalHandler, TextInputStyle } = require('devcodes-djs');

const feedbackModal = buildModal({
  id:    'feedback_form',
  title: 'Submit Feedback',
  fields: [
    {
      id:          'name',
      label:       'Your name',
      style:       TextInputStyle.Short,
      placeholder: 'e.g. John',
      required:    true,
    },
    {
      id:        'message',
      label:     'Your feedback',
      style:     TextInputStyle.Paragraph,
      minLength: 10,
      maxLength: 1000,
    },
  ],
});

// Show the modal from a slash command or button
slash.command({
  name:        'feedback',
  description: 'Submit feedback',
  execute: async (interaction) => {
    await interaction.showModal(feedbackModal);
  },
});

// Handle the submission
const modals = new ModalHandler()
  .on('feedback_form', async (interaction) => {
    const name    = interaction.fields.getTextInputValue('name');
    const message = interaction.fields.getTextInputValue('message');
    await interaction.reply({ content: `Thanks ${name}! Got: "${message}"`, ephemeral: true });
  });

modals.attach(client);
```

### Dynamic modal IDs

Use ID prefixes to handle modals with dynamic IDs:

```js
modals.on('report_', async (interaction) => {
  // matches "report_123", "report_456", etc.
  const userId = interaction.customId.replace('report_', '');
  await interaction.reply({ content: `Report filed for user ${userId}`, ephemeral: true });
});
```

---

## Select Menus

Build all 5 Discord select menu types and handle their interactions.

### String select

```js
const { SelectMenus, SelectHandler } = require('devcodes-djs');

const colorMenu = SelectMenus.stringSelect({
  id:          'color_pick',
  placeholder: 'Choose a color',
  options: [
    { label: 'Red',   value: 'red',   emoji: '🔴' },
    { label: 'Green', value: 'green', emoji: '🟢', default: true },
    { label: 'Blue',  value: 'blue',  emoji: '🔵' },
  ],
  maxValues: 2,
});

await message.reply({ components: [colorMenu] });

// Handle selection
const selects = new SelectHandler()
  .on('color_pick', async (interaction) => {
    await interaction.reply(`You picked: **${interaction.values.join(', ')}**`);
  });

selects.attach(client);
```

### Other select types

```js
// User picker
const picker = SelectMenus.userSelect({ id: 'pick_user', placeholder: 'Pick a user', maxValues: 3 });

// Role picker
const roles  = SelectMenus.roleSelect({ id: 'pick_role', placeholder: 'Pick a role' });

// Channel picker (text channels only)
const chans  = SelectMenus.channelSelect({ id: 'pick_chan', channelTypes: [ChannelType.GuildText] });

// Mentionable (users + roles)
const ment   = SelectMenus.mentionableSelect({ id: 'pick_mention' });
```

---

## Context Menus

Add right-click commands to users and messages.

```js
const { ContextMenuHandler } = require('devcodes-djs');

const ctx = new ContextMenuHandler({
  clientId: process.env.CLIENT_ID,
  token:    process.env.BOT_TOKEN,
  guildId:  process.env.GUILD_ID,
});

ctx
  .command({
    type:    'user',
    name:    'Get Avatar',
    execute: async (interaction) => {
      const url = interaction.targetUser.displayAvatarURL({ size: 512 });
      await interaction.reply({ content: url, ephemeral: true });
    },
  })
  .command({
    type:    'user',
    name:    'User Info',
    execute: async (interaction) => {
      const user = interaction.targetUser;
      await interaction.reply({
        embeds: [Embeds.info(`👤 ${user.tag}`, `ID: \`${user.id}\`\nCreated: <t:${Math.floor(user.createdTimestamp / 1000)}:R>`)],
        ephemeral: true,
      });
    },
  })
  .command({
    type:    'message',
    name:    'Translate Message',
    execute: async (interaction) => {
      await interaction.reply({ content: `📝 Content: ${interaction.targetMessage.content}`, ephemeral: true });
    },
  });

ctx.attach(client);

client.once('ready', () => ctx.deploy());
```

---

## Slash Commands

Register, deploy, and handle application (/) commands with full support for options, subcommands, permissions, and autocomplete.

### Basic setup

```js
const { Client, GatewayIntentBits } = require('discord.js');
const { SlashHandler, Embeds } = require('devcodes-djs');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const slash = new SlashHandler({
  clientId: process.env.CLIENT_ID,
  token:    process.env.BOT_TOKEN,
  guildId:  process.env.GUILD_ID, // omit for global commands
});

slash
  .command({
    name:        'ping',
    description: 'Check bot latency',
    execute: async (interaction) => {
      await interaction.reply({
        embeds: [Embeds.success('Pong!', `Latency: ${client.ws.ping}ms`)],
      });
    },
  })
  .command({
    name:        'say',
    description: 'Make the bot say something',
    options: [
      { name: 'message', description: 'What to say',        type: 'string',  required: true },
      { name: 'ephemeral', description: 'Only visible to you', type: 'boolean' },
    ],
    execute: async (interaction) => {
      const text      = interaction.options.getString('message', true);
      const ephemeral = interaction.options.getBoolean('ephemeral') ?? false;
      await interaction.reply({ content: text, ephemeral });
    },
  });

// Attach to client — handles interactionCreate automatically
slash.attach(client);

client.once('ready', async () => {
  await slash.deploy(); // register commands with Discord
  console.log('Ready!');
});

client.login(process.env.BOT_TOKEN);
```

### Subcommands

```js
slash.command({
  name:        'settings',
  description: 'Manage bot settings',
  subcommands: [
    {
      name:        'set',
      description: 'Change a setting',
      options: [
        { name: 'key',   description: 'Setting name',  type: 'string', required: true },
        { name: 'value', description: 'Setting value', type: 'string', required: true },
      ],
      execute: async (interaction) => {
        const key   = interaction.options.getString('key', true);
        const value = interaction.options.getString('value', true);
        await interaction.reply({ embeds: [Embeds.success('Updated', `\`${key}\` → \`${value}\``)] });
      },
    },
    {
      name:        'reset',
      description: 'Reset all settings to defaults',
      execute: async (interaction) => {
        await interaction.reply({ embeds: [Embeds.warn('Reset', 'All settings restored.')] });
      },
    },
  ],
});
```

### Autocomplete

```js
const fruits = ['apple', 'banana', 'cherry', 'durian', 'elderberry'];

slash.command({
  name:        'fruit',
  description: 'Pick a fruit',
  options: [
    { name: 'name', description: 'Fruit name', type: 'string', required: true, autocomplete: true },
  ],
  autocomplete: async (interaction) => {
    const focused = interaction.options.getFocused().toLowerCase();
    await interaction.respond(
      fruits.filter(f => f.startsWith(focused)).map(f => ({ name: f, value: f }))
    );
  },
  execute: async (interaction) => {
    await interaction.reply(`You picked: **${interaction.options.getString('name', true)}**`);
  },
});
```

### Permission guard

```js
slash.command({
  name:        'ban',
  description: 'Ban a user',
  permissions: ['BanMembers'],
  options: [
    { name: 'user', description: 'User to ban', type: 'user', required: true },
  ],
  execute: async (interaction) => {
    const user = interaction.options.getUser('user', true);
    await interaction.reply({ embeds: [Embeds.success('Banned', `${user.tag} was banned.`)] });
  },
});
```

### Option types

| Type | discord.js getter |
|------|-------------------|
| `string` | `options.getString(name)` |
| `integer` | `options.getInteger(name)` |
| `number` | `options.getNumber(name)` |
| `boolean` | `options.getBoolean(name)` |
| `user` | `options.getUser(name)` |
| `channel` | `options.getChannel(name)` |
| `role` | `options.getRole(name)` |
| `mentionable` | `options.getMentionable(name)` |
| `attachment` | `options.getAttachment(name)` |

### SlashHandlerConfig

| Option | Type | Description |
|--------|------|-------------|
| `clientId` | `string` | Your bot's application ID |
| `token` | `string` | Bot token for REST deployment |
| `guildId` | `string?` | Deploy to a specific guild (instant). Omit for global. |

---

## Command Handler

Replace giant `if/else` chains with a clean, chainable command registry. Supports aliases, per-user cooldowns, and permission guards.

```js
const { CommandHandler } = require('devcodes-djs');

const handler = new CommandHandler({ prefix: '!' });

handler
  .command('ping', async (message) => {
    await message.reply('Pong!');
  })
  .command('hello', async (message, args) => {
    await message.reply(`Hey ${args[0] ?? 'there'}!`);
  }, {
    aliases: ['hi', 'hey'],
    description: 'Say hello',
    usage: '[name]',
  })
  .command('ban', async (message, args) => {
    await message.reply(`Banned ${args[0]}`);
  }, {
    permissions: ['BanMembers'],
    cooldown: 5000,
    description: 'Ban a user',
    usage: '<user>',
  });

// In your messageCreate listener:
client.on('messageCreate', (message) => handler.handle(message));
```

### Auto help command

Use `handler.getCommands()` to build a dynamic help menu:

```js
handler.command('help', async (message) => {
  const lines = handler.getCommands()
    .map(cmd => `**!${cmd.name}** ${cmd.options.usage ?? ''} — ${cmd.options.description ?? ''}`)
    .join('\n');
  await message.reply(lines);
});
```

### HandlerConfig

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `prefix` | `string` | `"!"` | Command prefix |
| `ignoreBots` | `boolean` | `true` | Ignore messages from bots |

### CommandOptions

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `aliases` | `string[]` | `[]` | Alternative trigger names |
| `cooldown` | `number` | `0` | Per-user cooldown in ms |
| `permissions` | `PermissionResolvable[]` | `[]` | Required member permissions |
| `description` | `string` | — | Short description |
| `usage` | `string` | — | Usage hint e.g. `<user> [reason]` |

---

## Embed Presets

One-liner embeds with consistent colors — no more chaining `.setColor().setTitle().setFooter()` every time.

```js
const { Embeds } = require('devcodes-djs');

// ✅ Green
await message.reply({ embeds: [Embeds.success('Done!', 'Operation completed.')] });

// ❌ Red
await message.reply({ embeds: [Embeds.error('Failed', 'Something went wrong.')] });

// ℹ️ Blurple
await message.reply({ embeds: [Embeds.info('Info', 'Here is some information.')] });

// ⚠️ Yellow
await message.reply({ embeds: [Embeds.warn('Warning', 'Be careful.')] });

// 🎨 Any color
await message.reply({ embeds: [Embeds.custom(0xFF8800, { title: 'Custom', description: 'Orange embed' })] });
```

### With extra options

```js
await message.reply({
  embeds: [
    Embeds.success('Deployment Complete', 'Version **1.4.2** is live.', {
      fields: [
        { name: 'Environment', value: 'Production', inline: true },
        { name: 'Duration',    value: '42s',        inline: true },
      ],
      footer:    'Dev Codes CI',
      thumbnail: 'https://example.com/logo.png',
      timestamp: true,
    }),
  ],
});
```

### Preset colors

| Method | Color |
|--------|-------|
| `Embeds.success()` | `#57F287` (green) |
| `Embeds.error()` | `#ED4245` (red) |
| `Embeds.info()` | `#5865F2` (blurple) |
| `Embeds.warn()` | `#FEE75C` (yellow) |
| `Embeds.custom(color, opts)` | Any hex integer |

---

## Button Paginator

Reply with a paginated embed controlled by ⏮ ◀ ✖ ▶ ⏭ buttons. Buttons disable automatically at the first/last page and are removed when the collector times out.

```js
const { paginate, Embeds } = require('devcodes-djs');

handler.command('list', async (message) => {
  const pages = [
    Embeds.info('Page 1', 'First batch of results...'),
    Embeds.info('Page 2', 'Second batch of results...'),
    Embeds.info('Page 3', 'Third batch of results...'),
  ];

  await paginate(message, pages, { timeout: 60_000, authorOnly: true });
});
```

### PaginatorOptions

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `timeout` | `number` | `30000` | Button collector timeout in ms |
| `authorOnly` | `boolean` | `true` | Only the command author can interact |

---

## Containers (Components V2)

Build rich Discord Components V2 messages with a clean, fluent API. Containers support text, sections with thumbnails, separators, media galleries, and button rows — all composable.

> ⚠️ Components V2 messages require the `MessageFlags.IsComponentsV2` flag and **cannot** include embeds.

### Quick compose

```js
const { Containers } = require('devcodes-djs');
const { MessageFlags, ButtonBuilder, ButtonStyle } = require('discord.js');

const c = Containers.compose({
  color:     0x57F287,
  header:    '✅ Deployment complete',
  body:      'Version **1.4.2** is now live on production.',
  thumbnail: 'https://example.com/logo.png',
  images:    ['https://example.com/screenshot.png'],
  buttons: [
    new ButtonBuilder().setCustomId('view').setLabel('View Logs').setStyle(ButtonStyle.Primary),
    new ButtonBuilder().setCustomId('rollback').setLabel('Rollback').setStyle(ButtonStyle.Danger),
  ],
});

await message.reply({
  components: [c],
  flags: MessageFlags.IsComponentsV2,
});
```

### Build manually

Use the individual factory functions for full control:

```js
const {
  container, text, section, separator, mediaGallery, buttonRow,
  SeparatorSpacingSize,
} = require('devcodes-djs');
const { ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js');

const c = container(0x5865F2) // accent color
  .addTextDisplayComponents(
    text('**📦 New Release — v2.0.0**')
  )
  .addSeparatorComponents(
    separator(true, SeparatorSpacingSize.Large)
  )
  .addSectionComponents(
    section('A major update with breaking changes. Read the migration guide before upgrading.', 'https://example.com/icon.png')
  )
  .addTextDisplayComponents(
    text('**Changes:**\n- Removed deprecated API\n- New `compose()` API\n- Performance improvements')
  )
  .addSeparatorComponents(separator())
  .addMediaGalleryComponents(
    mediaGallery([
      { url: 'https://example.com/before.png', description: 'Before' },
      { url: 'https://example.com/after.png',  description: 'After'  },
    ])
  )
  .addActionRowComponents(
    buttonRow([
      new ButtonBuilder().setCustomId('changelog').setLabel('Changelog').setStyle(ButtonStyle.Primary),
      new ButtonBuilder().setURL('https://docs.example.com').setLabel('Docs').setStyle(ButtonStyle.Link),
    ])
  );

await message.reply({ components: [c], flags: MessageFlags.IsComponentsV2 });
```

### Container components

| Function | What it creates |
|----------|----------------|
| `container(color?, spoiler?)` | Top-level container, wraps everything |
| `text(content)` | Plain / markdown text block |
| `section(content, imageUrl?)` | Text + optional right-hand thumbnail |
| `separator(divider?, spacing?)` | Horizontal divider |
| `mediaGallery(items)` | Image gallery (up to 10 images) |
| `buttonRow(buttons[])` | Action row of buttons |
| `compose(opts)` | Build a complete container from options |

### `compose()` options

| Option | Type | Description |
|--------|------|-------------|
| `color` | `number` | Accent color hex integer |
| `header` | `string` | Bold header text |
| `body` | `string` | Body text (shown in a section) |
| `thumbnail` | `string` | Image URL next to body |
| `extra` | `string[]` | Additional text blocks |
| `images` | `string[]` | Media gallery image URLs |
| `imageSeparator` | `boolean` | Separator before gallery (default: `true`) |
| `buttons` | `ButtonBuilder[]` | Buttons row at the bottom |
| `spoiler` | `boolean` | Mark container as spoiler |

### Spoiler container

```js
const c = Containers.compose({
  header:  '🔞 Spoiler content',
  body:    'This will be hidden behind a spoiler.',
  spoiler: true,
});
```

---

## TypeScript

Full types for everything.

```ts
import {
  CommandHandler,
  Embeds,
  paginate,
  Containers,
  type HandlerConfig,
  type CommandOptions,
  type EmbedPresetOptions,
  type PaginatorOptions,
  type QuickContainerOptions,
} from 'devcodes-djs';
```

---

## Full bot example

```js
const { Client, GatewayIntentBits, MessageFlags, ButtonBuilder, ButtonStyle } = require('discord.js');
const { CommandHandler, Embeds, paginate, Containers } = require('devcodes-djs');

const client  = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] });
const handler = new CommandHandler({ prefix: '!' });

handler
  .command('ping', async (msg) => {
    await msg.reply({ embeds: [Embeds.success('Pong!', `Latency: ${client.ws.ping}ms`)] });
  })
  .command('list', async (msg) => {
    await paginate(msg, [
      Embeds.info('Results — Page 1', 'Item A\nItem B\nItem C'),
      Embeds.info('Results — Page 2', 'Item D\nItem E\nItem F'),
    ]);
  })
  .command('card', async (msg) => {
    const c = Containers.compose({
      color:     0x5865F2,
      header:    '🤖 Bot Info',
      body:      `Servers: **${client.guilds.cache.size}**\nPing: **${client.ws.ping}ms**`,
      thumbnail: client.user.displayAvatarURL(),
    });
    await msg.reply({ components: [c], flags: MessageFlags.IsComponentsV2 });
  });

client.on('messageCreate', (msg) => handler.handle(msg));
client.login(process.env.BOT_TOKEN);
```

---

## License

MIT © [azaresw](https://github.com/azaresw)

## Support

Join our Discord for help and updates: [discord.gg/ESh2Dp2xX9](https://discord.gg/ESh2Dp2xX9)
