- {displayedEvents.map(event => (
+ {events.map(event => (
{
variant="title/medium"
suppressHydrationWarning
>
- {formatEventDate(start, end)}
+ {formatEventDate(start, end, true)}
{location}
diff --git a/src/components/events/index.tsx b/src/components/events/index.tsx
new file mode 100644
index 00000000..18664aff
--- /dev/null
+++ b/src/components/events/index.tsx
@@ -0,0 +1,2 @@
+export { default as EventCarousel } from './EventCarousel';
+export { default as EventDisplay } from './EventDisplay';
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a4d61152..c2e5c438 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -61,6 +61,14 @@ export const capitalize = (text: string): string => {
return text.slice(0, 1).toUpperCase() + text.slice(1).toLowerCase();
};
+/**
+ * Given text, removes all non alphanumeric characters.
+ * This makes search terms more lenient.
+ */
+export const formatSearch = (text: string): string => {
+ return text.toLowerCase().replace(/[^0-9a-zA-Z]/g, '');
+};
+
/**
* Helper function to map each user to a numeric value deterministically
* TODO: Use the user's UUID to hash to a number since it will never change
@@ -133,6 +141,71 @@ export const formatURLEventTitle = (title: string): string => {
return encodeURIComponent(title.toLowerCase().trim().replace(/ /g, '-'));
};
+/** Year ACM was founded. */
+const START_YEAR = 2019;
+/** Number of seconds in a day. */
+const DAY_SECONDS = 86400;
+
+/**
+ * @returns The end of the current academic year.
+ */
+export const getEndYear = (): number => {
+ const today = new Date();
+ // Arbitrarily start the next academic year in August
+ return today.getMonth() < 7 ? today.getFullYear() : today.getFullYear() + 1;
+};
+
+/**
+ * @returns A list of all years that ACM has existed.
+ */
+export const getYears = () => {
+ const endYear = getEndYear();
+ const years = [];
+ for (let year = START_YEAR; year < endYear; year += 1) {
+ years.unshift({ value: String(year), label: `${year}–${year + 1}` });
+ }
+ return years;
+};
+
+/**
+ * Given a sort option, returns the range of times to filter events/attendances by.
+ * @param sort Either a year (2023, 2019, etc.) or a string option (past-week, all-time, etc.)
+ * @returns A range of times to filter results by.
+ */
+export const getDateRange = (sort: string | number) => {
+ const now = Date.now() / 1000;
+ let from;
+ let to;
+ switch (sort) {
+ case 'upcoming': {
+ from = now;
+ break;
+ }
+ case 'past-week': {
+ from = now - DAY_SECONDS * 7;
+ break;
+ }
+ case 'past-month': {
+ from = now - DAY_SECONDS * 28;
+ break;
+ }
+ case 'past-year': {
+ from = now - DAY_SECONDS * 365;
+ break;
+ }
+ case 'all-time': {
+ break;
+ }
+ default: {
+ const year = +sort;
+ // Arbitrarily academic years on August 1, which should be during the summer
+ from = new Date(year, 7, 1).getTime() / 1000;
+ to = new Date(year + 1, 7, 1).getTime() / 1000;
+ }
+ }
+ return { from, to };
+};
+
/**
* Returns the default (first) photo for a merchandise item.
* If there are no photos for this item, returns the default 404 image.
diff --git a/src/pages/events.tsx b/src/pages/events.tsx
index 8be8d91d..3c49f923 100644
--- a/src/pages/events.tsx
+++ b/src/pages/events.tsx
@@ -1,16 +1,172 @@
+import { Dropdown, PaginationControls, Typography } from '@/components/common';
+import { EventDisplay } from '@/components/events';
+import { EventAPI } from '@/lib/api';
import withAccessType from '@/lib/hoc/withAccessType';
-import { PermissionService } from '@/lib/services';
-import type { GetServerSideProps, NextPage } from 'next';
+import { CookieService, PermissionService } from '@/lib/services';
+import type { PublicAttendance, PublicEvent } from '@/lib/types/apiResponses';
+import { CookieType } from '@/lib/types/enums';
+import { formatSearch, getDateRange, getYears } from '@/lib/utils';
+import styles from '@/styles/pages/events.module.scss';
+import type { GetServerSideProps } from 'next';
+import { useMemo, useState } from 'react';
-const EventsPage: NextPage = () => {
- return