const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); const PRESET_EVENTS = { halloween: { name: 'Halloween Spooktacular', description: 'Spooky season is here! Earn bonus XP and unlock exclusive Halloween rewards.', emoji: 'šŸŽƒ', color: 0xff6600, xp_multiplier: 1.5, month: 10, start_day: 15, end_day: 31 }, christmas: { name: 'Winter Wonderland', description: 'Celebrate the holidays with festive rewards and XP bonuses!', emoji: 'šŸŽ„', color: 0x00a651, xp_multiplier: 2.0, month: 12, start_day: 15, end_day: 31 }, newyear: { name: 'New Year Celebration', description: 'Ring in the new year with special bonuses!', emoji: 'šŸŽ†', color: 0xffd700, xp_multiplier: 2.0, month: 1, start_day: 1, end_day: 7 }, valentine: { name: 'Valentine\'s Day', description: 'Spread the love with bonus XP for reactions and kind messages!', emoji: 'šŸ’•', color: 0xff69b4, xp_multiplier: 1.5, month: 2, start_day: 10, end_day: 14 }, easter: { name: 'Easter Egg Hunt', description: 'Hunt for hidden eggs and earn bonus rewards!', emoji: '🐰', color: 0x87ceeb, xp_multiplier: 1.5, month: 4, start_day: 1, end_day: 15 }, summer: { name: 'Summer Bash', description: 'Beat the heat with cool rewards and XP boosts!', emoji: 'ā˜€ļø', color: 0xffa500, xp_multiplier: 1.25, month: 7, start_day: 1, end_day: 31 }, anniversary: { name: 'Server Anniversary', description: 'Celebrate your server\'s special day!', emoji: 'šŸŽ‚', color: 0x9b59b6, xp_multiplier: 2.0, month: null, start_day: null, end_day: null } }; module.exports = { data: new SlashCommandBuilder() .setName('seasonal') .setDescription('Manage seasonal events and XP bonuses') .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) .addSubcommand(sub => sub.setName('active').setDescription('View currently active seasonal events')) .addSubcommand(sub => sub.setName('list').setDescription('List all configured seasonal events')) .addSubcommand(sub => sub.setName('create').setDescription('Create a custom seasonal event') .addStringOption(opt => opt.setName('name').setDescription('Event name').setRequired(true).setMaxLength(50)) .addStringOption(opt => opt.setName('description').setDescription('Event description').setRequired(true).setMaxLength(200)) .addStringOption(opt => opt.setName('start').setDescription('Start date (YYYY-MM-DD)').setRequired(true)) .addStringOption(opt => opt.setName('end').setDescription('End date (YYYY-MM-DD)').setRequired(true)) .addNumberOption(opt => opt.setName('multiplier').setDescription('XP multiplier (e.g., 1.5)').setMinValue(1).setMaxValue(5)) .addIntegerOption(opt => opt.setName('bonus_xp').setDescription('Bonus XP per message during event')) .addStringOption(opt => opt.setName('emoji').setDescription('Event emoji').setMaxLength(10))) .addSubcommand(sub => sub.setName('preset').setDescription('Enable a preset seasonal event') .addStringOption(opt => opt.setName('event').setDescription('Preset event to enable') .addChoices( { name: 'šŸŽƒ Halloween', value: 'halloween' }, { name: 'šŸŽ„ Christmas', value: 'christmas' }, { name: 'šŸŽ† New Year', value: 'newyear' }, { name: 'šŸ’• Valentine\'s Day', value: 'valentine' }, { name: '🐰 Easter', value: 'easter' }, { name: 'ā˜€ļø Summer Bash', value: 'summer' }, { name: 'šŸŽ‚ Anniversary', value: 'anniversary' } ).setRequired(true)) .addIntegerOption(opt => opt.setName('year').setDescription('Year for the event (defaults to current/next)')) .addStringOption(opt => opt.setName('start').setDescription('Custom start date for anniversary (YYYY-MM-DD)'))) .addSubcommand(sub => sub.setName('delete').setDescription('Delete a seasonal event') .addStringOption(opt => opt.setName('name').setDescription('Event name to delete').setRequired(true).setAutocomplete(true))) .addSubcommand(sub => sub.setName('announce').setDescription('Announce an active event to a channel') .addChannelOption(opt => opt.setName('channel').setDescription('Channel to announce in').setRequired(true))), async execute(interaction, supabase) { if (!supabase) { return interaction.reply({ content: 'Database not available.', ephemeral: true }); } const subcommand = interaction.options.getSubcommand(); if (subcommand === 'active') { const now = new Date(); const { data: events } = await supabase .from('seasonal_events') .select('*') .eq('guild_id', interaction.guildId) .lte('start_date', now.toISOString()) .gte('end_date', now.toISOString()) .order('start_date', { ascending: true }); if (!events || events.length === 0) { const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('šŸŽ‰ Active Seasonal Events') .setDescription('No seasonal events are currently active.\nUse `/seasonal create` or `/seasonal preset` to set one up!') .setTimestamp(); return interaction.reply({ embeds: [embed] }); } const embed = new EmbedBuilder() .setColor(0x22c55e) .setTitle('šŸŽ‰ Active Seasonal Events') .setTimestamp(); for (const event of events) { const endDate = new Date(event.end_date); const daysLeft = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)); embed.addFields({ name: `${event.emoji || 'šŸŽŠ'} ${event.name}`, value: `${event.description}\n**XP Multiplier:** ${event.xp_multiplier}x\n**Bonus XP:** ${event.bonus_xp || 0}/message\n**Ends:** (${daysLeft} days left)`, inline: false }); } await interaction.reply({ embeds: [embed] }); } if (subcommand === 'list') { const { data: events } = await supabase .from('seasonal_events') .select('*') .eq('guild_id', interaction.guildId) .order('start_date', { ascending: true }); if (!events || events.length === 0) { const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('šŸ“… Seasonal Events') .setDescription('No seasonal events configured.\nUse `/seasonal create` or `/seasonal preset` to add one!') .setTimestamp(); return interaction.reply({ embeds: [embed] }); } const now = new Date(); const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('šŸ“… Seasonal Events') .setFooter({ text: `${events.length} event(s) configured` }) .setTimestamp(); for (const event of events) { const startDate = new Date(event.start_date); const endDate = new Date(event.end_date); const isActive = now >= startDate && now <= endDate; const isPast = now > endDate; const status = isActive ? '🟢 Active' : isPast ? '⚫ Ended' : '🟔 Upcoming'; embed.addFields({ name: `${event.emoji || 'šŸŽŠ'} ${event.name} ${status}`, value: `${event.description}\n**Period:** - \n**XP:** ${event.xp_multiplier}x | **Bonus XP:** +${event.bonus_xp || 0}`, inline: false }); } await interaction.reply({ embeds: [embed] }); } if (subcommand === 'create') { const name = interaction.options.getString('name'); const description = interaction.options.getString('description'); const startStr = interaction.options.getString('start'); const endStr = interaction.options.getString('end'); const multiplier = interaction.options.getNumber('multiplier') || 1.5; const bonusXp = interaction.options.getInteger('bonus_xp') || 0; const emoji = interaction.options.getString('emoji') || 'šŸŽŠ'; const startDate = new Date(startStr); startDate.setHours(0, 0, 0, 0); const endDate = new Date(endStr); endDate.setHours(23, 59, 59, 999); if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { return interaction.reply({ content: 'Invalid date format. Use YYYY-MM-DD.', ephemeral: true }); } if (endDate <= startDate) { return interaction.reply({ content: 'End date must be after start date.', ephemeral: true }); } const { count } = await supabase .from('seasonal_events') .select('*', { count: 'exact', head: true }) .eq('guild_id', interaction.guildId); if (count >= 20) { return interaction.reply({ content: 'Maximum of 20 seasonal events per server.', ephemeral: true }); } const { error } = await supabase.from('seasonal_events').insert({ guild_id: interaction.guildId, name, description, emoji, start_date: startDate.toISOString(), end_date: endDate.toISOString(), xp_multiplier: multiplier, bonus_xp: bonusXp, created_by: interaction.user.id }); if (error) { console.error('Seasonal event create error:', error); return interaction.reply({ content: 'Failed to create event.', ephemeral: true }); } const embed = new EmbedBuilder() .setColor(0x22c55e) .setTitle(`${emoji} Seasonal Event Created!`) .setDescription(`**${name}**\n${description}`) .addFields( { name: 'Start', value: ``, inline: true }, { name: 'End', value: ``, inline: true }, { name: 'XP Multiplier', value: `${multiplier}x`, inline: true }, { name: 'Bonus XP', value: `+${bonusXp}/message`, inline: true } ) .setTimestamp(); await interaction.reply({ embeds: [embed] }); } if (subcommand === 'preset') { const presetKey = interaction.options.getString('event'); const preset = PRESET_EVENTS[presetKey]; const year = interaction.options.getInteger('year') || new Date().getFullYear(); const customStart = interaction.options.getString('start'); let startDate, endDate; if (presetKey === 'anniversary' && customStart) { startDate = new Date(customStart); endDate = new Date(startDate); endDate.setDate(endDate.getDate() + 7); } else if (preset.month) { startDate = new Date(year, preset.month - 1, preset.start_day); endDate = new Date(year, preset.month - 1, preset.end_day, 23, 59, 59); if (startDate < new Date() && presetKey !== 'newyear') { startDate.setFullYear(startDate.getFullYear() + 1); endDate.setFullYear(endDate.getFullYear() + 1); } } else { return interaction.reply({ content: 'Anniversary events require a custom start date.', ephemeral: true }); } const { error } = await supabase.from('seasonal_events').insert({ guild_id: interaction.guildId, name: preset.name, description: preset.description, emoji: preset.emoji, start_date: startDate.toISOString(), end_date: endDate.toISOString(), xp_multiplier: preset.xp_multiplier, bonus_xp: 0, preset_type: presetKey, created_by: interaction.user.id }); if (error) { if (error.code === '23505') { return interaction.reply({ content: 'This preset event already exists for that time period.', ephemeral: true }); } console.error('Preset event error:', error); return interaction.reply({ content: 'Failed to create preset event.', ephemeral: true }); } const embed = new EmbedBuilder() .setColor(preset.color) .setTitle(`${preset.emoji} ${preset.name} Enabled!`) .setDescription(preset.description) .addFields( { name: 'Start', value: ``, inline: true }, { name: 'End', value: ``, inline: true }, { name: 'XP Multiplier', value: `${preset.xp_multiplier}x`, inline: true } ) .setTimestamp(); await interaction.reply({ embeds: [embed] }); } if (subcommand === 'delete') { const eventName = interaction.options.getString('name'); const { data: event } = await supabase .from('seasonal_events') .select('id, name, emoji') .eq('guild_id', interaction.guildId) .ilike('name', `%${eventName}%`) .maybeSingle(); if (!event) { return interaction.reply({ content: `No event found matching "${eventName}".`, ephemeral: true }); } const { error } = await supabase .from('seasonal_events') .delete() .eq('id', event.id); if (error) { return interaction.reply({ content: 'Failed to delete event.', ephemeral: true }); } const embed = new EmbedBuilder() .setColor(0xef4444) .setTitle('Event Deleted') .setDescription(`${event.emoji || 'šŸŽŠ'} **${event.name}** has been removed.`) .setTimestamp(); await interaction.reply({ embeds: [embed] }); } if (subcommand === 'announce') { const channel = interaction.options.getChannel('channel'); const now = new Date(); const { data: events } = await supabase .from('seasonal_events') .select('*') .eq('guild_id', interaction.guildId) .lte('start_date', now.toISOString()) .gte('end_date', now.toISOString()); if (!events || events.length === 0) { return interaction.reply({ content: 'No active events to announce.', ephemeral: true }); } const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('šŸŽ‰ Seasonal Event Active!') .setDescription('Special bonuses are now available!') .setTimestamp(); for (const event of events) { const endDate = new Date(event.end_date); embed.addFields({ name: `${event.emoji || 'šŸŽŠ'} ${event.name}`, value: `${event.description}\n\n**Bonuses:**\n• ${event.xp_multiplier}x XP multiplier\n${event.bonus_xp > 0 ? `• +${event.bonus_xp} XP per message\n` : ''}• Ends: `, inline: false }); } embed.setFooter({ text: `${events.length} event(s) active` }); try { await channel.send({ embeds: [embed] }); await interaction.reply({ content: `Event announcement sent to ${channel}!`, ephemeral: true }); } catch (err) { await interaction.reply({ content: 'Failed to send announcement. Check bot permissions.', ephemeral: true }); } } }, async autocomplete(interaction, supabase) { if (!supabase) return; const focusedValue = interaction.options.getFocused(); const { data: events } = await supabase .from('seasonal_events') .select('name') .eq('guild_id', interaction.guildId) .ilike('name', `%${focusedValue}%`) .limit(25); const choices = (events || []).map(e => ({ name: e.name, value: e.name })); await interaction.respond(choices); } };