From 26952eab9a2b0b57904cec01a9bb36bb172eca04 Mon Sep 17 00:00:00 2001
From: Sean
Date: Fri, 29 Mar 2024 14:30:43 -0700
Subject: [PATCH 01/34] feedback API
---
src/lib/api/FeedbackAPI.ts | 82 ++++++++++++++++++++++++++++++++++++++
src/lib/api/index.ts | 1 +
src/lib/config.ts | 1 +
3 files changed, 84 insertions(+)
create mode 100644 src/lib/api/FeedbackAPI.ts
diff --git a/src/lib/api/FeedbackAPI.ts b/src/lib/api/FeedbackAPI.ts
new file mode 100644
index 00000000..088adefd
--- /dev/null
+++ b/src/lib/api/FeedbackAPI.ts
@@ -0,0 +1,82 @@
+import config from '@/lib/config';
+import { SubmitFeedbackRequest, UpdateFeedbackStatusRequest } from '@/lib/types/apiRequests';
+import {
+ GetFeedbackResponse,
+ PublicFeedback,
+ SubmitFeedbackResponse,
+ UpdateFeedbackStatusResponse,
+} from '@/lib/types/apiResponses';
+import { FeedbackStatus, FeedbackType } from '@/lib/types/enums';
+import axios from 'axios';
+
+/**
+ * Submit feedback
+ * @param token Bearer token. Authenticated user must strictly be in the
+ * `ACTIVE` state (not `PASSWORD_RESET`)
+ * @param title Title of feedback
+ * @param description Description of feedback. Must be at least 100 characters
+ * long.
+ * @param type Type of ACM offering that the feedback is addressed to.
+ * @returns The submitted feedback
+ */
+export const addFeedback = async (
+ token: string,
+ title: string,
+ description: string,
+ type: FeedbackType
+): Promise => {
+ const requestUrl = `${config.api.baseUrl}${config.api.endpoints.feedback}`;
+
+ const requestBody: SubmitFeedbackRequest = { feedback: { title, description, type } };
+
+ const response = await axios.post(requestUrl, requestBody, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data.feedback;
+};
+
+/**
+ * Get all feedback submitted by user, or by all users if current user is an
+ * admin.
+ * @param token Bearer token
+ * @returns List of submitted feedback
+ */
+export const getFeedback = async (token: string): Promise => {
+ const requestUrl = `${config.api.baseUrl}${config.api.endpoints.feedback}`;
+
+ const response = await axios.get(requestUrl, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data.feedback;
+};
+
+/**
+ * Set status (e.g. acknowledged, ignored) of feedback
+ * @param token Bearer token
+ * @param uuid Feedback ID
+ * @param status Status to set feedback to
+ * @returns The updated feedback object
+ */
+export const respondToFeedback = async (
+ token: string,
+ uuid: string,
+ status: FeedbackStatus
+): Promise => {
+ const requestUrl = `${config.api.baseUrl}${config.api.endpoints.feedback}/${uuid}`;
+
+ const requestBody: UpdateFeedbackStatusRequest = { status };
+
+ const response = await axios.post(requestUrl, requestBody, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ return response.data.feedback;
+};
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index d0d44c46..91b6d28e 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -1,5 +1,6 @@
export * as AuthAPI from './AuthAPI';
export * as EventAPI from './EventAPI';
+export * as FeedbackAPI from './FeedbackAPI';
export * as KlefkiAPI from './KlefkiAPI';
export * as LeaderboardAPI from './LeaderboardAPI';
export * as ResumeAPI from './ResumeAPI';
diff --git a/src/lib/config.ts b/src/lib/config.ts
index eb8031c9..71a5249d 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -37,6 +37,7 @@ const config = {
attendance: '/attendance',
forUserByUUID: '/attendance/user',
},
+ feedback: '/feedback',
leaderboard: '/leaderboard',
store: {
collection: '/merch/collection',
From fe1ffcb06538b5f728812f9f1b89a5d005531cce Mon Sep 17 00:00:00 2001
From: Sean
Date: Fri, 29 Mar 2024 21:28:57 -0700
Subject: [PATCH 02/34] Feedback form
---
src/components/events/FeedbackForm/index.tsx | 72 +++++++++++++++++++
.../events/FeedbackForm/style.module.scss | 20 ++++++
.../FeedbackForm/style.module.scss.d.ts | 10 +++
src/lib/utils.ts | 21 ++++--
4 files changed, 118 insertions(+), 5 deletions(-)
create mode 100644 src/components/events/FeedbackForm/index.tsx
create mode 100644 src/components/events/FeedbackForm/style.module.scss
create mode 100644 src/components/events/FeedbackForm/style.module.scss.d.ts
diff --git a/src/components/events/FeedbackForm/index.tsx b/src/components/events/FeedbackForm/index.tsx
new file mode 100644
index 00000000..9cdaa46b
--- /dev/null
+++ b/src/components/events/FeedbackForm/index.tsx
@@ -0,0 +1,72 @@
+import { Dropdown, Typography } from '@/components/common';
+import { FeedbackAPI } from '@/lib/api';
+import { FeedbackType } from '@/lib/types/enums';
+import { isEnum } from '@/lib/utils';
+import { useState } from 'react';
+import styles from './style.module.scss';
+
+const feedbackTypeNames: Record = {
+ GENERAL: 'ACM',
+ MERCH_STORE: 'Store',
+ BIT_BYTE: 'Bit-Byte Program',
+ AI: 'ACM AI',
+ CYBER: 'ACM Cyber',
+ DESIGN: 'ACM Design',
+ HACK: 'ACM Hack',
+ INNOVATE: 'ACM Innovate',
+};
+
+interface FeedbackFormProps {
+ authToken: string;
+}
+
+const FeedbackForm = ({ authToken }: FeedbackFormProps) => {
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [type, setType] = useState(FeedbackType.GENERAL);
+
+ return (
+
+ );
+};
+
+export default FeedbackForm;
diff --git a/src/components/events/FeedbackForm/style.module.scss b/src/components/events/FeedbackForm/style.module.scss
new file mode 100644
index 00000000..590cad57
--- /dev/null
+++ b/src/components/events/FeedbackForm/style.module.scss
@@ -0,0 +1,20 @@
+@use 'src/styles/vars.scss' as vars;
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ .field {
+ border: 2px solid currentColor;
+ border-radius: 0.75rem;
+ font-size: 1rem;
+ padding: 0.5rem 1rem;
+
+ &:is(textarea) {
+ min-height: 10rem;
+ padding: 1rem;
+ resize: vertical;
+ }
+ }
+}
diff --git a/src/components/events/FeedbackForm/style.module.scss.d.ts b/src/components/events/FeedbackForm/style.module.scss.d.ts
new file mode 100644
index 00000000..cb5301ec
--- /dev/null
+++ b/src/components/events/FeedbackForm/style.module.scss.d.ts
@@ -0,0 +1,10 @@
+export type Styles = {
+ field: string;
+ form: string;
+};
+
+export type ClassNames = keyof Styles;
+
+declare const styles: Styles;
+
+export default styles;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 55763694..da067acb 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -404,13 +404,24 @@ export const getOrderItemQuantities = (items: PublicOrderItem[]): PublicOrderIte
return Array.from(itemMap.values());
};
-/** Normalizes string as a capitalized community name. Defaults to General. */
-export const toCommunity = (community = ''): Community => {
- const formattedName = capitalize(community) as Community;
+export function isEnum>(
+ Enum: T,
+ value: string
+): value is T[keyof T] {
+ return Object.values(Enum).includes(value);
+}
- if (Object.values(Community).includes(formattedName)) return formattedName;
+export function stringToEnum>(
+ Enum: T,
+ value: string
+): T[keyof T] | null {
+ if (isEnum(Enum, value)) return value;
- return Community.GENERAL;
+ return null;
+}
+
+export const toCommunity = (community = ''): Community => {
+ return stringToEnum(Community, capitalize(community)) ?? Community.GENERAL;
};
/**
From 225dbb5f0d651f182abfe1e63fdbe57f66adc8b6 Mon Sep 17 00:00:00 2001
From: Sean
Date: Fri, 29 Mar 2024 21:40:22 -0700
Subject: [PATCH 03/34] Create a separate event page:
1. Can open multiple events in a new tab
2. Can't have a feedback
setTitle(e.currentTarget.value)}
+ aria-label="Feedback source"
+ placeholder="Source"
+ value={source}
+ onChange={e => setSource(e.currentTarget.value)}
className={styles.field}
/>