Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged to main. Finished get functions in backend and display on frontend. #49

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion backend/src/controllers/volunteerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,30 @@ async function shiftCheckIn(req: Request, res: Response) {
res.status(200).json(updatedVolunteer);
}

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
};

49 changes: 49 additions & 0 deletions backend/src/models/volunteerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,53 @@ export default class VolunteerModel {
};
}
}

async getPreferredClassesById(volunteer_id: string): Promise<any> {

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<any>(query, values);

if (results.length === 0) {
throw {
status: 400,
message: `No preferred classes found for given voluntter or volunteer not found`,
};
}

return results;
}
}
30 changes: 24 additions & 6 deletions backend/src/routes/volunteerRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import { Request, Response, Router } from "express";
import multer from 'multer';

import {
getVolunteerById,
getVolunteers,
shiftCheckIn,
updateVolunteer,
getPreferredClassesById
} from "../controllers/volunteerController.js";

import { body, param } from "express-validator";
import { RouteDefinition } from "../common/types.js";
import {
Expand All @@ -6,12 +17,6 @@ import {
setAvailabilityByVolunteerId,
updateAvailabilityByVolunteerId,
} from "../controllers/availabilityController.js";
import {
getVolunteerById,
getVolunteers,
shiftCheckIn,
updateVolunteer
} from "../controllers/volunteerController.js";

export const VolunteerRoutes: RouteDefinition = {
path: '/volunteer',
Expand Down Expand Up @@ -105,5 +110,18 @@ export const VolunteerRoutes: RouteDefinition = {
},
]
},
{
path: '/class_preferences',
children: [
{
path: '/:volunteer_id',
validation: [
param('volunteer_id').isUUID('4')
],
method: 'get',
action: getPreferredClassesById
}
]
}
]
};
2 changes: 2 additions & 0 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -61,6 +62,7 @@ function App() {
<Route path="classes" element={<Classes />} />
<Route path="schedule" element={<VolunteerSchedule />} />
<Route path="my-profile" element={<VolunteerProfile />} />
<Route path="class-preferences" element={<ClassPreferences />} />
</Route>
</Route>

Expand Down
10 changes: 10 additions & 0 deletions frontend/src/api/volunteerService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
128 changes: 128 additions & 0 deletions frontend/src/components/ClassPreferencesCard/index.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
65 changes: 65 additions & 0 deletions frontend/src/components/ClassPreferencesCard/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="class-pref-card" >
<div className="vertical-line" style={{ backgroundColor: lineColor }} />
<div className="card-content">
<div className="column segment-1">
<div className="card-text">
<h2 className="class-pref-time">{formatTime(start_time)}</h2>
<p>{timeDifference(end_time, start_time)}</p>
</div>
</div>
<div className="column segment-2">
<div className="card-text">
<h2>{name}</h2>
<p>{instruction.substring(0, 50)}{instruction.length > 40 ? '...' : ''}</p>
</div>
</div>
</div>
</div>
);
}

export default ClassPreferencesCard;
12 changes: 12 additions & 0 deletions frontend/src/components/volunteerLayout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ function VolunteerLayout() {
<img src={nav_item_settings} alt="Settings" />
{!collapsed && "Settings"}
</NavLink>


<NavLink
to="/volunteer/class-preferences"
className={({ isActive }) =>
isActive ? "NavbarText nav-item active" : "NavbarText nav-item"
}
>
{!collapsed && "Class Preferences"}
</NavLink>


</div>
<div className="nav-profile-card-container">
<NavProfileCard
Expand Down
Loading