diff --git a/commands/contests.js b/commands/contests.js index 298c9b5..3573809 100644 --- a/commands/contests.js +++ b/commands/contests.js @@ -1,14 +1,32 @@ const embedMessage = require('../utility/embed message'); -const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require('discord.js'); +const { SlashCommandBuilder, ActionRowBuilder, StringSelectMenuBuilder, SlashCommandNumberOption } = require('discord.js'); +const contestsInPaginate = require("../utility/contests-in"); // contests command to view ongoing and upcoming coding contests module.exports = { data: new SlashCommandBuilder() .setName('contests') + .addNumberOption( + new SlashCommandNumberOption() + .setName("start") + .setDescription( + "View contests starting in X time" + ) + .addChoices( + { name: '1 day', value: 1 }, + { name: '1 week', value: 7 }, + ) + ) .setDescription('View ongoing and upcoming coding contests'), async execute(interaction) { await interaction.deferReply(); + let days = interaction.options.getNumber("start"); + if (days != null) { + await contestsInPaginate(interaction, true); + return; + } + // Create the embed and selection box const embed = await embedMessage(interaction, 'CODING CONTESTS', 'Select a contest platform using the selection box below. CodeChef, LeetCode, HackerRank, CodeForces, AtCoder, HackerEarth, GeeksforGeeks and Coding Ninjas are the currently available platforms. Support for more platforms coming soon :sparkles:', false, 'https://github.com/roshan1337d/coding-contests-companion', true); const row = new ActionRowBuilder() @@ -48,9 +66,9 @@ module.exports = { emoji: { id: '1025657011360243782' }, }, { - label : 'Geeksforgeeks', - value : 'geeksforgeeks', - emoji: { id:'1110941777260711986'} + label: 'Geeksforgeeks', + value: 'geeksforgeeks', + emoji: { id: '1110941777260711986' } }, { label: 'Coding Ninjas', diff --git a/commands/help.js b/commands/help.js index 9edf9db..b652f18 100644 --- a/commands/help.js +++ b/commands/help.js @@ -10,8 +10,8 @@ module.exports = { await interaction.deferReply({ ephemeral: true }); respStr = `**FOR EVERYONE** -**[View All Contests](http://ignore-the-link.com)** -Use the \`/contests\` command. Select any contest platform using the selection box present below the message sent by the bot, to view all its ongoing and upcoming contests. +**[View Contests](http://ignore-the-link.com)** +Use the \`/contests\` command. Select any contest platform using the selection box present below the message sent by the bot, to view all its ongoing and upcoming contests. If you want to view contests starting in X time, you can use the optional start field of the command. **FOR ADMIN ONLY** **[Setup Contest Notifications](http://ignore-the-link.com)** diff --git a/database/mongo.js b/database/mongo.js index f2989ee..f097334 100644 --- a/database/mongo.js +++ b/database/mongo.js @@ -44,6 +44,14 @@ module.exports.getContestsStartingSoon = async function () { return contests; } +// Return an array of contests which start in coming X days +module.exports.getContestsStartingInXDays = async function (days) { + let contests = await contestSchema + .find({ start: { $lte: (Math.floor(Date.now() / 1000) + days * 86400), $gte: Math.floor(Date.now() / 1000) } }) + .sort({ start: 1 }); + return contests; +} + // Delete the finished contests from the db module.exports.deleteFinishedContests = async function () { await contestSchema.deleteMany({ end: { $lte: Math.floor(Date.now() / 1000) } }); diff --git a/interactions/contests-in.js b/interactions/contests-in.js new file mode 100644 index 0000000..584db22 --- /dev/null +++ b/interactions/contests-in.js @@ -0,0 +1,7 @@ +const contestsInPaginate = require("../utility/contests-in"); + +async function contestsInPaginateWrap(interaction) { + await contestsInPaginate(interaction, false); +} + +module.exports = contestsInPaginateWrap; \ No newline at end of file diff --git a/utility/contests-in.js b/utility/contests-in.js new file mode 100644 index 0000000..a91d750 --- /dev/null +++ b/utility/contests-in.js @@ -0,0 +1,90 @@ +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); + +// Data about the different supported platforms +const platforms = { + 'codechef': { 'name': 'CodeChef', 'url': 'https://www.codechef.com', 'thumb': 'https://i.imgur.com/XdBzq6c.jpg' }, + 'leetcode': { 'name': 'LeetCode', 'url': 'https://leetcode.com', 'thumb': 'https://i.imgur.com/slIjkP3.jpg' }, + 'hackerrank': { 'name': 'HackerRank', 'url': 'https://www.hackerrank.com', 'thumb': 'https://i.imgur.com/sduFQNq.jpg' }, + 'codeforces': { 'name': 'CodeForces', 'url': 'https://codeforces.com', 'thumb': 'https://i.imgur.com/EVmQOW5.jpg' }, + 'atcoder': { 'name': 'AtCoder', 'url': 'https://atcoder.jp', 'thumb': 'https://i.imgur.com/mfB9fEI.jpg' }, + 'hackerearth': { 'name': 'HackerEarth', 'url': 'https://www.hackerearth.com', 'thumb': 'https://i.imgur.com/CACYwoz.jpg' }, + 'geeksforgeeks': { 'name': 'Geeksforgeeks', 'url': 'https://practice.geeksforgeeks.org', 'thumb': 'https://i.imgur.com/ejRKy7l.jpg' }, + 'codingninjas': { 'name': 'Coding Ninjas', 'url': 'https://www.codingninjas.com', 'thumb': 'https://i.imgur.com/X9WJiRv.png' } +} + +// Total contests to show per page +// This is needed due to the 4k character limit of the embed description +const contestsPerPage = 6; + +// Updates the embed from /contests command when the select menu is used +async function contestsInPaginate(interaction, commandType) { + let pageNum; + let days; + + // To handle both the select menu, and the previous and next page buttons + if (interaction.isButton()) { + if (interaction.customId.substring(0, 16) != 'contestsInButton') return; + pageNum = parseInt(interaction.customId.substring(16, 17), 10); + days = parseInt(interaction.customId.substring(17, 18), 10); + } + else if (interaction.isCommand() && commandType) { + days = interaction.options.getNumber("start"); + pageNum = 0; + if (days == null) return; + } + else return; + + // Incase it takes longer than 3 seconds to respond + if (!commandType) + await interaction.deferUpdate(); + + // Get the platform from interaction and fetch its contests data from db + let data = await interaction.client.database.getContestsStartingInXDays(days); + + // Format the contests data for the embed body + let respStr = ""; + let contestsCount = data.length - contestsPerPage * pageNum; + let maxContests = pageNum * contestsPerPage + ((contestsCount > contestsPerPage) ? contestsPerPage : contestsCount); + for (let i = pageNum * contestsPerPage; i < maxContests; i++) { + let contestData = data[i]; + let hours = Math.floor(contestData['duration'] / 3600); + let mins = Math.floor((contestData['duration'] / 60) % 60); + respStr += `**[${contestData['name']}](${contestData['url']})**\n:calendar: **Start:** at \n:stopwatch: **Duration:** ${hours} ${hours === 1 ? 'hour' : 'hours'}${mins === 0 ? '' : (' and ' + mins + ' minutes')}\n:flags: **Platform:** ${platforms[contestData['platform']]['name']}`; + if (i !== maxContests - 1) respStr += "\n\n"; + } + if (!maxContests) respStr += "**No scheduled contests**\n\u200b"; + + // Previous and Next page buttons incase total contests exceeed contestsPerPage + const row1 = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`contestsInButton${pageNum - 1}${days}`) + .setLabel('Previous Page') + .setDisabled(!(pageNum > 0)) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId(`contestsInButton${pageNum + 1}${days}`) + .setLabel('Next Page') + .setDisabled(!(contestsCount > contestsPerPage)) + .setStyle(ButtonStyle.Primary), + ); + + // Don't add previous and next page buttons if total contests is less than contestsPerPage + const rows = []; + if (data.length > contestsPerPage) rows.push(row1); + + // Create the embed + let embed = new EmbedBuilder() + .setColor(0x1089DF) + .setTitle(`CONTESTS IN ${days} DAYS`) + .setDescription(respStr + "** **") + .setImage("https://i.imgur.com/Sj1bgx5.jpg") + + // Set current page number in footer if paginated + if (data.length > contestsPerPage) embed.setFooter({ text: `Current page ${pageNum + 1}/${Math.trunc(data.length / contestsPerPage) + 1}` }); + + // Send the embed with the components + await interaction.editReply({ embeds: [embed], components: rows }); +} + +module.exports = contestsInPaginate; \ No newline at end of file