{
export default AdminPage;
-const getServerSidePropsFunc: GetServerSideProps = async () => {
- return {
- props: {},
- };
+const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => {
+ const preview =
+ CookieService.getServerCookie(CookieType.USER_PREVIEW_ENABLED, { req, res }) ?? '';
+ return { props: { title: 'Admin Actions', preview } };
};
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
PermissionService.canViewAdminPage,
- config.homeRoute
+ { redirectTo: config.homeRoute }
);
diff --git a/src/pages/admin/milestone.tsx b/src/pages/admin/milestone.tsx
index 89e632cc..7b53daac 100644
--- a/src/pages/admin/milestone.tsx
+++ b/src/pages/admin/milestone.tsx
@@ -64,11 +64,14 @@ const AwardPointsPage: NextPage = () => {
export default AwardPointsPage;
const getServerSidePropsFunc: GetServerSideProps = async () => ({
- props: {},
+ props: {
+ title: 'Create Milestone',
+ description: "Award points to all active users (e.g. for ACM's 8 year anniversary)",
+ },
});
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
PermissionService.canAwardPoints,
- config.admin.homeRoute
+ { redirectTo: config.admin.homeRoute }
);
diff --git a/src/pages/admin/points.tsx b/src/pages/admin/points.tsx
index afab8a93..20222a0a 100644
--- a/src/pages/admin/points.tsx
+++ b/src/pages/admin/points.tsx
@@ -76,11 +76,11 @@ const AwardPointsPage: NextPage = () => {
export default AwardPointsPage;
const getServerSidePropsFunc: GetServerSideProps = async () => ({
- props: {},
+ props: { title: 'Award Bonus Points', description: 'Grant bonus points to a specific user' },
});
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
PermissionService.canAwardPoints,
- config.admin.homeRoute
+ { redirectTo: config.admin.homeRoute }
);
diff --git a/src/pages/admin/store/pickup/[uuid].tsx b/src/pages/admin/store/pickup/[uuid].tsx
new file mode 100644
index 00000000..a3dcb862
--- /dev/null
+++ b/src/pages/admin/store/pickup/[uuid].tsx
@@ -0,0 +1,146 @@
+import {
+ cancelPickupEvent,
+ completePickupEvent,
+} from '@/components/admin/event/AdminPickupEvent/AdminPickupEventForm';
+import {
+ PickupEventStatus,
+ PickupOrdersFulfillDisplay,
+ PickupOrdersPrepareDisplay,
+} from '@/components/admin/store';
+import { Button, Typography } from '@/components/common';
+import { EventCard } from '@/components/events';
+import { StoreAPI } from '@/lib/api';
+import config from '@/lib/config';
+import withAccessType from '@/lib/hoc/withAccessType';
+import { CookieService, PermissionService } from '@/lib/services';
+import { PublicOrderPickupEvent } from '@/lib/types/apiResponses';
+import { CookieType, OrderPickupEventStatus } from '@/lib/types/enums';
+import { formatEventDate } from '@/lib/utils';
+import styles from '@/styles/pages/StorePickupEventDetailsPage.module.scss';
+import { GetServerSideProps } from 'next';
+import Link from 'next/link';
+import router from 'next/router';
+import { useState } from 'react';
+
+interface PickupEventDetailsPageProps {
+ pickupEvent: PublicOrderPickupEvent;
+ token: string;
+}
+
+const PickupEventDetailsPage = ({ pickupEvent, token }: PickupEventDetailsPageProps) => {
+ const { uuid, status, title, start, end, orderLimit, description, linkedEvent, orders } =
+ pickupEvent;
+ const [ordersView, setOrdersView] = useState<'fulfill' | 'prepare'>('fulfill');
+
+ let ordersComponent;
+ if (orders && orders.length > 0)
+ ordersComponent =
+ ordersView === 'fulfill' ? (
+
+ ) : (
+
+ );
+
+ return (
+
+
+ {'< Back to dashboard'}
+
+
+
+ {linkedEvent ? (
+
+ ) : (
+
+ *Not linked to an event on the membership portal
+
+ )}
+
+
+
{title}
+
+ {status === OrderPickupEventStatus.ACTIVE ? (
+
+ ) : null}
+ {status === OrderPickupEventStatus.ACTIVE ? (
+
+ ) : null}
+
+ {formatEventDate(start, end, true)}
+
+
{`Max Orders: ${orderLimit}`}
+
+ {description}
+
+
+
+
+
+
Orders
+
+
+
+
+
+ {ordersComponent || 'No orders placed.'}
+
+
+
+ );
+};
+
+export default PickupEventDetailsPage;
+
+const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) => {
+ const uuid = params?.uuid as string;
+ const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
+ try {
+ const pickupEvent = await StoreAPI.getPickupEvent(token, uuid);
+ if (pickupEvent.orders)
+ pickupEvent.orders.sort((a, b) => {
+ const aName = a.user.lastName + a.user.firstName;
+ const bName = b.user.lastName + b.user.firstName;
+ return aName.localeCompare(bName);
+ });
+ return {
+ props: { title: pickupEvent.title, pickupEvent, token },
+ };
+ } catch (e) {
+ return { notFound: true };
+ }
+};
+
+export const getServerSideProps = withAccessType(
+ getServerSidePropsFunc,
+ PermissionService.canManagePickupEvents
+);
diff --git a/src/pages/admin/store/pickup/create.tsx b/src/pages/admin/store/pickup/create.tsx
new file mode 100644
index 00000000..155e30ad
--- /dev/null
+++ b/src/pages/admin/store/pickup/create.tsx
@@ -0,0 +1,44 @@
+// reference /store/item/new page
+
+import AdminPickupEventForm from '@/components/admin/event/AdminPickupEvent/AdminPickupEventForm';
+import { Navbar } from '@/components/store';
+import { config } from '@/lib';
+import { EventAPI } from '@/lib/api';
+import withAccessType from '@/lib/hoc/withAccessType';
+import { CookieService, PermissionService } from '@/lib/services';
+import { PrivateProfile, PublicEvent } from '@/lib/types/apiResponses';
+import { CookieType } from '@/lib/types/enums';
+import styles from '@/styles/pages/StoreItemEditPage.module.scss';
+import { GetServerSideProps } from 'next';
+
+interface CreatePickupEventPageProps {
+ user: PrivateProfile;
+ token: string;
+ futureEvents: PublicEvent[];
+}
+const CreatePickupEventPage = ({
+ user: { credits },
+ token,
+ futureEvents,
+}: CreatePickupEventPageProps) => {
+ return (
+
+ );
+};
+
+export default CreatePickupEventPage;
+
+const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => {
+ const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
+ const futureEvents = await EventAPI.getAllFutureEvents();
+ return { props: { title: 'Create Pickup Event', token, futureEvents } };
+};
+
+export const getServerSideProps = withAccessType(
+ getServerSidePropsFunc,
+ PermissionService.canEditMerchItems,
+ { redirectTo: config.admin.homeRoute }
+);
diff --git a/src/pages/admin/store/pickup/edit/[uuid].tsx b/src/pages/admin/store/pickup/edit/[uuid].tsx
new file mode 100644
index 00000000..ce01bc60
--- /dev/null
+++ b/src/pages/admin/store/pickup/edit/[uuid].tsx
@@ -0,0 +1,55 @@
+// reference /store/item/new page
+
+import AdminPickupEventForm from '@/components/admin/event/AdminPickupEvent/AdminPickupEventForm';
+import { Navbar } from '@/components/store';
+import { config } from '@/lib';
+import { EventAPI, StoreAPI } from '@/lib/api';
+import withAccessType from '@/lib/hoc/withAccessType';
+import { CookieService, PermissionService } from '@/lib/services';
+import { PrivateProfile, PublicEvent, PublicOrderPickupEvent } from '@/lib/types/apiResponses';
+import { CookieType } from '@/lib/types/enums';
+import styles from '@/styles/pages/StoreItemEditPage.module.scss';
+import { GetServerSideProps } from 'next';
+
+interface EditPickupEventProps {
+ user: PrivateProfile;
+ token: string;
+ futureEvents: PublicEvent[];
+ pickupEvent: PublicOrderPickupEvent;
+}
+const EditPickupEventPage = ({
+ user: { credits },
+ token,
+ futureEvents,
+ pickupEvent,
+}: EditPickupEventProps) => {
+ return (
+
+ );
+};
+
+export default EditPickupEventPage;
+
+const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) => {
+ const uuid = params?.uuid as string;
+ const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
+ const [futureEvents, pickupEvent] = await Promise.all([
+ EventAPI.getAllFutureEvents(),
+ StoreAPI.getPickupEvent(token, uuid),
+ ]);
+ return { props: { title: `Edit ${pickupEvent.title}`, token, futureEvents, pickupEvent } };
+};
+
+export const getServerSideProps = withAccessType(
+ getServerSidePropsFunc,
+ PermissionService.canEditMerchItems,
+ { redirectTo: config.admin.homeRoute }
+);
diff --git a/src/pages/admin/store/pickup/index.tsx b/src/pages/admin/store/pickup/index.tsx
new file mode 100644
index 00000000..881891e4
--- /dev/null
+++ b/src/pages/admin/store/pickup/index.tsx
@@ -0,0 +1,84 @@
+import { PickupEventCard } from '@/components/admin/store';
+import { Typography } from '@/components/common';
+import { config } from '@/lib';
+import { StoreAPI } from '@/lib/api';
+import withAccessType from '@/lib/hoc/withAccessType';
+import { CookieService, PermissionService } from '@/lib/services';
+import type { PublicOrderPickupEvent } from '@/lib/types/apiResponses';
+import { CookieType } from '@/lib/types/enums';
+import styles from '@/styles/pages/StorePickupEventPage.module.scss';
+import { GetServerSideProps } from 'next';
+import router from 'next/router';
+import { useState } from 'react';
+
+interface AdminPickupPageProps {
+ futurePickupEvents: PublicOrderPickupEvent[];
+ pastPickupEvents: PublicOrderPickupEvent[];
+}
+
+const AdminPickupPage = ({ futurePickupEvents, pastPickupEvents }: AdminPickupPageProps) => {
+ const [display, setDisplay] = useState<'past' | 'future'>('future');
+ const displayPickupEvents = display === 'past' ? pastPickupEvents : futurePickupEvents;
+ return (
+
+
+
+ Manage Pickup Events
+
+
+
+
+
+
+
+
+
+
+ {displayPickupEvents
+ .sort((x, y) => {
+ return Date.parse(x.start) - Date.parse(y.start);
+ })
+ .map(pickupEvent => (
+
+ ))}
+
+
+ );
+};
+
+export default AdminPickupPage;
+
+const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => {
+ const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
+ const futurePickupEventsPromise = StoreAPI.getFutureOrderPickupEvents(token);
+ const pastPickupEventsPromise = StoreAPI.getPastOrderPickupEvents(token);
+ const [futurePickupEvents, pastPickupEvents] = await Promise.all([
+ futurePickupEventsPromise,
+ pastPickupEventsPromise,
+ ]);
+ return { props: { title: 'Manage Pickup Events', futurePickupEvents, pastPickupEvents, token } };
+};
+
+export const getServerSideProps = withAccessType(
+ getServerSidePropsFunc,
+ PermissionService.canManagePickupEvents,
+ { redirectTo: config.homeRoute }
+);
diff --git a/src/pages/check-email.tsx b/src/pages/check-email.tsx
index 98c2c969..6b31426e 100644
--- a/src/pages/check-email.tsx
+++ b/src/pages/check-email.tsx
@@ -49,8 +49,6 @@ export const getServerSideProps: GetServerSideProps = async ({ query }) => {
};
}
return {
- props: {
- email,
- },
+ props: { title: 'Verify your email address', email },
};
};
diff --git a/src/pages/checkin.tsx b/src/pages/checkin.tsx
index 6aa01691..e1028da0 100644
--- a/src/pages/checkin.tsx
+++ b/src/pages/checkin.tsx
@@ -13,5 +13,5 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ query }) => {
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
- PermissionService.allUserTypes()
+ PermissionService.loggedInUser
);
diff --git a/src/pages/debug.tsx b/src/pages/debug.tsx
new file mode 100644
index 00000000..92e1b1b4
--- /dev/null
+++ b/src/pages/debug.tsx
@@ -0,0 +1,64 @@
+import { Button, Typography } from '@/components/common';
+import { config, showToast } from '@/lib';
+import withAccessType from '@/lib/hoc/withAccessType';
+import { PermissionService } from '@/lib/services';
+import { setClientCookie } from '@/lib/services/CookieService';
+import { CookieCartItem } from '@/lib/types/client';
+import { CookieType } from '@/lib/types/enums';
+import { GetServerSideProps } from 'next';
+
+const DebugPage = () => {
+ return (
+
+ Debug
+
+ Store
+
+
+ );
+};
+
+export default DebugPage;
+
+const getServerSidePropsFunc: GetServerSideProps = async () => {
+ if (!config.api.baseUrl.includes('testing'))
+ return {
+ notFound: true,
+ };
+
+ return { props: { title: 'Debug' } };
+};
+
+export const getServerSideProps = withAccessType(
+ getServerSidePropsFunc,
+ PermissionService.loggedInUser
+);
diff --git a/src/pages/discord.tsx b/src/pages/discord.tsx
index 330366be..51676fb6 100644
--- a/src/pages/discord.tsx
+++ b/src/pages/discord.tsx
@@ -9,10 +9,10 @@ const DiscordPage = () => {
export default DiscordPage;
const getServerSidePropsFunc: GetServerSideProps = async () => ({
- props: {},
+ props: { title: 'Discord' },
});
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
- PermissionService.allUserTypes()
+ PermissionService.loggedInUser
);
diff --git a/src/pages/events.tsx b/src/pages/events.tsx
index c6cc0f84..1e6a002b 100644
--- a/src/pages/events.tsx
+++ b/src/pages/events.tsx
@@ -1,9 +1,18 @@
import { Dropdown, PaginationControls, Typography } from '@/components/common';
+import { DIVIDER } from '@/components/common/Dropdown';
import { EventDisplay } from '@/components/events';
-import { EventAPI } from '@/lib/api';
+import { config } from '@/lib';
+import { EventAPI, UserAPI } from '@/lib/api';
import withAccessType from '@/lib/hoc/withAccessType';
+import useQueryState from '@/lib/hooks/useQueryState';
import { CookieService, PermissionService } from '@/lib/services';
import type { PublicAttendance, PublicEvent } from '@/lib/types/apiResponses';
+import {
+ FilterEventOptions,
+ isValidAttendanceFilter,
+ isValidCommunityFilter,
+ isValidDateFilter,
+} from '@/lib/types/client';
import { CookieType } from '@/lib/types/enums';
import { formatSearch, getDateRange, getYears } from '@/lib/utils';
import styles from '@/styles/pages/events.module.scss';
@@ -13,22 +22,23 @@ import { useMemo, useState } from 'react';
interface EventsPageProps {
events: PublicEvent[];
attendances: PublicAttendance[];
+ initialFilters: FilterEventOptions;
}
interface FilterOptions {
- query: string;
+ search: string;
communityFilter: string;
dateFilter: string | number;
- attendedFilter: string;
+ attendanceFilter: string;
}
const filterEvent = (
event: PublicEvent,
attendances: PublicAttendance[],
- { query, communityFilter, dateFilter, attendedFilter }: FilterOptions
+ { search, communityFilter, dateFilter, attendanceFilter }: FilterOptions
): boolean => {
// Filter search query
- if (query !== '' && !formatSearch(event.title).includes(formatSearch(query))) {
+ if (search !== '' && !formatSearch(event.title).includes(formatSearch(search))) {
return false;
}
// Filter by community
@@ -44,31 +54,67 @@ const filterEvent = (
return false;
}
// Filter by attendance
- if (attendedFilter === 'all') {
+ if (attendanceFilter === 'any') {
return true;
}
const attended = attendances.some(a => a.event.uuid === event.uuid);
- if (attendedFilter === 'attended' && !attended) {
+ if (attendanceFilter === 'attended' && !attended) {
return false;
}
- if (attendedFilter === 'not-attended' && attended) {
+ if (attendanceFilter === 'not-attended' && attended) {
return false;
}
return true;
};
+const DEFAULT_FILTER_STATE = {
+ community: 'all',
+ date: 'all-time',
+ attendance: 'any',
+ search: '',
+};
+
const ROWS_PER_PAGE = 25;
-const EventsPage = ({ events, attendances }: EventsPageProps) => {
- const [page, setPage] = useState(0);
- const [communityFilter, setCommunityFilter] = useState('all');
- const [dateFilter, setDateFilter] = useState('all-time');
- const [attendedFilter, setAttendedFilter] = useState('all');
- const [query, setQuery] = useState('');
+const EventsPage = ({ events, attendances, initialFilters }: EventsPageProps) => {
+ const [page, setPage] = useState(0);
const years = useMemo(getYears, []);
+ const validDate = (value: string): boolean => {
+ return isValidDateFilter(value) || years.some(o => o.value === value);
+ };
+
+ const [states, setStates] = useQueryState({
+ pathName: config.eventsRoute,
+ initialFilters,
+ queryStates: {
+ community: {
+ defaultValue: DEFAULT_FILTER_STATE.community,
+ valid: isValidCommunityFilter,
+ },
+ date: {
+ defaultValue: DEFAULT_FILTER_STATE.date,
+ valid: validDate,
+ },
+ attendance: {
+ defaultValue: DEFAULT_FILTER_STATE.attendance,
+ valid: isValidAttendanceFilter,
+ },
+ search: {
+ defaultValue: DEFAULT_FILTER_STATE.search,
+ // Any string is a valid search, so just return true.
+ valid: () => true,
+ },
+ },
+ });
+
+ const communityFilter = states.community?.value || DEFAULT_FILTER_STATE.community;
+ const dateFilter = states.date?.value || DEFAULT_FILTER_STATE.date;
+ const attendanceFilter = states.attendance?.value || DEFAULT_FILTER_STATE.attendance;
+ const search = states.search?.value || DEFAULT_FILTER_STATE.search;
+
const filteredEvents = events.filter(e =>
- filterEvent(e, attendances, { query, communityFilter, dateFilter, attendedFilter })
+ filterEvent(e, attendances, { search, communityFilter, dateFilter, attendanceFilter })
);
filteredEvents.sort((a, b) => {
@@ -91,9 +137,9 @@ const EventsPage = ({ events, attendances }: EventsPageProps) => {
type="search"
placeholder="Search Events"
aria-label="Search Events"
- value={query}
+ value={search}
onChange={e => {
- setQuery(e.currentTarget.value);
+ setStates('search', e.currentTarget.value);
setPage(0);
}}
/>
@@ -102,7 +148,7 @@ const EventsPage = ({ events, attendances }: EventsPageProps) => {
name="communityOptions"
ariaLabel="Filter events by community"
options={[
- { value: 'all', label: 'All communities' },
+ { value: 'all', label: 'All Communities' },
{ value: 'general', label: 'General' },
{ value: 'ai', label: 'AI' },
{ value: 'cyber', label: 'Cyber' },
@@ -111,7 +157,7 @@ const EventsPage = ({ events, attendances }: EventsPageProps) => {
]}
value={communityFilter}
onChange={v => {
- setCommunityFilter(v);
+ setStates('community', v);
setPage(0);
}}
/>
@@ -119,20 +165,20 @@ const EventsPage = ({ events, attendances }: EventsPageProps) => {
{
- setDateFilter(v);
+ setStates('date', v);
setPage(0);
}}
/>
@@ -140,16 +186,16 @@ const EventsPage = ({ events, attendances }: EventsPageProps) => {
{
- setAttendedFilter(v);
+ setStates('attendance', v);
setPage(0);
}}
/>
@@ -157,28 +203,40 @@ const EventsPage = ({ events, attendances }: EventsPageProps) => {
- {filteredEvents.length > 0 && (
+ {filteredEvents.length > 0 ? (
setPage(page)}
pages={Math.ceil(filteredEvents.length / ROWS_PER_PAGE)}
/>
- )}
+ ) : null}
);
};
export default EventsPage;
-const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => {
+const getServerSidePropsFunc: GetServerSideProps = async ({ req, res, query }) => {
const authToken = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
- const events = await EventAPI.getAllEvents();
- const attendances = await EventAPI.getAttendancesForUser(authToken);
- return { props: { events, attendances } };
+ const getEventsPromise = EventAPI.getAllEvents();
+ const getAttendancesPromise = UserAPI.getAttendancesForCurrentUser(authToken);
+
+ const [events, attendances] = await Promise.all([getEventsPromise, getAttendancesPromise]);
+
+ const {
+ community = DEFAULT_FILTER_STATE.community,
+ date = DEFAULT_FILTER_STATE.date,
+ attendance = DEFAULT_FILTER_STATE.attendance,
+ search = DEFAULT_FILTER_STATE.search,
+ } = query as FilterEventOptions;
+
+ const initialFilters = { community, date, attendance, search };
+
+ return { props: { title: 'Events', events, attendances, initialFilters } };
};
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
- PermissionService.allUserTypes()
+ PermissionService.loggedInUser
);
diff --git a/src/pages/events/[uuid].tsx b/src/pages/events/[uuid].tsx
new file mode 100644
index 00000000..15f210c6
--- /dev/null
+++ b/src/pages/events/[uuid].tsx
@@ -0,0 +1,75 @@
+import { Typography } from '@/components/common';
+import EventDetail from '@/components/events/EventDetail';
+import { Feedback, FeedbackForm } from '@/components/feedback';
+import { EventAPI, FeedbackAPI, UserAPI } from '@/lib/api';
+import withAccessType, { GetServerSidePropsWithUser } from '@/lib/hoc/withAccessType';
+import { CookieService, PermissionService } from '@/lib/services';
+import type { PublicEvent, PublicFeedback } from '@/lib/types/apiResponses';
+import { CookieType } from '@/lib/types/enums';
+import styles from '@/styles/pages/event.module.scss';
+import { useMemo, useState } from 'react';
+
+interface EventPageProps {
+ token: string;
+ event: PublicEvent;
+ attended: boolean;
+ feedback: PublicFeedback | null;
+}
+const EventPage = ({ token, event, attended, feedback: initFeedback }: EventPageProps) => {
+ const started = useMemo(() => new Date() >= new Date(event.start), [event.start]);
+ const [feedback, setFeedback] = useState(initFeedback);
+
+ let feedbackForm = null;
+ if (feedback) {
+ feedbackForm = (
+
+
+ Your Feedback
+
+
+
+ );
+ } else if (started) {
+ feedbackForm = (
+
+ );
+ }
+
+ return (
+
+
+ {feedbackForm}
+
+ );
+};
+
+export default EventPage;
+
+const getServerSidePropsFunc: GetServerSidePropsWithUser = async ({ params, req, res, user }) => {
+ const uuid = params?.uuid as string;
+ const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
+
+ try {
+ const [event, attendances, [feedback = null]] = await Promise.all([
+ EventAPI.getEvent(uuid, token),
+ UserAPI.getAttendancesForCurrentUser(token),
+ FeedbackAPI.getFeedback(token, { user: user.uuid, event: uuid }),
+ ]);
+ return {
+ props: {
+ title: event.title,
+ token,
+ event,
+ attended: attendances.some(attendance => attendance.event.uuid === uuid),
+ feedback,
+ },
+ };
+ } catch {
+ return { notFound: true };
+ }
+};
+
+export const getServerSideProps = withAccessType(
+ getServerSidePropsFunc,
+ PermissionService.loggedInUser
+);
diff --git a/src/pages/feedback.tsx b/src/pages/feedback.tsx
new file mode 100644
index 00000000..68dd6a95
--- /dev/null
+++ b/src/pages/feedback.tsx
@@ -0,0 +1,203 @@
+import { Dropdown, PaginationControls, Typography } from '@/components/common';
+import { Feedback, feedbackTypeNames } from '@/components/feedback';
+import { config } from '@/lib';
+import { FeedbackAPI } from '@/lib/api';
+import withAccessType from '@/lib/hoc/withAccessType';
+import useQueryState from '@/lib/hooks/useQueryState';
+import { CookieService, PermissionService } from '@/lib/services';
+import type { PrivateProfile, PublicFeedback } from '@/lib/types/apiResponses';
+import { CookieType, FeedbackStatus, FeedbackType, UserAccessType } from '@/lib/types/enums';
+import { isEnum } from '@/lib/utils';
+import styles from '@/styles/pages/feedback.module.scss';
+import type { GetServerSideProps } from 'next';
+import Link from 'next/link';
+import { useMemo, useState } from 'react';
+
+type FilterOptions = {
+ type: 'any' | FeedbackType;
+ status: 'any' | FeedbackStatus;
+ sort: 'chronological' | 'submitted-first';
+};
+
+const DEFAULT_FILTER_STATE: FilterOptions = {
+ type: 'any',
+ status: 'any',
+ sort: 'chronological',
+};
+
+const ROWS_PER_PAGE = 25;
+
+interface FeedbackPageProps {
+ user: PrivateProfile;
+ feedback: PublicFeedback[];
+ token: string;
+ initialFilters: FilterOptions;
+}
+const FeedbackPage = ({ user, feedback, token, initialFilters }: FeedbackPageProps) => {
+ /** Whether the user can respond to feedback */
+ const isAdmin = user.accessType === UserAccessType.ADMIN;
+ const [page, setPage] = useState(0);
+
+ const [states, setStates] = useQueryState({
+ pathName: config.feedbackRoute,
+ initialFilters,
+ queryStates: {
+ type: {
+ defaultValue: DEFAULT_FILTER_STATE.type,
+ valid: type => type === 'any' || isEnum(FeedbackType, type),
+ },
+ status: {
+ defaultValue: DEFAULT_FILTER_STATE.status,
+ valid: status => status === 'any' || isEnum(FeedbackStatus, status),
+ },
+ sort: {
+ defaultValue: DEFAULT_FILTER_STATE.sort,
+ valid: sort => sort === 'chronological' || sort === 'submitted-first',
+ },
+ },
+ });
+
+ const typeFilter = states.type?.value || DEFAULT_FILTER_STATE.type;
+ const statusFilter = states.status?.value || DEFAULT_FILTER_STATE.status;
+ const sortFilter = states.sort?.value || DEFAULT_FILTER_STATE.sort;
+
+ const filteredFeedback = useMemo(
+ () =>
+ feedback
+ .filter(
+ feedback =>
+ (typeFilter === 'any' || feedback.type === typeFilter) &&
+ (statusFilter === 'any' || feedback.status === statusFilter)
+ )
+ .sort((a, b) => {
+ // Put SUBMITTED feedback on top
+ if (sortFilter === 'submitted-first' && a.status !== b.status) {
+ return (
+ (a.status === FeedbackStatus.SUBMITTED ? 0 : 1) -
+ (b.status === FeedbackStatus.SUBMITTED ? 0 : 1)
+ );
+ }
+ // Otherwise, just put most recent first
+ return +new Date(b.timestamp) - +new Date(a.timestamp);
+ }),
+ [feedback, typeFilter, statusFilter, sortFilter]
+ );
+
+ return (
+
+
+ Feedback Submissions
+
+
+
+ ({ value, label })),
+ ]}
+ value={typeFilter}
+ onChange={v => {
+ setStates('type', v);
+ setPage(0);
+ }}
+ />
+
+
+
+ {
+ setStates('status', v);
+ setPage(0);
+ }}
+ />
+
+
+ {isAdmin ? (
+
+ {
+ setStates('sort', v);
+ setPage(0);
+ }}
+ />
+
+ ) : null}
+
+ {filteredFeedback.slice(page * ROWS_PER_PAGE, (page + 1) * ROWS_PER_PAGE).map(feedback => (
+
+ ))}
+ {filteredFeedback.length > 0 ? (
+
setPage(page)}
+ pages={Math.ceil(filteredFeedback.length / ROWS_PER_PAGE)}
+ />
+ ) : (
+
+ {typeFilter !== 'any' || statusFilter !== 'any' ? (
+ 'No feedback matches these criteria.'
+ ) : (
+ <>
+ You haven‘t submitted any feedback yet!{' '}
+
+ Review your recent events.
+
+ >
+ )}
+
+ )}
+
+ );
+};
+
+export default FeedbackPage;
+
+const getServerSidePropsFunc: GetServerSideProps = async ({ req, res, query }) => {
+ const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
+ const feedback = await FeedbackAPI.getFeedback(token);
+
+ const { type, status, sort } = query;
+
+ const initialFilters: FilterOptions = {
+ type:
+ type === 'any' || (typeof type === 'string' && isEnum(FeedbackType, type))
+ ? type
+ : DEFAULT_FILTER_STATE.type,
+ status:
+ status === 'any' || (typeof status === 'string' && isEnum(FeedbackStatus, status))
+ ? status
+ : DEFAULT_FILTER_STATE.status,
+ sort: sort === 'chronological' || sort === 'submitted-first' ? sort : DEFAULT_FILTER_STATE.sort,
+ };
+
+ return { props: { title: 'Feedback Submissions', feedback, token, initialFilters } };
+};
+
+export const getServerSideProps = withAccessType(
+ getServerSidePropsFunc,
+ PermissionService.loggedInUser
+);
diff --git a/src/pages/forgot-password.tsx b/src/pages/forgot-password.tsx
index c6d071b1..4395d56a 100644
--- a/src/pages/forgot-password.tsx
+++ b/src/pages/forgot-password.tsx
@@ -4,8 +4,8 @@ import { config, showToast } from '@/lib';
import { AuthManager } from '@/lib/managers';
import { ValidationService } from '@/lib/services';
import type { SendPasswordResetEmailRequest } from '@/lib/types/apiRequests';
-import { getMessagesFromError } from '@/lib/utils';
-import type { NextPage } from 'next';
+import { reportError } from '@/lib/utils';
+import type { GetServerSideProps, NextPage } from 'next';
import { useRouter } from 'next/router';
import { SubmitHandler, useForm } from 'react-hook-form';
import { AiOutlineMail } from 'react-icons/ai';
@@ -27,7 +27,7 @@ const ForgotPassword: NextPage = () => {
showToast('Success! Check your email shortly', `Email has been sent to ${email}`);
},
onFailCallback: error => {
- showToast('Error with email!', getMessagesFromError(error)[0]);
+ reportError('Error with email!', error);
},
});
};
@@ -60,3 +60,7 @@ const ForgotPassword: NextPage = () => {
};
export default ForgotPassword;
+
+export const getServerSideProps: GetServerSideProps = async () => ({
+ props: { title: 'Forgot Password' },
+});
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 568b7d36..211e97b5 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,53 +1,51 @@
-import { EventCarousel } from '@/components/events';
-import Hero from '@/components/home/Hero';
+import { Typography } from '@/components/common';
+import { CheckInModal, EventCarousel } from '@/components/events';
+import { UserProgress } from '@/components/profile/UserProgress';
import { config, showToast } from '@/lib';
import { EventAPI, UserAPI } from '@/lib/api';
import withAccessType from '@/lib/hoc/withAccessType';
import { attendEvent } from '@/lib/managers/EventManager';
import { CookieService, PermissionService } from '@/lib/services';
-import type {
- CustomErrorBody,
- PrivateProfile,
- PublicAttendance,
- PublicEvent,
-} from '@/lib/types/apiResponses';
+import type { PrivateProfile, PublicAttendance, PublicEvent } from '@/lib/types/apiResponses';
import { CookieType } from '@/lib/types/enums';
+import RaccoonGraphic from '@/public/assets/graphics/portal/raccoon-hero.svg';
+import WavesGraphic from '@/public/assets/graphics/portal/waves.svg';
+import CheckMark from '@/public/assets/icons/check-mark.svg';
import styles from '@/styles/pages/Home.module.scss';
import { GetServerSideProps } from 'next';
+import Link from 'next/link';
import { useEffect, useState } from 'react';
interface HomePageProps {
user: PrivateProfile;
- pastEvents: PublicEvent[];
+ attendedEvents: PublicEvent[];
upcomingEvents: PublicEvent[];
- liveEvents: PublicEvent[];
attendances: PublicAttendance[];
- checkInResponse: PublicEvent | CustomErrorBody | null;
+ checkInResponse: PublicEvent | { error: string } | null;
}
const processCheckInResponse = (
- response: PublicEvent | CustomErrorBody
+ response: PublicEvent | { error: string }
): PublicEvent | undefined => {
if ('uuid' in response) {
// If the response contains a uuid, the response is a PublicEvent.
- const title = `Checked in to ${response.title}!`;
- const subtitle = `Thanks for checking in! You earned ${response.pointValue} points.`;
- showToast(title, subtitle);
return response;
}
- showToast('Unable to checkin!', response.message);
+ showToast('Unable to checkin!', response.error);
return undefined;
};
const PortalHomePage = ({
user,
- pastEvents,
+ attendedEvents,
upcomingEvents,
- liveEvents,
attendances,
checkInResponse,
}: HomePageProps) => {
const [points, setPoints] = useState
(user.points);
+ const [checkinEvent, setCheckinEvent] = useState(undefined);
+ const [checkinModalVisible, setCheckinModalVisible] = useState(false);
+ const [checkinCode, setCheckinCode] = useState('');
const [attendance, setAttendance] = useState(attendances);
const checkin = async (attendanceCode: string): Promise => {
@@ -61,11 +59,13 @@ const PortalHomePage = ({
const newAttendance: PublicAttendance = {
user: user,
event,
- timestamp: new Date(),
+ timestamp: new Date().toString(),
asStaff: false,
feedback: [],
};
setAttendance(prevAttendances => [...prevAttendances, newAttendance]);
+ setCheckinEvent(event);
+ setCheckinModalVisible(true);
}
};
@@ -73,42 +73,100 @@ const PortalHomePage = ({
if (checkInResponse) {
// In dev mode, this runs twice because of reactStrictMode in nextConfig.
// This will only be run once in prod or deployment.
- processCheckInResponse(checkInResponse);
- // Clear the query params without re-triggering getServerSideProps.
- window.history.replaceState(null, '', config.homeRoute);
+ const event = processCheckInResponse(checkInResponse);
+ if (event) {
+ setCheckinEvent(event);
+ setCheckinModalVisible(true);
+ }
}
- }, [checkInResponse]);
+ }, [checkInResponse, user]);
+
+ const today = new Date().toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
return (
-
checkin(code)} />
-
- {liveEvents.length > 0 && (
-
- )}
-
- {upcomingEvents.length > 0 && (
- setCheckinModalVisible(false)}
+ />
+
+
+
- )}
-
- {pastEvents.length > 0 && (
-
- )}
+
+
+
+
+
+ {today}
+
+
+ {'Welcome to ACM, '}
+
+ {user.firstName}
+
+ !
+
+
+
+
+
+
+
+
+
+
+
+
);
};
@@ -126,8 +184,8 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ req, res, query }) =
// After that, fetch the other API calls.
const eventsPromise = EventAPI.getAllEvents();
- const attendancesPromise = EventAPI.getAttendancesForUser(authToken);
- const userPromise = UserAPI.getCurrentUser(authToken);
+ const attendancesPromise = UserAPI.getAttendancesForCurrentUser(authToken);
+ const userPromise = UserAPI.getCurrentUserAndRefreshCookie(authToken, { req, res });
const [events, attendances, user] = await Promise.all([
eventsPromise,
@@ -137,28 +195,24 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ req, res, query }) =
// Filter out events by time.
const now = new Date();
- const pastEvents: PublicEvent[] = [];
+ const attendedEvents: PublicEvent[] = [];
const upcomingEvents: PublicEvent[] = [];
- const liveEvents: PublicEvent[] = [];
events.forEach(e => {
- const start = new Date(e.start);
const end = new Date(e.end);
- if (end < now) {
- pastEvents.push(e);
- } else if (start > now) {
+ if (attendances.some(a => a.event.uuid === e.uuid)) {
+ attendedEvents.push(e);
+ }
+ if (end >= now) {
upcomingEvents.push(e);
- } else {
- liveEvents.push(e);
}
});
return {
props: {
user,
- pastEvents: pastEvents.slice(-10).reverse(),
+ attendedEvents: attendedEvents.slice(-10).reverse(),
upcomingEvents: upcomingEvents.slice(0, 10),
- liveEvents,
attendances,
checkInResponse: checkInResponse,
},
@@ -167,5 +221,5 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ req, res, query }) =
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
- PermissionService.allUserTypes()
+ PermissionService.loggedInUser
);
diff --git a/src/pages/leaderboard.tsx b/src/pages/leaderboard.tsx
index 592b6bb1..f2354621 100644
--- a/src/pages/leaderboard.tsx
+++ b/src/pages/leaderboard.tsx
@@ -1,4 +1,5 @@
import { Dropdown, PaginationControls } from '@/components/common';
+import { DIVIDER } from '@/components/common/Dropdown';
import { LeaderboardRow, TopThreeCard } from '@/components/leaderboard';
import { config } from '@/lib';
import { LeaderboardAPI } from '@/lib/api';
@@ -106,7 +107,7 @@ const LeaderboardPage = ({ sort, leaderboard, user: { uuid } }: LeaderboardProps
{ value: 'past-month', label: 'Past month' },
{ value: 'past-year', label: 'Past year' },
{ value: 'all-time', label: 'All time' },
- '---',
+ DIVIDER,
...years,
]}
value={sort}
@@ -115,15 +116,16 @@ const LeaderboardPage = ({ sort, leaderboard, user: { uuid } }: LeaderboardProps
setPage(0);
setScrollIntoView(0);
}}
+ className={styles.timeDropdown}
/>
- {topThreeUsers.length > 0 && (
+ {topThreeUsers.length > 0 ? (
- )}
- {leaderboardRows.length > 0 && (
+ ) : null}
+ {leaderboardRows.length > 0 ? (
@@ -551,7 +555,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => {
@@ -576,12 +580,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => {
reportError('Photo failed to upload', error);
}
}}
- onClose={reason => {
- setPfp(null);
- if (reason !== null) {
- showToast('This image format is not supported.');
- }
- }}
+ onClose={() => setPfp(null)}
/>