Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial layout for events page #103

Merged
merged 8 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/events/EventCard/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
}

.image {
aspect-ratio: 1.778;
flex-shrink: 0;
height: 11.25rem;
overflow: none;
position: relative;
width: 20rem;
width: 100%;
}

.info {
Expand Down
7 changes: 5 additions & 2 deletions src/components/events/EventDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Typography } from '@/components/common';
import EventCard from '@/components/events/EventCard';
import { PublicAttendance, PublicEvent } from '@/lib/types/apiResponses';
import styles from './style.module.scss';
Expand All @@ -8,10 +9,12 @@ interface EventDisplayProps {
}

const EventDisplay = ({ events, attendances }: EventDisplayProps) => {
const displayedEvents = events.slice(0, 20);
if (events.length === 0) {
return <Typography variant="title/small">No events found :(</Typography>;
}
return (
<div className={styles.container}>
{displayedEvents.map(event => (
{events.map(event => (
<EventCard
key={event.uuid}
event={event}
Expand Down
2 changes: 1 addition & 1 deletion src/components/events/EventModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const EventModal = ({ open, attended, event, onClose }: EventModalProps) => {
variant="title/medium"
suppressHydrationWarning
>
{formatEventDate(start, end)}
{formatEventDate(start, end, true)}
</Typography>
<Typography className={styles.eventInfo} variant="title/medium">
{location}
Expand Down
2 changes: 2 additions & 0 deletions src/components/events/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as EventCarousel } from './EventCarousel';
export { default as EventDisplay } from './EventDisplay';
73 changes: 73 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
170 changes: 163 additions & 7 deletions src/pages/events.tsx
Original file line number Diff line number Diff line change
@@ -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 <h1>Events Page</h1>;
interface EventsPageProps {
events: PublicEvent[];
attendances: PublicAttendance[];
}

interface FilterOptions {
query: string;
communityFilter: string;
dateFilter: string | number;
attendedFilter: string;
}

const filterEvent = (
event: PublicEvent,
attendances: PublicAttendance[],
{ query, communityFilter, dateFilter, attendedFilter }: FilterOptions
): boolean => {
// Filter search query
if (query !== '' && !formatSearch(event.title).includes(formatSearch(query))) {
return false;
}
// Filter by community
if (communityFilter !== 'all' && event.committee.toLowerCase() !== communityFilter) {
return false;
}
// Filter by date
const { from, to } = getDateRange(dateFilter);
if (from !== undefined && new Date(event.start) < new Date(from * 1000)) {
return false;
}
if (to !== undefined && new Date(event.end) > new Date(to * 1000)) {
return false;
}
// Filter by attendance
if (attendedFilter === 'all') {
return true;
}
const attended = attendances.some(a => a.event.uuid === event.uuid);
if (attendedFilter === 'attended' && !attended) {
return false;
}
if (attendedFilter === 'not-attended' && attended) {
return false;
}
return true;
};

const ROWS_PER_PAGE = 25;
const EventsPage = ({ events, attendances }: EventsPageProps) => {
const [page, setPage] = useState(0);
const [communityFilter, setCommunityFilter] = useState('all');
const [dateFilter, setDateFilter] = useState('upcoming');
const [attendedFilter, setAttendedFilter] = useState('all');
const [query, setQuery] = useState('');

const years = useMemo(getYears, []);

const filteredEvents = events.filter(e =>
filterEvent(e, attendances, { query, communityFilter, dateFilter, attendedFilter })
);
const displayedEvents = filteredEvents.slice(page * ROWS_PER_PAGE, (page + 1) * ROWS_PER_PAGE);

return (
<div className={styles.page}>
<Typography variant="headline/heavy/small">Events</Typography>
<div className={styles.controls}>
<input
className={styles.search}
type="search"
placeholder="Search Events"
aria-label="Search Events"
value={query}
onChange={e => {
setQuery(e.currentTarget.value);
setPage(0);
}}
/>
<div className={styles.filterOption}>
<Dropdown
name="communityOptions"
ariaLabel="Filter events by community"
options={[
{ value: 'all', label: 'All communities' },
{ value: 'general', label: 'General' },
{ value: 'ai', label: 'AI' },
{ value: 'cyber', label: 'Cyber' },
{ value: 'design', label: 'Design' },
{ value: 'hack', label: 'Hack' },
]}
value={communityFilter}
onChange={v => {
setCommunityFilter(v);
setPage(0);
}}
/>
</div>

<div className={styles.filterOption}>
<Dropdown
name="timeOptions"
ariaLabel="Filter events by time"
options={[
{ value: 'upcoming', label: 'Upcoming' },
{ value: 'past-week', label: 'Past week' },
{ value: 'past-month', label: 'Past month' },
{ value: 'past-year', label: 'Past year' },
{ value: 'all-time', label: 'All time' },
'---',
alexzhang1618 marked this conversation as resolved.
Show resolved Hide resolved
...years,
]}
value={dateFilter}
onChange={v => {
setDateFilter(v);
setPage(0);
}}
/>
</div>

<div className={styles.filterOption}>
<Dropdown
name="timeOptions"
ariaLabel="Filter events by attendance"
options={[
{ value: 'all', label: 'Any attendance' },
{ value: 'attended', label: 'Attended' },
{ value: 'not-attended', label: 'Not attended' },
]}
value={attendedFilter}
onChange={v => {
setAttendedFilter(v);
setPage(0);
}}
/>
</div>
</div>
<EventDisplay events={displayedEvents} attendances={attendances} />

{filteredEvents.length > 0 && (
<PaginationControls
page={page}
onPage={page => setPage(page)}
pages={Math.ceil(filteredEvents.length / ROWS_PER_PAGE)}
/>
)}
</div>
);
};

export default EventsPage;

const getServerSidePropsFunc: GetServerSideProps = async () => ({
props: {},
});
const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => {
const authToken = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
const events = await EventAPI.getAllEvents();
const attendances = await EventAPI.getAttendancesForUser(authToken);

return { props: { events, attendances } };
};

export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import EventCarousel from '@/components/events/EventCarousel';
import { EventCarousel } from '@/components/events';
import { EventAPI } from '@/lib/api';
import withAccessType from '@/lib/hoc/withAccessType';
import { CookieService, PermissionService } from '@/lib/services';
Expand Down
51 changes: 8 additions & 43 deletions src/pages/leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,13 @@ import { CookieService, PermissionService } from '@/lib/services';
import { SlidingLeaderboardQueryParams } from '@/lib/types/apiRequests';
import { PrivateProfile, PublicProfile } from '@/lib/types/apiResponses';
import { CookieType } from '@/lib/types/enums';
import { getProfilePicture, getUserRank } from '@/lib/utils';
import { getDateRange, getEndYear, getProfilePicture, getUserRank, getYears } from '@/lib/utils';
import MyPositionIcon from '@/public/assets/icons/my-position-icon.svg';
import styles from '@/styles/pages/leaderboard.module.scss';
import { GetServerSideProps } from 'next';
import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';

/** Year ACM was founded. */
const START_YEAR = 2019;
/** Number of seconds in a day. */
const DAY_SECONDS = 86400;

interface Match {
index: number;
length: number;
Expand All @@ -31,37 +26,14 @@ function filter<T extends PublicProfile>(users: T[], query: string): (T & { matc
});
}

function getEndYear(): number {
const today = new Date();
// Arbitrarily start the next academic year in August
return today.getMonth() < 7 ? today.getFullYear() : today.getFullYear() + 1;
}

function getLeaderboardRange(sort: string | number, limit = 0): SlidingLeaderboardQueryParams {
const params: SlidingLeaderboardQueryParams = { limit };
const now = Date.now() / 1000;
switch (sort) {
case 'past-week': {
params.from = now - DAY_SECONDS * 7;
break;
}
case 'past-month': {
params.from = now - DAY_SECONDS * 28;
break;
}
case 'past-year': {
params.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
params.from = new Date(year, 7, 1).getTime() / 1000;
params.to = new Date(year + 1, 7, 1).getTime() / 1000;
}
const { from, to } = getDateRange(sort);
if (from !== undefined) {
params.from = from;
}
if (to !== undefined) {
params.to = to;
}
return params;
}
Expand All @@ -83,14 +55,7 @@ const LeaderboardPage = ({ sort, leaderboard, user: { uuid } }: LeaderboardProps
// user presses the button multiple times
const [scrollIntoView, setScrollIntoView] = useState(0);

const years = useMemo(() => {
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;
}, []);
const years = useMemo(getYears, []);

const { allRows, myIndex } = useMemo(() => {
const results = leaderboard.map((user, index) => ({ ...user, position: index + 1 }));
Expand Down
Loading