From 92135f2da798d8a33fdbc5256d9df89a5716d890 Mon Sep 17 00:00:00 2001 From: Larissa-Chelius Date: Wed, 22 Oct 2025 14:14:22 -0400 Subject: [PATCH] All Eslint errors and warnings handled --- .eslintignore | 2 - .eslintrc.json | 3 + config.example.ts | 6 +- install-deps.js | 108 ++--- src/commands/jobs/jobform.ts | 2 + src/commands/jobs/jobs.ts | 446 ++++++----------- src/commands/reminders/remindermenu.ts | 20 +- src/lib/types/Reminder.d.ts | 18 +- src/lib/utils/jobUtils/jobDatabase.ts | 2 +- src/newreminders/constants.ts | 30 +- src/newreminders/email-handlers.ts | 184 ++++--- src/newreminders/index.ts | 2 +- src/newreminders/job-handlers.ts | 567 +++++++++++----------- src/newreminders/menu-handlers.ts | 638 +++++++++++++------------ src/newreminders/reminder-handlers.ts | 454 +++++++++--------- src/newreminders/types.ts | 28 +- src/newreminders/ui.ts | 332 ++++++------- src/newreminders/utils.ts | 199 ++++---- src/pieces/tasks.ts | 405 ++++++++-------- 19 files changed, 1658 insertions(+), 1788 deletions(-) delete mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 04c01ba7..00000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -dist/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 21369264..a9bc1468 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,6 +6,9 @@ }, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, + + "ignorePatterns": ["dist/**"], + "rules": { "no-extra-parens": ["warn", "all", { "nestedBinaryExpressions": false diff --git a/config.example.ts b/config.example.ts index 1cf95c5b..0efb6091 100644 --- a/config.example.ts +++ b/config.example.ts @@ -220,9 +220,9 @@ export const PREFIX = config.PREFIX; export const BLACKLIST = [config.BLACKLIST]; export const GITHUB_TOKEN = config.ENV_GITHUB_TOKEN; -export const APP_ID = config.APP_ID; -export const APP_KEY = config.APP_KEY; -export const MAP_KEY = config.MAP_KEY; +export const { APP_ID } = config; +export const { APP_KEY } = config; +export const { MAP_KEY } = config; // eslint-disable-next-line prefer-destructuring export const MONGO = config.MONGO; diff --git a/install-deps.js b/install-deps.js index 9597137b..f1d73d5f 100644 --- a/install-deps.js +++ b/install-deps.js @@ -1,66 +1,62 @@ -const { spawnSync } = require("child_process"); -const path = require("path"); +const { spawnSync } = require('child_process'); +const path = require('path'); const os = process.platform; -const isWindows = process.platform === "win32"; +const isWindows = process.platform === 'win32'; -//Function to check if running as Administrator +// Function to check if running as Administrator function isAdmin() { - if (!isWindows) return true; - const result = spawnSync("powershell.exe", [ - "-NoProfile", "-ExecutionPolicy", "Bypass", - "-Command", "([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)" - ], { encoding: "utf8" }); + if (!isWindows) return true; + const result = spawnSync('powershell.exe', [ + '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-Command', '([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)' + ], { encoding: 'utf8' }); - return result.stdout.trim() === "True"; + return result.stdout.trim() === 'True'; } if (isWindows) { - console.log("🟣 Installing Windows dependencies..."); - - if (!isAdmin()) { - console.log("🔴 Not running as admin. Restarting as administrator..."); - - //Relaunch as admin to keep window open - const scriptPath = path.join(__dirname, "install-deps-windows.ps1"); - const result = spawnSync("powershell.exe", [ - "-NoProfile", "-ExecutionPolicy", "Bypass", - "-Command", `Start-Process powershell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \\"${scriptPath}\\"' -Verb RunAs -Wait` - ], { stdio: "inherit", shell: true }); - - if (result.error) { - console.error("⚠️ Failed to elevate PowerShell:", result.error); - process.exit(1); - } - - console.log("✅ Dependency installation completed!"); - process.exit(result.status); - } - - //Run PowerShell script synchronously (when already elevated) - const scriptPath = path.join(__dirname, "install-deps-windows.ps1"); - const result = spawnSync("powershell.exe", [ - "-NoProfile", "-ExecutionPolicy", "Bypass", - "-File", scriptPath - ], { stdio: "inherit", shell: true }); - - if (result.status !== 0) { - console.error("⚠️ PowerShell script failed with exit code:", result.status); - process.exit(1); - } - - console.log("✅ Windows dependency installation complete."); - -} else if (os === "linux") { - console.log("🟢 Installing Linux dependencies..."); - spawnSync("bash", ["./install-deps-linux.sh"], { stdio: "inherit" }); - -} else if (os === "darwin") { - console.log("🍎 Installing macOS dependencies..."); - spawnSync("bash", ["./install-deps-mac.sh"], { stdio: "inherit" }); - + console.log('🟣 Installing Windows dependencies...'); + + if (!isAdmin()) { + console.log('🔴 Not running as admin. Restarting as administrator...'); + + // Relaunch as admin to keep window open + const scriptPath = path.join(__dirname, 'install-deps-windows.ps1'); + const result = spawnSync('powershell.exe', [ + '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-Command', `Start-Process powershell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \\"${scriptPath}\\"' -Verb RunAs -Wait` + ], { stdio: 'inherit', shell: true }); + + if (result.error) { + console.error('⚠️ Failed to elevate PowerShell:', result.error); + process.exit(1); + } + + console.log('✅ Dependency installation completed!'); + process.exit(result.status); + } + + // Run PowerShell script synchronously (when already elevated) + const scriptPath = path.join(__dirname, 'install-deps-windows.ps1'); + const result = spawnSync('powershell.exe', [ + '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-File', scriptPath + ], { stdio: 'inherit', shell: true }); + + if (result.status !== 0) { + console.error('⚠️ PowerShell script failed with exit code:', result.status); + process.exit(1); + } + + console.log('✅ Windows dependency installation complete.'); +} else if (os === 'linux') { + console.log('🟢 Installing Linux dependencies...'); + spawnSync('bash', ['./install-deps-linux.sh'], { stdio: 'inherit' }); +} else if (os === 'darwin') { + console.log('🍎 Installing macOS dependencies...'); + spawnSync('bash', ['./install-deps-mac.sh'], { stdio: 'inherit' }); } else { - console.log("❌ Unsupported OS detected."); - process.exit(1); - + console.log('❌ Unsupported OS detected.'); + process.exit(1); } diff --git a/src/commands/jobs/jobform.ts b/src/commands/jobs/jobform.ts index 829dc710..137f22d3 100644 --- a/src/commands/jobs/jobform.ts +++ b/src/commands/jobs/jobform.ts @@ -34,6 +34,7 @@ const questions: string[] = [ ]; export default class JobFormCommand extends Command { + name = 'jobform'; description = 'Starts a job preferences form via direct message.'; options: ApplicationCommandOptionData[] = []; @@ -148,4 +149,5 @@ export default class JobFormCommand extends Command { // start the first question ask(); } + } diff --git a/src/commands/jobs/jobs.ts b/src/commands/jobs/jobs.ts index 3d18fd38..e19b7f7f 100644 --- a/src/commands/jobs/jobs.ts +++ b/src/commands/jobs/jobs.ts @@ -16,53 +16,52 @@ import { TextInputStyle, ModalSubmitInteraction, User, - TextBasedChannel, TextChannel, DMChannel, NewsChannel, - ThreadChannel, -} from "discord.js"; -import fetchJobListings from "@root/src/lib/utils/jobUtils/Adzuna_job_search"; -import { JobResult } from "@root/src/lib/types/JobResult"; -import { Interest } from "@root/src/lib/types/Interest"; -import { JobData } from "@root/src/lib/types/JobData"; -import { Command } from "@lib/types/Command"; -import { DB, BOT, MAP_KEY } from "@root/config"; -import { MongoClient } from "mongodb"; -import { sendToFile } from "@root/src/lib/utils/generalUtils"; -import axios from "axios"; -import { JobPreferences } from "@root/src/lib/types/JobPreferences"; -import { PDFDocument, StandardFonts, rgb } from "pdf-lib"; + ThreadChannel +} from 'discord.js'; +import fetchJobListings from '@root/src/lib/utils/jobUtils/Adzuna_job_search'; +import { JobResult } from '@root/src/lib/types/JobResult'; +import { Interest } from '@root/src/lib/types/Interest'; +import { JobData } from '@root/src/lib/types/JobData'; +import { Command } from '@lib/types/Command'; +import { DB, BOT, MAP_KEY } from '@root/config'; +import { MongoClient } from 'mongodb'; +import axios from 'axios'; +import { JobPreferences } from '@root/src/lib/types/JobPreferences'; +import { PDFDocument, StandardFonts, rgb, PDFFont } from 'pdf-lib'; // Temporary storage for user job data const userJobData = new Map(); export default class extends Command { + description = `Get a listing of jobs based on your interests and preferences.`; extendedHelp = `This command will return a listing of jobs based on your interests and preferences.`; options: ApplicationCommandOptionData[] = [ { - name: "filter", - description: "Filter options for job listings", + name: 'filter', + description: 'Filter options for job listings', type: ApplicationCommandOptionType.String, required: false, choices: [ - { name: "Date Posted: recent", value: "date" }, - { name: "Salary: high-low average", value: "salary" }, - { name: "Alphabetical: A-Z", value: "alphabetical" }, - { name: "Distance: shortest-longest", value: "distance" }, - ], - }, + { name: 'Date Posted: recent', value: 'date' }, + { name: 'Salary: high-low average', value: 'salary' }, + { name: 'Alphabetical: A-Z', value: 'alphabetical' }, + { name: 'Distance: shortest-longest', value: 'distance' } + ] + } ]; private sanitizeText(text: string): string { return text - .replace(/[^\u0000-\u007F]/gu, "") // Remove non-ASCII (U+0000 through U+007F) - .replace(/•/g, "*") // Replace bullet points + .replace(/[^\u0000-\u007F]/gu, '') // Remove non-ASCII (U+0000 through U+007F) + .replace(/•/g, '*') // Replace bullet points .replace(/[“”]/g, '"') // Replace smart double quotes .replace(/[‘’]/g, "'") // Replace smart single quotes - .replace(/\s+/g, " ") // Collapse multiple whitespace + .replace(/\s+/g, ' ') // Collapse multiple whitespace .trim(); } @@ -78,9 +77,7 @@ export default class extends Command { const bulletPointIndent = 20; const subBulletPointIndent = 30; - const helveticaBold = await pdfDoc.embedFont( - StandardFonts.HelveticaBold - ); + const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica); // Draw title decoration @@ -92,7 +89,7 @@ export default class extends Command { y: yPosition + 50, width: lineWidth, height: lineHeight, - color: rgb(135 / 255, 59 / 255, 29 / 255), + color: rgb(135 / 255, 59 / 255, 29 / 255) }); currentPage.drawRectangle({ @@ -100,7 +97,7 @@ export default class extends Command { y: yPosition + 50, width: lineWidth, height: lineHeight, - color: rgb(237 / 255, 118 / 255, 71 / 255), + color: rgb(237 / 255, 118 / 255, 71 / 255) }); currentPage.drawRectangle({ @@ -108,18 +105,18 @@ export default class extends Command { y: yPosition + 50, width: lineWidth, height: lineHeight, - color: rgb(13 / 255, 158 / 255, 198 / 255), + color: rgb(13 / 255, 158 / 255, 198 / 255) }); yPosition -= 40; // Draw title - currentPage.drawText("Your Job Listings", { + currentPage.drawText('Your Job Listings', { x: margin, y: yPosition + 50, size: titleFontSize, font: helveticaBold, - color: rgb(114 / 255, 53 / 255, 9 / 255), + color: rgb(114 / 255, 53 / 255, 9 / 255) }); yPosition -= 40; @@ -129,7 +126,7 @@ export default class extends Command { y: yPosition + 50, width: lineWidth / 2, height: lineHeight - 8, - color: rgb(135 / 255, 59 / 255, 29 / 255), + color: rgb(135 / 255, 59 / 255, 29 / 255) }); yPosition -= 10; @@ -140,7 +137,7 @@ export default class extends Command { title: this.sanitizeText(job.title), location: this.sanitizeText(job.location), salary: this.sanitizeText(this.formatSalaryforPDF(job)), - link: job.link, // URLs should be ASCII already + link: job.link // URLs should be ASCII already }; // Add new page if needed @@ -168,16 +165,16 @@ export default class extends Command { y: yPosition + 30, size: fontSize + 10, font: helveticaBold, - color: rgb(241 / 255, 113 / 255, 34 / 255), + color: rgb(241 / 255, 113 / 255, 34 / 255) }); yPosition -= 30; } // Add job details const details = [ - { label: "Location", value: sanitizedJob.location }, - { label: "Salary", value: sanitizedJob.salary }, - { label: "Apply Here", value: sanitizedJob.link }, + { label: 'Location', value: sanitizedJob.location }, + { label: 'Salary', value: sanitizedJob.salary }, + { label: 'Apply Here', value: sanitizedJob.link } ]; for (const detail of details) { @@ -185,10 +182,7 @@ export default class extends Command { `• ${detail.label}`, helveticaBold, fontSize + 5, - width - - margin * 2 - - bulletPointIndent - - subBulletPointIndent + width - margin * 2 - bulletPointIndent - subBulletPointIndent ); for (const line of labelLines) { @@ -202,7 +196,7 @@ export default class extends Command { y: yPosition + 25, size: fontSize + 5, font: helveticaBold, - color: rgb(94 / 255, 74 / 255, 74 / 255), + color: rgb(94 / 255, 74 / 255, 74 / 255) }); yPosition -= fontSize + 10; } @@ -211,10 +205,7 @@ export default class extends Command { `•${detail.value}`, helvetica, fontSize + 3, - width - - margin * 2 - - bulletPointIndent - - subBulletPointIndent + width - margin * 2 - bulletPointIndent - subBulletPointIndent ); for (const line of valueLines) { @@ -228,7 +219,7 @@ export default class extends Command { y: yPosition + 20, size: fontSize + 3, font: helvetica, - color: rgb(13 / 255, 158 / 255, 198 / 255), + color: rgb(13 / 255, 158 / 255, 198 / 255) }); yPosition -= fontSize + 5; } @@ -245,13 +236,13 @@ export default class extends Command { private wrapText( text: string, - font: any, + font: PDFFont, fontSize: number, maxWidth: number ): string[] { - const words = text.split(" "); + const words = text.split(' '); const lines: string[] = []; - let currentLine = ""; + let currentLine = ''; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; @@ -278,10 +269,10 @@ export default class extends Command { interaction: ChatInputCommandInteraction ): Promise> { const userID = interaction.user.id; - const filterBy = interaction.options.getString("filter") ?? "salary"; + const filterBy = interaction.options.getString('filter') ?? 'salary'; const client = await MongoClient.connect(DB.CONNECTION, { - useUnifiedTopology: true, + useUnifiedTopology: true }); const db = client.db(BOT.NAME).collection(DB.USERS); const jobformAnswers: JobPreferences | null = ( @@ -300,7 +291,7 @@ export default class extends Command { preference: jobformAnswers.answers.employmentType, jobType: jobformAnswers.answers.workType, distance: jobformAnswers.answers.travelDistance, - filterBy: filterBy, + filterBy: filterBy }; const interests: Interest = { @@ -308,16 +299,13 @@ export default class extends Command { interest2: jobformAnswers.answers.interest2, interest3: jobformAnswers.answers.interest3, interest4: jobformAnswers.answers.interest4, - interest5: jobformAnswers.answers.interest5, + interest5: jobformAnswers.answers.interest5 }; - const APIResponse: JobResult[] = await fetchJobListings( - jobData, - interests - ); + const APIResponse: JobResult[] = await fetchJobListings(jobData, interests); if (APIResponse.length === 0) { - await interaction.reply("No jobs found based on your interests."); + await interaction.reply('No jobs found based on your interests.'); return; } @@ -325,260 +313,182 @@ export default class extends Command { userJobData.set(userID, { jobs: APIResponse, index: 0 }); // Create embed and buttons for the first job - const { embed, row } = this.createJobEmbed( - APIResponse[0], - 0, - APIResponse.length - ); + const { embed, row } = this.createJobEmbed(APIResponse[0], 0, APIResponse.length); await interaction.reply({ embeds: [embed], components: [row] }); // Listen for button interactions const collector = interaction.channel?.createMessageComponentCollector({ componentType: ComponentType.Button, - time: 60000, + time: 60000 }); - collector?.on("collect", async (i) => { + collector?.on('collect', async (i) => { if (i.user.id !== userID) { - await i.reply({ - content: "This is not your interaction!", - ephemeral: true, - }); + await i.reply({ content: 'This is not your interaction!', ephemeral: true }); return; } const userData = userJobData.get(userID); if (!userData) return; + // eslint-disable-next-line prefer-const -- jobs is mutated via splice() let { jobs, index } = userData; switch (i.customId) { - case "previous": + case 'previous': index = index > 0 ? index - 1 : jobs.length - 1; break; - case "next": + case 'next': index = index < jobs.length - 1 ? index + 1 : 0; break; - case "remove": + case 'remove': jobs.splice(index, 1); if (jobs.length === 0) { - await i.update({ - content: "No more jobs to display.", - embeds: [], - components: [], - }); + await i.update({ content: 'No more jobs to display.', embeds: [], components: [] }); userJobData.delete(userID); return; } index = index >= jobs.length ? 0 : index; break; - case "download": + case 'download': await i.deferReply({ ephemeral: true }); try { const pdfBuffer = await this.generateJobPDF(jobs); - const attachment = new AttachmentBuilder( - pdfBuffer - ).setName("job_listings.pdf"); - - await i.editReply({ - content: - "Here are your job listings in PDF format:", - files: [attachment], - }); + const attachment = new AttachmentBuilder(pdfBuffer).setName('job_listings.pdf'); + await i.editReply({ content: 'Here are your job listings in PDF format:', files: [attachment] }); } catch (error) { - console.error("Error generating PDF:", error); + console.error('Error generating PDF:', error); await i.editReply({ - content: - "Failed to generate PDF. The job listings may contain unsupported characters.", + content: 'Failed to generate PDF. The job listings may contain unsupported characters.' }); } break; - case "share": { + case 'share': { // Show the modal - console.log("🔹 [share] button clicked, showing modal"); await i.showModal( new ModalBuilder() - .setCustomId("shareJobModal") - .setTitle("Share Job") + .setCustomId('shareJobModal') + .setTitle('Share Job') .addComponents( new ActionRowBuilder().addComponents( new TextInputBuilder() - .setCustomId("recipient") - .setLabel( - "Tag user or enter channel ID" - ) + .setCustomId('recipient') + .setLabel('Tag user or enter channel ID') .setStyle(TextInputStyle.Short) .setRequired(true) ), new ActionRowBuilder().addComponents( new TextInputBuilder() - .setCustomId("message") - .setLabel("Add a message (optional)") + .setCustomId('message') + .setLabel('Add a message (optional)') .setStyle(TextInputStyle.Paragraph) .setRequired(false) ) ) ); - console.log("🔹 [share] awaiting modal submit"); try { const modalInteraction = await i.awaitModalSubmit({ - filter: (mi) => - mi.customId === "shareJobModal" && - mi.user.id === userID, - time: 60_000, - }); - console.log("🔹 [share] modal submitted with fields:", { - recipient: - modalInteraction.fields.getTextInputValue( - "recipient" - ), - message: - modalInteraction.fields.getTextInputValue( - "message" - ), + filter: (mi) => mi.customId === 'shareJobModal' && mi.user.id === userID, + time: 60_000 }); - const userData = userJobData.get(userID); - console.log( - "🔹 [share] userData for user:", - userID, - userData - ); - if (!userData) { + const userData2 = userJobData.get(userID); + if (!userData2) { return modalInteraction.reply({ - content: - "⚠️ Session expired—please restart your job search.", - ephemeral: true, + content: '⚠️ Session expired—please restart your job search.', + ephemeral: true }); } - await this.handleShareModal( - modalInteraction, - userData.jobs[userData.index] - ); - console.log("🔹 [share] handleShareModal completed"); - } catch (err) { - console.log( - "🔹 [share] awaitModalSubmit timed out or threw:", - err - ); + await this.handleShareModal(modalInteraction, userData2.jobs[userData2.index]); + } catch { + // swallow timeout or other modal errors } return; } } + // Update user data userJobData.set(userID, { jobs, index }); - // Update embed and buttons - const { embed, row } = this.createJobEmbed( - jobs[index], - index, - jobs.length - ); - await i.update({ embeds: [embed], components: [row] }); + // Update embed and buttons (rename on destructure to avoid shadowing) + const { embed: newEmbed, row: newRow } = this.createJobEmbed(jobs[index], index, jobs.length); + await i.update({ embeds: [newEmbed], components: [newRow] }); }); - collector?.on("end", () => { + collector?.on('end', () => { userJobData.delete(userID); }); } - private async handleShareModal( - modal: ModalSubmitInteraction, - job: JobResult - ) { + private async handleShareModal(modal: ModalSubmitInteraction, job: JobResult) { // Ack the modal immediately so Discord stops showing the spinner await modal.deferReply({ ephemeral: true }); - const raw = modal.fields.getTextInputValue("recipient").trim(); + const raw = modal.fields.getTextInputValue('recipient').trim(); let targetUser: User | null = null; // Only these channel classes have .send() - let targetChannel: - | TextChannel - | DMChannel - | NewsChannel - | ThreadChannel - | null = null; + let targetChannel: TextChannel | DMChannel | NewsChannel | ThreadChannel | null = null; // Try mention (<@...>), channel mention (<#...>), or raw ID const idMatch = raw.match(/^<@!?(\d+)>$|^<#(\d+)>$|^(\d{17,19})$/); if (idMatch) { const id = idMatch[1] || idMatch[2] || idMatch[3]; - //as a User + // as a User targetUser = await modal.client.users.fetch(id).catch(() => null); // as a TextChannel/DMChannel/NewsChannel/ThreadChannel if (!targetUser && modal.guild) { const ch = modal.guild.channels.cache.get(id); - if ( - ch instanceof TextChannel || - ch instanceof DMChannel || - ch instanceof NewsChannel || - ch instanceof ThreadChannel - ) { + if (ch instanceof TextChannel || ch instanceof DMChannel || ch instanceof NewsChannel || ch instanceof ThreadChannel) { targetChannel = ch; } } } // Fallback: look up username#discriminator in cache - if (!targetUser && !targetChannel && raw.includes("#")) { + if (!targetUser && !targetChannel && raw.includes('#')) { const lowerTag = raw.toLowerCase(); - targetUser = - modal.client.users.cache.find( - (u) => u.tag.toLowerCase() === lowerTag - ) || null; + targetUser = modal.client.users.cache.find((user) => user.tag.toLowerCase() === lowerTag) || null; } if (!targetUser && !targetChannel) { return modal.editReply({ content: - "❌ Couldn’t resolve that as a user or channel. Please use a user mention (`<@ID>`), channel mention (`<#ID>`), raw ID, or a cached `username#1234`.", + '❌ Couldn’t resolve that as a user or channel. Please use a user mention (`<@ID>`), channel mention (`<#ID>`), raw ID, or a cached `username#1234`.' }); } - // 5) Build the embed to share + // Build the embed to share const shareEmbed = new EmbedBuilder() .setTitle(`Job Shared: ${job.title}`) - .setDescription( - `${ - modal.fields.getTextInputValue("message") || "" - }\n\n**Shared by:** <@${modal.user.id}>` - ) + .setDescription(`${modal.fields.getTextInputValue('message') || ''}\n\n**Shared by:** <@${modal.user.id}>`) .addFields( - { name: "Location", value: job.location, inline: true }, - { - name: "Posted", - value: new Date(job.created).toDateString(), - inline: true, - }, - { name: "Apply Here", value: `[Click here](${job.link})` } + { name: 'Location', value: job.location, inline: true }, + { name: 'Posted', value: new Date(job.created).toDateString(), inline: true }, + { name: 'Apply Here', value: `[Click here](${job.link})` } ) - .setColor("#4CAF50"); + .setColor('#4CAF50'); try { if (targetUser) { await targetUser.send({ embeds: [shareEmbed] }); - await modal.editReply({ - content: `✅ Job shared with <@${targetUser.id}>!`, - }); - } else { - await targetChannel!.send({ embeds: [shareEmbed] }); - await modal.editReply({ - content: `✅ Job shared in <#${targetChannel!.id}>!`, - }); + await modal.editReply({ content: `✅ Job shared with <@${targetUser.id}>!` }); + } else if (targetChannel) { + await targetChannel.send({ embeds: [shareEmbed] }); + await modal.editReply({ content: `✅ Job shared in <#${targetChannel.id}>!` }); } } catch (err) { - console.error("Failed to deliver shared job:", err); + console.error('Failed to deliver shared job:', err); await modal.editReply({ - content: - "⚠️ Couldn’t deliver the share—perhaps the user has DMs closed or I lack permission in that channel.", + content: '⚠️ Couldn’t deliver the share—perhaps the user has DMs closed or I lack permission in that channel.' }); } } + createJobEmbed( job: JobResult, index: number, @@ -586,48 +496,20 @@ export default class extends Command { ): { embed: EmbedBuilder; row: ActionRowBuilder } { const embed = new EmbedBuilder() .setTitle(job.title) - .setDescription( - `**Location:** ${job.location}\n**Date Posted:** ${new Date( - job.created - ).toDateString()}` - ) + .setDescription(`**Location:** ${job.location}\n**Date Posted:** ${new Date(job.created).toDateString()}`) .addFields( - { name: "Salary", value: this.formatSalary(job), inline: true }, - { - name: "Apply Here", - value: `[Click here](${job.link})`, - inline: true, - } + { name: 'Salary', value: this.formatSalary(job), inline: true }, + { name: 'Apply Here', value: `[Click here](${job.link})`, inline: true } ) .setFooter({ text: `Job ${index + 1} of ${totalJobs}` }) - .setColor("#0099ff"); + .setColor('#0099ff'); const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("previous") - .setLabel("Previous") - .setStyle(ButtonStyle.Primary) - .setDisabled(totalJobs === 1), - new ButtonBuilder() - .setCustomId("remove") - .setLabel("Remove") - .setStyle(ButtonStyle.Danger) - .setDisabled(totalJobs === 1), - new ButtonBuilder() - .setCustomId("next") - .setLabel("Next") - .setStyle(ButtonStyle.Primary) - .setDisabled(totalJobs === 1), - new ButtonBuilder() - .setCustomId("download") - .setLabel("Download PDF") - .setStyle(ButtonStyle.Success) - .setEmoji("📄"), - new ButtonBuilder() - .setCustomId("share") - .setLabel("Share") - .setStyle(ButtonStyle.Secondary) - .setEmoji("↗️") + new ButtonBuilder().setCustomId('previous').setLabel('Previous').setStyle(ButtonStyle.Primary).setDisabled(totalJobs === 1), + new ButtonBuilder().setCustomId('remove').setLabel('Remove').setStyle(ButtonStyle.Danger).setDisabled(totalJobs === 1), + new ButtonBuilder().setCustomId('next').setLabel('Next').setStyle(ButtonStyle.Primary).setDisabled(totalJobs === 1), + new ButtonBuilder().setCustomId('download').setLabel('Download PDF').setStyle(ButtonStyle.Success).setEmoji('📄'), + new ButtonBuilder().setCustomId('share').setLabel('Share').setStyle(ButtonStyle.Secondary).setEmoji('↗️') ); return { embed, row }; @@ -637,14 +519,10 @@ export default class extends Command { formatSalary(job: JobResult): string { const avgSalary = (Number(job.salaryMax) + Number(job.salaryMin)) / 2; const formattedAvgSalary = this.formatCurrency(avgSalary); - const formattedSalaryMax = - this.formatCurrency(Number(job.salaryMax)) !== "N/A" - ? this.formatCurrency(Number(job.salaryMax)) - : ""; - const formattedSalaryMin = - this.formatCurrency(Number(job.salaryMin)) !== "N/A" - ? this.formatCurrency(Number(job.salaryMin)) - : ""; + const formattedSalaryMax + = this.formatCurrency(Number(job.salaryMax)) !== 'N/A' ? this.formatCurrency(Number(job.salaryMax)) : ''; + const formattedSalaryMin + = this.formatCurrency(Number(job.salaryMin)) !== 'N/A' ? this.formatCurrency(Number(job.salaryMin)) : ''; return formattedSalaryMin && formattedSalaryMax ? `Avg: ${formattedAvgSalary}\nMin: ${formattedSalaryMin}\nMax: ${formattedSalaryMax}` @@ -654,14 +532,10 @@ export default class extends Command { formatSalaryforPDF(job: JobResult): string { const avgSalary = (Number(job.salaryMax) + Number(job.salaryMin)) / 2; const formattedAvgSalary = this.formatCurrency(avgSalary); - const formattedSalaryMax = - this.formatCurrency(Number(job.salaryMax)) !== "N/A" - ? this.formatCurrency(Number(job.salaryMax)) - : ""; - const formattedSalaryMin = - this.formatCurrency(Number(job.salaryMin)) !== "N/A" - ? this.formatCurrency(Number(job.salaryMin)) - : ""; + const formattedSalaryMax + = this.formatCurrency(Number(job.salaryMax)) !== 'N/A' ? this.formatCurrency(Number(job.salaryMax)) : ''; + const formattedSalaryMin + = this.formatCurrency(Number(job.salaryMin)) !== 'N/A' ? this.formatCurrency(Number(job.salaryMin)) : ''; return formattedSalaryMin && formattedSalaryMax ? `Avg: ${formattedAvgSalary}, Min: ${formattedSalaryMin}, Max: ${formattedSalaryMax}` @@ -670,83 +544,45 @@ export default class extends Command { formatCurrency(currency: number): string { return isNaN(currency) - ? "N/A" - : `${new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(Number(currency))}`; + ? 'N/A' + : `${new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(Number(currency))}`; } stripMarkdown(message: string, owner: string): string { return message .replace( - new RegExp( - `## Hey <@${owner}>!\\s*## Here's your list of job/internship recommendations:?`, - "g" - ), - "" + new RegExp(`## Hey <@${owner}>!\\s*## Here's your list of job/internship recommendations:?`, 'g'), + '' ) - .replace(/\[read more about the job and apply here\]/g, "") - .replace(/\((https?:\/\/[^\s)]+)\)/g, "$1") - .replace(/\*\*([^*]+)\*\*/g, "$1") - .replace(/##+\s*/g, "") - .replace(/###|-\#\s*/g, "") + .replace(/\[read more about the job and apply here\]/g, '') + .replace(/\((https?:\/\/[^\s)]+)\)/g, '$1') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/##+\s*/g, '') + .replace(/###|-#\s*/g, '') // remove unnecessary escape: \# .trim(); } - headerMessage(owner: string, filterBy: string): string { - return `## Hey <@${owner}>! - ### **__Please read this disclaimer before reading your list of jobs/internships__:** - -# Please be aware that the job listings displayed are retrieved from a third-party API. \ - While we strive to provide accurate information, we cannot guarantee the legitimacy or security \ - of all postings. Exercise caution when sharing personal information, submitting resumes, or registering \ - on external sites. Always verify the authenticity of job applications before proceeding. Additionally, \ - some job postings may contain inaccuracies due to API limitations, which are beyond our control. We apologize for any inconvenience this may cause and appreciate your understanding. - ## Here's your list of job/internship recommendations${ - filterBy && filterBy !== "default" - ? ` (filtered based on ${ - filterBy === "date" ? "date posted" : filterBy - }):` - : ":" - } - `; - } - - calculateDistance( - lat1: number, - lon1: number, - lat2: number, - lon2: number - ): number { + calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const toRadians = (degrees: number) => degrees * (Math.PI / 180); - const Radius = 3958.8; // Radius of the Earth in miles const dLat = toRadians(lat2 - lat1); const dLon = toRadians(lon2 - lon1); - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(toRadians(lat1)) * - Math.cos(toRadians(lat2)) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2); + const a + = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - const distance = - (lat1 === 0 && lon1 === 0) || (lat2 === 0 && lon2 === 0) - ? -1 - : Radius * c; + const distance = (lat1 === 0 && lon1 === 0) || (lat2 === 0 && lon2 === 0) ? -1 : Radius * c; return distance; } - async queryCoordinates(location: string): Promise { + async queryCoordinates(location: string): Promise<{ lat: number; lng: number }> { const preferredCity = encodeURIComponent(location); - - const baseURL = `https://maps.google.com/maps/api/geocode/json?address=${preferredCity}&components=country:US&key=${MAP_KEY}`; + const baseURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${preferredCity}&components=country:US&key=${MAP_KEY}`; const response = await axios.get(baseURL); - const coordinates: { lat: number; lng: number } = { + return { lat: response.data.results[0].geometry.location.lat, - lng: response.data.results[0].geometry.location.lng, + lng: response.data.results[0].geometry.location.lng }; - - return coordinates; } + } diff --git a/src/commands/reminders/remindermenu.ts b/src/commands/reminders/remindermenu.ts index 42840878..d97ed3a4 100644 --- a/src/commands/reminders/remindermenu.ts +++ b/src/commands/reminders/remindermenu.ts @@ -6,13 +6,15 @@ import { Command } from '@lib/types/Command'; import { showMainMenu } from '../../newreminders/menu-handlers'; export default class extends Command { - description = `View ${BOT.NAME} reminders menu.`; - extendedHelp = 'Create reminders for anything - one-time or recurring job alerts with optional email notifications.'; - options = []; // No options needed as we are using buttons - async run( - interaction: ChatInputCommandInteraction - ): Promise { - await showMainMenu(interaction); - } -} \ No newline at end of file + description = `View ${BOT.NAME} reminders menu.`; + extendedHelp = 'Create reminders for anything - one-time or recurring job alerts with optional email notifications.'; + options = []; // No options needed as we are using buttons + + async run( + interaction: ChatInputCommandInteraction + ): Promise { + await showMainMenu(interaction); + } + +} diff --git a/src/lib/types/Reminder.d.ts b/src/lib/types/Reminder.d.ts index 2eab37ef..34e91417 100644 --- a/src/lib/types/Reminder.d.ts +++ b/src/lib/types/Reminder.d.ts @@ -1,10 +1,10 @@ export interface Reminder { - owner: string; - expires: Date; - content: string; - repeat: null | 'daily' | 'weekly'; - mode: 'public' | 'private'; - filterBy?: 'relevance' | 'salary' | 'date' | 'default' | null; - emailNotification?: boolean; - emailAddress?: string; -} \ No newline at end of file + owner: string; + expires: Date; + content: string; + repeat: null | 'daily' | 'weekly'; + mode: 'public' | 'private'; + filterBy?: 'relevance' | 'salary' | 'date' | 'default' | null; + emailNotification?: boolean; + emailAddress?: string; +} diff --git a/src/lib/utils/jobUtils/jobDatabase.ts b/src/lib/utils/jobUtils/jobDatabase.ts index 0a7e9f12..96cf843f 100644 --- a/src/lib/utils/jobUtils/jobDatabase.ts +++ b/src/lib/utils/jobUtils/jobDatabase.ts @@ -21,7 +21,7 @@ export class JobPreferenceAPI { const updateObject = {}; // Checks if the answer provided is accuate. - const [city, workType, employmentType, travelDistance, _, interest1, interest2, interest3, interest4, interest5] = answers.map((a) => titleCase(a.trim())); + const [city, workType, employmentType, travelDistance, , interest1, interest2, interest3, interest4, interest5] = answers.map((a) => titleCase(a.trim())); if (city) { updateObject['jobPreferences.answers.city'] = city; } if (workType) { updateObject['jobPreferences.answers.workType'] = workType; } diff --git a/src/newreminders/constants.ts b/src/newreminders/constants.ts index 75875e30..8137979b 100644 --- a/src/newreminders/constants.ts +++ b/src/newreminders/constants.ts @@ -2,22 +2,22 @@ // Emoji constants for button icons export const EMOJI = { - REMINDER: '⏰', - JOB: '💼', - VIEW: '📋', - CANCEL: '✖️', - TIME: '🕒', - REPEAT: '🔄', - BACK: '↩️', - EMAIL: '📧' + REMINDER: '⏰', + JOB: '💼', + VIEW: '📋', + CANCEL: '✖️', + TIME: '🕒', + REPEAT: '🔄', + BACK: '↩️', + EMAIL: '📧' }; // Color constants for embeds (using Discord.js ColorResolvable) export const COLORS = { - PRIMARY: 0x5865F2, // Discord Blurple - SUCCESS: 0x57F287, // Green - DANGER: 0xED4245, // Red - WARNING: 0xFEE75C, // Yellow - SECONDARY: 0x9BA4EC, // Light Blurple - INFO: 0x5CBEFE // Light Blue -}; \ No newline at end of file + PRIMARY: 0x5865F2, // Discord Blurple + SUCCESS: 0x57F287, // Green + DANGER: 0xED4245, // Red + WARNING: 0xFEE75C, // Yellow + SECONDARY: 0x9BA4EC, // Light Blurple + INFO: 0x5CBEFE // Light Blue +}; diff --git a/src/newreminders/email-handlers.ts b/src/newreminders/email-handlers.ts index 2de0c9bc..385a0470 100644 --- a/src/newreminders/email-handlers.ts +++ b/src/newreminders/email-handlers.ts @@ -1,110 +1,108 @@ // Email handling functionality for reminders -import { ModalSubmitInteraction, ButtonInteraction } from "discord.js"; -import { ReminderData, JobReminderData } from "./types"; -import { COLORS, EMOJI } from "./constants"; -import { createEmailInputModal } from "./ui"; -import { createErrorEmbed, isValidEmail } from "./utils"; -import { completeReminderCreation } from "./reminder-handlers"; -import { completeJobReminderCreation } from "./job-handlers"; +import { ButtonInteraction } from 'discord.js'; +import { createEmailInputModal } from './ui'; +import { createErrorEmbed, isValidEmail } from './utils'; +import { completeReminderCreation } from './reminder-handlers'; +import { completeJobReminderCreation } from './job-handlers'; -/** +/* * Show modal to collect email address for standard reminders */ export async function showEmailModal(buttonInteraction: ButtonInteraction): Promise { - // Create modal for email address - const modal = createEmailInputModal(); + // Create modal for email address + const modal = createEmailInputModal(); - // Show the modal - await buttonInteraction.showModal(modal); + // Show the modal + await buttonInteraction.showModal(modal); - try { - // Wait for modal submission - const modalInteraction = await buttonInteraction.awaitModalSubmit({ - time: 180000, // 3 minutes (extended) - filter: (i) => - i.customId === 'email_modal' && - i.user.id === buttonInteraction.user.id - }); + try { + // Wait for modal submission + const modalInteraction = await buttonInteraction.awaitModalSubmit({ + time: 180000, // 3 minutes (extended) + filter: (i) => + i.customId === 'email_modal' + && i.user.id === buttonInteraction.user.id + }); - // Process modal submission - const email = modalInteraction.fields.getTextInputValue('email'); - - // Simple email validation - if (!isValidEmail(email)) { - const errorEmbed = createErrorEmbed( - "Invalid Email Address", - `**"${email}"** does not appear to be a valid email address.` - ).setFooter({ text: 'Please try again with a valid email address' }); - - // Reply with error - await modalInteraction.reply({ - embeds: [errorEmbed], - ephemeral: true - }); - return; - } + // Process modal submission + const email = modalInteraction.fields.getTextInputValue('email'); - // Finalize the reminder creation with email - await completeReminderCreation( - buttonInteraction, - true, - email, - modalInteraction - ); - } catch (error) { - console.error('Error in email modal submission:', error); - - // Only try to update if we haven't already replied - try { - // Fallback to creating the reminder without email - await completeReminderCreation(buttonInteraction, false, null); - } catch (updateError) { - console.error('Error updating after email modal error:', updateError); - } - } + // Simple email validation + if (!isValidEmail(email)) { + const errorEmbed = createErrorEmbed( + 'Invalid Email Address', + `**"${email}"** does not appear to be a valid email address.` + ).setFooter({ text: 'Please try again with a valid email address' }); + + // Reply with error + await modalInteraction.reply({ + embeds: [errorEmbed], + ephemeral: true + }); + return; + } + + // Finalize the reminder creation with email + await completeReminderCreation( + buttonInteraction, + true, + email, + modalInteraction + ); + } catch (error) { + console.error('Error in email modal submission:', error); + + // Only try to update if we haven't already replied + try { + // Fallback to creating the reminder without email + await completeReminderCreation(buttonInteraction, false, null); + } catch (updateError) { + console.error('Error updating after email modal error:', updateError); + } + } } -/** +/* * Show modal to collect email address for job reminders */ export async function showJobEmailModal(buttonInteraction: ButtonInteraction): Promise { - // Create modal for email address - const modal = createEmailInputModal(true); + // Create modal for email address + const modal = createEmailInputModal(true); - // Show the modal - await buttonInteraction.showModal(modal); + // Show the modal + await buttonInteraction.showModal(modal); - try { - // Wait for modal submission - const modalInteraction = await buttonInteraction.awaitModalSubmit({ - time: 180000, // 3 minutes - filter: (mi) => - mi.customId === 'job_email_modal' && - mi.user.id === buttonInteraction.user.id - }); + try { + // Wait for modal submission + const modalInteraction = await buttonInteraction.awaitModalSubmit({ + time: 180000, // 3 minutes + filter: (mi) => + mi.customId === 'job_email_modal' + && mi.user.id === buttonInteraction.user.id + }); - // Process the email and finalize - const email = modalInteraction.fields.getTextInputValue('email'); - - // Email validation - if (!isValidEmail(email)) { - await modalInteraction.reply({ - content: `Invalid email address format. Please try again.`, - ephemeral: true - }); - return; - } - - // Finalize job reminder creation with email - await completeJobReminderCreation( - buttonInteraction, - true, - email, - modalInteraction - ); - } catch (error) { - console.error('Error in job email modal submission:', error); - // Fallback to no email - await completeJobReminderCreation(buttonInteraction, false, null); - } -} \ No newline at end of file + // Process the email and finalize + const email = modalInteraction.fields.getTextInputValue('email'); + + // Email validation + if (!isValidEmail(email)) { + await modalInteraction.reply({ + content: `Invalid email address format. Please try again.`, + ephemeral: true + }); + return; + } + + // Finalize job reminder creation with email + await completeJobReminderCreation( + buttonInteraction, + true, + email, + modalInteraction + ); + } catch (error) { + console.error('Error in job email modal submission:', error); + // Fallback to no email + await completeJobReminderCreation(buttonInteraction, false, null); + } +} diff --git a/src/newreminders/index.ts b/src/newreminders/index.ts index 6fa0f90f..95ad43a2 100644 --- a/src/newreminders/index.ts +++ b/src/newreminders/index.ts @@ -12,4 +12,4 @@ export * from './utils'; export * from './email-handlers'; export * from './reminder-handlers'; export * from './job-handlers'; -export * from './menu-handlers'; \ No newline at end of file +export * from './menu-handlers'; diff --git a/src/newreminders/job-handlers.ts b/src/newreminders/job-handlers.ts index 39c1850f..e14fcfed 100644 --- a/src/newreminders/job-handlers.ts +++ b/src/newreminders/job-handlers.ts @@ -1,299 +1,298 @@ // Job reminder handling functionality -import { - ButtonInteraction, - ModalSubmitInteraction, - ComponentType, - EmbedBuilder -} from "discord.js"; -import { JobReminderData } from "./types"; -import { COLORS, EMOJI } from "./constants"; -import { - createBackButton, - createEmailOptionsEmbed, - createEmailOptionsButtons, - createJobReminderModal -} from "./ui"; -import { createErrorEmbed, createJobReminderSuccessEmbed, checkJobReminderForButton } from "./utils"; -import { DB } from "@root/config"; -import { Reminder } from "@lib/types/Reminder"; -import { reminderTime } from "@root/src/lib/utils/generalUtils"; -import { showJobEmailModal } from "./email-handlers"; - -/** +import { + ButtonInteraction, + ModalSubmitInteraction, + ComponentType, + EmbedBuilder +} from 'discord.js'; +import { JobReminderData } from './types'; +import { COLORS, EMOJI } from './constants'; +import { + createBackButton, + createEmailOptionsEmbed, + createEmailOptionsButtons, + createJobReminderModal +} from './ui'; +import { createErrorEmbed, createJobReminderSuccessEmbed, checkJobReminderForButton } from './utils'; +import { DB } from '@root/config'; +import { Reminder } from '@lib/types/Reminder'; +import { reminderTime } from '@root/src/lib/utils/generalUtils'; +import { showJobEmailModal } from './email-handlers'; + +/* * Handle creating a job reminder */ export async function handleCreateJobReminder(buttonInteraction: ButtonInteraction): Promise { - // Check for existing job reminder using our utility that works with ButtonInteraction - if (await checkJobReminderForButton(buttonInteraction)) { - const errorEmbed = createErrorEmbed( - "Job Reminder Already Exists", - "You currently already have a job reminder set. To clear your existing job reminder, use the CANCEL button and provide the reminder number." - ); - - await buttonInteraction.update({ - embeds: [errorEmbed], - components: [createBackButton()], // Add back button - }); - return; - } - - // Create modal for job reminder settings - const modal = createJobReminderModal(); - - // Show the modal - await buttonInteraction.showModal(modal); - - // Wait for modal submission - try { - const modalInteraction = await buttonInteraction.awaitModalSubmit({ - time: 180000, // 3 minutes (extended) - filter: (i: ModalSubmitInteraction) => - i.customId === 'job_reminder_modal' && - i.user.id === buttonInteraction.user.id - }); - - // Process modal submission - let repeatValue = modalInteraction.fields.getTextInputValue('repeat').toLowerCase(); - let filterValue = modalInteraction.fields.getTextInputValue('filter').toLowerCase(); - - // Validate repeat input - if (repeatValue !== 'daily' && repeatValue !== 'weekly' && repeatValue !== 'monthly') { - const errorEmbed = createErrorEmbed( - "Invalid Repeat Option", - `**"${repeatValue}"** is not a valid repeat option. Please use "daily", "weekly", or "monthly".` - ); - - // Defer the modal reply to acknowledge it without sending a visible message - await modalInteraction.deferUpdate(); - - // Update the original message with the error - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()], // Add back button - }); - - return; - } - - // Validate filter input - const validFilters = ['default', 'relevance', 'salary', 'date']; - if (!validFilters.includes(filterValue)) { - filterValue = 'default'; // Fallback to default if invalid - } - - // Store job reminder data - const jobReminderData: JobReminderData = { - repeatValue, - filterValue, - buttonInteraction, - modalInteraction - }; - - // Ask about email notifications - await askForJobEmailNotification(jobReminderData); - - } catch (error) { - console.error('Error in modal submission:', error); - const errorEmbed = createErrorEmbed( - "Job Alert Creation Failed", - "The job alert creation process timed out or an error occurred." - ); - - // Update the original button interaction instead of creating a new message - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()], // Add back button - }); - } + // Check for existing job reminder using our utility that works with ButtonInteraction + if (await checkJobReminderForButton(buttonInteraction)) { + const errorEmbed = createErrorEmbed( + 'Job Reminder Already Exists', + 'You currently already have a job reminder set. To clear your existing job reminder, use the CANCEL button and provide the reminder number.' + ); + + await buttonInteraction.update({ + embeds: [errorEmbed], + components: [createBackButton()] // Add back button + }); + return; + } + + // Create modal for job reminder settings + const modal = createJobReminderModal(); + + // Show the modal + await buttonInteraction.showModal(modal); + + // Wait for modal submission + try { + const modalInteraction = await buttonInteraction.awaitModalSubmit({ + time: 180000, // 3 minutes (extended) + filter: (i: ModalSubmitInteraction) => + i.customId === 'job_reminder_modal' + && i.user.id === buttonInteraction.user.id + }); + + // Process modal submission + const repeatValue = modalInteraction.fields.getTextInputValue('repeat').toLowerCase(); + let filterValue = modalInteraction.fields.getTextInputValue('filter').toLowerCase(); + + // Validate repeat input + if (repeatValue !== 'daily' && repeatValue !== 'weekly' && repeatValue !== 'monthly') { + const errorEmbed = createErrorEmbed( + 'Invalid Repeat Option', + `**"${repeatValue}"** is not a valid repeat option. Please use "daily", "weekly", or "monthly".` + ); + + // Defer the modal reply to acknowledge it without sending a visible message + await modalInteraction.deferUpdate(); + + // Update the original message with the error + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] // Add back button + }); + + return; + } + + // Validate filter input + const validFilters = ['default', 'relevance', 'salary', 'date']; + if (!validFilters.includes(filterValue)) { + filterValue = 'default'; // Fallback to default if invalid + } + + // Store job reminder data + const jobReminderData: JobReminderData = { + repeatValue, + filterValue, + buttonInteraction, + modalInteraction + }; + + // Ask about email notifications + await askForJobEmailNotification(jobReminderData); + } catch (error) { + console.error('Error in modal submission:', error); + const errorEmbed = createErrorEmbed( + 'Job Alert Creation Failed', + 'The job alert creation process timed out or an error occurred.' + ); + + // Update the original button interaction instead of creating a new message + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] // Add back button + }); + } } -/** +/* * Ask if the user wants email notifications for job reminders */ export async function askForJobEmailNotification(jobReminderData: JobReminderData): Promise { - const { buttonInteraction, modalInteraction } = jobReminderData; - - // Create embed asking about email notifications - const emailEmbed = createEmailOptionsEmbed(true); - - // Create Yes/No buttons - const emailRow = createEmailOptionsButtons(true); - - // Store the job reminder data in the client's temporary collection - modalInteraction.client.jobReminderTemp = jobReminderData; - - // Defer the modal reply to acknowledge it - await modalInteraction.deferUpdate(); - - // Update original message to ask about email - const message = await buttonInteraction.editReply({ - embeds: [emailEmbed], - components: [emailRow] - }); - - // Create a dedicated collector for this specific message - const collector = message.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 60000 // 1 minute timeout - }); - - collector.on('collect', async (i) => { - // Make sure it's the right user - if (i.user.id !== buttonInteraction.user.id) { - await i.reply({ - content: 'This button is not for you.', - ephemeral: true - }); - return; - } - - // Stop the collector since we've handled the interaction - collector.stop(); - - if (i.customId === 'job_email_yes') { - await showJobEmailModal(i); - } else if (i.customId === 'job_email_no') { - // Handle "No, Discord only" option - await completeJobReminderCreation(buttonInteraction, false, null); - } - }); - - // Handle collector end (timeout) - collector.on('end', collected => { - if (collected.size === 0) { - // If no buttons were pressed, create without email - completeJobReminderCreation(buttonInteraction, false, null); - } - }); + const { buttonInteraction, modalInteraction } = jobReminderData; + + // Create embed asking about email notifications + const emailEmbed = createEmailOptionsEmbed(true); + + // Create Yes/No buttons + const emailRow = createEmailOptionsButtons(true); + + // Store the job reminder data in the client's temporary collection + modalInteraction.client.jobReminderTemp = jobReminderData; + + // Defer the modal reply to acknowledge it + await modalInteraction.deferUpdate(); + + // Update original message to ask about email + const message = await buttonInteraction.editReply({ + embeds: [emailEmbed], + components: [emailRow] + }); + + // Create a dedicated collector for this specific message + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 60000 // 1 minute timeout + }); + + collector.on('collect', async (i) => { + // Make sure it's the right user + if (i.user.id !== buttonInteraction.user.id) { + await i.reply({ + content: 'This button is not for you.', + ephemeral: true + }); + return; + } + + // Stop the collector since we've handled the interaction + collector.stop(); + + if (i.customId === 'job_email_yes') { + await showJobEmailModal(i); + } else if (i.customId === 'job_email_no') { + // Handle "No, Discord only" option + await completeJobReminderCreation(buttonInteraction, false, null); + } + }); + + // Handle collector end (timeout) + collector.on('end', collected => { + if (collected.size === 0) { + // If no buttons were pressed, create without email + completeJobReminderCreation(buttonInteraction, false, null); + } + }); } -/** +/* * Create and store the job reminder with or without email */ export async function completeJobReminderCreation( - buttonInteraction: ButtonInteraction, - withEmail: boolean, - email: string | null, - modalInteraction?: ModalSubmitInteraction + buttonInteraction: ButtonInteraction, + withEmail: boolean, + email: string | null, + modalInteraction?: ModalSubmitInteraction ): Promise { - try { - // Get the job reminder data - const jobReminderData = buttonInteraction.client.jobReminderTemp; - - // Check if we have valid job reminder data - if (!jobReminderData || !jobReminderData.repeatValue || !jobReminderData.filterValue) { - const errorEmbed = createErrorEmbed( - "Error Creating Job Alert", - "Missing job alert information. Please try creating your job alert again." - ); - - // If we have a modal interaction, respond to that - if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { - await modalInteraction.reply({ - embeds: [errorEmbed], - ephemeral: true - }); - } else { - // Otherwise try to update the button interaction - try { - await buttonInteraction.update({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - } catch (updateError) { - // If updating fails, try editing - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - } - } - return; - } - - const { repeatValue, filterValue } = jobReminderData; - - // Create the job reminder object - const jobReminder: Reminder = { - owner: buttonInteraction.user.id, - content: 'Job Reminder', - mode: 'private', - expires: new Date(), // Set to now, will be handled by the job scheduler - repeat: repeatValue as 'daily' | 'weekly', - filterBy: filterValue as 'default' | 'relevance' | 'salary' | 'date', - emailNotification: withEmail, - emailAddress: withEmail ? email : null - }; - - // Store the job reminder in the database - await buttonInteraction.client.mongo - .collection(DB.REMINDERS) - .insertOne(jobReminder); - - // Create success embed - const successEmbed = createJobReminderSuccessEmbed( - repeatValue, - filterValue, - reminderTime(jobReminder), - withEmail, - email - ); - - // Handle the response based on which interaction is available - if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { - // If we have a modal interaction that hasn't been replied to yet - await modalInteraction.reply({ - content: "Your job alert has been created successfully!", - ephemeral: true - }); - - // Update the original message - await buttonInteraction.editReply({ - embeds: [successEmbed], - components: [createBackButton()] - }); - } else { - // Otherwise try to update the button interaction - try { - await buttonInteraction.update({ - embeds: [successEmbed], - components: [createBackButton()] - }); - } catch (updateError) { - // If updating fails, try editing - await buttonInteraction.editReply({ - embeds: [successEmbed], - components: [createBackButton()] - }); - } - } - - // Clean up temporary data - delete buttonInteraction.client.jobReminderTemp; - } catch (error) { - console.error('Error in completeJobReminderCreation:', error); - - // Try to give feedback through any available channel - const errorEmbed = new EmbedBuilder() - .setColor(COLORS.WARNING) - .setTitle(`${EMOJI.JOB} Job Alert Process Completed`) - .setDescription("Your job alert has been created, but there was an issue updating the display.") - .setTimestamp(); - - try { - if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { - await modalInteraction.reply({ - embeds: [errorEmbed], - ephemeral: true - }); - } else { - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - } - } catch (secondError) { - console.error('Even the error handler failed:', secondError); - } - } -} \ No newline at end of file + try { + // Get the job reminder data + const jobReminderData = buttonInteraction.client.jobReminderTemp; + + // Check if we have valid job reminder data + if (!jobReminderData || !jobReminderData.repeatValue || !jobReminderData.filterValue) { + const errorEmbed = createErrorEmbed( + 'Error Creating Job Alert', + 'Missing job alert information. Please try creating your job alert again.' + ); + + // If we have a modal interaction, respond to that + if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { + await modalInteraction.reply({ + embeds: [errorEmbed], + ephemeral: true + }); + } else { + // Otherwise try to update the button interaction + try { + await buttonInteraction.update({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + } catch (updateError) { + // If updating fails, try editing + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + } + } + return; + } + + const { repeatValue, filterValue } = jobReminderData; + + // Create the job reminder object + const jobReminder: Reminder = { + owner: buttonInteraction.user.id, + content: 'Job Reminder', + mode: 'private', + expires: new Date(), // Set to now, will be handled by the job scheduler + repeat: repeatValue as 'daily' | 'weekly', + filterBy: filterValue as 'default' | 'relevance' | 'salary' | 'date', + emailNotification: withEmail, + emailAddress: withEmail ? email : null + }; + + // Store the job reminder in the database + await buttonInteraction.client.mongo + .collection(DB.REMINDERS) + .insertOne(jobReminder); + + // Create success embed + const successEmbed = createJobReminderSuccessEmbed( + repeatValue, + filterValue, + reminderTime(jobReminder), + withEmail, + email + ); + + // Handle the response based on which interaction is available + if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { + // If we have a modal interaction that hasn't been replied to yet + await modalInteraction.reply({ + content: 'Your job alert has been created successfully!', + ephemeral: true + }); + + // Update the original message + await buttonInteraction.editReply({ + embeds: [successEmbed], + components: [createBackButton()] + }); + } else { + // Otherwise try to update the button interaction + try { + await buttonInteraction.update({ + embeds: [successEmbed], + components: [createBackButton()] + }); + } catch (updateError) { + // If updating fails, try editing + await buttonInteraction.editReply({ + embeds: [successEmbed], + components: [createBackButton()] + }); + } + } + + // Clean up temporary data + delete buttonInteraction.client.jobReminderTemp; + } catch (error) { + console.error('Error in completeJobReminderCreation:', error); + + // Try to give feedback through any available channel + const errorEmbed = new EmbedBuilder() + .setColor(COLORS.WARNING) + .setTitle(`${EMOJI.JOB} Job Alert Process Completed`) + .setDescription('Your job alert has been created, but there was an issue updating the display.') + .setTimestamp(); + + try { + if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { + await modalInteraction.reply({ + embeds: [errorEmbed], + ephemeral: true + }); + } else { + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + } + } catch (secondError) { + console.error('Even the error handler failed:', secondError); + } + } +} diff --git a/src/newreminders/menu-handlers.ts b/src/newreminders/menu-handlers.ts index d49709e1..2c543f62 100644 --- a/src/newreminders/menu-handlers.ts +++ b/src/newreminders/menu-handlers.ts @@ -1,342 +1,346 @@ // Menu handling functionality for the reminder system -import { - ChatInputCommandInteraction, - ButtonInteraction, - ComponentType, - EmbedBuilder -} from "discord.js"; -import { COLORS, EMOJI } from "./constants"; -import { - createMainMenuEmbed, - createMainMenuButtons, - createBackButton, - createCancelReminderModal -} from "./ui"; -import { handleCreateReminder, completeReminderCreation } from "./reminder-handlers"; -import { handleCreateJobReminder } from "./job-handlers"; -import { DB } from "@root/config"; -import { Reminder } from "@lib/types/Reminder"; -import { createErrorEmbed, getReminderIcon } from "./utils"; -import { reminderTime } from "@root/src/lib/utils/generalUtils"; +import { + ChatInputCommandInteraction, + ButtonInteraction, + ComponentType, + EmbedBuilder, + Message +} from 'discord.js'; +import { COLORS, EMOJI } from './constants'; +import { + createMainMenuEmbed, + createMainMenuButtons, + createBackButton, + createCancelReminderModal +} from './ui'; +import { handleCreateReminder, completeReminderCreation } from './reminder-handlers'; +import { handleCreateJobReminder } from './job-handlers'; +import { DB } from '@root/config'; +import { Reminder } from '@lib/types/Reminder'; +import { createErrorEmbed, getReminderIcon } from './utils'; +import { reminderTime } from '@root/src/lib/utils/generalUtils'; // Import the email handlers -import { showEmailModal, showJobEmailModal } from "./email-handlers"; +import { showEmailModal, showJobEmailModal } from './email-handlers'; -/** +/* * Display the main menu for the reminder system */ export async function showMainMenu(interaction: ChatInputCommandInteraction | ButtonInteraction): Promise { - // Create a stylish initial embed - const embed = createMainMenuEmbed( - interaction.user.username, - interaction.user.displayAvatarURL() - ); - - // Create buttons with emojis and clear labels - const row = createMainMenuButtons(); - - // Check if this is the initial interaction or a follow-up - if (interaction instanceof ChatInputCommandInteraction) { - // Initial command interaction - const response = await interaction.reply({ - embeds: [embed], - components: [row], - ephemeral: true - }); - - // Create collector for button interactions - createButtonCollector(response); - } else { - // A button interaction (going back to main menu) - await interaction.update({ - embeds: [embed], - components: [row] - }); - } + // Create a stylish initial embed + const embed = createMainMenuEmbed( + interaction.user.username, + interaction.user.displayAvatarURL() + ); + + // Create buttons with emojis and clear labels + const row = createMainMenuButtons(); + + // Check if this is the initial interaction or a follow-up + if (interaction instanceof ChatInputCommandInteraction) { + // Initial command interaction + await interaction.reply({ + embeds: [embed], + components: [row], + ephemeral: true + }); + + // Fetch the sent message (since fetchReply is deprecated) + const response = (await interaction.fetchReply()) as Message; + + // Create collector for button interactions + createButtonCollector(response); + } else { + // A button interaction (going back to main menu) + await interaction.update({ + embeds: [embed], + components: [row] + }); + } } -/** +/* * Create a button collector for the main menu */ -export function createButtonCollector(response: any): void { - const collector = response.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 120000 // 2 minute timeout (extended) - }); - - collector.on('collect', async (buttonInteraction: ButtonInteraction) => { - // Handle button clicks - switch (buttonInteraction.customId) { - case 'create_reminder': - await handleCreateReminder(buttonInteraction); - break; - case 'create_job_reminder': - await handleCreateJobReminder(buttonInteraction); - break; - case 'view_reminders': - await handleViewReminders(buttonInteraction); - break; - case 'cancel_reminder': - await handleCancelReminder(buttonInteraction); - break; - case 'back_to_menu': - // Handle going back to the main menu - await showMainMenu(buttonInteraction); - break; - case 'email_yes': - // Actually call the showEmailModal function when the email_yes button is clicked - await showEmailModal(buttonInteraction); - break; - case 'job_email_yes': - // Call the job email modal function when the job_email_yes button is clicked - await showJobEmailModal(buttonInteraction); - break; - case 'email_no': - // Handle no for email notification - retrieve the reminder data - const reminderData = buttonInteraction.client.reminderTemp; - if (reminderData) { - await handleEmailNoForReminder(buttonInteraction); - } else { - const errorEmbed = createErrorEmbed( - "Error Processing Reminder", - "Something went wrong while processing your reminder. Please try creating it again." - ); - - await buttonInteraction.update({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - } - break; - case 'job_email_no': - // Handle no for job email notification - await completeReminderCreation(buttonInteraction, false, null); - break; - } - }); - - collector.on('end', async (collected) => { - if (collected.size === 0) { - const timeoutEmbed = createErrorEmbed( - "Reminder Action Timed Out", - "You can run the command again to set up a reminder." - ); - - await response.interaction.editReply({ - embeds: [timeoutEmbed], - components: [] - }); - } - }); +export function createButtonCollector(response: Message): void { + const collector = response.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 120000 // 2 minute timeout (extended) + }); + + collector.on('collect', async (buttonInteraction: ButtonInteraction) => { + // Handle button clicks + switch (buttonInteraction.customId) { + case 'create_reminder': + await handleCreateReminder(buttonInteraction); + break; + case 'create_job_reminder': + await handleCreateJobReminder(buttonInteraction); + break; + case 'view_reminders': + await handleViewReminders(buttonInteraction); + break; + case 'cancel_reminder': + await handleCancelReminder(buttonInteraction); + break; + case 'back_to_menu': + // Handle going back to the main menu + await showMainMenu(buttonInteraction); + break; + case 'email_yes': + // Actually call the showEmailModal function when the email_yes button is clicked + await showEmailModal(buttonInteraction); + break; + case 'job_email_yes': + // Call the job email modal function when the job_email_yes button is clicked + await showJobEmailModal(buttonInteraction); + break; + case 'email_no': { + // Handle no for email notification - retrieve the reminder data + const reminderData = buttonInteraction.client.reminderTemp; + if (reminderData) { + await handleEmailNoForReminder(buttonInteraction); + } else { + const errorEmbed = createErrorEmbed( + 'Error Processing Reminder', + 'Something went wrong while processing your reminder. Please try creating it again.' + ); + + await buttonInteraction.update({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + } + break; + } + case 'job_email_no': + // Handle no for job email notification + await completeReminderCreation(buttonInteraction, false, null); + break; + } + }); + + collector.on('end', async (collected) => { + if (collected.size === 0) { + const timeoutEmbed = createErrorEmbed( + 'Reminder Action Timed Out', + 'You can run the command again to set up a reminder.' + ); + + await response.edit({ + embeds: [timeoutEmbed], + components: [] + }); + } + }); } -/** +/* * Handle the view reminders button */ export async function handleViewReminders(buttonInteraction: ButtonInteraction): Promise { - const reminders: Array = await buttonInteraction.client.mongo - .collection(DB.REMINDERS) - .find({ owner: buttonInteraction.user.id }) - .toArray(); - - reminders.sort((a, b) => a.expires.valueOf() - b.expires.valueOf()); - - if (reminders.length < 1) { - const noRemindersEmbed = new EmbedBuilder() - .setColor(COLORS.INFO) - .setTitle(`${EMOJI.VIEW} No Reminders Found`) - .setDescription('You don\'t have any pending reminders!') - .setFooter({ text: 'Use the CREATE REMINDER button to set one up' }) - .setTimestamp(); - - // Update instead of reply to replace the message - buttonInteraction.update({ - embeds: [noRemindersEmbed], - components: [createBackButton()], // Add back button - }); - return; - } - - const embeds: Array = []; - reminders.forEach((reminder, i) => { - if (i % 10 === 0) { // Reduced to 10 reminders per embed for better readability - embeds.push( - new EmbedBuilder() - .setTitle(`${EMOJI.VIEW} Your Reminders (${reminders.length})`) - .setColor(COLORS.INFO) - .setDescription('Here are all your pending reminders:') - .setFooter({ - text: `Page ${Math.floor(i / 10) + 1}/${Math.ceil(reminders.length / 10)}` - }) - .setTimestamp() - ); - } - - const hidden = reminder.mode === 'private'; - const isJobReminder = reminder.content === 'Job Reminder'; - const icon = getReminderIcon(reminder); - - embeds[Math.floor(i / 10)].addFields({ - name: `${i + 1}. ${icon} ${ - hidden - ? isJobReminder - ? 'Job Alert' - : 'Private Reminder' - : reminder.content - }`, - value: hidden - ? `${EMOJI.REPEAT} **${reminder.repeat}** job reminder filtered by **${reminder.filterBy}**${ - reminder.emailNotification ? `\n${EMOJI.EMAIL} Email notifications to: ${reminder.emailAddress}` : '' - }` - : `${EMOJI.TIME} Due: **${reminderTime(reminder)}**${ - reminder.emailNotification ? `\n${EMOJI.EMAIL} Email notifications to: ${reminder.emailAddress}` : '' - }` - }); - }); - - // Add back button to the response - const backButton = createBackButton(); - - // Update instead of reply to replace the message - await buttonInteraction.update({ - embeds, - components: [backButton], // Add back button - }); + const reminders: Array = await buttonInteraction.client.mongo + .collection(DB.REMINDERS) + .find({ owner: buttonInteraction.user.id }) + .toArray(); + + reminders.sort((a, b) => a.expires.valueOf() - b.expires.valueOf()); + + if (reminders.length < 1) { + const noRemindersEmbed = new EmbedBuilder() + .setColor(COLORS.INFO) + .setTitle(`${EMOJI.VIEW} No Reminders Found`) + .setDescription('You don\'t have any pending reminders!') + .setFooter({ text: 'Use the CREATE REMINDER button to set one up' }) + .setTimestamp(); + + // Update instead of reply to replace the message + buttonInteraction.update({ + embeds: [noRemindersEmbed], + components: [createBackButton()] // Add back button + }); + return; + } + + const embeds: Array = []; + reminders.forEach((reminder, i) => { + if (i % 10 === 0) { // Reduced to 10 reminders per embed for better readability + embeds.push( + new EmbedBuilder() + .setTitle(`${EMOJI.VIEW} Your Reminders (${reminders.length})`) + .setColor(COLORS.INFO) + .setDescription('Here are all your pending reminders:') + .setFooter({ + text: `Page ${Math.floor(i / 10) + 1}/${Math.ceil(reminders.length / 10)}` + }) + .setTimestamp() + ); + } + + const hidden = reminder.mode === 'private'; + const isJobReminder = reminder.content === 'Job Reminder'; + const icon = getReminderIcon(reminder); + + embeds[Math.floor(i / 10)].addFields({ + name: `${i + 1}. ${icon} ${ + hidden + ? isJobReminder + ? 'Job Alert' + : 'Private Reminder' + : reminder.content + }`, + value: hidden + ? `${EMOJI.REPEAT} **${reminder.repeat}** job reminder filtered by **${reminder.filterBy}**${ + reminder.emailNotification ? `\n${EMOJI.EMAIL} Email notifications to: ${reminder.emailAddress}` : '' + }` + : `${EMOJI.TIME} Due: **${reminderTime(reminder)}**${ + reminder.emailNotification ? `\n${EMOJI.EMAIL} Email notifications to: ${reminder.emailAddress}` : '' + }` + }); + }); + + // Add back button to the response + const backButton = createBackButton(); + + // Update instead of reply to replace the message + await buttonInteraction.update({ + embeds, + components: [backButton] // Add back button + }); } -/** +/* * Handle the cancel reminder button */ export async function handleCancelReminder(buttonInteraction: ButtonInteraction): Promise { - // Create modal for reminder cancellation - const modal = createCancelReminderModal(); - - // Show the modal - await buttonInteraction.showModal(modal); - - // Wait for modal submission - try { - const modalInteraction = await buttonInteraction.awaitModalSubmit({ - time: 60000, // 1 minute - filter: (i) => - i.customId === 'cancel_reminder_modal' && - i.user.id === buttonInteraction.user.id - }); - - // Process modal submission - const reminderNumStr = modalInteraction.fields.getTextInputValue('reminder_number'); - const reminderNum = parseInt(reminderNumStr) - 1; // Convert to 0-based index - - if (isNaN(reminderNum) || reminderNum < 0) { - const errorEmbed = createErrorEmbed( - "Invalid Reminder Number", - `**"${reminderNumStr}"** is not a valid reminder number. Please enter a positive integer.` - ).setFooter({ text: 'Use the VIEW REMINDERS button to see your reminders and their numbers' }); - - // Defer the modal reply to acknowledge it without sending a visible message - await modalInteraction.deferUpdate(); - - // Update the original message with the error - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()], // Add back button - }); - - return; - } - - // Get user's reminders and sort them - const reminders: Array = await modalInteraction.client.mongo - .collection(DB.REMINDERS) - .find({ owner: modalInteraction.user.id }) - .toArray(); - - reminders.sort((a, b) => a.expires.valueOf() - b.expires.valueOf()); - - // Check if the reminder exists - const reminder = reminders[reminderNum]; - if (!reminder) { - const notFoundEmbed = createErrorEmbed( - "Reminder Not Found", - `I couldn't find reminder **#${reminderNum + 1}**.` - ).setFooter({ text: 'Use the VIEW REMINDERS button to see your current reminders' }); - - // Defer the modal reply to acknowledge it without sending a visible message - await modalInteraction.deferUpdate(); - - // Update the original message with the error - await buttonInteraction.editReply({ - embeds: [notFoundEmbed], - components: [createBackButton()], // Add back button - }); - - return; - } - - // Delete the reminder - await modalInteraction.client.mongo - .collection(DB.REMINDERS) - .findOneAndDelete(reminder); - - const hidden = reminder.mode === 'private'; - const isJobReminder = reminder.content === 'Job Reminder'; - const emailInfo = reminder.emailNotification ? `\nEmail notifications to ${reminder.emailAddress} have been canceled.` : ''; - - const successEmbed = new EmbedBuilder() - .setColor(COLORS.SUCCESS) - .setTitle(`${EMOJI.CANCEL} Reminder Cancelled`) - .setDescription( - `Successfully cancelled reminder **#${reminderNum + 1}**: ${ - hidden - ? (isJobReminder ? 'Job Alert' : 'Private Reminder') - : `"${reminder.content}"` - }${emailInfo}` - ) - .setTimestamp(); - - // Defer the modal reply to acknowledge it without sending a visible message - await modalInteraction.deferUpdate(); - - // Update the original message with the success info - await buttonInteraction.editReply({ - embeds: [successEmbed], - components: [createBackButton()], // Add back button - }); - - } catch (error) { - console.error('Error in modal submission:', error); - - const errorEmbed = createErrorEmbed( - "Cancellation Failed", - "The reminder cancellation process timed out or an error occurred." - ); - - // Update the original button interaction - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()], // Add back button - }); - } + // Create modal for reminder cancellation + const modal = createCancelReminderModal(); + + // Show the modal + await buttonInteraction.showModal(modal); + + // Wait for modal submission + try { + const modalInteraction = await buttonInteraction.awaitModalSubmit({ + time: 60000, // 1 minute + filter: (i) => + i.customId === 'cancel_reminder_modal' + && i.user.id === buttonInteraction.user.id + }); + + // Process modal submission + const reminderNumStr = modalInteraction.fields.getTextInputValue('reminder_number'); + const reminderNum = parseInt(reminderNumStr) - 1; // Convert to 0-based index + + if (isNaN(reminderNum) || reminderNum < 0) { + const errorEmbed = createErrorEmbed( + 'Invalid Reminder Number', + `**"${reminderNumStr}"** is not a valid reminder number. Please enter a positive integer.` + ).setFooter({ text: 'Use the VIEW REMINDERS button to see your reminders and their numbers' }); + + // Defer the modal reply to acknowledge it without sending a visible message + await modalInteraction.deferUpdate(); + + // Update the original message with the error + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] // Add back button + }); + + return; + } + + // Get user's reminders and sort them + const reminders: Array = await modalInteraction.client.mongo + .collection(DB.REMINDERS) + .find({ owner: modalInteraction.user.id }) + .toArray(); + + reminders.sort((a, b) => a.expires.valueOf() - b.expires.valueOf()); + + // Check if the reminder exists + const reminder = reminders[reminderNum]; + if (!reminder) { + const notFoundEmbed = createErrorEmbed( + 'Reminder Not Found', + `I couldn't find reminder **#${reminderNum + 1}**.` + ).setFooter({ text: 'Use the VIEW REMINDERS button to see your current reminders' }); + + // Defer the modal reply to acknowledge it without sending a visible message + await modalInteraction.deferUpdate(); + + // Update the original message with the error + await buttonInteraction.editReply({ + embeds: [notFoundEmbed], + components: [createBackButton()] // Add back button + }); + + return; + } + + // Delete the reminder + await modalInteraction.client.mongo + .collection(DB.REMINDERS) + .findOneAndDelete(reminder); + + const hidden = reminder.mode === 'private'; + const isJobReminder = reminder.content === 'Job Reminder'; + const emailInfo = reminder.emailNotification ? `\nEmail notifications to ${reminder.emailAddress} have been canceled.` : ''; + + const successEmbed = new EmbedBuilder() + .setColor(COLORS.SUCCESS) + .setTitle(`${EMOJI.CANCEL} Reminder Cancelled`) + .setDescription( + `Successfully cancelled reminder **#${reminderNum + 1}**: ${ + hidden + ? isJobReminder ? 'Job Alert' : 'Private Reminder' + : `"${reminder.content}"` + }${emailInfo}` + ) + .setTimestamp(); + + // Defer the modal reply to acknowledge it without sending a visible message + await modalInteraction.deferUpdate(); + + // Update the original message with the success info + await buttonInteraction.editReply({ + embeds: [successEmbed], + components: [createBackButton()] // Add back button + }); + } catch (error) { + console.error('Error in modal submission:', error); + + const errorEmbed = createErrorEmbed( + 'Cancellation Failed', + 'The reminder cancellation process timed out or an error occurred.' + ); + + // Update the original button interaction + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] // Add back button + }); + } } -/** +/* * Handle the email no button for standard reminders */ export async function handleEmailNoForReminder(buttonInteraction: ButtonInteraction): Promise { - // Get the reminder data from client storage - const reminderData = buttonInteraction.client.reminderTemp; - - if (reminderData) { - // Finalize without email - await completeReminderCreation(buttonInteraction, false, null); - } else { - const errorEmbed = createErrorEmbed( - "Error Processing Reminder", - "Something went wrong while processing your reminder. Please try creating it again." - ); - - await buttonInteraction.update({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - } -} \ No newline at end of file + // Get the reminder data from client storage + const reminderData = buttonInteraction.client.reminderTemp; + + if (reminderData) { + // Finalize without email + await completeReminderCreation(buttonInteraction, false, null); + } else { + const errorEmbed = createErrorEmbed( + 'Error Processing Reminder', + 'Something went wrong while processing your reminder. Please try creating it again.' + ); + + await buttonInteraction.update({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + } +} diff --git a/src/newreminders/reminder-handlers.ts b/src/newreminders/reminder-handlers.ts index 853edf4b..36ddf3d4 100644 --- a/src/newreminders/reminder-handlers.ts +++ b/src/newreminders/reminder-handlers.ts @@ -1,243 +1,241 @@ // Standard reminder handling functionality -import { - ButtonInteraction, - ModalBuilder, - ModalSubmitInteraction, - EmbedBuilder -} from "discord.js"; -import { ReminderData } from "./types"; -import { COLORS, EMOJI } from "./constants"; -import { - createBackButton, - createEmailOptionsEmbed, - createEmailOptionsButtons, - createReminderModal -} from "./ui"; -import { createErrorEmbed, createReminderSuccessEmbed } from "./utils"; -import { DB } from "@root/config"; -import { Reminder } from "@lib/types/Reminder"; -import parse from "parse-duration"; -import { reminderTime } from "@root/src/lib/utils/generalUtils"; - -/** +import { + ButtonInteraction, + ModalSubmitInteraction, + EmbedBuilder +} from 'discord.js'; +import { ReminderData } from './types'; +import { COLORS, EMOJI } from './constants'; +import { + createBackButton, + createEmailOptionsEmbed, + createEmailOptionsButtons, + createReminderModal +} from './ui'; +import { createErrorEmbed, createReminderSuccessEmbed } from './utils'; +import { DB } from '@root/config'; +import { Reminder } from '@lib/types/Reminder'; +import parse from 'parse-duration'; +import { reminderTime } from '@root/src/lib/utils/generalUtils'; + +/* * Handle the create reminder button interaction */ export async function handleCreateReminder(buttonInteraction: ButtonInteraction): Promise { - // Store reference to original message - const originalMessage = buttonInteraction.message; - - // Create modal for reminder details - const modal = createReminderModal(); - - // Show the modal - await buttonInteraction.showModal(modal); - - // Wait for modal submission - try { - const modalInteraction = await buttonInteraction.awaitModalSubmit({ - time: 180000, // 3 minutes (extended) - filter: (i: ModalSubmitInteraction) => - i.customId === 'reminder_modal' && - i.user.id === buttonInteraction.user.id - }); - - // Process modal submission - const content = modalInteraction.fields.getTextInputValue('content'); - const rawDuration = modalInteraction.fields.getTextInputValue('duration'); - const duration = parse(rawDuration); - - if (!duration) { - const errorEmbed = createErrorEmbed( - "Invalid Time Format", - `**"${rawDuration}"** is not a valid duration.\nYou can use words like hours, minutes, seconds, days, weeks, months, or years.` - ).setFooter({ text: 'Try something like "3 hours" or "2 days"' }); - - // Defer the modal reply to acknowledge it without sending a visible message - await modalInteraction.deferUpdate(); - - // Update the original message with the error - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - - return; - } - - // Calculate the expiry date - const expiryDate = new Date(duration + Date.now()); - - // Store the reminder data temporarily - const reminderData: ReminderData = { - content, - expiryDate, - buttonInteraction, - modalInteraction - }; - - // Ask the user if they want email notifications - await askForEmailNotification(reminderData); - - } catch (error) { - console.error('Error in modal submission:', error); - - const errorEmbed = createErrorEmbed( - "Reminder Creation Failed", - "The reminder creation process timed out or an error occurred." - ); - - // Update the original button interaction - await buttonInteraction.editReply({ - embeds: [errorEmbed], - components: [createBackButton()], // Add back button - }); - } + // Store reference to original message + + // Unused but might need later + // const originalMessage = buttonInteraction.message; + + // Create modal for reminder details + const modal = createReminderModal(); + + // Show the modal + await buttonInteraction.showModal(modal); + + // Wait for modal submission + try { + const modalInteraction = await buttonInteraction.awaitModalSubmit({ + time: 180000, // 3 minutes (extended) + filter: (i: ModalSubmitInteraction) => + i.customId === 'reminder_modal' + && i.user.id === buttonInteraction.user.id + }); + + // Process modal submission + const content = modalInteraction.fields.getTextInputValue('content'); + const rawDuration = modalInteraction.fields.getTextInputValue('duration'); + const duration = parse(rawDuration); + + if (!duration) { + const errorEmbed = createErrorEmbed( + 'Invalid Time Format', + `**"${rawDuration}"** is not a valid duration.\nYou can use words like hours, minutes, seconds, days, weeks, months, or years.` + ).setFooter({ text: 'Try something like "3 hours" or "2 days"' }); + + // Defer the modal reply to acknowledge it without sending a visible message + await modalInteraction.deferUpdate(); + + // Update the original message with the error + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + + return; + } + + // Calculate the expiry date + const expiryDate = new Date(duration + Date.now()); + + // Store the reminder data temporarily + const reminderData: ReminderData = { + content, + expiryDate, + buttonInteraction, + modalInteraction + }; + + // Ask the user if they want email notifications + await askForEmailNotification(reminderData); + } catch (error) { + console.error('Error in modal submission:', error); + + const errorEmbed = createErrorEmbed( + 'Reminder Creation Failed', + 'The reminder creation process timed out or an error occurred.' + ); + + // Update the original button interaction + await buttonInteraction.editReply({ + embeds: [errorEmbed], + components: [createBackButton()] // Add back button + }); + } } -/** +/* * Ask if the user wants email notifications for standard reminders */ export async function askForEmailNotification(reminderData: ReminderData): Promise { - const { buttonInteraction, modalInteraction } = reminderData; - - // Create embed asking about email notifications - const emailEmbed = createEmailOptionsEmbed(); - - // Create Yes/No buttons - const emailRow = createEmailOptionsButtons(); - - // Store the reminder data in the client's temporary collection - // This way we can access it when the user makes a choice - modalInteraction.client.reminderTemp = reminderData; - - // Defer the modal reply to acknowledge it - await modalInteraction.deferUpdate(); - - // Update original message to ask about email - await buttonInteraction.editReply({ - embeds: [emailEmbed], - components: [emailRow] - }); + const { buttonInteraction, modalInteraction } = reminderData; + + // Create embed asking about email notifications + const emailEmbed = createEmailOptionsEmbed(); + + // Create Yes/No buttons + const emailRow = createEmailOptionsButtons(); + + // Store the reminder data in the client's temporary collection + // This way we can access it when the user makes a choice + modalInteraction.client.reminderTemp = reminderData; + + // Defer the modal reply to acknowledge it + await modalInteraction.deferUpdate(); + + // Update original message to ask about email + await buttonInteraction.editReply({ + embeds: [emailEmbed], + components: [emailRow] + }); } -/** +/* * Create and store the reminder with or without email */ export async function completeReminderCreation( - buttonInteraction: ButtonInteraction, - withEmail: boolean, - email: string | null, - modalInteraction?: ModalSubmitInteraction + buttonInteraction: ButtonInteraction, + withEmail: boolean, + email: string | null, + modalInteraction?: ModalSubmitInteraction ): Promise { - try { - // Get the reminder data - const reminderData = buttonInteraction.client.reminderTemp; - - // Check if we have valid reminder data - if (!reminderData || !reminderData.content || !reminderData.expiryDate) { - const errorEmbed = createErrorEmbed( - "Error Creating Reminder", - "Missing reminder information. Please try creating your reminder again." - ); - - // If we have a modal interaction, respond to that - if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { - await modalInteraction.reply({ - embeds: [errorEmbed], - ephemeral: true - }); - } else { - // Otherwise try to update the button interaction - await buttonInteraction.update({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - } - return; - } - - const { content, expiryDate } = reminderData; - - // Create the reminder object - const reminder: Reminder = { - owner: buttonInteraction.user.id, - content, - mode: 'public', // could be changed to private if needed - expires: expiryDate, - repeat: null, // No repeat by default - emailNotification: withEmail, - emailAddress: withEmail ? email : null - }; - - // Store the reminder in the database - await buttonInteraction.client.mongo - .collection(DB.REMINDERS) - .insertOne(reminder); - - // Create success embed - const successEmbed = createReminderSuccessEmbed( - content, - reminderTime(reminder), - withEmail, - email - ); - - // Handle the response based on which interaction is available - if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { - // If we have a modal interaction that hasn't been replied to yet - await modalInteraction.reply({ - content: "Your reminder has been created successfully!", - ephemeral: true - }); - - // Update the original message - await buttonInteraction.editReply({ - embeds: [successEmbed], - components: [createBackButton()] - }); - } else { - // Otherwise try to update the button interaction - // First check if we can update - if (!buttonInteraction.replied) { - await buttonInteraction.update({ - embeds: [successEmbed], - components: [createBackButton()] - }); - } else { - // If we can't update, try to edit the reply - await buttonInteraction.editReply({ - embeds: [successEmbed], - components: [createBackButton()] - }); - } - } - - // Clean up temporary data - delete buttonInteraction.client.reminderTemp; - } catch (error) { - console.error('Error in completeReminderCreation:', error); - - // Try to give feedback through any available channel - const errorEmbed = new EmbedBuilder() - .setColor(COLORS.WARNING) - .setTitle(`${EMOJI.REMINDER} Reminder Process Completed`) - .setDescription("Your reminder has been created, but there was an issue updating the display.") - .setTimestamp(); - - try { - if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { - await modalInteraction.reply({ - embeds: [errorEmbed], - ephemeral: true - }); - } else if (!buttonInteraction.replied) { - await buttonInteraction.update({ - embeds: [errorEmbed], - components: [createBackButton()] - }); - } - } catch (secondError) { - console.error('Even the error handler failed:', secondError); - } - } -} \ No newline at end of file + try { + // Get the reminder data + const reminderData = buttonInteraction.client.reminderTemp; + + // Check if we have valid reminder data + if (!reminderData || !reminderData.content || !reminderData.expiryDate) { + const errorEmbed = createErrorEmbed( + 'Error Creating Reminder', + 'Missing reminder information. Please try creating your reminder again.' + ); + + // If we have a modal interaction, respond to that + if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { + await modalInteraction.reply({ + embeds: [errorEmbed], + ephemeral: true + }); + } else { + // Otherwise try to update the button interaction + await buttonInteraction.update({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + } + return; + } + + const { content, expiryDate } = reminderData; + + // Create the reminder object + const reminder: Reminder = { + owner: buttonInteraction.user.id, + content, + mode: 'public', // could be changed to private if needed + expires: expiryDate, + repeat: null, // No repeat by default + emailNotification: withEmail, + emailAddress: withEmail ? email : null + }; + + // Store the reminder in the database + await buttonInteraction.client.mongo + .collection(DB.REMINDERS) + .insertOne(reminder); + + // Create success embed + const successEmbed = createReminderSuccessEmbed( + content, + reminderTime(reminder), + withEmail, + email + ); + + // Handle the response based on which interaction is available + if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { + // If we have a modal interaction that hasn't been replied to yet + await modalInteraction.reply({ + content: 'Your reminder has been created successfully!', + ephemeral: true + }); + + // Update the original message + await buttonInteraction.editReply({ + embeds: [successEmbed], + components: [createBackButton()] + }); + } else if (!buttonInteraction.replied) { + // Otherwise try to update the button interaction + // First check if we can update + await buttonInteraction.update({ + embeds: [successEmbed], + components: [createBackButton()] + }); + } else { + // If we can't update, try to edit the reply + await buttonInteraction.editReply({ + embeds: [successEmbed], + components: [createBackButton()] + }); + } + + // Clean up temporary data + delete buttonInteraction.client.reminderTemp; + } catch (error) { + console.error('Error in completeReminderCreation:', error); + + // Try to give feedback through any available channel + const errorEmbed = new EmbedBuilder() + .setColor(COLORS.WARNING) + .setTitle(`${EMOJI.REMINDER} Reminder Process Completed`) + .setDescription('Your reminder has been created, but there was an issue updating the display.') + .setTimestamp(); + + try { + if (modalInteraction && !modalInteraction.replied && !modalInteraction.deferred) { + await modalInteraction.reply({ + embeds: [errorEmbed], + ephemeral: true + }); + } else if (!buttonInteraction.replied) { + await buttonInteraction.update({ + embeds: [errorEmbed], + components: [createBackButton()] + }); + } + } catch (secondError) { + console.error('Even the error handler failed:', secondError); + } + } +} diff --git a/src/newreminders/types.ts b/src/newreminders/types.ts index b6012451..f283236f 100644 --- a/src/newreminders/types.ts +++ b/src/newreminders/types.ts @@ -1,26 +1,26 @@ // Type definitions for the reminder system -import { ButtonInteraction, ChatInputCommandInteraction, ModalSubmitInteraction, Client } from "discord.js"; +import { ButtonInteraction, ModalSubmitInteraction } from 'discord.js'; // Store reminder data temporarily during creation flow export interface ReminderData { - content: string; - expiryDate: Date; - buttonInteraction: ButtonInteraction; - modalInteraction: ModalSubmitInteraction; + content: string; + expiryDate: Date; + buttonInteraction: ButtonInteraction; + modalInteraction: ModalSubmitInteraction; } // Store job reminder data temporarily during creation flow export interface JobReminderData { - repeatValue: string; - filterValue: string; - buttonInteraction: ButtonInteraction; - modalInteraction: ModalSubmitInteraction; + repeatValue: string; + filterValue: string; + buttonInteraction: ButtonInteraction; + modalInteraction: ModalSubmitInteraction; } // Extend the Discord.js Client to include our temporary storage properties declare module 'discord.js' { - interface Client { - reminderTemp?: ReminderData; - jobReminderTemp?: JobReminderData; - } -} \ No newline at end of file + interface Client { + reminderTemp?: ReminderData; + jobReminderTemp?: JobReminderData; + } +} diff --git a/src/newreminders/ui.ts b/src/newreminders/ui.ts index 00e900a1..e251059a 100644 --- a/src/newreminders/ui.ts +++ b/src/newreminders/ui.ts @@ -1,208 +1,208 @@ // UI component creation functions -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; -import { COLORS, EMOJI } from "./constants"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { COLORS, EMOJI } from './constants'; -/** +/* * Creates a back button to return to the main menu */ export function createBackButton(): ActionRowBuilder { - return new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('back_to_menu') - .setLabel('Back to Menu') - .setEmoji(EMOJI.BACK) - .setStyle(ButtonStyle.Secondary) - ); + return new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('back_to_menu') + .setLabel('Back to Menu') + .setEmoji(EMOJI.BACK) + .setStyle(ButtonStyle.Secondary) + ); } -/** +/* * Creates the main menu embed */ export function createMainMenuEmbed(username: string, avatarURL: string): EmbedBuilder { - return new EmbedBuilder() - .setColor(COLORS.PRIMARY) - .setTitle(`${EMOJI.REMINDER} Reminder System`) - .setDescription('What would you like to do?') - .setFooter({ - text: `Requested by ${username}`, - iconURL: avatarURL - }) - .setTimestamp(); + return new EmbedBuilder() + .setColor(COLORS.PRIMARY) + .setTitle(`${EMOJI.REMINDER} Reminder System`) + .setDescription('What would you like to do?') + .setFooter({ + text: `Requested by ${username}`, + iconURL: avatarURL + }) + .setTimestamp(); } -/** +/* * Creates the main menu buttons */ export function createMainMenuButtons(): ActionRowBuilder { - return new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('create_reminder') - .setLabel('Create Reminder') - .setEmoji(EMOJI.REMINDER) - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId('create_job_reminder') - .setLabel('Job Alert') - .setEmoji(EMOJI.JOB) - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('view_reminders') - .setLabel('View All') - .setEmoji(EMOJI.VIEW) - .setStyle(ButtonStyle.Success), - new ButtonBuilder() - .setCustomId('cancel_reminder') - .setLabel('Cancel') - .setEmoji(EMOJI.CANCEL) - .setStyle(ButtonStyle.Danger) - ); + return new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('create_reminder') + .setLabel('Create Reminder') + .setEmoji(EMOJI.REMINDER) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId('create_job_reminder') + .setLabel('Job Alert') + .setEmoji(EMOJI.JOB) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('view_reminders') + .setLabel('View All') + .setEmoji(EMOJI.VIEW) + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId('cancel_reminder') + .setLabel('Cancel') + .setEmoji(EMOJI.CANCEL) + .setStyle(ButtonStyle.Danger) + ); } -/** +/* * Creates a reminder modal */ export function createReminderModal(): ModalBuilder { - const modal = new ModalBuilder() - .setCustomId('reminder_modal') - .setTitle(`${EMOJI.REMINDER} Create New Reminder`); - - // Add inputs for content and duration - const contentInput = new TextInputBuilder() - .setCustomId('content') - .setLabel("What would you like to be reminded of?") - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder("Enter your reminder message here...") - .setRequired(true); - - const durationInput = new TextInputBuilder() - .setCustomId('duration') - .setLabel("When would you like to be reminded?") - .setPlaceholder('e.g. 1 hour, 30 minutes, 2 days, tomorrow at 3pm') - .setStyle(TextInputStyle.Short) - .setRequired(true); - - // Create action rows with inputs - const contentRow = new ActionRowBuilder().addComponents(contentInput); - const durationRow = new ActionRowBuilder().addComponents(durationInput); - - // Add action rows to the modal - modal.addComponents(contentRow, durationRow); - - return modal; + const modal = new ModalBuilder() + .setCustomId('reminder_modal') + .setTitle(`${EMOJI.REMINDER} Create New Reminder`); + + // Add inputs for content and duration + const contentInput = new TextInputBuilder() + .setCustomId('content') + .setLabel('What would you like to be reminded of?') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('Enter your reminder message here...') + .setRequired(true); + + const durationInput = new TextInputBuilder() + .setCustomId('duration') + .setLabel('When would you like to be reminded?') + .setPlaceholder('e.g. 1 hour, 30 minutes, 2 days, tomorrow at 3pm') + .setStyle(TextInputStyle.Short) + .setRequired(true); + + // Create action rows with inputs + const contentRow = new ActionRowBuilder().addComponents(contentInput); + const durationRow = new ActionRowBuilder().addComponents(durationInput); + + // Add action rows to the modal + modal.addComponents(contentRow, durationRow); + + return modal; } -/** +/* * Creates a job reminder modal */ export function createJobReminderModal(): ModalBuilder { - const modal = new ModalBuilder() - .setCustomId('job_reminder_modal') - .setTitle(`${EMOJI.JOB} Create Job Alert`); - - // Add inputs for repeat frequency and filter type - const repeatInput = new TextInputBuilder() - .setCustomId('repeat') - .setLabel('How often would you like to receive alerts?') - .setPlaceholder('Type "daily", "weekly", or "monthly"') - .setStyle(TextInputStyle.Short) - .setRequired(true); - - const filterInput = new TextInputBuilder() - .setCustomId('filter') - .setLabel('Sort jobs by?') - .setPlaceholder('default, relevance, salary, or date') - .setStyle(TextInputStyle.Short) - .setRequired(true) - .setValue('default'); // Default value - - // Create action rows with inputs - const repeatRow = new ActionRowBuilder().addComponents(repeatInput); - const filterRow = new ActionRowBuilder().addComponents(filterInput); - - // Add action rows to the modal - modal.addComponents(repeatRow, filterRow); - - return modal; + const modal = new ModalBuilder() + .setCustomId('job_reminder_modal') + .setTitle(`${EMOJI.JOB} Create Job Alert`); + + // Add inputs for repeat frequency and filter type + const repeatInput = new TextInputBuilder() + .setCustomId('repeat') + .setLabel('How often would you like to receive alerts?') + .setPlaceholder('Type "daily", "weekly", or "monthly"') + .setStyle(TextInputStyle.Short) + .setRequired(true); + + const filterInput = new TextInputBuilder() + .setCustomId('filter') + .setLabel('Sort jobs by?') + .setPlaceholder('default, relevance, salary, or date') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setValue('default'); // Default value + + // Create action rows with inputs + const repeatRow = new ActionRowBuilder().addComponents(repeatInput); + const filterRow = new ActionRowBuilder().addComponents(filterInput); + + // Add action rows to the modal + modal.addComponents(repeatRow, filterRow); + + return modal; } -/** +/* * Creates an email notification options embed */ -export function createEmailOptionsEmbed(isJobReminder: boolean = false): EmbedBuilder { - return new EmbedBuilder() - .setColor(COLORS.INFO) - .setTitle(`${EMOJI.EMAIL} Would you like to receive this ${isJobReminder ? 'job alert' : 'reminder'} by email too?`) - .setDescription(`Choose whether you want to also receive this ${isJobReminder ? 'job alert' : 'reminder'} via email when it triggers.`) - .setFooter({ text: 'Email notifications are optional' }) - .setTimestamp(); +export function createEmailOptionsEmbed(isJobReminder = false): EmbedBuilder { + return new EmbedBuilder() + .setColor(COLORS.INFO) + .setTitle(`${EMOJI.EMAIL} Would you like to receive this ${isJobReminder ? 'job alert' : 'reminder'} by email too?`) + .setDescription(`Choose whether you want to also receive this ${isJobReminder ? 'job alert' : 'reminder'} via email when it triggers.`) + .setFooter({ text: 'Email notifications are optional' }) + .setTimestamp(); } -/** +/* * Creates email notification option buttons */ -export function createEmailOptionsButtons(isJobReminder: boolean = false): ActionRowBuilder { - return new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(isJobReminder ? 'job_email_yes' : 'email_yes') - .setLabel('Yes, send email') - .setEmoji(EMOJI.EMAIL) - .setStyle(ButtonStyle.Success), - new ButtonBuilder() - .setCustomId(isJobReminder ? 'job_email_no' : 'email_no') - .setLabel('No, Discord only') - .setStyle(ButtonStyle.Secondary) - ); +export function createEmailOptionsButtons(isJobReminder = false): ActionRowBuilder { + return new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(isJobReminder ? 'job_email_yes' : 'email_yes') + .setLabel('Yes, send email') + .setEmoji(EMOJI.EMAIL) + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(isJobReminder ? 'job_email_no' : 'email_no') + .setLabel('No, Discord only') + .setStyle(ButtonStyle.Secondary) + ); } -/** +/* * Creates an email input modal */ -export function createEmailInputModal(isJobReminder: boolean = false): ModalBuilder { - const modal = new ModalBuilder() - .setCustomId(isJobReminder ? 'job_email_modal' : 'email_modal') - .setTitle(`${EMOJI.EMAIL} Email Notification${isJobReminder ? ' for Job Alerts' : ''}`); - - // Add input for email address - const emailInput = new TextInputBuilder() - .setCustomId('email') - .setLabel(`Email address for ${isJobReminder ? 'job alerts' : 'notifications'}:`) - .setStyle(TextInputStyle.Short) - .setPlaceholder("Enter your email address here...") - .setRequired(true); - - // Create action row with input - const emailRow = new ActionRowBuilder().addComponents(emailInput); - - // Add action row to the modal - modal.addComponents(emailRow); - - return modal; +export function createEmailInputModal(isJobReminder = false): ModalBuilder { + const modal = new ModalBuilder() + .setCustomId(isJobReminder ? 'job_email_modal' : 'email_modal') + .setTitle(`${EMOJI.EMAIL} Email Notification${isJobReminder ? ' for Job Alerts' : ''}`); + + // Add input for email address + const emailInput = new TextInputBuilder() + .setCustomId('email') + .setLabel(`Email address for ${isJobReminder ? 'job alerts' : 'notifications'}:`) + .setStyle(TextInputStyle.Short) + .setPlaceholder('Enter your email address here...') + .setRequired(true); + + // Create action row with input + const emailRow = new ActionRowBuilder().addComponents(emailInput); + + // Add action row to the modal + modal.addComponents(emailRow); + + return modal; } -/** +/* * Creates a cancel reminder modal */ export function createCancelReminderModal(): ModalBuilder { - const modal = new ModalBuilder() - .setCustomId('cancel_reminder_modal') - .setTitle(`${EMOJI.CANCEL} Cancel Reminder`); - - // Add input for reminder number - const reminderNumInput = new TextInputBuilder() - .setCustomId('reminder_number') - .setLabel("Which reminder would you like to cancel?") - .setPlaceholder('Enter the number (e.g. 1, 2, 3)') - .setStyle(TextInputStyle.Short) - .setRequired(true); - - // Create action row with input - const reminderNumRow = new ActionRowBuilder().addComponents(reminderNumInput); - - // Add action row to the modal - modal.addComponents(reminderNumRow); - - return modal; -} \ No newline at end of file + const modal = new ModalBuilder() + .setCustomId('cancel_reminder_modal') + .setTitle(`${EMOJI.CANCEL} Cancel Reminder`); + + // Add input for reminder number + const reminderNumInput = new TextInputBuilder() + .setCustomId('reminder_number') + .setLabel('Which reminder would you like to cancel?') + .setPlaceholder('Enter the number (e.g. 1, 2, 3)') + .setStyle(TextInputStyle.Short) + .setRequired(true); + + // Create action row with input + const reminderNumRow = new ActionRowBuilder().addComponents(reminderNumInput); + + // Add action row to the modal + modal.addComponents(reminderNumRow); + + return modal; +} diff --git a/src/newreminders/utils.ts b/src/newreminders/utils.ts index 0324e506..44b9d34e 100644 --- a/src/newreminders/utils.ts +++ b/src/newreminders/utils.ts @@ -1,115 +1,148 @@ // Utility functions for the reminders system -import { COLORS, EMOJI } from "./constants"; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ButtonInteraction } from "discord.js"; -import { Reminder } from "@lib/types/Reminder"; +import { COLORS, EMOJI } from './constants'; +import { EmbedBuilder, ButtonInteraction } from 'discord.js'; +import { Reminder } from '@lib/types/Reminder'; /** - * Validates an email address format + * Validates an email address format. + * + * @param {string} email - The email address to validate. + * @returns {boolean} True if the email has a valid format; otherwise, false. */ export function isValidEmail(email: string): boolean { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); } /** - * Creates an error embed with the specified message + * Creates an error embed with the specified title and description. + * + * @param {string} title - The error title to display in the embed. + * @param {string} description - The error description to display in the embed. + * @returns {EmbedBuilder} A configured EmbedBuilder instance. */ export function createErrorEmbed(title: string, description: string): EmbedBuilder { - return new EmbedBuilder() - .setColor(COLORS.DANGER) - .setTitle(`${EMOJI.CANCEL} ${title}`) - .setDescription(description) - .setTimestamp(); + return new EmbedBuilder() + .setColor(COLORS.DANGER) + .setTitle(`${EMOJI.CANCEL} ${title}`) + .setDescription(description) + .setTimestamp(); } /** - * Creates a success embed for a standard reminder + * Creates a success embed for a standard reminder. + * + * @param {string} content - The reminder content/message. + * @param {string} expiryTime - A human-readable time string for when the reminder will trigger. + * @param {boolean} [withEmail=false] - Whether email notifications are enabled for this reminder. + * @param {string|null} [email=null] - The email address to notify, if applicable. + * @returns {EmbedBuilder} A configured EmbedBuilder instance. */ export function createReminderSuccessEmbed( - content: string, - expiryTime: string, - withEmail: boolean = false, - email: string = null + content: string, + expiryTime: string, + withEmail = false, + email: string | null = null ): EmbedBuilder { - const embed = new EmbedBuilder() - .setColor(COLORS.SUCCESS) - .setTitle(`${EMOJI.REMINDER} Reminder Set!`) - .setDescription(`I'll remind you about that at **${expiryTime}**.`) - .addFields({ - name: 'Reminder Content', - value: `> ${content}` - }); - - // Add email info if applicable - if (withEmail && email) { - embed.addFields({ - name: 'Email Notification', - value: `You'll also receive an email at **${email}** when this reminder triggers.` - }); - } - - embed.setTimestamp(); - - return embed; + const embed = new EmbedBuilder() + .setColor(COLORS.SUCCESS) + .setTitle(`${EMOJI.REMINDER} Reminder Set!`) + .setDescription(`I'll remind you about that at **${expiryTime}**.`) + .addFields({ + name: 'Reminder Content', + value: `> ${content}` + }); + + // Add email info if applicable + if (withEmail && email) { + embed.addFields({ + name: 'Email Notification', + value: `You'll also receive an email at **${email}** when this reminder triggers.` + }); + } + + embed.setTimestamp(); + + return embed; } /** - * Creates a success embed for a job reminder + * Creates a success embed for a job reminder. + * + * @param {string} repeatValue - The repeat frequency (e.g., "daily", "weekly"). + * @param {string} filterValue - The sort/filter selection (e.g., "date", "salary"). + * @param {string} expiryTime - A human-readable time string for when the first alert will trigger. + * @param {boolean} [withEmail=false] - Whether email notifications are enabled. + * @param {string|null} [email=null] - The email address to notify, if applicable. + * @returns {EmbedBuilder} A configured EmbedBuilder instance. */ export function createJobReminderSuccessEmbed( - repeatValue: string, - filterValue: string, - expiryTime: string, - withEmail: boolean = false, - email: string = null + repeatValue: string, + filterValue: string, + expiryTime: string, + withEmail = false, + email: string | null = null ): EmbedBuilder { - const embed = new EmbedBuilder() - .setColor(COLORS.SECONDARY) - .setTitle(`${EMOJI.JOB} Job Alert Created`) - .setDescription( - `I'll send you job opportunities **${repeatValue}** starting at **${expiryTime}**.` - ) - .addFields( - { name: 'Frequency', value: `${repeatValue.charAt(0).toUpperCase() + repeatValue.slice(1)}`, inline: true }, - { name: 'Sorted By', value: `${filterValue.charAt(0).toUpperCase() + filterValue.slice(1)}`, inline: true } - ); - - // Add email info if applicable - if (withEmail && email) { - embed.addFields({ - name: 'Email Notification', - value: `You'll also receive job alerts at **${email}** when they trigger.` - }); - } - - embed.setFooter({ text: 'You can update your preferences anytime' }) - .setTimestamp(); - - return embed; + const embed = new EmbedBuilder() + .setColor(COLORS.SECONDARY) + .setTitle(`${EMOJI.JOB} Job Alert Created`) + .setDescription( + `I'll send you job opportunities **${repeatValue}** starting at **${expiryTime}**.` + ) + .addFields( + { + name: 'Frequency', + value: `${repeatValue.charAt(0).toUpperCase() + repeatValue.slice(1)}`, + inline: true + }, + { + name: 'Sorted By', + value: `${filterValue.charAt(0).toUpperCase() + filterValue.slice(1)}`, + inline: true + } + ); + + // Add email info if applicable + if (withEmail && email) { + embed.addFields({ + name: 'Email Notification', + value: `You'll also receive job alerts at **${email}** when they trigger.` + }); + } + + embed.setFooter({ text: 'You can update your preferences anytime' }).setTimestamp(); + + return embed; } /** - * Format reminder icon based on type + * Returns an icon string for a reminder based on its type and whether email is enabled. + * + * @param {Reminder} reminder - The reminder object to inspect. + * @returns {string} The icon string (may include the email icon). */ export function getReminderIcon(reminder: Reminder): string { - const isJobReminder = reminder.content === 'Job Reminder'; - const emailIcon = reminder.emailNotification ? ` ${EMOJI.EMAIL}` : ''; - - return `${isJobReminder ? EMOJI.JOB : EMOJI.REMINDER}${emailIcon}`; + const isJobReminder = reminder.content === 'Job Reminder'; + const emailIcon = reminder.emailNotification ? ` ${EMOJI.EMAIL}` : ''; + + return `${isJobReminder ? EMOJI.JOB : EMOJI.REMINDER}${emailIcon}`; } /** - * Helper for checking job reminder that accepts ButtonInteraction - * This wraps the original checkJobReminder function to handle ButtonInteraction + * Checks if the current user (from a ButtonInteraction) already has a Job Reminder. + * This is a convenience wrapper that queries the reminders collection. + * + * @param {ButtonInteraction} buttonInteraction - The interaction to use for user and client context. + * @returns {Promise} True if the user has a Job Reminder; otherwise, false. */ -export async function checkJobReminderForButton(buttonInteraction: ButtonInteraction): Promise { - const DB = (await import('@root/config')).DB; - const reminders = await buttonInteraction.client.mongo - .collection(DB.REMINDERS) - .find({ owner: buttonInteraction.user.id }) - .toArray(); +export async function checkJobReminderForButton( + buttonInteraction: ButtonInteraction +): Promise { + const { DB } = await import('@root/config'); + const reminders = await buttonInteraction.client.mongo + .collection(DB.REMINDERS) + .find({ owner: buttonInteraction.user.id }) + .toArray(); - return reminders.some( - (reminder) => reminder.content === 'Job Reminder' - ); -} \ No newline at end of file + return reminders.some((reminder) => reminder.content === 'Job Reminder'); +} diff --git a/src/pieces/tasks.ts b/src/pieces/tasks.ts index 148636b2..0e6bb04c 100644 --- a/src/pieces/tasks.ts +++ b/src/pieces/tasks.ts @@ -5,8 +5,8 @@ import { CHANNELS, DB, GMAIL, - MAP_KEY, -} from "@root/config"; + MAP_KEY +} from '@root/config'; import { ActionRowBuilder, AttachmentBuilder, @@ -15,26 +15,26 @@ import { ChannelType, Client, EmbedBuilder, - TextChannel, -} from "discord.js"; -import { schedule } from "node-cron"; -import { Reminder } from "@lib/types/Reminder"; -import { Poll, PollResult } from "@lib/types/Poll"; -import { MongoClient } from "mongodb"; -import fetchJobListings from "../lib/utils/jobUtils/Adzuna_job_search"; -import { sendToFile } from "../lib/utils/generalUtils"; -import { JobData } from "../lib/types/JobData"; -import { Interest } from "../lib/types/Interest"; -import { JobResult } from "../lib/types/JobResult"; -import nodemailer from "nodemailer"; -import { JobPreferences } from "../lib/types/JobPreferences"; -import axios from "axios"; -import { PDFDocument, PDFFont, rgb, StandardFonts } from "pdf-lib"; -import { generateHistogram } from "../commands/jobs/histogram"; + TextChannel +} from 'discord.js'; +import { schedule } from 'node-cron'; +import { Reminder } from '@lib/types/Reminder'; +import { Poll, PollResult } from '@lib/types/Poll'; +import { MongoClient } from 'mongodb'; +import fetchJobListings from '../lib/utils/jobUtils/Adzuna_job_search'; +import { sendToFile } from '../lib/utils/generalUtils'; +import { JobData } from '../lib/types/JobData'; +import { Interest } from '../lib/types/Interest'; +import { JobResult } from '../lib/types/JobResult'; +import nodemailer from 'nodemailer'; +import { JobPreferences } from '../lib/types/JobPreferences'; +import axios from 'axios'; +import { PDFDocument, PDFFont, rgb, StandardFonts } from 'pdf-lib'; +import { generateHistogram } from '../commands/jobs/histogram'; async function register(bot: Client): Promise { - schedule("0/30 * * * * *", () => { - handleCron(bot).catch(async (error) => bot.emit("error", error)); + schedule('0/30 * * * * *', () => { + handleCron(bot).catch(async (error) => bot.emit('error', error)); }); } @@ -48,7 +48,7 @@ async function checkPolls(bot: Client): Promise { .collection(DB.POLLS) .find({ expires: { $lte: new Date() } }) .toArray(); - const emotes = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]; + const emotes = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; polls.forEach(async (poll) => { const mdTimestamp = ``; const resultMap = new Map(); @@ -70,29 +70,29 @@ async function checkPolls(bot: Client): Promise { let winMessage: string; const winCount = winners[0].users.length; if (winCount === 0) { - winMessage = "It looks like no one has voted!"; + winMessage = 'It looks like no one has voted!'; } else if (winners.length === 1) { winMessage = `**${ winners[0].option }** has won the poll with ${winCount} vote${ - winCount === 1 ? "" : "s" + winCount === 1 ? '' : 's' }!`; } else { winMessage = `**${winners .slice(0, -1) .map((win) => win.option) - .join(", ")} and ${ + .join(', ')} and ${ winners.slice(-1)[0].option }** have won the poll with ${winCount} vote${ - winCount === 1 ? "" : "s" + winCount === 1 ? '' : 's' } each!`; } - let choiceText = ""; + let choiceText = ''; let count = 0; resultMap.forEach((value, key) => { choiceText += `${emotes[count++]} ${key}: ${value} vote${ - value === 1 ? "" : "s" + value === 1 ? '' : 's' }\n`; }); @@ -109,11 +109,11 @@ async function checkPolls(bot: Client): Promise { `This poll was created by ${owner.displayName} and ended **${mdTimestamp}**` ) .addFields({ - name: `Winner${winners.length === 1 ? "" : "s"}`, - value: winMessage, + name: `Winner${winners.length === 1 ? '' : 's'}`, + value: winMessage }) - .addFields({ name: "Choices", value: choiceText }) - .setColor("Random"); + .addFields({ name: 'Choices', value: choiceText }) + .setColor('Random'); pollMsg.edit({ embeds: [pollEmbed], components: [] }); @@ -123,15 +123,15 @@ async function checkPolls(bot: Client): Promise { .setTitle(poll.question) .setDescription(`${owner}'s poll has ended!`) .addFields({ - name: `Winner${winners.length === 1 ? "" : "s"}`, - value: winMessage, + name: `Winner${winners.length === 1 ? '' : 's'}`, + value: winMessage }) .addFields({ - name: "Original poll", - value: `Click [here](${pollMsg.url}) to see the original poll.`, + name: 'Original poll', + value: `Click [here](${pollMsg.url}) to see the original poll.` }) - .setColor("Random"), - ], + .setColor('Random') + ] }); await bot.mongo.collection(DB.POLLS).findOneAndDelete(poll); @@ -144,7 +144,7 @@ async function getJobFormData( userID: string ): Promise<[JobData, Interest, JobResult[]]> { const client = await MongoClient.connect(DB.CONNECTION, { - useUnifiedTopology: true, + useUnifiedTopology: true }); const db = client.db(BOT.NAME).collection(DB.USERS); @@ -166,7 +166,7 @@ async function getJobFormData( preference: answers.workType, jobType: answers.employmentType, distance: answers.travelDistance, - filterBy: "default", + filterBy: 'default' }; // Build the interests section for filtering jobs further @@ -175,7 +175,7 @@ async function getJobFormData( interest2: answers.interest2, interest3: answers.interest3, interest4: answers.interest4, - interest5: answers.interest5, + interest5: answers.interest5 }; // Fetch jobs using Adzuna with the above criteria @@ -185,11 +185,11 @@ async function getJobFormData( function formatCurrency(currency: number): string { return isNaN(currency) - ? "N/A" - : `${new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(Number(currency))}`; + ? 'N/A' + : `${new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(Number(currency))}`; } function calculateDistance( @@ -204,15 +204,15 @@ function calculateDistance( const R = 3958.8; // Radius of the Earth in miles const dLat = toRadians(lat2 - lat1); const dLon = toRadians(lon2 - lon1); - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(toRadians(lat1)) * - Math.cos(toRadians(lat2)) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2); + const a + = (Math.sin(dLat / 2) * Math.sin(dLat / 2)) + + (Math.cos(toRadians(lat1)) + * Math.cos(toRadians(lat2)) + * Math.sin(dLon / 2) + * Math.sin(dLon / 2)); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - const distance = - (lat1 === 0 && lon1 === 0) || (lat2 === 0 && lon2 === 0) ? -1 : R * c; + const distance + = (lat1 === 0 && lon1 === 0) || (lat2 === 0 && lon2 === 0) ? -1 : R * c; return distance; } @@ -225,20 +225,20 @@ async function queryCoordinates( const response = await axios.get(baseURL); const coordinates: { lat: number; lng: number } = { lat: response.data.results[0].geometry.location.lat, - lng: response.data.results[0].geometry.location.lng, + lng: response.data.results[0].geometry.location.lng }; return coordinates; } export function titleCase(jobTitle: string | undefined | null): string { - if (!jobTitle) return ""; // fallback for null or undefined + if (!jobTitle) return ''; // fallback for null or undefined return jobTitle .toLowerCase() - .replace(/[()]/g, "") - .split(" ") + .replace(/[()]/g, '') + .split(' ') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); + .join(' '); } async function listJobs( @@ -248,7 +248,7 @@ async function listJobs( // Conditionally sort jobs by salary if sortBy is 'salary' const cityCoordinates = await queryCoordinates(jobForm[0].city); - if (filterBy === "salary") { + if (filterBy === 'salary') { jobForm[2].sort((a, b) => { const avgA = (Number(a.salaryMax) + Number(a.salaryMin)) / 2; const avgB = (Number(b.salaryMax) + Number(b.salaryMin)) / 2; @@ -259,14 +259,14 @@ async function listJobs( return avgB - avgA; // Descending order }); - } else if (filterBy === "alphabetical") { - jobForm[2].sort((a, b) => (a.title > b.title ? 1 : -1)); - } else if (filterBy === "date") { + } else if (filterBy === 'alphabetical') { + jobForm[2].sort((a, b) => a.title > b.title ? 1 : -1); + } else if (filterBy === 'date') { jobForm[2].sort( (a, b) => new Date(b.created).getTime() - new Date(a.created).getTime() ); - } else if (filterBy === "distance") { + } else if (filterBy === 'distance') { // cityCoordinates = await this.queryCoordinates(jobForm[0].city); jobForm[2].sort((a, b) => { @@ -281,32 +281,32 @@ async function listJobs( }); } - let jobList = ""; + let jobList = ''; for (let i = 0; i < jobForm[2].length; i++) { - const avgSalary = - (Number(jobForm[2][i].salaryMax) + - Number(jobForm[2][i].salaryMin)) / - 2; + const avgSalary + = (Number(jobForm[2][i].salaryMax) + + Number(jobForm[2][i].salaryMin)) + / 2; const formattedAvgSalary = formatCurrency(avgSalary); - const formattedSalaryMax = - formatCurrency(Number(jobForm[2][i].salaryMax)) !== "N/A" + const formattedSalaryMax + = formatCurrency(Number(jobForm[2][i].salaryMax)) !== 'N/A' ? formatCurrency(Number(jobForm[2][i].salaryMax)) - : ""; - const formattedSalaryMin = - formatCurrency(Number(jobForm[2][i].salaryMin)) !== "N/A" + : ''; + const formattedSalaryMin + = formatCurrency(Number(jobForm[2][i].salaryMin)) !== 'N/A' ? formatCurrency(Number(jobForm[2][i].salaryMin)) - : ""; + : ''; const jobDistance = calculateDistance( cityCoordinates.lat, cityCoordinates.lng, Number(jobForm[2][i].latitude), Number(jobForm[2][i].longitude) ); - const formattedDistance = - jobDistance !== -1 ? `${jobDistance.toFixed(2)} miles` : "N/A"; + const formattedDistance + = jobDistance !== -1 ? `${jobDistance.toFixed(2)} miles` : 'N/A'; - const salaryDetails = - formattedSalaryMin && formattedSalaryMax + const salaryDetails + = formattedSalaryMin && formattedSalaryMax ? `, Min: ${formattedSalaryMin}, Max: ${formattedSalaryMax}` : formattedAvgSalary; @@ -314,20 +314,20 @@ async function listJobs( \t\t* **Salary Average:** ${formattedAvgSalary}${salaryDetails} \t\t* **Location:** ${jobForm[2][i].location} \t\t* **Date Posted:** ${new Date( - jobForm[2][i].created - ).toDateString()} at ${new Date( - jobForm[2][i].created - ).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })} + jobForm[2][i].created + ).toDateString()} at ${new Date( + jobForm[2][i].created +).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })} \t\t* **Apply here:** [read more about the job and apply here](${ - jobForm[2][i].link - }) + jobForm[2][i].link +}) \t\t* **Distance:** ${formattedDistance} - ${i !== jobForm[2].length - 1 ? "\n" : ""}`; + ${i !== jobForm[2].length - 1 ? '\n' : ''}`; } return ( - jobList || - "### Unfortunately, there were no jobs found based on your interests :(. Consider updating your interests or waiting until something is found." + jobList + || '### Unfortunately, there were no jobs found based on your interests :(. Consider updating your interests or waiting until something is found.' ); } @@ -335,43 +335,43 @@ export async function jobMessage( reminder: Reminder | string, userID: string ): Promise<{ - message: string; - pdfBuffer: Buffer; - embed: EmbedBuilder; - row: ActionRowBuilder; - jobResults: JobResult[]; -}> { + message: string; + pdfBuffer: Buffer; + embed: EmbedBuilder; + row: ActionRowBuilder; + jobResults: JobResult[]; + }> { const jobFormData: [JobData, Interest, JobResult[]] = await getJobFormData( userID ); let filterBy: string; if ( - typeof reminder === "object" && - "filterBy" in reminder && - reminder.filterBy + typeof reminder === 'object' + && 'filterBy' in reminder + && reminder.filterBy ) { filterBy = String(reminder.filterBy); - } else if (typeof reminder === "object") { - filterBy = "default"; + } else if (typeof reminder === 'object') { + filterBy = 'default'; } else { - filterBy = - typeof reminder === "string" && reminder ? reminder : "default"; + filterBy + = typeof reminder === 'string' && reminder ? reminder : 'default'; } const cityCoordinates = await queryCoordinates(jobFormData[0].city); for (let i = 0; i < jobFormData[2].length; i++) { const job = jobFormData[2][i]; - const distance = - (Math.round( + const distance + = (Math.round( calculateDistance( cityCoordinates.lat, cityCoordinates.lng, Number(job.latitude), Number(job.longitude) ) + Number.EPSILON - ) * - 100) / - 100; // Round to 2 decimal places + ) + * 100) + / 100; // Round to 2 decimal places jobFormData[2][i].distance = distance; } @@ -383,11 +383,11 @@ export async function jobMessage( const message = `## Hey <@${userID}>! ## Here's your list of job/internship recommendations: Based on your interests in **${jobFormData[1].interest1}**, **${ - jobFormData[1].interest2 - }**, \ + jobFormData[1].interest2 +}**, \ **${jobFormData[1].interest3}**, **${jobFormData[1].interest4}**, and **${ - jobFormData[1].interest5 - }**, I've found these jobs you may find interesting. Please note that while you may get\ + jobFormData[1].interest5 +}**, I've found these jobs you may find interesting. Please note that while you may get\ job/internship recommendations from the same company,\ their positions/details/applications/salary WILL be different and this is not a glitch/bug! Here's your personalized list: @@ -410,16 +410,16 @@ export function stripMarkdown(message: string, owner: string): string { .replace( new RegExp( `## Hey <@${owner}>!\\s*## Here's your list of job/internship recommendations:?`, - "g" + 'g' ), - "" + '' ) // Remove specific header - .replace(/\[read more about the job and apply here\]/g, "") - .replace(/\((https?:\/\/[^\s)]+)\)/g, "$1") - .replace(/\*\*([^*]+)\*\*/g, "$1") - .replace(/##+\s*/g, "") + .replace(/\[read more about the job and apply here\]/g, '') + .replace(/\((https?:\/\/[^\s)]+)\)/g, '$1') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/##+\s*/g, '') // eslint-disable-next-line no-useless-escape - .replace(/###|-\#\s*/g, "") + .replace(/###|-\#\s*/g, '') .trim() ); } @@ -433,12 +433,12 @@ of all postings. Exercise caution when sharing personal information, submitting on external sites. Always verify the authenticity of job applications before proceeding. Additionally, \ some job postings may contain inaccuracies due to API limitations, which are beyond our control. We apologize for any inconvenience this may cause and appreciate your understanding. ## Here's your list of job/internship recommendations${ - filterBy && filterBy !== "default" - ? ` (filtered based on ${ - filterBy === "date" ? "date posted" : filterBy - }):` - : ":" - } + filterBy && filterBy !== 'default' + ? ` (filtered based on ${ + filterBy === 'date' ? 'date posted' : filterBy + }):` + : ':' +} `; } @@ -451,11 +451,11 @@ async function sendEmailNotification(reminder: Reminder): Promise { // Create Gmail transporter using credentials from config const mailer = nodemailer.createTransport({ - service: "gmail", + service: 'gmail', auth: { user: GMAIL.USER, - pass: GMAIL.APP_PASSWORD, - }, + pass: GMAIL.APP_PASSWORD + } }); try { @@ -463,7 +463,7 @@ async function sendEmailNotification(reminder: Reminder): Promise { let subject: string; let htmlContent: string; - const isJobReminder = reminder.content === "Job Reminder"; + const isJobReminder = reminder.content === 'Job Reminder'; if (isJobReminder) { subject = `Job Alert from ${BOT.NAME}`; @@ -492,14 +492,14 @@ async function sendEmailNotification(reminder: Reminder): Promise { from: GMAIL.USER, to: reminder.emailAddress, subject: subject, - html: htmlContent, + html: htmlContent }); console.log( `Email notification sent to ${reminder.emailAddress} for reminder: ${reminder.content}` ); } catch (error) { - console.error("Failed to send email notification:", error); + console.error('Failed to send email notification:', error); } } @@ -516,7 +516,7 @@ async function checkReminders(bot: Client): Promise { await sendEmailNotification(reminder); } - if (reminder.mode === "public") { + if (reminder.mode === 'public') { pubChan.send( `<@${reminder.owner}>, here's the reminder you asked for: **${reminder.content}**` ); @@ -524,13 +524,14 @@ async function checkReminders(bot: Client): Promise { bot.users .fetch(reminder.owner) .then(async (user) => { - const result = await jobMessage(reminder, user.id); + await jobMessage(reminder, user.id); // const { message } = result; - const message = "placeholder"; // Placeholder for the message variable - const { pdfBuffer } = result; + const message = 'placeholder'; // Placeholder for the message variable + // Is this needed? + // const { pdfBuffer } = result; if (message.length < 2000) { user.send(message).catch((err) => { - console.log("ERROR:", err); + console.log('ERROR:', err); pubChan.send( `<@${reminder.owner}>, I tried to send you a DM about your private reminder but it looks like you have DMs closed. Please enable DMs in the future if you'd like to get private reminders.` @@ -541,11 +542,11 @@ async function checkReminders(bot: Client): Promise { attachments.push( await sendToFile( stripMarkdown( - message.split("---")[0], + message.split('---')[0], reminder.owner ), - "txt", - "list-of-jobs-internships", + 'txt', + 'list-of-jobs-internships', false ) ); @@ -554,13 +555,13 @@ async function checkReminders(bot: Client): Promise { reminder.owner, reminder.filterBy ), - files: attachments as AttachmentBuilder[], + files: attachments as AttachmentBuilder[] }); } }) .catch((error) => { console.log( - "ERROR CALLED ----------------------------------------------------" + 'ERROR CALLED ----------------------------------------------------' ); console.error( `Failed to fetch user with ID: ${reminder.owner}`, @@ -577,15 +578,15 @@ async function checkReminders(bot: Client): Promise { owner: reminder.owner, // Preserve email notification settings for repeated reminders emailNotification: reminder.emailNotification, - emailAddress: reminder.emailAddress, + emailAddress: reminder.emailAddress }; - if (reminder.repeat === "daily") { + if (reminder.repeat === 'daily') { newReminder.expires.setDate(reminder.expires.getDate() + 1); bot.mongo .collection(DB.REMINDERS) .findOneAndReplace(reminder, newReminder); - } else if (reminder.repeat === "weekly") { + } else if (reminder.repeat === 'weekly') { newReminder.expires.setDate(reminder.expires.getDate() + 7); bot.mongo .collection(DB.REMINDERS) @@ -624,14 +625,14 @@ export async function generateJobPDF( // Draw the title. const lineHeight = 10; // Height of the line - const lineWidth = (width - margin * 2) / 3; + const lineWidth = (width - (margin * 2)) / 3; currentPage.drawRectangle({ x: margin, y: yPosition + 50, width: lineWidth, height: lineHeight, - color: rgb(135 / 255, 59 / 255, 29 / 255), // red color + color: rgb(135 / 255, 59 / 255, 29 / 255) // red color }); // Draw the second color segment @@ -640,27 +641,27 @@ export async function generateJobPDF( y: yPosition + 50, width: lineWidth, height: lineHeight, - color: rgb(237 / 255, 118 / 255, 71 / 255), // orangey color + color: rgb(237 / 255, 118 / 255, 71 / 255) // orangey color }); // Draw the third color segment currentPage.drawRectangle({ - x: margin + lineWidth * 2, + x: margin + (lineWidth * 2), y: yPosition + 50, width: lineWidth, height: lineHeight, - color: rgb(13 / 255, 158 / 255, 198 / 255), // Blue color + color: rgb(13 / 255, 158 / 255, 198 / 255) // Blue color }); yPosition -= 40; // Adjust spacing below the line - currentPage.drawText("List of Jobs PDF", { + currentPage.drawText('List of Jobs PDF', { x: margin, y: yPosition + 50, size: titleFontSize, font: HelveticaBold, - color: rgb(114 / 255, 53 / 255, 9 / 255), + color: rgb(114 / 255, 53 / 255, 9 / 255) }); yPosition -= 40; @@ -669,7 +670,7 @@ export async function generateJobPDF( y: yPosition + 50, width: lineWidth / 2, height: lineHeight - 8, - color: rgb(135 / 255, 59 / 255, 29 / 255), // red color + color: rgb(135 / 255, 59 / 255, 29 / 255) // red color }); yPosition -= 10; @@ -677,12 +678,12 @@ export async function generateJobPDF( for (let i = 0; i < jobs.length; i++) { const job = jobs[i]; - if (yPosition - fontSize * 2 < margin) { + if (yPosition - (fontSize * 2) < margin) { currentPage = pdfDoc.addPage(); yPosition = currentPage.getHeight() - margin - 20; } - const maxWidth = width - margin * 2; // Calculate available width + const maxWidth = width - (margin * 2); // Calculate available width const wrappedTitle = wrapText( `${i + 1}. ${job.title}`, HelveticaBold, @@ -692,7 +693,7 @@ export async function generateJobPDF( for (const line of wrappedTitle) { // Check if there's enough space for the line - if (yPosition - fontSize * 2 < margin) { + if (yPosition - (fontSize * 2) < margin) { currentPage = pdfDoc.addPage(); yPosition = currentPage.getHeight() - margin - 20; } @@ -702,7 +703,7 @@ export async function generateJobPDF( y: yPosition + 30, size: fontSize + 10, font: HelveticaBold, - color: rgb(241 / 255, 113 / 255, 34 / 255), + color: rgb(241 / 255, 113 / 255, 34 / 255) }); yPosition -= 30; // Adjust spacing between lines @@ -711,17 +712,17 @@ export async function generateJobPDF( // Draw the bullet points for location, salary, and apply link. const bulletPoints = [ { - label: "Location", + label: 'Location', value: `${job.location}, ${ job.distance >= 0 ? `${job.distance} miles from ${titleCase( - jobForm[0].city - )}` - : "" - } `, + jobForm[0].city + )}` + : '' + } ` }, - { label: "Salary", value: formatSalaryforPDF(job) }, - { label: "Apply Here", value: job.link }, + { label: 'Salary', value: formatSalaryforPDF(job) }, + { label: 'Apply Here', value: job.link } ]; const jobTitle = encodeURIComponent(job.title); @@ -731,7 +732,7 @@ export async function generateJobPDF( const data = Object.entries(response.data.histogram).map( ([value, frequency]: [string, number]) => ({ value, - frequency, + frequency }) ); let noValues = true; @@ -749,13 +750,13 @@ export async function generateJobPDF( for (const point of bulletPoints) { // Check if there's enough space on the page, and add a new page if needed. - if (yPosition - fontSize * 2 < margin) { + if (yPosition - (fontSize * 2) < margin) { currentPage = pdfDoc.addPage(); yPosition = currentPage.getHeight() - margin - 20; } - const maxLabelWidth = - width - margin * 2 - bulletPointIndent - subBulletPointIndent; + const maxLabelWidth + = width - (margin * 2) - bulletPointIndent - subBulletPointIndent; const wrappedLabel = wrapText( `• ${point.label}`, HelveticaBold, @@ -766,7 +767,7 @@ export async function generateJobPDF( // Draw the wrapped label for (const line of wrappedLabel) { // Check if there's enough space for the line - if (yPosition - fontSize * 2 < margin) { + if (yPosition - (fontSize * 2) < margin) { currentPage = pdfDoc.addPage(); yPosition = currentPage.getHeight() - margin - 20; } @@ -776,16 +777,16 @@ export async function generateJobPDF( y: yPosition + 25, size: fontSize + 5, font: HelveticaBold, - color: rgb(94 / 255, 74 / 255, 74 / 255), + color: rgb(94 / 255, 74 / 255, 74 / 255) }); - if (point.label === "Salary" && !noValues) { + if (point.label === 'Salary' && !noValues) { currentPage.drawImage(imageBytes, { // Change space check so it doesn't go off the page - x: currentPage.getWidth() / 2 - imageDims.width / 2, + x: (currentPage.getWidth() / 2) - (imageDims.width / 2), y: yPosition - imageDims.height - 10, width: imageDims.width, - height: imageDims.height, + height: imageDims.height }); yPosition -= imageDims.height + 30; // Adjust spacing for the image @@ -795,8 +796,8 @@ export async function generateJobPDF( } const combinedText = `•${point.value}`; - const maxValueWidth = - width - margin * 2 - bulletPointIndent - subBulletPointIndent; + const maxValueWidth + = width - (margin * 2) - bulletPointIndent - subBulletPointIndent; const wrappedValue = wrapText( combinedText, HelveticaBold, @@ -806,7 +807,7 @@ export async function generateJobPDF( for (const line of wrappedValue) { // Check if there's enough space for the line - if (yPosition - fontSize * 2 < margin) { + if (yPosition - (fontSize * 2) < margin) { currentPage = pdfDoc.addPage(); yPosition = currentPage.getHeight() - margin - 20; } @@ -816,7 +817,7 @@ export async function generateJobPDF( y: yPosition + 20, size: fontSize + 3, font: HelveticaBold, - color: rgb(13 / 255, 158 / 255, 198 / 255), + color: rgb(13 / 255, 158 / 255, 198 / 255) }); yPosition -= fontSize + 5; // Adjust spacing between lines @@ -845,36 +846,36 @@ export function createJobEmbed( ).toDateString()}` ) .addFields( - { name: "Salary", value: formatSalaryforPDF(job), inline: true }, + { name: 'Salary', value: formatSalaryforPDF(job), inline: true }, { - name: "Apply Here", + name: 'Apply Here', value: `[Click here](${job.link})`, - inline: true, + inline: true } ) .setFooter({ text: `Job ${index + 1} of ${totalJobs}` }) - .setColor("#0099ff"); + .setColor('#0099ff'); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() - .setCustomId("previous") - .setLabel("Previous") + .setCustomId('previous') + .setLabel('Previous') .setStyle(ButtonStyle.Primary) .setDisabled(totalJobs === 1), new ButtonBuilder() - .setCustomId("remove") - .setLabel("Remove") + .setCustomId('remove') + .setLabel('Remove') .setStyle(ButtonStyle.Danger) .setDisabled(totalJobs === 1), new ButtonBuilder() - .setCustomId("next") - .setLabel("Next") + .setCustomId('next') + .setLabel('Next') .setStyle(ButtonStyle.Primary) .setDisabled(totalJobs === 1), // ----------------ADDED DOWNLOAD BUTTON------------------- new ButtonBuilder() - .setCustomId("download") - .setLabel("Download") + .setCustomId('download') + .setLabel('Download') .setStyle(ButtonStyle.Success) ); return { embed, row }; @@ -886,9 +887,9 @@ function wrapText( fontSize: number, maxWidth: number ): string[] { - const words = text.split(" "); + const words = text.split(' '); const lines: string[] = []; - let currentLine = ""; + let currentLine = ''; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; @@ -900,15 +901,15 @@ function wrapText( if (currentLine) { lines.push(currentLine); } - currentLine = ""; + currentLine = ''; // Handle long words that exceed maxWidth let remainingWord = word; while (font.widthOfTextAtSize(remainingWord, fontSize) > maxWidth) { const splitIndex = Math.floor( - (maxWidth / - font.widthOfTextAtSize(remainingWord, fontSize)) * - remainingWord.length + (maxWidth + / font.widthOfTextAtSize(remainingWord, fontSize)) + * remainingWord.length ); const chunk = remainingWord.slice(0, splitIndex); lines.push(chunk); @@ -928,14 +929,14 @@ function wrapText( function formatSalaryforPDF(job: JobResult): string { const avgSalary = (Number(job.salaryMax) + Number(job.salaryMin)) / 2; const formattedAvgSalary = formatCurrency(avgSalary); - const formattedSalaryMax = - formatCurrency(Number(job.salaryMax)) !== "N/A" + const formattedSalaryMax + = formatCurrency(Number(job.salaryMax)) !== 'N/A' ? formatCurrency(Number(job.salaryMax)) - : ""; - const formattedSalaryMin = - formatCurrency(Number(job.salaryMin)) !== "N/A" + : ''; + const formattedSalaryMin + = formatCurrency(Number(job.salaryMin)) !== 'N/A' ? formatCurrency(Number(job.salaryMin)) - : ""; + : ''; return formattedSalaryMin && formattedSalaryMax ? `Avg: ${formattedAvgSalary}, Min: ${formattedSalaryMin}, Max: ${formattedSalaryMax}` @@ -946,7 +947,7 @@ async function sortJobResults( jobForm: [JobData, Interest, JobResult[]], filterBy: string ): Promise { - if (filterBy === "salary") { + if (filterBy === 'salary') { jobForm[2].sort((a, b) => { const avgA = (Number(a.salaryMax) + Number(a.salaryMin)) / 2; const avgB = (Number(b.salaryMax) + Number(b.salaryMin)) / 2; @@ -957,14 +958,14 @@ async function sortJobResults( return avgB - avgA; // Descending order }); - } else if (filterBy === "alphabetical") { - jobForm[2].sort((a, b) => (a.title > b.title ? 1 : -1)); - } else if (filterBy === "date") { + } else if (filterBy === 'alphabetical') { + jobForm[2].sort((a, b) => a.title > b.title ? 1 : -1); + } else if (filterBy === 'date') { jobForm[2].sort( (a, b) => new Date(b.created).getTime() - new Date(a.created).getTime() ); - } else if (filterBy === "distance") { + } else if (filterBy === 'distance') { jobForm[2].sort((a, b) => { const distanceA = a.distance; const distanceB = b.distance;