From 985db0c12fd3fbeaf6efdba682b3edb261d5c3d0 Mon Sep 17 00:00:00 2001 From: Yifei Fang Date: Sun, 20 Jul 2025 17:19:42 -0600 Subject: [PATCH 01/23] fixes to add course --- backend/controllers/courseController.ts | 2 + backend/controllers/surveyController.ts | 229 +++++++++--------- backend/models/courseModel.ts | 4 +- backend/models/surveyModel.ts | 2 +- backend/routes/surveyRoutes.ts | 2 +- frontend/package-lock.json | 83 ++++++- frontend/package.json | 1 + frontend/src/App.tsx | 1 + .../Admin/ComponentPage/CertificateCard.tsx | 158 ++++++------ .../pages/Admin/ComponentPage/SurveyCard.tsx | 8 +- .../Admin/ComponentPage/WorkshopCard.tsx | 8 +- .../Admin/EditCoursePage/editCoursePage.tsx | 156 +++++------- .../pages/Admin/HandoutsPage/handoutsPage.tsx | 3 + frontend/src/pages/Admin/Pricing/Pricing.tsx | 201 +++++++++------ .../pages/Admin/ProductPage/ProductPage.tsx | 19 +- .../src/pages/Admin/SurveyPage/Survey.tsx | 23 +- frontend/src/store/useCourseEditStore.ts | 2 + 17 files changed, 528 insertions(+), 374 deletions(-) diff --git a/backend/controllers/courseController.ts b/backend/controllers/courseController.ts index 0bbdca4..d200ef2 100644 --- a/backend/controllers/courseController.ts +++ b/backend/controllers/courseController.ts @@ -118,6 +118,7 @@ export const createCourse = async ( productInfo, shortUrl, draft, + registrationLimit, } = req.body; // Validate required fields @@ -175,6 +176,7 @@ export const createCourse = async ( productInfo, shortUrl, draft, + registrationLimit, }); const savedCourseResponse = await newCourseResponse.save(); diff --git a/backend/controllers/surveyController.ts b/backend/controllers/surveyController.ts index e9b4bc5..6a0d535 100644 --- a/backend/controllers/surveyController.ts +++ b/backend/controllers/surveyController.ts @@ -5,126 +5,139 @@ import Survey, { ISurvey } from "../models/surveyModel"; // @route GET /api/surveys // @access Public export const getSurvey = async (req: Request, res: Response): Promise => { - try { - // Use `findOneAndUpdate` to find the survey and create one if it doesn't exist - const survey = await Survey.findOneAndUpdate( - {}, // Find the first document (survey) - { $setOnInsert: { questions: [] } }, // If no document is found, insert a new one with empty questions - { new: true, upsert: true } // Return the updated/new document, and if not found, create it - ).populate({ - path: "questions", - model: "Question", - }); - - res.status(200).json(survey); - } catch (error) { - if (error instanceof Error) { - res.status(500).json({ message: error.message }); - } else { - res.status(500).json({ message: "An unknown error occurred" }); - } - } + try { + // Use `findOneAndUpdate` to find the survey and create one if it doesn't exist + const survey = await Survey.findOneAndUpdate( + {}, // Find the first document (survey) + { $setOnInsert: { questions: [] } }, // If no document is found, insert a new one with empty questions + { new: true, upsert: true } // Return the updated/new document, and if not found, create it + ).populate({ + path: "questions", + model: "Question", + }); + + res.status(200).json(survey); + } catch (error) { + if (error instanceof Error) { + res.status(500).json({ message: error.message }); + } else { + res.status(500).json({ message: "An unknown error occurred" }); + } + } }; // @desc Create a new survey (only if it doesn't already exist) // @route POST /api/surveys // @access Public -// export const createSurvey = async (req: Request, res: Response): Promise => { -// try { -// // Check if a survey already exists -// const existingSurvey = await Survey.findOne(); - -// if (existingSurvey) { -// res.status(200).json({ -// survey: existingSurvey, -// message: "Survey already exists. You can update it instead.", -// }); -// return; -// } - -// const { questions } = req.body; - -// if (!questions) { -// res.status(400).json({ message: "Questions are required" }); -// return; -// } - -// // Create a new survey -// const survey = new Survey({ -// questions, -// }); -// await survey.save(); - -// res.status(201).json({ -// survey, -// message: "Survey created successfully", -// }); -// } catch (error) { -// if (error instanceof Error) { -// res.status(500).json({ message: error.message }); -// } else { -// res.status(500).json({ message: "An unknown error occurred" }); -// } -// } -// }; +export const createSurvey = async ( + req: Request, + res: Response +): Promise => { + try { + // Check if a survey already exists + const existingSurvey = await Survey.findOne(); + + if (existingSurvey) { + res.status(200).json({ + survey: existingSurvey, + message: "Survey already exists. You can update it instead.", + }); + return; + } + + const { questions } = req.body; + + if (!questions) { + res.status(400).json({ message: "Questions are required" }); + return; + } + + // Create a new survey + const survey = new Survey({ + questions, + }); + await survey.save(); + + res.status(201).json({ + survey, + message: "Survey created successfully", + }); + } catch (error) { + if (error instanceof Error) { + res.status(500).json({ message: error.message }); + } else { + res.status(500).json({ message: "An unknown error occurred" }); + } + } +}; // @desc Update the existing survey // @route PUT /api/surveys // @access Public -export const updateSurvey = async (req: Request, res: Response): Promise => { - try { - const { questions } = req.body; - - // Find the single existing survey (since there is only one survey) - const survey = await Survey.findOne(); - - if (!survey) { - res.status(404).json({ message: "Survey not found" }); - return; - } - - // Update the survey with new questions - survey.question = questions; - await survey.save(); - - res.status(200).json(survey); - } catch (error) { - if (error instanceof Error) { - res.status(500).json({ message: error.message }); - } else { - res.status(500).json({ message: "An unknown error occurred" }); - } - } +export const updateSurvey = async ( + req: Request, + res: Response +): Promise => { + try { + const { questions } = req.body; + + console.log(questions); + + // Find the single existing survey (since there is only one survey) + const survey = await Survey.findOne(); + + if (!survey) { + res.status(404).json({ message: "Survey not found" }); + return; + } + + // Update the survey with new questions + survey.questions = questions; + await survey.save(); + console.log("survey", survey); + + res.status(200).json(survey); + } catch (error) { + if (error instanceof Error) { + console.error(error); + res.status(500).json({ message: error.message }); + } else { + res.status(500).json({ message: "An unknown error occurred" }); + } + } }; // @desc Delete the survey // @route DELETE /api/surveys // @access Public -export const deleteSurvey = async (req: Request, res: Response): Promise => { - try { - // Since there is only one survey, no need to look for it by ID - const survey = await Survey.findOne(); - - if (!survey) { - res.status(404).json({ - success: false, - message: "Survey not found", - }); - return; - } - - // Delete the survey - await Survey.deleteOne({ _id: survey._id }); - - res.status(200).json({ - success: true, - message: "Survey deleted successfully", - }); - } catch (error) { - if (error instanceof Error) { - res.status(500).json({ message: error.message }); - } else { - res.status(500).json({ message: "An unknown error occurred" }); - } - } +export const deleteSurvey = async ( + req: Request, + res: Response +): Promise => { + try { + // Since there is only one survey, no need to look for it by ID + const survey = await Survey.findOne(); + + if (!survey) { + res.status(404).json({ + success: false, + message: "Survey not found", + }); + return; + } + + // Delete the survey + await Survey.deleteOne({ _id: survey._id }); + + res.status(200).json({ + success: true, + message: "Survey deleted successfully", + }); + } catch (error) { + if (error instanceof Error) { + res.status(500).json({ message: error.message }); + } else { + res.status(500).json({ message: "An unknown error occurred" }); + } + } }; diff --git a/backend/models/courseModel.ts b/backend/models/courseModel.ts index bcfdb1a..e5f032b 100644 --- a/backend/models/courseModel.ts +++ b/backend/models/courseModel.ts @@ -33,6 +33,7 @@ export interface ICourse extends Document { productInfo: string; shortUrl: string; draft: boolean; + registrationLimit: number; } const CourseSchema: Schema = new Schema( @@ -49,7 +50,7 @@ const CourseSchema: Schema = new Schema( ref: "Rating", }, ], - className: { type: String, required: true }, + className: { type: String, required: false }, discussion: { type: String, required: false }, categories: [{ type: String, required: false }], creditNumber: { type: Number, required: false }, @@ -96,6 +97,7 @@ const CourseSchema: Schema = new Schema( productInfo: { type: String, required: false }, shortUrl: { type: String, required: false }, draft: { type: Boolean, required: true, default: true }, + registrationLimit: { type: Number, required: false, default: 0 }, }, { timestamps: true, diff --git a/backend/models/surveyModel.ts b/backend/models/surveyModel.ts index f30d6f1..626a623 100644 --- a/backend/models/surveyModel.ts +++ b/backend/models/surveyModel.ts @@ -5,7 +5,7 @@ import { IQuestion } from "./questionModel"; export interface ISurvey extends Document { // Define fields here: id: string; - question: mongoose.Types.ObjectId[]; + questions: mongoose.Types.ObjectId[]; courseId: mongoose.Types.ObjectId; } diff --git a/backend/routes/surveyRoutes.ts b/backend/routes/surveyRoutes.ts index e665d07..d4bb3ab 100644 --- a/backend/routes/surveyRoutes.ts +++ b/backend/routes/surveyRoutes.ts @@ -15,7 +15,7 @@ router.get("/", getSurvey); // router.post("/", createSurvey); // PUT update survey by ID -router.put("/:id", updateSurvey); +router.put("/", updateSurvey); // DELETE survey by ID router.delete("/:id", deleteSurvey); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index db1519d..8b2dcb7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "quill": "^2.0.3", "react": "^18.3.1", "react-country-state-city": "^1.1.12", + "react-datepicker": "^8.4.0", "react-dom": "^18.3.1", "react-icons": "^5.4.0", "react-quill": "^2.0.0", @@ -3039,28 +3040,56 @@ "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==" }, "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.13", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.13.tgz", + "integrity": "sha512-Qmj6t9TjgWAvbygNEu1hj4dbHI9CY0ziCMIJrmYoDIn9TUAH5lRmiIeZmRd4c6QEZkzdoH7jNnoNyoY1AIESiA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.4", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@googlemaps/js-api-loader": { @@ -6241,6 +6270,15 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -14194,6 +14232,21 @@ "react": ">=16" } }, + "node_modules/react-datepicker": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.4.0.tgz", + "integrity": "sha512-6nPDnj8vektWCIOy9ArS3avus9Ndsyz5XgFCJ7nBxXASSpBdSL6lG9jzNNmViPOAOPh6T5oJyGaXuMirBLECag==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -16328,6 +16381,12 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7d80b36..3687688 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "quill": "^2.0.3", "react": "^18.3.1", "react-country-state-city": "^1.1.12", + "react-datepicker": "^8.4.0", "react-dom": "^18.3.1", "react-icons": "^5.4.0", "react-quill": "^2.0.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c06dff..5a0877b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import React from "react"; import AppRoutes from "./routes/appRoutes"; +import "react-datepicker/dist/react-datepicker.css"; const App: React.FC = () => { return ( diff --git a/frontend/src/pages/Admin/ComponentPage/CertificateCard.tsx b/frontend/src/pages/Admin/ComponentPage/CertificateCard.tsx index e9924a3..c7c94df 100644 --- a/frontend/src/pages/Admin/ComponentPage/CertificateCard.tsx +++ b/frontend/src/pages/Admin/ComponentPage/CertificateCard.tsx @@ -1,51 +1,66 @@ import { Dispatch, SetStateAction } from "react"; import Dropdown from "../../../components/dropdown-select"; -import {Vault, Eye, Wifi} from "lucide-react"; +import { Vault, Eye, Wifi } from "lucide-react"; import DisplayBar from "./DisplayBar"; interface WorkshopProps { - workshop: any; - prerequisites: { survey: string; certificate: string }; - setPrerequisites: Dispatch>; + workshop: any; + prerequisites: { survey: string; certificate: string }; + setPrerequisites: Dispatch< + SetStateAction<{ survey: string; certificate: string }> + >; } interface CheckboxProps { - label: string; - checked: boolean; - onChange: (checked: boolean) => void; - } - - export function Checkbox({ label, checked, onChange }: CheckboxProps) { - return ( - - ); - } - + label: string; + checked: boolean; + onChange: (checked: boolean) => void; +} -export default function CertificateCard({ workshop, prerequisites, setPrerequisites }: WorkshopProps) { - const formatMenuItems = [ - { label: "None Selected", onClick: () => setPrerequisites((prev) => ({ ...prev, survey: "None Selected" })) }, - { label: "Survey", onClick: () => setPrerequisites((prev) => ({ ...prev, certificate: "Survey" })) } - ]; +export function Checkbox({ label, checked, onChange }: CheckboxProps) { + return ( + + ); +} - return ( -
-
-
- -

{workshop?.name || "Certificate"}

-
- -

Certificate Template

-

Prerequisites

+export default function CertificateCard({ + workshop, + prerequisites, + setPrerequisites, +}: WorkshopProps) { + const formatMenuItems = [ + { + label: "None Selected", + onClick: () => + setPrerequisites((prev) => ({ ...prev, survey: "None Selected" })), + }, + { + label: "Survey", + onClick: () => + setPrerequisites((prev) => ({ ...prev, certificate: "Survey" })), + }, + ]; + + return ( +
+
+
+ +

+ {workshop?.name || "Certificate"} +

+
+ +

Certificate Template

+ {/*

Prerequisites

@@ -56,37 +71,42 @@ export default function CertificateCard({ workshop, prerequisites, setPrerequisi throw new Error("Function not implemented."); } } />
- -
Preview
-
-

Content

-
- -
-
-
- -
-
- - - -
- -
+ */} +
Preview
+
+

Content

+
+ +
+
+ +
+
+ +
+
+ -
-
- ); + {/*
+ +
*/} +
+
+ ); } diff --git a/frontend/src/pages/Admin/ComponentPage/SurveyCard.tsx b/frontend/src/pages/Admin/ComponentPage/SurveyCard.tsx index ff3639f..2a22d19 100644 --- a/frontend/src/pages/Admin/ComponentPage/SurveyCard.tsx +++ b/frontend/src/pages/Admin/ComponentPage/SurveyCard.tsx @@ -124,7 +124,7 @@ export default function SurveyCard({

Survey has {survey.questions.length} questions{" "}

-

Prerequisites

+ {/*

Prerequisites

- + */}
Preview
@@ -171,11 +171,11 @@ export default function SurveyCard({ Edit Component -
+ {/*
-
+
*/}
); diff --git a/frontend/src/pages/Admin/ComponentPage/WorkshopCard.tsx b/frontend/src/pages/Admin/ComponentPage/WorkshopCard.tsx index 62e01b7..0f21809 100644 --- a/frontend/src/pages/Admin/ComponentPage/WorkshopCard.tsx +++ b/frontend/src/pages/Admin/ComponentPage/WorkshopCard.tsx @@ -158,7 +158,7 @@ export default function WorkshopCard({

Live Event {formatWebinarDate(webinar.startTime)}

-

Prerequisites

+ {/*

Prerequisites

- + */}
Preview
@@ -211,11 +211,11 @@ export default function WorkshopCard({ Edit Component -
+ {/*
-
+
*/}
); diff --git a/frontend/src/pages/Admin/EditCoursePage/editCoursePage.tsx b/frontend/src/pages/Admin/EditCoursePage/editCoursePage.tsx index ac78370..c248609 100644 --- a/frontend/src/pages/Admin/EditCoursePage/editCoursePage.tsx +++ b/frontend/src/pages/Admin/EditCoursePage/editCoursePage.tsx @@ -227,9 +227,9 @@ const EditCourse = () => {

Details

-
-
-

Catalog

+
+
+

Thumbnail

e.preventDefault()} @@ -283,42 +283,6 @@ const EditCourse = () => { )}
-

Banner Image

-
e.preventDefault()} - onDrop={handleBannerDrop} - > -
- {/* Upload Icon */} - ⬆️ - - {/* Upload Text */} -

- Choose a file or drag & drop it here -

-

- JPEG or PNG format, up to 50MB -

- - {/* File Input */} - - - - {/* Show Selected File */} -
-
{bannerImage && (
@@ -335,22 +299,22 @@ const EditCourse = () => { )}
-
-
+
+

Title

setField("className", e.target.value)} />
-

Summary

-
+

Summary

+