diff --git a/src/commands/general/calreminder.ts b/src/commands/general/calreminder.ts index 2696af9f..d7fae225 100644 --- a/src/commands/general/calreminder.ts +++ b/src/commands/general/calreminder.ts @@ -1,7 +1,6 @@ -/* eslint-disable camelcase */ -import { DB } from '@root/config'; -import { Command } from '@root/src/lib/types/Command'; -import { Reminder } from '@root/src/lib/types/Reminder'; +import { DB } from "@root/config"; +import { Command } from "@root/src/lib/types/Command"; +import { Reminder } from "@root/src/lib/types/Reminder"; import { ActionRowBuilder, ApplicationCommandOptionData, @@ -9,26 +8,25 @@ import { ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, - ComponentType -} from 'discord.js'; -import parse from 'parse-duration'; -import { PagifiedSelectMenu } from '@root/src/lib/types/PagifiedSelect'; -import { retrieveEvents } from '@root/src/lib/auth'; -import { calendar_v3 } from 'googleapis'; -import { MongoClient } from 'mongodb'; -const MONGO_URI = process.env.DB_CONN_STRING || ''; + ComponentType, +} from "discord.js"; +import parse from "parse-duration"; +import { PagifiedSelectMenu } from "@root/src/lib/utils/calendarUtils"; +import { retrieveEvents } from "@root/src/lib/auth"; +import { calendar_v3 } from "googleapis"; +import { MongoClient } from "mongodb"; +const MONGO_URI = process.env.DB_CONN_STRING || ""; export default class extends Command { - - name = 'calreminder'; - description = 'Setup reminders for calendar events'; + name = "calreminder"; + description = "Setup reminders for calendar events"; options: ApplicationCommandOptionData[] = [ { - name: 'classname', - description: 'Course ID', + name: "classname", + description: "Course ID", type: ApplicationCommandOptionType.String, - required: true - } + required: true, + }, ]; async run(interaction: ChatInputCommandInteraction): Promise { @@ -36,7 +34,7 @@ export default class extends Command { let offsetMenu: PagifiedSelectMenu; function generateMessage( - repeatInterval: 'every_event' | null, + repeatInterval: "every_event" | null, chosenEvent?: calendar_v3.Schema$Event, chosenOffset?: number, renderMenus = false, @@ -46,18 +44,19 @@ export default class extends Command { if (renderMenus) { eventMenu = new PagifiedSelectMenu(); eventMenu.createSelectMenu({ - customId: 'select_event', - placeHolder: 'Select an event', - minimumValues: 1 + customId: "select_event", + placeHolder: "Select an event", + minimumValues: 1, + maximumValues: 1, }); let defaultSet = false; filteredEvents.forEach((event, index) => { if (!event.start?.dateTime) return; - const isDefault - = !defaultSet - && chosenEvent?.start?.dateTime === event.start?.dateTime; + const isDefault = + !defaultSet && + chosenEvent?.start?.dateTime === event.start?.dateTime; if (isDefault) defaultSet = true; @@ -67,7 +66,7 @@ export default class extends Command { description: `Starts at: ${new Date( event.start.dateTime ).toLocaleString()}`, - default: isDefault + default: isDefault, }); }); @@ -75,32 +74,33 @@ export default class extends Command { // Create offset select menu const offsetOptions = [ - { label: 'At event', value: '0' }, - { label: '10 minutes before', value: '10m' }, - { label: '30 minutes before', value: '30m' }, - { label: '1 hour before', value: '1h' }, - { label: '1 day before', value: '1d' } + { label: "At event", value: "0" }, + { label: "10 minutes before", value: "10m" }, + { label: "30 minutes before", value: "30m" }, + { label: "1 hour before", value: "1h" }, + { label: "1 day before", value: "1d" }, ]; offsetMenu = new PagifiedSelectMenu(); offsetMenu.createSelectMenu({ - customId: 'select_offset', - placeHolder: 'Select reminder offset', - maximumValues: 1 + customId: "select_offset", + placeHolder: "Select reminder offset", + minimumValues: 1, + maximumValues: 1, }); let offsetDefaultSet = false; offsetOptions.forEach((option) => { - const isDefault - = !offsetDefaultSet - && chosenOffset === parse(option.value); + const isDefault = + !offsetDefaultSet && + chosenOffset === parse(option.value); if (isDefault) offsetDefaultSet = true; offsetMenu.addOption({ label: option.label, value: option.value, - default: isDefault + default: isDefault, }); }); @@ -115,22 +115,22 @@ export default class extends Command { // 3) Generate repeat button const toggleRepeatButton = new ButtonBuilder() - .setCustomId('toggle_repeat') + .setCustomId("toggle_repeat") .setLabel( - repeatInterval === 'every_event' - ? 'Repeat: On' - : 'Repeat: Off' + repeatInterval === "every_event" + ? "Repeat: On" + : "Repeat: Off" ) .setStyle(ButtonStyle.Secondary); // 4) Generate set reminder button const setReminder = new ButtonBuilder() - .setCustomId('set_reminder') - .setLabel('Set Reminder') + .setCustomId("set_reminder") + .setLabel("Set Reminder") .setStyle(ButtonStyle.Success); - const setReminderAndRepeatRow - = new ActionRowBuilder().addComponents( + const setReminderAndRepeatRow = + new ActionRowBuilder().addComponents( toggleRepeatButton, setReminder ); @@ -138,18 +138,18 @@ export default class extends Command { return [ ...eventMenuRows, ...offsetMenuRows, - setReminderAndRepeatRow + setReminderAndRepeatRow, ]; } const courseCode = interaction.options - .getString('classname') + .getString("classname") ?.toUpperCase(); if (!courseCode) { await interaction.reply({ - content: '❗ You must specify a class name.', - ephemeral: true + content: "❗ You must specify a class name.", + ephemeral: true, }); return; } @@ -158,15 +158,15 @@ export default class extends Command { let calendar: { calendarId: string; calendarName: string }; try { const client = new MongoClient(MONGO_URI, { - useUnifiedTopology: true + useUnifiedTopology: true, }); await client.connect(); - const db = client.db('CalendarDatabase'); - const collection = db.collection('calendarIds'); + const db = client.db("CalendarDatabase"); + const collection = db.collection("calendarIds"); const calendarInDB = await collection.findOne({ - calendarName: { $regex: `^${courseCode}$`, $options: 'i' } + calendarName: { $regex: `^${courseCode}$`, $options: "i" }, }); await client.close(); @@ -174,20 +174,20 @@ export default class extends Command { if (!calendarInDB) { await interaction.reply({ content: `⚠️ There are no matching calendars with course code **${courseCode}**.`, - ephemeral: true + ephemeral: true, }); return; } calendar = { calendarId: calendarInDB.calendarId, - calendarName: calendarInDB.calendarName + calendarName: calendarInDB.calendarName, }; } catch (error) { - console.error('Calendar lookup failed:', error); + console.error("Calendar lookup failed:", error); await interaction.reply({ content: `❌ Database error while fetching calendar for **${courseCode}**.`, - ephemeral: true + ephemeral: true, }); return; } @@ -198,8 +198,8 @@ export default class extends Command { if (!events || events.length === 0) { await interaction.reply({ content: - '⚠️ Failed to fetch calendar events or no events found.', - ephemeral: true + "⚠️ Failed to fetch calendar events or no events found.", + ephemeral: true, }); return; } @@ -208,7 +208,7 @@ export default class extends Command { let chosenEvent: calendar_v3.Schema$Event = null; let chosenOffset: number = null; - let repeatInterval: 'every_event' = null; + let repeatInterval: "every_event" = null; let activeReminderId: string = null; const initialComponents = generateMessage( @@ -223,24 +223,24 @@ export default class extends Command { const replyMessage = await interaction.reply({ components: initialComponents, - ephemeral: true + ephemeral: true, }); // Main collector for event & offset const collector = replyMessage.createMessageComponentCollector({ componentType: ComponentType.StringSelect, - time: 60_000 + time: 60_000, }); - collector.on('collect', async (i) => { - if (i.customId === 'select_event') { - const [, indexStr] = i.values[0].split('::'); + collector.on("collect", async (i) => { + if (i.customId === "select_event") { + const [eventDateStr, indexStr] = i.values[0].split("::"); const selectedIndex = parseInt(indexStr); chosenEvent = filteredEvents[selectedIndex]; await i.deferUpdate(); - } else if (i.customId === 'select_offset') { + } else if (i.customId === "select_offset") { const rawOffsetStr = i.values[0]; - chosenOffset = rawOffsetStr === '0' ? 0 : parse(rawOffsetStr); + chosenOffset = rawOffsetStr === "0" ? 0 : parse(rawOffsetStr); await i.deferUpdate(); } }); @@ -248,12 +248,12 @@ export default class extends Command { // Button collector for Cancel and Set Reminder const buttonCollector = replyMessage.createMessageComponentCollector({ componentType: ComponentType.Button, - time: 300_000 // 5 minutes + time: 300_000, // 5 minutes }); - buttonCollector.on('collect', async (btnInt) => { - if (btnInt.customId === 'toggle_repeat') { - repeatInterval = repeatInterval ? null : 'every_event'; + buttonCollector.on("collect", async (btnInt) => { + if (btnInt.customId === "toggle_repeat") { + repeatInterval = repeatInterval ? null : "every_event"; const updatedComponents = generateMessage( repeatInterval, @@ -265,9 +265,9 @@ export default class extends Command { ); await btnInt.update({ - components: updatedComponents + components: updatedComponents, }); - } else if (btnInt.customId === 'set_reminder') { + } else if (btnInt.customId === "set_reminder") { // If user hasn’t selected both fields, just silently acknowledge if (!chosenEvent || chosenOffset === null) { if (!btnInt.deferred && !btnInt.replied) { @@ -286,8 +286,8 @@ export default class extends Command { if (remindDate.getTime() <= Date.now()) { await btnInt.editReply({ content: - '⏰ That reminder time is in the past. No reminder was set.', - components: [] + "⏰ That reminder time is in the past. No reminder was set.", + components: [], }); collector.stop(); buttonCollector.stop(); @@ -305,11 +305,11 @@ export default class extends Command { const reminder: Reminder = { owner: btnInt.user.id, content: eventInfo, - mode: 'public', + mode: "public", expires: repeatInterval ? new Date(remindDate.getTime() + EXPIRE_BUFFER_MS) // give repeat reminders more time : remindDate, // one-time reminders - repeat: repeatInterval + repeat: repeatInterval, }; let result; @@ -319,11 +319,11 @@ export default class extends Command { .insertOne(reminder); activeReminderId = result.insertedId; } catch (err) { - console.error('Failed to insert reminder:', err); + console.error("Failed to insert reminder:", err); await btnInt.editReply({ content: - '❌ Failed to save reminder. Please try again later.', - components: [] + "❌ Failed to save reminder. Please try again later.", + components: [], }); buttonCollector.stop(); return; @@ -331,12 +331,12 @@ export default class extends Command { // Build Cancel button row const cancelButton = new ButtonBuilder() - .setCustomId('cancel_reminder') - .setLabel('Cancel Reminder') + .setCustomId("cancel_reminder") + .setLabel("Cancel Reminder") .setStyle(ButtonStyle.Danger); - const buttonRow - = new ActionRowBuilder().addComponents( + const buttonRow = + new ActionRowBuilder().addComponents( cancelButton ); @@ -348,11 +348,11 @@ export default class extends Command { repeatInterval ? `\n🔁 Repeats every event (for up to 180 days) ` - : '' + : "" }`, - components: [buttonRow] + components: [buttonRow], }); - } else if (btnInt.customId === 'cancel_reminder') { + } else if (btnInt.customId === "cancel_reminder") { try { // 1) Defer *a new reply* (ephemeral) if (!btnInt.deferred && !btnInt.replied) { @@ -368,32 +368,39 @@ export default class extends Command { // 3) Send brand new ephemeral follow-up await btnInt.followUp({ - content: '❌ Your reminder has been canceled.', - ephemeral: true + content: "❌ Your reminder has been canceled.", + ephemeral: true, }); // 4) Stop the collector buttonCollector.stop(); } catch (err) { - console.error('Failed to cancel reminder:', err); + console.error("Failed to cancel reminder:", err); } } const actions: Record void> = { - 'next_button:select_event': () => eventMenu.currentPage++, - 'prev_button:select_event': () => eventMenu.currentPage--, - 'next_button:select_offset': () => offsetMenu.currentPage++, - 'prev_button:select_offset': () => offsetMenu.currentPage-- + "next_button:select_event": () => eventMenu.currentPage++, + "prev_button:select_event": () => eventMenu.currentPage--, + "next_button:select_offset": () => offsetMenu.currentPage++, + "prev_button:select_offset": () => offsetMenu.currentPage--, }; const action = actions[btnInt.customId]; if (action) { await btnInt.deferUpdate(); action(); - const newRows = generateMessage(repeatInterval); + + const newRows = generateMessage( + repeatInterval, + chosenEvent, + chosenOffset, + true, // ← force menus to regenerate + eventMenu.currentPage, // ← keep the event page + offsetMenu.currentPage // ← keep the offset page + ); await btnInt.editReply({ components: newRows }); } }); } - }