From 5a098c6a50ccb9380d074fdc15005c2e2fa4732e Mon Sep 17 00:00:00 2001 From: Khai Phan Date: Thu, 30 Jan 2025 13:49:59 -0800 Subject: [PATCH 01/13] Merged to main. Finished get functions in backend and display on frontend. --- .../src/controllers/volunteerController.ts | 24 +++- backend/src/models/volunteerModel.ts | 49 +++++++ backend/src/routes/volunteerRoutes.ts | 8 +- frontend/src/App.js | 2 + frontend/src/api/volunteerService.js | 10 ++ .../components/ClassPreferencesCard/index.css | 128 +++++++++++++++++ .../components/ClassPreferencesCard/index.js | 65 +++++++++ .../src/components/volunteerLayout/index.js | 12 ++ frontend/src/pages/ClassPreferences/index.css | 96 +++++++++++++ frontend/src/pages/ClassPreferences/index.js | 134 ++++++++++++++++++ frontend/src/styles.css | 21 +++ 11 files changed, 547 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/ClassPreferencesCard/index.css create mode 100644 frontend/src/components/ClassPreferencesCard/index.js create mode 100644 frontend/src/pages/ClassPreferences/index.css create mode 100644 frontend/src/pages/ClassPreferences/index.js diff --git a/backend/src/controllers/volunteerController.ts b/backend/src/controllers/volunteerController.ts index da390f0b..446ed03d 100644 --- a/backend/src/controllers/volunteerController.ts +++ b/backend/src/controllers/volunteerController.ts @@ -91,8 +91,30 @@ async function shiftCheckIn(req: Request, res: Response) { } } +async function getPreferredClassesById(req: Request, res: Response) { + const { volunteer_id } = req.params; + + if (!volunteer_id) { + return res.status(400).json({ + error: "Missing required parameter: 'user_id'", + }); + } + + try { + const preferred_classes = await volunteerModel.getPreferredClassesById(volunteer_id); + res.status(200).json(preferred_classes); + } catch (error: any) { + return res.status(error.status ?? 500).json({ + error: error.message + }); + } +} + export { getVolunteerById, - getVolunteers, shiftCheckIn, updateVolunteer + getVolunteers, + shiftCheckIn, + updateVolunteer, + getPreferredClassesById }; diff --git a/backend/src/models/volunteerModel.ts b/backend/src/models/volunteerModel.ts index 83f0aba1..71ab093e 100644 --- a/backend/src/models/volunteerModel.ts +++ b/backend/src/models/volunteerModel.ts @@ -150,4 +150,53 @@ export default class VolunteerModel { }; } } + + async getPreferredClassesById(volunteer_id: string): Promise { + + const query = ` + WITH + class_info AS ( + SELECT + c.class_id, + c.class_name, + c.instructions + FROM class c + ), + schedule_info AS ( + SELECT + s.fk_class_id AS class_id, + GROUP_CONCAT(s.start_time ORDER BY s.start_time) AS start_times, + GROUP_CONCAT(s.end_time ORDER BY s.start_time) AS end_times, + GROUP_CONCAT(s.day ORDER BY s.start_time) AS days_of_week + FROM schedule s + GROUP BY s.fk_class_id + ) + + SELECT + ci.class_id, + ci.class_name, + cp.class_rank, + COALESCE(si.start_times, NULL) AS start_times, + COALESCE(si.end_times, NULL) AS end_times, + COALESCE(si.days_of_week, NULL) AS day_of_week, + ci.instructions + FROM volunteers v + JOIN class_preferences cp ON v.volunteer_id = cp.fk_volunteer_id + JOIN class_info ci ON ci.class_id = cp.fk_class_id + LEFT JOIN schedule_info si ON ci.class_id = si.class_id + WHERE v.volunteer_id = ?; + `; + const values = [volunteer_id]; + + const [results, _] = await connectionPool.query(query, values); + + if (results.length === 0) { + throw { + status: 400, + message: `No preferred classes found for given voluntter or volunteer not found`, + }; + } + + return results; + } } diff --git a/backend/src/routes/volunteerRoutes.ts b/backend/src/routes/volunteerRoutes.ts index 2539cb69..63d6a600 100644 --- a/backend/src/routes/volunteerRoutes.ts +++ b/backend/src/routes/volunteerRoutes.ts @@ -5,7 +5,8 @@ import { getVolunteerById, getVolunteers, shiftCheckIn, - updateVolunteer + updateVolunteer, + getPreferredClassesById } from "../controllers/volunteerController.js"; import { @@ -60,4 +61,9 @@ router.post('/shiftCheckIn', (req: Request, res: Response) => { shiftCheckIn(req, res) }); +// get preferred classes by volunteer id +router.get("/class_preferences/:volunteer_id", (req: Request, res: Response) => { + getPreferredClassesById(req, res); +}); + export default router; diff --git a/frontend/src/App.js b/frontend/src/App.js index 7cd8377c..12243170 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -13,6 +13,7 @@ import VolunteerLogin from "./pages/VolunteerLogin"; import VolunteerProfile from "./pages/VolunteerProfile"; import VolunteerResetPassword from "./pages/VolunteerResetPassword"; import VolunteerSignup from "./pages/VolunteerSignup"; +import ClassPreferences from "./pages/ClassPreferences"; function App() { const [isVolunteer, setIsVolunteer] = useState(false); @@ -61,6 +62,7 @@ function App() { } /> } /> } /> + } /> diff --git a/frontend/src/api/volunteerService.js b/frontend/src/api/volunteerService.js index f1196e4f..4db2c611 100644 --- a/frontend/src/api/volunteerService.js +++ b/frontend/src/api/volunteerService.js @@ -70,4 +70,14 @@ export const updateVolunteerAvailability = async (volunteer_id, availability) => console.error('Error updating volunteer availability:', error); throw error; } +}; + +export const fetchUserPreferredClases = async (volunteer_id) => { + try { + const response = await api.get(`/volunteer/class_preferences/${volunteer_id}`); + return response.data; + } catch (error) { + console.error('Error fetching volunteer class preferences data:', error); + throw error; + } }; \ No newline at end of file diff --git a/frontend/src/components/ClassPreferencesCard/index.css b/frontend/src/components/ClassPreferencesCard/index.css new file mode 100644 index 00000000..e4b6ce94 --- /dev/null +++ b/frontend/src/components/ClassPreferencesCard/index.css @@ -0,0 +1,128 @@ +@import '../../styles.css'; + +/* General Card Styles */ +.class-pref-card { + display: flex; + width: 45%; + margin: .5rem; + padding: .5rem .8rem; + border: 1px solid #ccc; + border-radius: .8rem; + box-shadow: 0px 4px 16px rgba(15, 17, 17, 0.1); + min-height: 3.5rem; + align-items: center; + background-color: white; +} + +.class-pref-card:hover { + cursor: pointer; + filter: brightness(0.95); +} + +/* Vertical Line Styles */ +.vertical-line { + width: .5rem; + background-color: var(--grey); + border-radius: 1.5rem; + height: 80%; + min-height: 3rem; + margin-right: 1rem; +} + +/* Card Content Layout */ +.card-content { + display: flex; + width: 100%; + align-items: center; + height: 80%; +} + + +.segment-1 { + flex: 1; + display: flex; + flex-direction: column; +} + +.segment-2 { + flex: 4; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +/* Text and Typography */ +.card-text { + margin: 0; + font-family: var(--font-primary); +} + +.card-text h2 { + margin: .25rem 0; + font-size: 1rem; + font-weight: 400; + color: var(--text-dark, #0F1111); +} + +.card-text p { + margin: 0; + font-size: var(--small-text); + font-weight: 400; + color: var(--text-grey, #808080); +} + +.segment-1 > .card-text > h2 { + font-weight: 500; +} + + +/* Button Layout */ +.button-container { + display: flex; + justify-content: center; + align-items: center; +} + +.check-in-button { + position: relative; + height: 100%; + border-radius: 8px; + border: 1px solid var(--primary-blue); + background: #FFF; + box-shadow: 0px 4px 16px 0px rgba(15, 17, 17, 0.05); + padding: 0 1rem; + display: flex; + align-items: center; +} + +/* Button Interactions */ +.check-in-button:not(:disabled):hover { + cursor: pointer; + background: var(--grey); + border: none; +} + +.check-in-button:disabled { + background-color: #d3d3d3; + border: 1px solid #d3d3d3; + box-shadow: none; +} + +/* Icon within Button */ +.card-button-icon { + width: 1rem; + height: 1rem; + margin-right: .5rem; +} + +/* Responsive Adjustments */ +@media (max-width: 1000px) { + .segment-1 { + display: none; + } + + .segment-2 { + flex: 1; + flex-direction: column; + } +} \ No newline at end of file diff --git a/frontend/src/components/ClassPreferencesCard/index.js b/frontend/src/components/ClassPreferencesCard/index.js new file mode 100644 index 00000000..2292e47b --- /dev/null +++ b/frontend/src/components/ClassPreferencesCard/index.js @@ -0,0 +1,65 @@ +import './index.css'; + +function ClassPreferencesCard({ classData }) { + + const RANK1_COLOR = "rgba(67, 133, 172, 1)"; + const RANK2_COLOR = "rgba(67, 133, 172, 0.7)"; + const RANK3_COLOR = "rgba(67, 133, 172, 0.3)"; + + const rank = classData.class_rank; + const name = classData.class_name; + const instruction = classData.instructions; + const start_time = classData.start_times.split(",")[0]; + const end_time = classData.end_times.split(",")[0]; + + const timeDifference = (end, start) => { + // Assume classes are done within one day, end time > start time + const e = end.split(":"); + const s = start.split(":"); + if (e[1] < s[1]) { + return `${Number(e[0]) - 1 - Number(s[0])} hour ${-(Number(e[1]) - Number(s[1]))} min`; + } else if (e[1] > s[1]) { + return `${Number(e[0]) - Number(s[0])} hour ${(Number(e[1]) - Number(s[1]))} min`; + } else { + return `${Number(e[0]) - Number(s[0])} hour`; + } + }; + + let lineColor; + if (rank === 1) { + lineColor = RANK1_COLOR; + } else if (rank === 2) { + lineColor = RANK2_COLOR; + } else { + lineColor = RANK3_COLOR; + } + + const formatTime = (time) => { + const [hour, minute] = time.split(":").map(Number); + const period = hour >= 12 ? "PM" : "AM"; + const formattedHour = hour % 12 || 12; + return `${formattedHour}:${minute.toString().padStart(2, "0")} ${period}`; + }; + + return ( +
+
+
+
+
+

{formatTime(start_time)}

+

{timeDifference(end_time, start_time)}

+
+
+
+
+

{name}

+

{instruction.substring(0, 50)}{instruction.length > 40 ? '...' : ''}

+
+
+
+
+ ); +} + +export default ClassPreferencesCard; \ No newline at end of file diff --git a/frontend/src/components/volunteerLayout/index.js b/frontend/src/components/volunteerLayout/index.js index b385cc30..1f6c16bd 100644 --- a/frontend/src/components/volunteerLayout/index.js +++ b/frontend/src/components/volunteerLayout/index.js @@ -115,6 +115,18 @@ function VolunteerLayout() { Settings {!collapsed && "Settings"} + + + + isActive ? "NavbarText nav-item active" : "NavbarText nav-item" + } + > + {!collapsed && "Class Preferences"} + + +
{ + const getCurrentUSerPrefferedClasses = async () => { + const volunteerID = localStorage.getItem('volunteerID'); + // setUserId(user_id); + const classes_p = await fetchUserPreferredClases(volunteerID); + if (classes_p!=null && classes_p.length > 0) { + let res = {}; + let rank1 = []; + let rank2 = []; + let rank3 = []; + for (let i = 0; i < classes_p.length; i++) { + if (classes_p[i].class_rank === 1) { + rank1.push(classes_p[i]); + } else if (classes_p[i].class_rank === 2) { + rank2.push(classes_p[i]); + } else { + rank3.push(classes_p[i]); + } + } + res[1] = rank1; + res[2] = rank2; + res[3] = rank3; + setPreferredClasses(res); + } + }; + getCurrentUSerPrefferedClasses(); + + }, []); + + useEffect(()=> { + console.log(preferredClasses); + }, [preferredClasses]); + + function renderClasses (rank) { + if (preferredClasses == null || preferredClasses[rank].length == 0) { + return <>You have not chosen class preferences...; + } + + return ( + <> + {preferredClasses[rank].map((class_, index) => ( + + ))} + + ); + }; + + + + return ( +
+
+

Class Preferences

+ +
+
+
+
My Preferences
+ + +
+
+
+
+
+ Most Preferred +
+ +
+ +
+
+ {renderClasses(1)} +
+
+ +
+
+
+
+ More Preferred +
+ +
+
+
+ {renderClasses(2)} +
+
+ +
+
+
+
+ Preferred +
+ +
+
+
+ {renderClasses(3)} +
+
+ +
+
+ ); +}; + +export default ClassPreferences; \ No newline at end of file diff --git a/frontend/src/styles.css b/frontend/src/styles.css index e8a546ae..20f884a3 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -51,3 +51,24 @@ body { .notyf { font-family: var(--font-primary); } + +.save-button { + background: #1687CA; + padding: 10px 16px; + border: none; + border-radius: 8px; + color: white; + font-size: 14px; + font-weight: 400; +} +.save-button:hover, .cancel-button:hover { + filter: brightness(0.7); + cursor: pointer; +} +.cancel-button { + background-color: white; + color: black; + padding: 10px 16px; + border: 1px solid #1687CA ; + border-radius: 8px ; +} \ No newline at end of file From 02bdbd8411f58e8b1a86303954b556691c1d7110 Mon Sep 17 00:00:00 2001 From: Khai Phan Date: Sat, 1 Feb 2025 11:23:01 -0800 Subject: [PATCH 02/13] Refactored into Route Definition --- backend/src/controllers/volunteerController.ts | 8 +++++--- backend/src/routes/volunteerRoutes.ts | 4 ++-- frontend/src/api/volunteerService.js | 4 ++-- frontend/src/pages/ClassPreferences/index.js | 10 ++++++---- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/src/controllers/volunteerController.ts b/backend/src/controllers/volunteerController.ts index 6ebced09..c67a34d2 100644 --- a/backend/src/controllers/volunteerController.ts +++ b/backend/src/controllers/volunteerController.ts @@ -44,13 +44,15 @@ async function getPreferredClassesById(req: Request, res: Response) { if (!volunteer_id) { return res.status(400).json({ - error: "Missing required parameter: 'user_id'", + error: "Missing required parameter: 'volunteer_id'", }); } try { - const preferred_classes = await volunteerModel.getPreferredClassesById(volunteer_id); - res.status(200).json(preferred_classes); + // const preferred_classes = await volunteerModel.getPreferredClassesById(volunteer_id); + res.status(200).json({gay: "gay"}); + + // res.status(200).json(preferred_classes); } catch (error: any) { return res.status(error.status ?? 500).json({ error: error.message diff --git a/backend/src/routes/volunteerRoutes.ts b/backend/src/routes/volunteerRoutes.ts index ab46316a..89b8d34c 100644 --- a/backend/src/routes/volunteerRoutes.ts +++ b/backend/src/routes/volunteerRoutes.ts @@ -111,14 +111,14 @@ export const VolunteerRoutes: RouteDefinition = { ] }, { - path: '/class_preferences', + path: '/class-preferences', children: [ { path: '/:volunteer_id', + method: 'get', validation: [ param('volunteer_id').isUUID('4') ], - method: 'get', action: getPreferredClassesById } ] diff --git a/frontend/src/api/volunteerService.js b/frontend/src/api/volunteerService.js index 62cb2bbb..eba4a374 100644 --- a/frontend/src/api/volunteerService.js +++ b/frontend/src/api/volunteerService.js @@ -72,9 +72,9 @@ export const updateVolunteerAvailability = async (volunteer_id, availability) => } }; -export const fetchUserPreferredClases = async (volunteer_id) => { +export const fetchUserPreferredClasses = async (volunteer_id) => { try { - const response = await api.get(`/volunteer/class_preferences/${volunteer_id}`); + const response = await api.get(`/volunteer/class-preferences/${volunteer_id}`); return response.data; } catch (error) { console.error('Error fetching volunteer class preferences data:', error); diff --git a/frontend/src/pages/ClassPreferences/index.js b/frontend/src/pages/ClassPreferences/index.js index f89ad5a1..9e863135 100644 --- a/frontend/src/pages/ClassPreferences/index.js +++ b/frontend/src/pages/ClassPreferences/index.js @@ -1,7 +1,7 @@ import "./index.css"; import React, {useEffect, useState} from 'react'; import edit_icon from "../../assets/edit-icon.png" -import { fetchUserPreferredClases } from "../../api/volunteerService"; +import { fetchUserPreferredClasses } from "../../api/volunteerService"; import ClassPreferencesCard from "../../components/ClassPreferencesCard"; function ClassPreferences() { @@ -9,10 +9,12 @@ function ClassPreferences() { const [allClasses, setAllClasses] = useState(null); useEffect(() => { - const getCurrentUSerPrefferedClasses = async () => { + const getCurrentUserPrefferedClasses = async () => { const volunteerID = localStorage.getItem('volunteerID'); + console.log("Volunteer id: " + volunteerID); + // setUserId(user_id); - const classes_p = await fetchUserPreferredClases(volunteerID); + const classes_p = await fetchUserPreferredClasses(volunteerID); if (classes_p!=null && classes_p.length > 0) { let res = {}; let rank1 = []; @@ -33,7 +35,7 @@ function ClassPreferences() { setPreferredClasses(res); } }; - getCurrentUSerPrefferedClasses(); + getCurrentUserPrefferedClasses(); }, []); From 1cd0418b6a1c453301055a161a6850f1cadae2d1 Mon Sep 17 00:00:00 2001 From: Khai Phan Date: Sat, 1 Feb 2025 11:30:53 -0800 Subject: [PATCH 03/13] Refactored into Route Definition --- backend/src/controllers/volunteerController.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/volunteerController.ts b/backend/src/controllers/volunteerController.ts index c67a34d2..21d3991b 100644 --- a/backend/src/controllers/volunteerController.ts +++ b/backend/src/controllers/volunteerController.ts @@ -49,10 +49,9 @@ async function getPreferredClassesById(req: Request, res: Response) { } try { - // const preferred_classes = await volunteerModel.getPreferredClassesById(volunteer_id); - res.status(200).json({gay: "gay"}); + const preferred_classes = await volunteerModel.getPreferredClassesById(volunteer_id); - // res.status(200).json(preferred_classes); + res.status(200).json(preferred_classes); } catch (error: any) { return res.status(error.status ?? 500).json({ error: error.message From 151b2d8a6311842b90b45cf04119c6eb9eeba5d8 Mon Sep 17 00:00:00 2001 From: Khai Phan Date: Sat, 1 Feb 2025 12:44:01 -0800 Subject: [PATCH 04/13] Refactored into Route Definition --- backend/src/routes/volunteerRoutes.ts | 26 ++++++++++---------- frontend/src/pages/ClassPreferences/index.js | 1 - 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/backend/src/routes/volunteerRoutes.ts b/backend/src/routes/volunteerRoutes.ts index 89b8d34c..342ea847 100644 --- a/backend/src/routes/volunteerRoutes.ts +++ b/backend/src/routes/volunteerRoutes.ts @@ -81,6 +81,19 @@ export const VolunteerRoutes: RouteDefinition = { }, ] }, + { + path: '/class-preferences', + children: [ + { + path: '/:volunteer_id', + method: 'get', + validation: [ + param('volunteer_id').isUUID('4') + ], + action: getPreferredClassesById + } + ] + }, { path: '/:volunteer_id', validation: [ @@ -110,18 +123,5 @@ export const VolunteerRoutes: RouteDefinition = { }, ] }, - { - path: '/class-preferences', - children: [ - { - path: '/:volunteer_id', - method: 'get', - validation: [ - param('volunteer_id').isUUID('4') - ], - action: getPreferredClassesById - } - ] - } ] }; diff --git a/frontend/src/pages/ClassPreferences/index.js b/frontend/src/pages/ClassPreferences/index.js index 9e863135..ae1c67cc 100644 --- a/frontend/src/pages/ClassPreferences/index.js +++ b/frontend/src/pages/ClassPreferences/index.js @@ -11,7 +11,6 @@ function ClassPreferences() { useEffect(() => { const getCurrentUserPrefferedClasses = async () => { const volunteerID = localStorage.getItem('volunteerID'); - console.log("Volunteer id: " + volunteerID); // setUserId(user_id); const classes_p = await fetchUserPreferredClasses(volunteerID); From 09701f1e296551a2eb19bdf8fda5ca8255bf5dfc Mon Sep 17 00:00:00 2001 From: Khai Phan Date: Mon, 3 Feb 2025 21:23:35 -0800 Subject: [PATCH 05/13] Added Class Preferences Card on My Profile Page and modal for modifying class preferences --- backend/src/models/volunteerModel.ts | 7 - frontend/public/index.html | 1 + frontend/src/assets/no-class-preferences.png | Bin 0 -> 14866 bytes .../components/ClassPreferencesCard/index.css | 2 +- .../components/ClassPreferencesCard/index.js | 10 +- frontend/src/components/Modal/index.css | 46 ++++++ frontend/src/components/Modal/index.js | 27 ++++ .../src/components/volunteerLayout/index.js | 11 -- .../availabilityGrid/index.css | 10 ++ .../availabilityGrid/index.js | 2 +- .../changePasswordCard/index.css | 10 ++ .../changePasswordCard/index.js | 2 +- .../classPreferencesCard/index.css | 75 ++++++++- .../classPreferencesCard/index.js | 148 ++++++++++-------- .../volunteerDetailsCard/index.css | 11 +- .../volunteerDetailsCard/index.js | 2 +- frontend/src/pages/ClassPreferences/index.js | 32 +++- frontend/src/pages/VolunteerProfile/index.js | 4 + 18 files changed, 302 insertions(+), 98 deletions(-) create mode 100644 frontend/src/assets/no-class-preferences.png create mode 100644 frontend/src/components/Modal/index.css create mode 100644 frontend/src/components/Modal/index.js diff --git a/backend/src/models/volunteerModel.ts b/backend/src/models/volunteerModel.ts index e17443f6..ec018c29 100644 --- a/backend/src/models/volunteerModel.ts +++ b/backend/src/models/volunteerModel.ts @@ -192,13 +192,6 @@ export default class VolunteerModel { const [results, _] = await connectionPool.query(query, values); - if (results.length === 0) { - throw { - status: 400, - message: `No preferred classes found for given voluntter or volunteer not found`, - }; - } - return results; } } diff --git a/frontend/public/index.html b/frontend/public/index.html index c4685d2b..5af52772 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -26,6 +26,7 @@
+