diff --git a/src/commands/stats.ts b/src/commands/stats.ts index 02d2123..60f4077 100644 --- a/src/commands/stats.ts +++ b/src/commands/stats.ts @@ -5,6 +5,8 @@ import { import type { Command } from "workers-discord"; import getStats from "../util/stats"; +import checkDate from "../util/check"; +import { bold, date, italic, money, number } from "../util/format"; import type { CtxWithEnv } from "../env"; const statsCommand: Command = { @@ -15,18 +17,120 @@ const statsCommand: Command = { wait( (async () => { const stats = await getStats(context.env.STATS_API_ENDPOINT); - console.log(stats); + + // Check if Jingle Jam is running + const start = new Date(stats.event.start); + const check = checkDate(start); + // TODO: Re-enable this check once testing is done + // if (check) return edit({ content: check }); + + // Check if Jingle Jam has finished + const end = new Date(stats.event.end); + if (isNaN(+end)) throw new Error("Invalid end date"); + + // Time since launch + // TODO: Switch back to using the current time once testing is done + // const now = new Date(); + const now = end; + const timeSinceLaunch = Math.min( + now.getTime() - start.getTime(), + end.getTime() - start.getTime(), + ); + const hoursSinceLaunch = Math.max( + timeSinceLaunch / 1000 / 60 / 60, + 1, + ); + const daysSinceLaunch = Math.max(hoursSinceLaunch / 24, 1); + + // Format some stats + const totalRaised = bold( + money( + "£", + stats.raised.yogscast + stats.raised.fundraisers, + ), + ); + const totalYogscast = bold(money("£", stats.raised.yogscast)); + const totalFundraisers = bold( + money("£", stats.raised.fundraisers), + ); + + const historyRaised = bold( + money( + "£", + stats.raised.yogscast + + stats.raised.fundraisers + + stats.history.reduce( + (total, year) => total + year.total.pounds, + 0, + ), + ), + ); + const historyDonations = bold( + number( + stats.donations.count + + stats.history.reduce( + (total, year) => total + year.donations, + 0, + ), + ), + ); + const historyYears = bold(number(stats.history.length)); + const historyOldest = bold( + Math.min( + ...stats.history.map((year) => year.year), + ).toString(), + ); + + const bundles = bold(number(stats.collections.redeemed)); + const average = bold( + money("£", stats.raised.yogscast / stats.donations.count), + ); + + const perHourDonations = bold( + number(stats.donations.count / hoursSinceLaunch, 0), + ); + const perDayDonations = bold( + number(stats.donations.count / daysSinceLaunch, 0), + ); + const perHourBundles = bold( + number(stats.collections.redeemed / hoursSinceLaunch, 0), + ); + const perDayBundles = bold( + number(stats.collections.redeemed / daysSinceLaunch, 0), + ); await edit({ - content: `Total Raised: ${ - stats.raised.yogscast + stats.raised.fundraisers - }`, + content: [ + bold( + `:snowflake: Jingle Jam ${stats.event.year} Stats`, + ), + italic( + `Last updated ${date(new Date(stats.date), true)}`, + ), + "", + `:money_with_wings: We've raised a total of ${totalRaised} for charity during Jingle Jam ${stats.event.year} so far!`, + ` Of that, ${totalYogscast} by the Yogscast, and ${totalFundraisers} from community fundraisers.`, + "", + `:scroll: Over the past ${historyYears} years, plus this year, we've raised a total of ${historyRaised} for charity!`, + ` Since ${historyOldest}, we've received ${historyDonations} charitable donations as part of Jingle Jam.`, + "", + `:package: So far, ${bundles} bundles have been redeemed, with the average donation being ${average}.`, + ` That works out to an average of ${perHourBundles} bundles claimed per hour, or ${perDayBundles} bundles per day.`, + ` We've also received an average of ${perHourDonations} donations per hour, or ${perDayDonations} donations per day.`, + "", + `:heart: Thank you for supporting some wonderful causes! Get involved at `, + ].join("\n"), }); })().catch(async (error) => { console.error(error); await edit({ - content: "An error occurred while fetching the stats.", + content: [ + ":pensive: Sorry, an error occurred while fetching the stats.", + italic( + "An ~~angry~~ polite message has been sent to the team letting them know.", + ), + ].join("\n"), }); }), ); @@ -34,7 +138,7 @@ const statsCommand: Command = { return response({ type: InteractionResponseType.ChannelMessageWithSource, data: { - content: "Fetching...", + content: ":package: Fetching the latest bundle stats...", flags: MessageFlags.Ephemeral, }, }); diff --git a/src/util/check.ts b/src/util/check.ts new file mode 100644 index 0000000..63fa0e9 --- /dev/null +++ b/src/util/check.ts @@ -0,0 +1,23 @@ +import { date, time } from "./format"; + +const checkDate = (start: Date) => { + // Ensure we have a valid date + if (isNaN(+start)) throw new Error("Invalid start date"); + + // If the date is in the past, we're fine + if (start < new Date()) return null; + + // If we're within the same day, show the time + if (start.getDate() === new Date().getDate()) { + return `The Jingle Jam bundle hasn't launched yet! Get ready to raise money for some awesome causes and grab the bundle when it goes live at ${time( + start, + )}.`; + } + + // Otherwise, show the date + return `It's not currently Jingle Jam time. We look forward to seeing you on ${date( + start, + )} when we'll be back to raise money again!`; +}; + +export default checkDate; diff --git a/src/util/format.ts b/src/util/format.ts new file mode 100644 index 0000000..0fd2c3a --- /dev/null +++ b/src/util/format.ts @@ -0,0 +1,43 @@ +export const money = (currency: string, amount: number) => + `${currency}${amount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; + +export const date = (date: Date, time = false) => + date.toLocaleString(undefined, { + month: "long", + day: "numeric", + ...(time && { + hour: "numeric", + minute: "numeric", + timeZone: "GMT", + timeZoneName: "short", + }), + }); + +export const time = (date: Date, seconds = false) => + date.toLocaleString(undefined, { + hour: "numeric", + minute: "numeric", + timeZone: "GMT", + timeZoneName: "short", + ...(seconds && { + second: "numeric", + }), + }); + +export const number = ( + number: number, + decimals: number | undefined = undefined, +) => + number.toLocaleString(undefined, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); + +export const bold = (text: string, escape = true) => + `**${escape ? text.replace(/\*/g, "\\*") : text}**`; + +export const italic = (text: string, escape = true) => + `*${escape ? text.replace(/\*/g, "\\*") : text}*`;