Skip to content

Commit

Permalink
contest starting in x time
Browse files Browse the repository at this point in the history
  • Loading branch information
dashroshan committed Sep 28, 2023
1 parent 3049af3 commit be43142
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 6 deletions.
26 changes: 22 additions & 4 deletions commands/contests.js
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions commands/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)**
Expand Down
8 changes: 8 additions & 0 deletions database/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) } });
Expand Down
7 changes: 7 additions & 0 deletions interactions/contests-in.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const contestsInPaginate = require("../utility/contests-in");

async function contestsInPaginateWrap(interaction) {
await contestsInPaginate(interaction, false);
}

module.exports = contestsInPaginateWrap;
90 changes: 90 additions & 0 deletions utility/contests-in.js
Original file line number Diff line number Diff line change
@@ -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:** <t:${contestData['start']}:D> at <t:${contestData['start']}:t>\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;

0 comments on commit be43142

Please sign in to comment.