From 29c902c435d88a78b6a49aa746d56b52b7a7291c Mon Sep 17 00:00:00 2001 From: Yifei Fang Date: Thu, 3 Apr 2025 16:16:54 -0500 Subject: [PATCH 01/28] set up sendgrid --- backend/config/sendgrid.ts | 24 ++++++++++++++++ backend/package-lock.json | 59 +++++++++++++++++++++++++++++++++----- backend/package.json | 1 + 3 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 backend/config/sendgrid.ts diff --git a/backend/config/sendgrid.ts b/backend/config/sendgrid.ts new file mode 100644 index 0000000..3dcf81d --- /dev/null +++ b/backend/config/sendgrid.ts @@ -0,0 +1,24 @@ +import sgMail from "@sendgrid/mail"; + +sgMail.setApiKey(process.env.SENDGRID_API_KEY!); + +export const sendEmail = async ( + to: string | string[], + templateId: string, + dynamicTemplateData: Record +) => { + const msg = { + to, + from: "fostersourcedevs@gmail.com", // Must be a verified sender + templateId, + dynamic_template_data: dynamicTemplateData, + }; + + try { + await sgMail.send(msg); + console.log("Email sent"); + } catch (error: any) { + console.error(error.response?.body || error); + throw new Error("Failed to send email"); + } +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index 5f61e49..235f751 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,6 +8,7 @@ "name": "backend", "version": "1.0.0", "dependencies": { + "@sendgrid/mail": "^8.1.4", "backend": "file:", "cloudinary": "^1.41.3", "cors": "^2.8.5", @@ -1686,6 +1687,41 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@sendgrid/client": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.4.tgz", + "integrity": "sha512-VxZoQ82MpxmjSXLR3ZAE2OWxvQIW2k2G24UeRPr/SYX8HqWLV/8UBN15T2WmjjnEb5XSmFImTJOKDzzSeKr9YQ==", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.7.4" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.4.tgz", + "integrity": "sha512-MUpIZykD9ARie8LElYCqbcBhGGMaA/E6I7fEcG7Hc2An26QJyLtwOaKQ3taGp8xO8BICPJrSKuYV4bDeAJKFGQ==", + "dependencies": { + "@sendgrid/client": "^8.1.4", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2258,8 +2294,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "devOptional": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/b4a": { "version": "1.6.7", @@ -2743,7 +2788,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "devOptional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2957,7 +3001,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2982,7 +3025,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -3611,7 +3653,6 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "dev": true, "funding": [ { "type": "individual", @@ -3632,7 +3673,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6249,6 +6289,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/backend/package.json b/backend/package.json index 8d1f556..94a8de6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,7 @@ "format:check": "prettier --check ." }, "dependencies": { + "@sendgrid/mail": "^8.1.4", "backend": "file:", "cloudinary": "^1.41.3", "cors": "^2.8.5", From b4e17f994e1b8d87d8ad61ba835330c11fb30bca Mon Sep 17 00:00:00 2001 From: Jonathan Kim <113380851+jkim-21@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:46:34 -0500 Subject: [PATCH 02/28] Implement user type frontend page --- .../Admin/UserTypesPage/UserTypesPage.tsx | 77 +++++++++++++++++++ frontend/src/routes/appRoutes.tsx | 2 + 2 files changed, 79 insertions(+) create mode 100644 frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx new file mode 100644 index 0000000..0b11c79 --- /dev/null +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -0,0 +1,77 @@ +// frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx + +import React, { useEffect, useState } from "react"; +import apiClient from "../../../services/apiClient"; +import { Pencil, Trash2 } from "lucide-react"; + +interface UserType { + _id: string; + name: string; + userCount: number; +} + +export default function UserTypesPage() { + const [userTypes, setUserTypes] = useState([]); + + useEffect(() => { + const fetchUserTypes = async () => { + try { + const res = await apiClient.get("/user-types"); + setUserTypes(res.data); + } catch (err) { + console.error("Error fetching user types:", err); + } + }; + + fetchUserTypes(); + }, []); + + return ( +
+
+
+

User Types

+ +
+ + + + + + + + + + + {userTypes.length === 0 ? ( + + + + ) : ( + userTypes.map((ut) => ( + + + + + + )) + )} + +
TypeNumber of UsersActions
+ No user types found. +
{ut.name}{ut.userCount} + + +
+
+
+ ); +} diff --git a/frontend/src/routes/appRoutes.tsx b/frontend/src/routes/appRoutes.tsx index 0bc9563..a0bd57e 100644 --- a/frontend/src/routes/appRoutes.tsx +++ b/frontend/src/routes/appRoutes.tsx @@ -38,6 +38,7 @@ import Registrants from "../pages/Admin/NewProductPage/Registrants"; import SurveySummary from "../pages/Admin/SurveySummaryPage/SurveySummary"; import CourseManagerPage from "../pages/Admin/CourseManagerPage/CourseManagerPage"; import UserManagementPage from "../pages/Admin/UserManagementPage/Users"; +import UserTypesPage from "../pages/Admin/UserTypesPage/UserTypesPage"; // import AdminPage from "../pages/Admin/AdminPage"; function AppRoutes() { @@ -202,6 +203,7 @@ function AppRoutes() { } /> } /> } /> + } /> } /> Date: Mon, 7 Apr 2025 13:52:50 -0500 Subject: [PATCH 03/28] Format table to be similar to all users page --- frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx index 0b11c79..310a06b 100644 --- a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -27,11 +27,11 @@ export default function UserTypesPage() { }, []); return ( -
-
+
+

User Types

-
From 483152f97f390682aaf833249c8abc71b67ad78b Mon Sep 17 00:00:00 2001 From: theannachen Date: Mon, 14 Apr 2025 15:16:49 -0500 Subject: [PATCH 04/28] existing meetings work --- backend/app.ts | 2 + backend/controllers/zoomController.ts | 129 ++++++++++++++++++ backend/routes/zoomRoutes.ts | 15 ++ .../pages/Admin/WorkshopCreation/Meeting.tsx | 46 +++++++ .../ModalComponents/ExistingMeetingList.tsx | 61 +++++++++ .../pages/Admin/WorkshopCreation/Webinar.tsx | 7 +- .../WorkshopCreation/WorkshopCreation.tsx | 65 ++++++++- 7 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 backend/controllers/zoomController.ts create mode 100644 backend/routes/zoomRoutes.ts create mode 100644 frontend/src/pages/Admin/WorkshopCreation/Meeting.tsx create mode 100644 frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx diff --git a/backend/app.ts b/backend/app.ts index 132f85b..2084adb 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -29,6 +29,7 @@ import upload from "./middlewares/upload"; import { uploadImage } from "./controllers/uploadController"; import uploadRoutes from "./routes/uploadRoutes"; import dotenv from "dotenv"; +import zoomRoutes from "./routes/zoomRoutes"; dotenv.config(); const app: Application = express(); @@ -89,6 +90,7 @@ app.use("/api/handout", verifyFirebaseAuth, handoutRoutes); app.use('/api/emails', verifyFirebaseAuth, emailRoutes); app.use("/api/speakers", verifyFirebaseAuth, speakerRoutes); app.use("/api/upload", verifyFirebaseAuth, uploadRoutes); +app.use("/api/zoom", verifyFirebaseAuth, zoomRoutes) // Error middleware app.use((err: any, req: Request, res: Response, next: NextFunction): void => { diff --git a/backend/controllers/zoomController.ts b/backend/controllers/zoomController.ts new file mode 100644 index 0000000..5534964 --- /dev/null +++ b/backend/controllers/zoomController.ts @@ -0,0 +1,129 @@ +import {Request, Response} from "express"; +import dotenv from "dotenv"; +dotenv.config(); +// @desc Get a bearer token +// @route GET /api/zoom/token +// @access Public +async function getToken(){ + try { + const credentials = btoa(`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`); + + const token = await fetch(`https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`, { + method: "POST", + headers: { + "Authorization": `Basic ${credentials}`, + "Content-Type": "application/x-www-form-urlencoded" + } + }) + return (await token.json()).access_token + } catch (error) { + console.error(error); + return undefined + } +} + +export const getMeetings = async ( + req: Request, + res: Response +): Promise => { + try { + await getToken().then(async (token) => { + const response = await fetch(`https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/meetings`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/x-www-form-urlencoded", + } + }) + let meetings = await response.json() + res.status(200).json(meetings) + }) + } catch (error) { + console.error(error); + res.status(500).json({ + success: false, + message: "Internal service error", + }); + } +}; + +export const getWebinars = async ( + req: Request, + res: Response +): Promise => { + try { + await getToken().then(async (token) => { + const webinars = await fetch(`https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/webinars`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/x-www-form-urlencoded", + } + }) + console.log(await webinars.json()) + res.status(200).json({ + webinars: webinars + }) + }) + } catch (error) { + console.error(error); + res.status(500).json({ + success: false, + message: "Internal service error", + }); + } +}; + +export const createMeeting = async ( + req: Request, + res: Response +): Promise => { + try { + await getToken().then(async (token) => { + const meetings = await fetch(`https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/meetings`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/x-www-form-urlencoded", + } + }) + console.log(await meetings.json()) + res.status(200).json({ + meetings: meetings + }) + }) + } catch (error) { + console.error(error); + res.status(500).json({ + success: false, + message: "Internal service error", + }); + } +}; + +export const createWebinar = async ( + req: Request, + res: Response +): Promise => { + try { + await getToken().then(async (token) => { + const meetings = await fetch(`https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/meetings`, { + method: "GET", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/x-www-form-urlencoded", + } + }) + console.log(await meetings.json()) + res.status(200).json({ + meetings: meetings + }) + }) + } catch (error) { + console.error(error); + res.status(500).json({ + success: false, + message: "Internal service error", + }); + } +}; \ No newline at end of file diff --git a/backend/routes/zoomRoutes.ts b/backend/routes/zoomRoutes.ts new file mode 100644 index 0000000..e4cbaae --- /dev/null +++ b/backend/routes/zoomRoutes.ts @@ -0,0 +1,15 @@ +import express from "express"; +import { + createMeeting, createWebinar, + getMeetings, getWebinars +} from "../controllers/zoomController"; + +const router = express.Router(); + +router.get("/meetings", getMeetings); +router.get("/webinars", getWebinars); + +router.post("/meeting", createMeeting) +router.post("/webinar", createWebinar) + +export default router; diff --git a/frontend/src/pages/Admin/WorkshopCreation/Meeting.tsx b/frontend/src/pages/Admin/WorkshopCreation/Meeting.tsx new file mode 100644 index 0000000..0eefa9f --- /dev/null +++ b/frontend/src/pages/Admin/WorkshopCreation/Meeting.tsx @@ -0,0 +1,46 @@ +import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; +import apiClient from "../../../services/apiClient"; + +interface MeetingComponentProps { + meetingData: any; + setMeetingData: Dispatch>; + openModal: string | null; + setOpenModal: Dispatch>; +} + +export default function MeetingComponent({ + meetingData, + setMeetingData, + openModal, + setOpenModal, + }: MeetingComponentProps) { + return ( +
+ {meetingData.meetingID !== "string" && ( +
+

Selected Meeting:

+

+ {meetingData.topic} -{" "} + {new Date(meetingData.startTime).toLocaleString()} +

+
+ )} +
+ + +
+
+ ); +} diff --git a/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx new file mode 100644 index 0000000..9292676 --- /dev/null +++ b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx @@ -0,0 +1,61 @@ +import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; +import apiClient from "../../../../services/apiClient"; + +interface ExistingMeetingListProps { + setMeetingData: Dispatch>; + setOpenModal: Dispatch>; +} + +export default function ExistingMeetingList({ + setMeetingData, + setOpenModal, + }: ExistingMeetingListProps) { + const [meetings, setMeetings] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchMeetings() { + try { + const res = await apiClient.get("zoom/meetings"); + setMeetings(res.data.meetings); + } catch (err) { + console.error("Failed to fetch meetings", err); + } finally { + setLoading(false); + } + } + fetchMeetings(); + }, []); + + if (loading) return

Loading meetings...

; + + return ( +
+ {meetings.map((meeting) => ( +
{ + setMeetingData({ + meetingID: meeting.id.toString(), + startTime: meeting.start_time, + duration: meeting.duration, + serviceType: "Zoom", + authParticipants: false, + autoRecord: false, + enablePractice: false, + topic: meeting.topic, + join_url: meeting.join_url, + }); + setOpenModal(null); + }} + > +

{meeting.topic}

+

+ {new Date(meeting.start_time).toLocaleString()} +

+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Admin/WorkshopCreation/Webinar.tsx b/frontend/src/pages/Admin/WorkshopCreation/Webinar.tsx index 1bbcbdd..d00733c 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/Webinar.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/Webinar.tsx @@ -1,8 +1,7 @@ import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; -import {WebinarType} from "../../../shared/types/Webinar"; interface WebinarComponentProps { - webinarData:WebinarType; + webinarData:any; setWebinarData: Dispatch>; openModal:string|null; setOpenModal: Dispatch>; @@ -12,7 +11,7 @@ export default function WebinarComponent( ) { return (
- - + +
) } \ No newline at end of file diff --git a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx index 52cc9bb..a7ee1f9 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx @@ -3,6 +3,8 @@ import WebinarComponent from "./Webinar"; import InPersonComponent from "./InPerson"; import OnDemandComponent from "./OnDemand"; import Modal from "./Modal"; +import MeetingComponent from "./Meeting"; +import ExistingMeetingList from "./ModalComponents/ExistingMeetingList"; interface WorkshopCreationProps { workshopName: string; @@ -13,7 +15,7 @@ export default function WorkshopCreation({ const [formData, setFormData] = useState({ title: "", summary: "", - type: "webinar", + type: "meeting", audioInstructions: "", markAttendance: false, requireAttendance: false, @@ -33,6 +35,16 @@ export default function WorkshopCreation({ enablePractice: false, }); + const [meetingData, setMeetingData] = useState({ + serviceType: "", + meetingID: "string", + startTime: new Date(), + duration: 0, + authParticipants: false, + autoRecord: false, + enablePractice: false, + }); + const [inPersonData, setInPersonData] = useState({ startTime: null, duration: 0, @@ -43,7 +55,7 @@ export default function WorkshopCreation({ embeddingLink: "", }); - const [openModal, setOpenModal] = useState<"New" | "Existing" | null>(null); + const [openModal, setOpenModal] = useState<"NewWebinar" | "ExistingWebinar" | "NewMeeting" | "ExistingMeeting" | null>(null); const handleChange = (e: any) => { const { name, value } = e.target; @@ -85,6 +97,17 @@ export default function WorkshopCreation({
+
)} diff --git a/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx b/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx index 99bf39a..825557c 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx @@ -26,16 +26,6 @@ const Modal: React.FC = ({ isOpen, onClose, title, children }) => { {/* Content */}
{children}
- - {/* Footer */} -
- -
); diff --git a/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx index 9292676..ad0ef88 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingMeetingList.tsx @@ -38,7 +38,7 @@ export default function ExistingMeetingList({ onClick={() => { setMeetingData({ meetingID: meeting.id.toString(), - startTime: meeting.start_time, + start_time: meeting.start_time, duration: meeting.duration, serviceType: "Zoom", authParticipants: false, diff --git a/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/NewMeeting.tsx b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/NewMeeting.tsx new file mode 100644 index 0000000..0cadba5 --- /dev/null +++ b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/NewMeeting.tsx @@ -0,0 +1,104 @@ +import {Dispatch, SetStateAction, useState} from 'react'; +import apiClient from "../../../../services/apiClient"; +interface NewMeetingProps { + setMeetingData: Dispatch>; + setOpenModal: Dispatch>; +} + +export default function NewMeeting({ setMeetingData, setOpenModal }:NewMeetingProps) { + const [topic, setTopic] = useState(''); + const [startTime, setStartTime] = useState(''); + const [duration, setDuration] = useState(60); + const [loading, setLoading] = useState(false); + + const currentDateTime = new Date(); + const minDateTime = currentDateTime.toISOString().slice(0, 16); + + const isValidStartTime = startTime && new Date(startTime) >= currentDateTime; + + const createMeeting = async () => { + setLoading(true); + try { + const meeting = (await apiClient.post("zoom/meeting", { + topic, + start_time: new Date(startTime).toISOString(), + duration: duration + })).data.meeting + setMeetingData({meetingID: meeting.id.toString(), + start_time: meeting.start_time, + duration: meeting.duration, + serviceType: "Zoom", + authParticipants: false, + autoRecord: false, + enablePractice: false, + topic: meeting.topic, + join_url: meeting.join_url,}); + setOpenModal(false); + } catch (err) { + console.error(err) + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + setTopic(e.target.value)} + required + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2" + /> +

This is the title your attendees will see.

+
+ +
+ + setStartTime(e.target.value)} + required + min={minDateTime} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" + /> +
+ +
+ + setDuration(parseInt(e.target.value))} + required + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2" + /> +
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx index a7ee1f9..91d78b8 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx @@ -5,6 +5,7 @@ import OnDemandComponent from "./OnDemand"; import Modal from "./Modal"; import MeetingComponent from "./Meeting"; import ExistingMeetingList from "./ModalComponents/ExistingMeetingList"; +import NewMeeting from "./ModalComponents/NewMeeting"; interface WorkshopCreationProps { workshopName: string; @@ -313,9 +314,10 @@ export default function WorkshopCreation({ onClose={() => setOpenModal(null)} title="Create New Meeting" > -

- This is the placeholder for adding a new meeting (once we get zoom). -

+ {/* Existing Meeting Modal */} From 5569eae720ff23c814dcb8b7f4dfd5ba30011b97 Mon Sep 17 00:00:00 2001 From: theannachen Date: Mon, 14 Apr 2025 19:29:01 -0500 Subject: [PATCH 06/28] webinars also work noww --- backend/controllers/zoomController.ts | 30 +++-- .../pages/Admin/WorkshopCreation/Modal.tsx | 4 +- .../ModalComponents/ExistingWebinarList.tsx | 62 +++++++++++ .../ModalComponents/NewWebinar.tsx | 104 ++++++++++++++++++ .../WorkshopCreation/WorkshopCreation.tsx | 14 ++- 5 files changed, 193 insertions(+), 21 deletions(-) create mode 100644 frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingWebinarList.tsx create mode 100644 frontend/src/pages/Admin/WorkshopCreation/ModalComponents/NewWebinar.tsx diff --git a/backend/controllers/zoomController.ts b/backend/controllers/zoomController.ts index f8fbe7a..0b7fd5f 100644 --- a/backend/controllers/zoomController.ts +++ b/backend/controllers/zoomController.ts @@ -53,14 +53,15 @@ export const getWebinars = async ( ): Promise => { try { await getToken().then(async (token) => { - const webinars = await fetch(`https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/webinars`, { + + const response = await fetch(`https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/webinars`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/x-www-form-urlencoded", } }) - console.log(await webinars.json()) + let webinars = (await response.json()).webinars res.status(200).json({ webinars: webinars }) @@ -115,29 +116,26 @@ export const createWebinar = async ( req: Request, res: Response ): Promise => { + const { topic, startTime, duration } = req.body; try { await getToken().then(async (token) => { - const response = await fetch('https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/meetings', { + const response = await fetch(`https://api.zoom.us/v2/users/${process.env.ZOOM_USER_ID}/webinars`, { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: token + "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ - topic: 'My Meeting', - type: 2, - start_time: '2022-03-25T07:32:55Z', - duration: 60, + topic, + start_time: startTime, + duration: duration }) }); - - if (!response.ok) { - const errorData = await response.json(); - console.error('Error creating meeting:', errorData); - } else { - const data = await response.json(); - console.log('Meeting created:', data); - } + let webinar = await response.json() + console.log(webinar) + res.status(200).json({ + webinar: webinar + }) }) } catch (error) { console.error(error); diff --git a/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx b/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx index 825557c..f8f02d2 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/Modal.tsx @@ -25,7 +25,9 @@ const Modal: React.FC = ({ isOpen, onClose, title, children }) => { {title &&

{title}

} {/* Content */} -
{children}
+
+ {children} +
); diff --git a/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingWebinarList.tsx b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingWebinarList.tsx new file mode 100644 index 0000000..48f3138 --- /dev/null +++ b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/ExistingWebinarList.tsx @@ -0,0 +1,62 @@ +import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; +import apiClient from "../../../../services/apiClient"; + +interface ExistingWebinarListProps { + setWebinarData: Dispatch>; + setOpenModal: Dispatch>; +} + +export default function ExistingWebinarList({ + setWebinarData, + setOpenModal, + }: ExistingWebinarListProps) { + const [webinars, setWebinars] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchWebinars() { + try { + const res = await apiClient.get("zoom/webinars"); + + setWebinars(res.data.webinars); + } catch (err) { + console.error("Failed to fetch webinars", err); + } finally { + setLoading(false); + } + } + fetchWebinars(); + }, []); + + if (loading) return

Loading meetings...

; + + return ( +
+ {webinars.map((webinar) => ( +
{ + setWebinarData({ + meetingID: webinar.id.toString(), + start_time: webinar.start_time, + duration: webinar.duration, + serviceType: "Zoom", + authParticipants: false, + autoRecord: false, + enablePractice: false, + topic: webinar.topic, + join_url: webinar.join_url, + }); + setOpenModal(null); + }} + > +

{webinar.topic}

+

+ {new Date(webinar.start_time).toLocaleString()} +

+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/NewWebinar.tsx b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/NewWebinar.tsx new file mode 100644 index 0000000..06c0815 --- /dev/null +++ b/frontend/src/pages/Admin/WorkshopCreation/ModalComponents/NewWebinar.tsx @@ -0,0 +1,104 @@ +import {Dispatch, SetStateAction, useState} from 'react'; +import apiClient from "../../../../services/apiClient"; +interface NewWebinarProps { + setWebinarData: Dispatch>; + setOpenModal: Dispatch>; +} + +export default function NewWebinar({ setWebinarData, setOpenModal }:NewWebinarProps) { + const [topic, setTopic] = useState(''); + const [startTime, setStartTime] = useState(''); + const [duration, setDuration] = useState(60); + const [loading, setLoading] = useState(false); + + const currentDateTime = new Date(); + const minDateTime = currentDateTime.toISOString().slice(0, 16); + + const isValidStartTime = startTime && new Date(startTime) >= currentDateTime; + + const createWebinar = async () => { + setLoading(true); + try { + const webinar = (await apiClient.post("zoom/webinar", { + topic, + start_time: new Date(startTime).toISOString(), + duration: duration + })).data.meeting + setWebinarData({webinarID: webinar.id.toString(), + start_time: webinar.start_time, + duration: webinar.duration, + serviceType: "Zoom", + authParticipants: false, + autoRecord: false, + enablePractice: false, + topic: webinar.topic, + join_url: webinar.join_url,}); + setOpenModal(false); + } catch (err) { + console.error(err) + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + setTopic(e.target.value)} + required + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2" + /> +

This is the title your attendees will see.

+
+ +
+ + setStartTime(e.target.value)} + required + min={minDateTime} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" + /> +
+ +
+ + setDuration(parseInt(e.target.value))} + required + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2" + /> +
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx index 91d78b8..7757fb6 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx @@ -6,6 +6,8 @@ import Modal from "./Modal"; import MeetingComponent from "./Meeting"; import ExistingMeetingList from "./ModalComponents/ExistingMeetingList"; import NewMeeting from "./ModalComponents/NewMeeting"; +import ExistingWebinarList from "./ModalComponents/ExistingWebinarList"; +import NewWebinar from "./ModalComponents/NewWebinar"; interface WorkshopCreationProps { workshopName: string; @@ -338,9 +340,10 @@ export default function WorkshopCreation({ onClose={() => setOpenModal(null)} title="Create New Webinar" > -

- This is the placeholder for adding a new webinar (once we get zoom). -

+ {/* Existing Webinar Modal */} @@ -349,7 +352,10 @@ export default function WorkshopCreation({ onClose={() => setOpenModal(null)} title="Add Existing Webinar" > -

This is placeholder for finding existing webinars.

+
); From 4b545faf4da7d6c463962da90853f1d710123467 Mon Sep 17 00:00:00 2001 From: Rachel Koh Date: Sun, 20 Apr 2025 16:36:02 -0500 Subject: [PATCH 07/28] checkpoint --- backend/package-lock.json | 2 +- .../pages/Admin/UserManagementPage/Users.tsx | 15 ++ package-lock.json | 205 ++++++++++++++++++ 3 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/backend/package-lock.json b/backend/package-lock.json index 5cf3ebb..aaa03ec 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,8 +20,8 @@ "multer": "^1.4.5-lts.1", "multer-storage-cloudinary": "^4.0.0", "paypal-rest-sdk": "^1.8.1", - "react-select-country-list": "^2.2.3", "pdfkit": "^0.16.0", + "react-select-country-list": "^2.2.3", "ts-node": "^10.9.2" }, "devDependencies": { diff --git a/frontend/src/pages/Admin/UserManagementPage/Users.tsx b/frontend/src/pages/Admin/UserManagementPage/Users.tsx index ecddeca..1e753d8 100644 --- a/frontend/src/pages/Admin/UserManagementPage/Users.tsx +++ b/frontend/src/pages/Admin/UserManagementPage/Users.tsx @@ -40,6 +40,7 @@ interface User { timezone?: string; language: "English" | "Spanish"; selected?: boolean; + certification?: string; } interface UserForm { @@ -56,6 +57,7 @@ interface UserForm { userType: string; timezone: string; language: "English" | "Spanish"; + certification?: string; } interface SpeakerProduct { @@ -113,6 +115,7 @@ const UserManagementPage: React.FC = () => { userType: "", timezone: "", language: "English", + certification: "", }); const [editingUserId, setEditingUserId] = useState(null); const [currentSpeaker, setCurrentSpeaker] = useState(null); @@ -155,6 +158,7 @@ const UserManagementPage: React.FC = () => { country: user.country || "", phoneNumber: user.phone || "", language: user.language || "English", + certification: user.certification || "", selected: false, })); @@ -199,6 +203,7 @@ const UserManagementPage: React.FC = () => { country: userData.country, phone: userData.phoneNumber, language: userData.language, + certification: userData.certification, }; }; @@ -229,6 +234,7 @@ const UserManagementPage: React.FC = () => { country: userForm.country, phoneNumber: userForm.phoneNumber, language: userForm.language, + certification: userForm.certification, } : user ) @@ -256,6 +262,7 @@ const UserManagementPage: React.FC = () => { country: userForm.country, phoneNumber: userForm.phoneNumber, language: userForm.language, + certification: userForm.certification, selected: false, }; @@ -286,6 +293,7 @@ const UserManagementPage: React.FC = () => { userType: "", timezone: "", language: "English", + certification: "", }); }; @@ -304,6 +312,7 @@ const UserManagementPage: React.FC = () => { userType: user.userType, timezone: user.timezone || "", language: user.language || "English", + certification: user.certification || "", }); setEditingUserId(user._id || null); setIsUserModalOpen(true); @@ -506,6 +515,9 @@ const UserManagementPage: React.FC = () => { Language + + Certified Through + Actions @@ -556,6 +568,9 @@ const UserManagementPage: React.FC = () => { {user.language} + + {user.certification} +
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..081e103 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,205 @@ +{ + "name": "fostersource", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.13.tgz", + "integrity": "sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", + "license": "(OFL-1.1 AND MIT)", + "engines": { + "node": ">=0.10.3" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.469.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz", + "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-router": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.2.tgz", + "integrity": "sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.2.tgz", + "integrity": "sha512-VJOQ+CDWFDGaWdrG12Nl+d7yHtLaurNgAQZVgaIy7/Xd+DojgmYLosFfZdGz1wpxmjJIAkAMVTKWcvkx1oggAw==", + "license": "MIT", + "dependencies": { + "react-router": "7.0.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + } + } +} From 8073168d4699349c33476ead7f14001ce6f1d149 Mon Sep 17 00:00:00 2001 From: Rachel Koh Date: Sun, 20 Apr 2025 16:50:49 -0500 Subject: [PATCH 08/28] Retrieve all info from backend + display certification on users table (admin portal) --- backend/controllers/userController.ts | 4 +- .../pages/Admin/UserManagementPage/Users.tsx | 45 +++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/backend/controllers/userController.ts b/backend/controllers/userController.ts index 10da7ef..d44d2b6 100644 --- a/backend/controllers/userController.ts +++ b/backend/controllers/userController.ts @@ -28,7 +28,9 @@ export const getUsers = async (req: Request, res: Response): Promise => { const users = await User.find(query) .skip(skip) .limit(Number(limit)) - .select("name email userType company"); + .select( + "name email role company certification address1 city state zip phone language certification" + ); const total = await User.countDocuments(query); diff --git a/frontend/src/pages/Admin/UserManagementPage/Users.tsx b/frontend/src/pages/Admin/UserManagementPage/Users.tsx index 1e753d8..be5ab32 100644 --- a/frontend/src/pages/Admin/UserManagementPage/Users.tsx +++ b/frontend/src/pages/Admin/UserManagementPage/Users.tsx @@ -142,25 +142,32 @@ const UserManagementPage: React.FC = () => { const response = await apiClient.get(`/users?${params.toString()}`); - const mappedUsers = response.data.users.map((user: any) => ({ - _id: user._id, - id: user._id, - firstName: user.name?.split(" ")[0] || "", - lastName: user.name?.split(" ")[1] || "", - name: user.name, - email: user.email, - userType: user.userType || "", - company: user.company || "", - addressLine: user.address1 || "", - city: user.city || "", - stateProvinceRegion: user.state || "", - zipPostalCode: user.zip || "", - country: user.country || "", - phoneNumber: user.phone || "", - language: user.language || "English", - certification: user.certification || "", - selected: false, - })); + console.log("Raw API response:", response.data.users); + + const mappedUsers = response.data.users.map((user: any) => { + console.log("Individual user data:", user); + const mappedUser = { + _id: user._id, + id: user._id, + firstName: user.name?.split(" ")[0] || "", + lastName: user.name?.split(" ")[1] || "", + name: user.name, + email: user.email, + userType: user.role || user.userType || "", + company: user.company || "", + addressLine: user.address1 || "", + city: user.city || "", + stateProvinceRegion: user.state || "", + zipPostalCode: user.zip || "", + country: user.country || "", + phoneNumber: user.phone || "", + language: user.language || "English", + certification: user.certification || "", + selected: false, + }; + console.log("Mapped user:", mappedUser); + return mappedUser; + }); setUsers(mappedUsers); setTotalUsers(response.data.total); From f1051df27c8c9fd3f9321affc4acdd23f65fcbf2 Mon Sep 17 00:00:00 2001 From: theannachen Date: Sun, 20 Apr 2025 22:13:03 -0500 Subject: [PATCH 09/28] finish linking stuff and removing unnecessary course stuff --- backend/controllers/courseController.ts | 21 +- backend/controllers/zoomController.ts | 2 - backend/models/courseModel.ts | 20 +- backend/package-lock.json | 2 +- .../pages/Admin/WorkshopCreation/InPerson.tsx | 6 +- .../pages/Admin/WorkshopCreation/OnDemand.tsx | 33 ++- .../WorkshopCreation/WorkshopCreation.tsx | 199 ++---------------- 7 files changed, 59 insertions(+), 224 deletions(-) diff --git a/backend/controllers/courseController.ts b/backend/controllers/courseController.ts index c9493f0..607d9df 100644 --- a/backend/controllers/courseController.ts +++ b/backend/controllers/courseController.ts @@ -105,8 +105,6 @@ export const createCourse = async ( ratings, className, discussion, - components, - isLive, categories, creditNumber, courseDescription, @@ -115,17 +113,14 @@ export const createCourse = async ( cost, instructorDescription, instructorRole, - lengthCourse, - time, instructorName, - isInPerson, students, managers, speakers, - courseType, regStart, regEnd, productType, + productInfo, shortUrl, draft, } = req.body; @@ -134,23 +129,18 @@ export const createCourse = async ( if (!draft) if ( !className || - isLive === undefined || creditNumber === undefined || !courseDescription || !thumbnailPath || cost === undefined || - !lengthCourse || - !time || !instructorName || - isInPerson === undefined || - !courseType || !regEnd ) { // console.log("[createCourse] Validation failed. Missing required fields"); res.status(400).json({ success: false, message: - "Please provide className, isLive, creditNumber, thumbnailPath, cost, lengthCourse, time, instructorName, isInPerson, courseType, and regStart", + "Please provide className, isLive, creditNumber, thumbnailPath, cost, lengthCourse, instructorName, and regStart", }); return; } @@ -172,8 +162,6 @@ export const createCourse = async ( ratings, className, discussion, - components, - isLive, categories, creditNumber, courseDescription, @@ -182,17 +170,14 @@ export const createCourse = async ( cost, instructorDescription, instructorRole, - lengthCourse, - time, instructorName, - isInPerson, students, managers, speakers, - courseType, regStart, regEnd, productType, + productInfo, shortUrl, draft, }); diff --git a/backend/controllers/zoomController.ts b/backend/controllers/zoomController.ts index 0b7fd5f..494bac3 100644 --- a/backend/controllers/zoomController.ts +++ b/backend/controllers/zoomController.ts @@ -108,8 +108,6 @@ export const createMeeting = async ( message: "Internal service error", }); } - - }; export const createWebinar = async ( diff --git a/backend/models/courseModel.ts b/backend/models/courseModel.ts index 0ce7f93..c9708b5 100644 --- a/backend/models/courseModel.ts +++ b/backend/models/courseModel.ts @@ -9,7 +9,6 @@ export interface ICourse extends Document { ratings: (mongoose.Types.ObjectId | IRating)[]; className: string; discussion: string; - components: (mongoose.Types.ObjectId | Object)[]; isLive: boolean; categories: string[]; creditNumber: number; @@ -20,16 +19,14 @@ export interface ICourse extends Document { instructorName: string; instructorDescription: string; instructorRole: string; - courseType: "webinar" | "course" | "meeting"; - lengthCourse: number; - time: Date; - isInPerson: boolean; students: (mongoose.Types.ObjectId | IUser)[]; //for the users managers: (mongoose.Types.ObjectId | IUser)[]; speakers: (mongoose.Types.ObjectId | ISpeaker)[]; regStart: Date; regEnd: Date; - productType: string[]; + //Virtual Training - Live Meeting, In-Person Training, Virtual Training - On Demand, Virtual Training - Live Webinar + productType: string; + productInfo: string shortUrl: string; draft: boolean; } @@ -50,8 +47,6 @@ const CourseSchema: Schema = new Schema( ], className: { type: String, required: true }, discussion: { type: String, required: false }, - components: [{ type: Schema.Types.Mixed, required: false }], - isLive: { type: Boolean, required: false }, categories: [{ type: String, required: false }], creditNumber: { type: Number, required: false }, courseDescription: { type: String, required: false }, @@ -60,7 +55,6 @@ const CourseSchema: Schema = new Schema( cost: { type: Number, required: false }, instructorDescription: { type: String, required: false }, instructorRole: { type: String, required: false }, - lengthCourse: { type: Number, required: false }, time: { type: Date, required: false }, instructorName: { type: String, required: false }, isInPerson: { type: Boolean, required: false }, @@ -70,11 +64,6 @@ const CourseSchema: Schema = new Schema( ref: "User", }, ], - courseType: { - type: String, - enum: ["webinar", "course", "meeting"], - required: true, - }, managers: [ { type: Schema.Types.ObjectId, @@ -89,7 +78,8 @@ const CourseSchema: Schema = new Schema( ], regStart: { type: Date, required: false }, regEnd: { type: Date, required: false }, - productType: [{ type: String, required: false }], + productType: { type: String, required: false }, + productInfo: {type:String, required: false}, shortUrl: { type: String, required: false }, draft: { type: Boolean, required: true, default: true }, }, diff --git a/backend/package-lock.json b/backend/package-lock.json index 5cf3ebb..aaa03ec 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,8 +20,8 @@ "multer": "^1.4.5-lts.1", "multer-storage-cloudinary": "^4.0.0", "paypal-rest-sdk": "^1.8.1", - "react-select-country-list": "^2.2.3", "pdfkit": "^0.16.0", + "react-select-country-list": "^2.2.3", "ts-node": "^10.9.2" }, "devDependencies": { diff --git a/frontend/src/pages/Admin/WorkshopCreation/InPerson.tsx b/frontend/src/pages/Admin/WorkshopCreation/InPerson.tsx index d8b9fcb..da94cec 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/InPerson.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/InPerson.tsx @@ -2,11 +2,7 @@ import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; import {WebinarType} from "../../../shared/types/Webinar"; interface InPersonComponentProps { - inPersonData:{ - startTime: Date | null, - duration: number, - location: string - }; + inPersonData:any; setInPersonData: Dispatch>; } export default function InPersonComponent({inPersonData, setInPersonData}:InPersonComponentProps) { diff --git a/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx b/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx index caffc1c..bde91b5 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx @@ -1,9 +1,9 @@ import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; +import apiClient from "../../../services/apiClient"; +import {getCleanCourseData} from "../../../store/useCourseEditStore"; interface OnDemandComponentProps { - onDemandData: { - embeddingLink: string; - }; + onDemandData: any; setOnDemandData: Dispatch>; } export default function OnDemandComponent({ @@ -14,17 +14,40 @@ export default function OnDemandComponent({ const { name, value } = e.target; setOnDemandData({ ...onDemandData, [name]: value }); }; + const course = getCleanCourseData(); + + const createVideo = async () => { + try { + const response = await apiClient.post("/videos", { + title: course.className, + description: course.courseDescription, + videoUrl: onDemandData.embeddingLink, + courseId: course._id, + published: true, // could be toggled later + }); + } catch (error) { + console.error(error); + } + }; return ( -
+
+ +
); } diff --git a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx index 142aae5..7be612a 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx @@ -13,46 +13,26 @@ import apiClient from "../../../services/apiClient"; import { getCleanCourseData } from "../../../store/useCourseEditStore"; export default function WorkshopCreation() { - const [formData, setFormData] = useState({ - title: "", - summary: "", - type: "meeting", - audioInstructions: "", - markAttendance: false, - requireAttendance: false, - gradeUser: false, - emailUnattended: false, - hideAfter: false, - minTime: 0, - }); const [webinarData, setWebinarData] = useState({ - serviceType: "", - meetingID: "string", - startTime: new Date(), - duration: 0, - authParticipants: false, - autoRecord: false, - enablePractice: false, + serviceType: "webinar", + meetingURL: "string", }); const [meetingData, setMeetingData] = useState({ - serviceType: "", + serviceType: "meeting", meetingID: "string", - startTime: new Date(), - duration: 0, - authParticipants: false, - autoRecord: false, - enablePractice: false, }); const [inPersonData, setInPersonData] = useState({ + serviceType: "in person", startTime: null, duration: 0, location: "", }); const [onDemandData, setOnDemandData] = useState({ + serviceType: "on demand", embeddingLink: "", }); @@ -65,27 +45,23 @@ export default function WorkshopCreation() { const course = getCleanCourseData(); + const [formData, setFormData] = useState({ + className: course.className, + courseDescription: course.courseDescription, + type: "meeting", + markAttendance: false, + requireAttendance: false, + gradeUser: false, + emailUnattended: false, + hideAfter: false, + minTime: 0, + }); + const handleSubmit = (event: any) => { event.preventDefault(); console.log("Form submitted:", formData); }; - const createVideo = async () => { - try { - const response = await apiClient.post("/videos", { - title: formData.title, - description: formData.summary, - videoUrl: onDemandData.embeddingLink, - courseId: course._id, - published: true, // could be toggled later - }); - } catch (error) { - console.error(error); - } - }; - - // TODO: get video or webinar - return (
@@ -93,7 +69,7 @@ export default function WorkshopCreation() { Summary