diff --git a/.github/_workflows/run-checks.yml b/.github/workflows/run-checks.yml
similarity index 74%
rename from .github/_workflows/run-checks.yml
rename to .github/workflows/run-checks.yml
index 1c5d262c..4b8468ca 100644
--- a/.github/_workflows/run-checks.yml
+++ b/.github/workflows/run-checks.yml
@@ -2,9 +2,12 @@ name: Run Checks
on:
push:
- branches: ["main"]
+ # branches: ["main"]
+ branches: ["dev"]
+
pull_request:
- branches: ["main"]
+ # branches: ["main"]
+ branches: ["dev"]
jobs:
run-checks:
@@ -22,7 +25,8 @@ jobs:
- name: Install dependencies
run:
npm exec --workspaces -- npx rimraf node_modules && npx rimraf
- node_modules && yarn install --frozen-lockfile
+ node_modules && yarn install
+ # node_modules && yarn install --frozen-lockfile
- name: Build React project
run: cd client && yarn run build
diff --git a/client/package.json b/client/package.json
index b8d85865..e2741849 100644
--- a/client/package.json
+++ b/client/package.json
@@ -14,6 +14,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@chakra-ui/icons": "^2.0.0",
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
@@ -32,8 +33,8 @@
"react-icons": "^5.3.0",
"react-qr-code": "^2.0.15",
"react-router-dom": "^6.26.1",
- "zod": "^3.23.8",
- "recharts": "^2.7.2"
+ "recharts": "^2.7.2",
+ "zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
diff --git a/client/public/card_images/ballet.svg b/client/public/card_images/ballet.svg
new file mode 100644
index 00000000..fe10cd6b
--- /dev/null
+++ b/client/public/card_images/ballet.svg
@@ -0,0 +1,38 @@
+
+
+
\ No newline at end of file
diff --git a/client/public/card_images/classical.svg b/client/public/card_images/classical.svg
new file mode 100644
index 00000000..aafd0a33
--- /dev/null
+++ b/client/public/card_images/classical.svg
@@ -0,0 +1,35 @@
+
+
+
\ No newline at end of file
diff --git a/client/public/card_images/pop.svg b/client/public/card_images/pop.svg
new file mode 100644
index 00000000..0b8b9d2a
--- /dev/null
+++ b/client/public/card_images/pop.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/client/public/card_images/pop2.svg b/client/public/card_images/pop2.svg
new file mode 100644
index 00000000..2b44336a
--- /dev/null
+++ b/client/public/card_images/pop2.svg
@@ -0,0 +1,79 @@
+
+
+
\ No newline at end of file
diff --git a/client/src/components/dashboard/cseLogo.png b/client/public/dashboard/cseLogo.png
similarity index 100%
rename from client/src/components/dashboard/cseLogo.png
rename to client/public/dashboard/cseLogo.png
diff --git a/client/src/components/dashboard/sidebarImgs/classes.svg b/client/public/dashboard/sidebarImgs/classes.svg
similarity index 100%
rename from client/src/components/dashboard/sidebarImgs/classes.svg
rename to client/public/dashboard/sidebarImgs/classes.svg
diff --git a/client/src/components/dashboard/sidebarImgs/dashboard.svg b/client/public/dashboard/sidebarImgs/dashboard.svg
similarity index 100%
rename from client/src/components/dashboard/sidebarImgs/dashboard.svg
rename to client/public/dashboard/sidebarImgs/dashboard.svg
diff --git a/client/src/components/dashboard/sidebarImgs/redirect.svg b/client/public/dashboard/sidebarImgs/redirect.svg
similarity index 100%
rename from client/src/components/dashboard/sidebarImgs/redirect.svg
rename to client/public/dashboard/sidebarImgs/redirect.svg
diff --git a/client/src/components/dashboard/sidebarImgs/settings.svg b/client/public/dashboard/sidebarImgs/settings.svg
similarity index 100%
rename from client/src/components/dashboard/sidebarImgs/settings.svg
rename to client/public/dashboard/sidebarImgs/settings.svg
diff --git a/client/src/components/dashboard/sidebarImgs/students.svg b/client/public/dashboard/sidebarImgs/students.svg
similarity index 100%
rename from client/src/components/dashboard/sidebarImgs/students.svg
rename to client/public/dashboard/sidebarImgs/students.svg
diff --git a/client/src/components/dashboard/sidebarImgs/teachers.svg b/client/public/dashboard/sidebarImgs/teachers.svg
similarity index 100%
rename from client/src/components/dashboard/sidebarImgs/teachers.svg
rename to client/public/dashboard/sidebarImgs/teachers.svg
diff --git a/client/src/components/login/logo.png b/client/public/logo.png
similarity index 100%
rename from client/src/components/login/logo.png
rename to client/public/logo.png
diff --git a/client/src/components/navbar/bookings_img.svg b/client/public/navbar/bookings_img.svg
similarity index 100%
rename from client/src/components/navbar/bookings_img.svg
rename to client/public/navbar/bookings_img.svg
diff --git a/client/src/components/navbar/dashboard_img.svg b/client/public/navbar/dashboard_img.svg
similarity index 100%
rename from client/src/components/navbar/dashboard_img.svg
rename to client/public/navbar/dashboard_img.svg
diff --git a/client/src/components/navbar/discovery_img.svg b/client/public/navbar/discovery_img.svg
similarity index 100%
rename from client/src/components/navbar/discovery_img.svg
rename to client/public/navbar/discovery_img.svg
diff --git a/client/src/components/navbar/profile_img.svg b/client/public/navbar/profile_img.svg
similarity index 100%
rename from client/src/components/navbar/profile_img.svg
rename to client/public/navbar/profile_img.svg
diff --git a/client/src/components/navbar/resources_img.svg b/client/public/navbar/resources_img.svg
similarity index 100%
rename from client/src/components/navbar/resources_img.svg
rename to client/public/navbar/resources_img.svg
diff --git a/client/src/components/profile/arrow.svg b/client/public/profile/arrow.svg
similarity index 100%
rename from client/src/components/profile/arrow.svg
rename to client/public/profile/arrow.svg
diff --git a/client/public/student.png b/client/public/student.png
new file mode 100644
index 00000000..d53038a7
Binary files /dev/null and b/client/public/student.png differ
diff --git a/client/public/teacher.png b/client/public/teacher.png
new file mode 100644
index 00000000..81ceae34
Binary files /dev/null and b/client/public/teacher.png differ
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 862ccc7a..09c3dc38 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -16,8 +16,8 @@ import ClassDashboard, {
OverallClassDashboard,
} from "./components/dashboard/classDashboard/ClassDashboard";
import ClassInfoDashboard from "./components/dashboard/classInfoDashboard/ClassInfoDashboard";
-import EventInfoDashboard from "./components/dashboard/eventInfoDashboard/EventInfoDashboard";
import { Dashboard, DashboardHome } from "./components/dashboard/Dashboard";
+import EventInfoDashboard from "./components/dashboard/eventInfoDashboard/EventInfoDashboard";
import SettingsDashboard from "./components/dashboard/settingsDashboard/SettingsDashboard";
import { StudentDashboard } from "./components/dashboard/studentDashboard/StudentDashboard";
import { StudentInfoDashboard } from "./components/dashboard/studentInfoDashboard/StudentInfoDashboard";
@@ -26,7 +26,6 @@ import { TeacherInfoDashboard } from "./components/dashboard/teacherInfoDashboar
import { Discovery } from "./components/discovery/Discovery";
import { CreateEvent } from "./components/forms/createEvent";
import { Login } from "./components/login/Login";
-import { Landing } from "./components/signup/Landing";
import { L } from "./components/logout/Logout";
import { Playground } from "./components/playground/Playground";
import { Profile } from "./components/profile/Profile";
@@ -34,14 +33,15 @@ import { Settings } from "./components/profile/Settings";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { Resources } from "./components/resources/Resources";
import { Reviews } from "./components/reviews/Reviews";
+import { Landing } from "./components/signup/Landing";
import { Signup } from "./components/signup/Signup";
import Request from "./components/teacher-signup/requests/Request";
import { TeacherSignup } from "./components/teacher-signup/TeacherSignup";
import { AuthProvider } from "./contexts/AuthContext";
import { BackendProvider } from "./contexts/BackendContext";
import { RoleProvider } from "./contexts/RoleContext";
-import { ForgotPasswordConfirmation } from "./utils/auth/ForgotPasswordConfirmation"
import { ForgotPassword } from "./utils/auth/ForgotPassword";
+import { ForgotPasswordConfirmation } from "./utils/auth/ForgotPasswordConfirmation";
const App = () => {
return (
diff --git a/client/src/components/CatchAll.tsx b/client/src/components/CatchAll.tsx
index bea4831f..10a3edb0 100644
--- a/client/src/components/CatchAll.tsx
+++ b/client/src/components/CatchAll.tsx
@@ -6,7 +6,8 @@ export const CatchAll = () => {
const navigate = useNavigate();
useEffect(() => {
- navigate("/dashboard");
+ // navigate("/dashboard");
+ navigate("/profile");
}, [navigate]);
return
Route not found... redirecting...
;
diff --git a/client/src/components/ProtectedRoute.tsx b/client/src/components/ProtectedRoute.tsx
index b186c633..70de22c8 100644
--- a/client/src/components/ProtectedRoute.tsx
+++ b/client/src/components/ProtectedRoute.tsx
@@ -20,7 +20,8 @@ export const ProtectedRoute = ({
return currentUser && isValidRole ? (
element
) : currentUser ? (
-
+ //
+
) : (
);
diff --git a/client/src/components/bookings/Bookings.jsx b/client/src/components/bookings/Bookings.jsx
index ae58a7c3..43dcc8fa 100644
--- a/client/src/components/bookings/Bookings.jsx
+++ b/client/src/components/bookings/Bookings.jsx
@@ -1,18 +1,14 @@
-import { memo, useEffect, useState } from "react";
+import { useEffect, useState } from "react";
import {
- Badge,
Box,
- Button,
Card,
CardBody,
CardHeader,
+ Center,
Flex,
Heading,
HStack,
- Input,
- InputGroup,
- InputLeftElement,
Modal,
ModalBody,
ModalContent,
@@ -25,40 +21,40 @@ import {
Tabs,
Text,
useDisclosure,
- useToast,
VStack,
} from "@chakra-ui/react";
-import {
- FaClock,
- FaMapMarkerAlt,
- FaMicrophoneAlt,
- FaMusic,
- FaSearch,
- FaUser,
-} from "react-icons/fa";
-import {
- GiAbstract001,
- GiBallerinaShoes,
- GiBoombox,
- GiCartwheel,
- GiTambourine,
-} from "react-icons/gi";
-import { MdAdd, MdArrowBackIosNew, MdMoreHoriz } from "react-icons/md";
+// import {
+// FaClock,
+// FaMapMarkerAlt,
+// FaMicrophoneAlt,
+// FaMusic,
+// FaSearch,
+// FaUser,
+// } from "react-icons/fa";
+// import {
+// GiAbstract001,
+// GiBallerinaShoes,
+// GiBoombox,
+// GiCartwheel,
+// GiTambourine,
+// } from "react-icons/gi";
+import { MdArrowBackIosNew, MdMoreHoriz } from "react-icons/md";
import { useNavigate } from "react-router-dom";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
-import { formatDate, formatTime } from "../../utils/formatDateTime";
+// import { formatDate, formatTime } from "../../utils/formatDateTime";
import { CreateClassForm } from "../forms/createClasses";
import CreateEvent from "../forms/createEvent";
import { Navbar } from "../navbar/Navbar";
+// import { InfoModal } from "./InfoModal";
import { SearchBar } from "../searchbar/SearchBar";
import { ClassCard } from "../shared/ClassCard";
import { EventCard } from "../shared/EventCard";
import { CancelModal } from "./CancelModal";
+import { ClassTeacherCard } from "./ClassTeacherCard";
import { ConfirmationModal } from "./ConfirmationModal";
-import { InfoModal } from "./InfoModal";
import { TeacherCancelModal } from "./TeacherCancelModal";
import { TeacherConfirmationModal } from "./TeacherConfirmationModal";
import { TeacherEditModal } from "./TeacherEditModal";
@@ -66,12 +62,10 @@ import { TeacherViewModal } from "./TeacherViewModal";
import { ViewModal } from "./ViewModal";
export const Bookings = () => {
- const toast = useToast();
const navigate = useNavigate();
const { isOpen, onOpen, onClose } = useDisclosure();
const { currentUser, role } = useAuthContext();
const { backend } = useBackendContext();
- const [loading, setLoading] = useState(true);
const [currentModal, setCurrentModal] = useState("view");
const [classes, setClasses] = useState([]);
const [events, setEvents] = useState([]);
@@ -89,99 +83,182 @@ export const Bookings = () => {
const [coreqId, setCoreqId] = useState();
const [tags, setTags] = useState([]);
const [tagFilter, setTagFilter] = useState({});
- const [lastToggledTag, setLastToggledTag] = useState(null);
const [classTagsMap, setClassTagsMap] = useState({});
const [eventTagsMap, setEventTagsMap] = useState({});
- const [refresh, setRefresh] = useState(0);
+ const [refresh, setRefresh] = useState(-1);
+ const [magic, setMagic] = useState(-1);
- const isTeacher = role === "teacher";
- const [activeTab, setActiveTab] = useState("classes");
+ const reloadStudentClasses = async () => {
+ try {
+ const [classesRes, classTagsRes] = await Promise.all([
+ backend.get(`/class-enrollments/student/${user_id}`),
+ backend.get(`/class-tags/enrolled-class-tags/${user_id}`),
+ ]);
+ setClasses(classesRes.data);
+ const newClassTagsMap = {};
+ classTagsRes.data.forEach((item) => {
+ newClassTagsMap[item.classId] = item.tagArray;
+ });
+ setClassTagsMap(newClassTagsMap);
+ } catch (error) {
+ console.error("Error reloading student classes:", error);
+ }
+ };
- const toggleClasses = () => {
- setActiveTab("classes");
+ const reloadStudentEvents = async () => {
+ try {
+ const [eventsRes, eventTagsRes] = await Promise.all([
+ backend.get(`/event-enrollments/student/${user_id}`),
+ backend.get(`/event-tags/enrolled-event-tags/${user_id}`),
+ ]);
+ setEvents(eventsRes.data);
+ const newEventTagsMap = {};
+ eventTagsRes.data.forEach((item) => {
+ newEventTagsMap[item.eventId] = item.tagArray;
+ });
+ setEventTagsMap(newEventTagsMap);
+ } catch (error) {
+ console.error("Error reloading student events:", error);
+ }
};
- const toggleEvents = () => {
- setActiveTab("events");
+ const reloadTeacherClasses = async () => {
+ try {
+ const [classesRes, classDraftsRes, classTagsRes] = await Promise.all([
+ backend.get(`/classes/published`),
+ backend.get(`/classes/drafts`),
+ backend.get(`/class-tags/all-class-tags`),
+ ]);
+ setClasses(classesRes.data);
+ setDraftClasses(classDraftsRes.data);
+ const newClassTagsMap = {};
+ classTagsRes.data.forEach((item) => {
+ newClassTagsMap[item.classId] = item.tagArray;
+ });
+ setClassTagsMap(newClassTagsMap);
+ } catch (error) {
+ console.error("Error reloading teacher classes:", error);
+ }
+ };
+
+ const reloadTeacherEvents = async () => {
+ try {
+ const [eventsRes, eventDraftsRes, eventTagsRes] = await Promise.all([
+ backend.get(`/events/published`),
+ backend.get(`/events/drafts`),
+ backend.get(`/event-tags/all-event-tags`),
+ ]);
+ setEvents(eventsRes.data);
+ setDraftEvents(eventDraftsRes.data);
+ const newEventTagsMap = {};
+ eventTagsRes.data.forEach((item) => {
+ newEventTagsMap[item.eventId] = item.tagArray;
+ });
+ setEventTagsMap(newEventTagsMap);
+ } catch (error) {
+ console.error("Error reloading teacher events:", error);
+ }
};
useEffect(() => {
+ if (!role) return;
if (currentUser && role !== "student") {
- backend.get(`/events/published`).then((res) => setEvents(res.data));
- backend.get(`/classes/published`).then((res) => setClasses(res.data));
- backend.get(`/events/drafts`).then((res) => setDraftEvents(res.data));
- backend.get(`/classes/drafts`).then((res) => setDraftClasses(res.data));
- backend.get("/events").then((res) => setAllEvents(res.data));
+ // First get all classes and events
+ const fetchData = async () => {
+ try {
+ // Get all the basic data
+ const [
+ classesRes,
+ eventsRes,
+ draftEventsRes,
+ draftClassesRes,
+ allEventsRes,
+ ] = await Promise.all([
+ backend.get(`/classes/published`),
+ backend.get(`/events/published`),
+ backend.get(`/events/drafts`),
+ backend.get(`/classes/drafts`),
+ backend.get("/events/all"),
+ ]);
+
+ const allClasses = classesRes.data;
+ const allDraftClasses = draftClassesRes.data;
+
+ const newClassTagsMap = {};
+ const classTagsRes = await backend.get("/class-tags/all-class-tags");
+ classTagsRes.data.forEach((item) => {
+ newClassTagsMap[item.classId] = item.tagArray;
+ });
+ // console.log("Class Tags Map:", newClassTagsMap);
+ const newEventTagsMap = {};
+ const eventTagsRes = await backend.get("/event-tags/all-event-tags");
+ eventTagsRes.data.forEach((item) => {
+ newEventTagsMap[item.eventId] = item.tagArray;
+ });
+ // console.log("Event Tags Map:", newEventTagsMap);
+
+ // Set all the state
+ setClassTagsMap(newClassTagsMap);
+ setEventTagsMap(newEventTagsMap);
+ setClasses(allClasses);
+ setEvents(eventsRes.data);
+ setDraftEvents(draftEventsRes.data);
+ setDraftClasses(allDraftClasses);
+ setAllEvents(allEventsRes.data);
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ }
+ };
+
+ fetchData();
} else if (currentUser && role === "student") {
- backend
- .get(`/users/${currentUser.uid}`)
- .then((userRes) => {
+ const fetchData = async () => {
+ try {
+ const userRes = await backend.get(`/users/${currentUser.uid}`);
const userId = userRes.data[0].id;
setUserId(userId);
-
- backend
- .get(`/class-enrollments/student/${userId}`)
- .then(async (res) => {
- const enrolledClasses = res.data;
- setClasses(enrolledClasses);
-
- const tagsPromises = enrolledClasses.map((cls) =>
- backend.get(`/class-tags/tags/${cls.id}`)
- );
-
- const tagsResults = await Promise.all(tagsPromises);
- const newClassTagsMap = {};
-
- enrolledClasses.forEach((cls, index) => {
- newClassTagsMap[cls.id] = tagsResults[index].data.map(
- (tag) => tag.id
- );
- });
-
- setClassTagsMap(newClassTagsMap);
- })
- .catch((err) => {
- console.log("Error fetching class enrollments:", err);
- });
-
- backend
- .get(`/event-enrollments/student/${userId}`)
- .then(async (res) => {
- const enrolledEvents = res.data;
- setEvents(enrolledEvents);
-
- const tagsPromises = enrolledEvents.map((evt) =>
- backend.get(`/event-tags/tags/${evt.id}`)
- );
-
- const tagsResults = await Promise.all(tagsPromises);
- const newEventTagsMap = {};
-
- enrolledEvents.forEach((evt, index) => {
- newEventTagsMap[evt.id] = tagsResults[index].data.map(
- (tag) => tag.id
- );
- });
-
- setEventTagsMap(newEventTagsMap);
- })
- .catch((err) => {
- console.log("Error fetching event enrollments:", err);
- });
- })
- .catch((err) => {
- console.log("Error fetching user:", err);
- });
+ const [enrolledClassesRes, enrolledEventsRes] = await Promise.all([
+ backend.get(`/class-enrollments/student/${userId}`),
+ backend.get(`/event-enrollments/student/${userId}`),
+ ]);
+ const enrolledClasses = enrolledClassesRes.data;
+ const enrolledEvents = enrolledEventsRes.data;
+ setClasses(enrolledClasses);
+ setEvents(enrolledEvents);
+
+ const newClassTagsMap = {};
+ const newEventTagsMap = {};
+ const classTagsRes = await backend.get(
+ `/class-tags/enrolled-class-tags/${userId}`
+ );
+ classTagsRes.data.forEach((item) => {
+ newClassTagsMap[item.classId] = item.tagArray;
+ });
+ // console.log("Student Class Tags Map:", newClassTagsMap);
+ const eventTagsRes = await backend.get(
+ `/event-tags/enrolled-event-tags/${userId}`
+ );
+ eventTagsRes.data.forEach((item) => {
+ newEventTagsMap[item.eventId] = item.tagArray;
+ });
+ // console.log("Student Event Tags Map:", newEventTagsMap);
+ setClassTagsMap(newClassTagsMap);
+ setEventTagsMap(newEventTagsMap);
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ }
+ };
+ fetchData();
}
- }, [backend, currentUser, isTeacher, refresh]);
+ }, [backend, currentUser, refresh, role]);
useEffect(() => {
const attendedClasses = classes.filter((c) => c.attendance !== null);
const attendedEvents = events.filter((e) => e.attendance !== null);
setAttended([...attendedClasses, ...attendedEvents]);
setDrafts([...draftClasses, ...draftEvents]);
- }, [classes, events]);
+ }, [classes, events, draftClasses, draftEvents]);
useEffect(() => {
const fetchTags = async () => {
@@ -197,15 +274,14 @@ export const Bookings = () => {
setTagFilter(initialTagFilter);
setTags(initialTags);
- // console.log(initialTags);
} catch (error) {
console.error("Error fetching tags:", error);
}
};
fetchTags();
- }, []);
+ }, [backend]);
- const handleFilterToggle = (id) => {
+ const handleFilterToggle = (id) => () => {
setTagFilter((prev) => ({
...prev,
[id]: !prev[id],
@@ -222,10 +298,8 @@ export const Bookings = () => {
if (data.length > 0) {
const eventId = data[0].eventId;
- console.log("Fetched coreqId:", eventId);
setCoreqId(eventId);
} else {
- console.log("No corequisite found for class:", selectedCard.id);
setCoreqId(null);
}
} catch (err) {
@@ -238,15 +312,11 @@ export const Bookings = () => {
}, [backend, selectedCard, isOpen]);
const onCloseModal = (type) => {
- setSelectedCard(null);
- setCurrentModal("view");
- onClose();
- reloadClassesAndDrafts();
if (type !== 3) {
toast({
- title: type == 0 ? "Class Published." : "Event Published.", // 0 == "class" || 1 == "event"
+ title: tabIndex === 0 ? "Class Published." : "Event Published.", // 0 == "class" || 1 == "event"
description:
- type == 0
+ type === 0
? "Class is visible to students."
: "Event is visible to students.",
status: "success",
@@ -255,55 +325,162 @@ export const Bookings = () => {
position: "top",
});
}
+ if (tabIndex === 0) {
+ if (role !== "student") {
+ Promise.all([reloadTeacherClasses()]).then(() => {
+ setCurrentModal("view");
+ onClose();
+ });
+ } else {
+ Promise.all([reloadStudentClasses()]).then(() => {
+ setCurrentModal("view");
+ onClose();
+ });
+ }
+ } else if (tabIndex === 1) {
+ if (role !== "student") {
+ Promise.all([reloadTeacherEvents()]).then(() => {
+ setSelectedCard(null);
+ setCurrentModal("view");
+ onClose();
+ });
+ } else {
+ Promise.all([reloadStudentEvents()]).then(() => {
+ setSelectedCard(null);
+ setCurrentModal("view");
+ onClose();
+ });
+ }
+ } else {
+ if (role !== "student") {
+ Promise.all([reloadTeacherClasses(), reloadTeacherEvents()]).then(
+ () => {
+ setSelectedCard(null);
+ setCurrentModal("view");
+ onClose();
+ }
+ );
+ } else {
+ Promise.all([reloadStudentClasses(), reloadStudentEvents()]).then(
+ () => {
+ setSelectedCard(null);
+ setCurrentModal("view");
+ onClose();
+ }
+ );
+ }
+ }
+ console.log(classTagsMap);
};
- const onOpenModal = (data) => {
- setClassData(data);
- onOpen();
- };
-
- const triggerRefresh = () => {
- setRefresh(refresh + 1);
- console.log("Refresh triggered");
- };
- // https://dmitripavlutin.com/how-to-compare-objects-in-javascript/#4-deep-equality
- const deepEquality = (object1, object2) => {
- if (object1 === null || object2 === null) return object1 === object2;
- const keys1 = Object.keys(object1);
- const keys2 = Object.keys(object2);
- if (keys1.length !== keys2.length) {
- return false;
+ const onCloseEditModal = () => {
+ if (tabIndex === 0) {
+ if (role !== "student") {
+ Promise.all([reloadTeacherClasses()]).then(() => {
+ setCurrentModal("view");
+ setMagic((prev) => -1 * prev);
+ });
+ } else {
+ Promise.all([reloadStudentClasses()]).then(() => {
+ setCurrentModal("view");
+ setMagic((prev) => -1 * prev);
+ });
+ }
+ } else if (tabIndex === 1) {
+ if (role !== "student") {
+ Promise.all([reloadTeacherEvents()]).then(() => {
+ setCurrentModal("view");
+ setMagic((prev) => -1 * prev);
+ });
+ } else {
+ Promise.all([reloadStudentEvents()]).then(() => {
+ setCurrentModal("view");
+ setMagic((prev) => -1 * prev);
+ });
+ }
+ } else {
+ if (role !== "student") {
+ Promise.all([reloadTeacherClasses(), reloadTeacherEvents()]).then(
+ () => {
+ setCurrentModal("view");
+ setMagic((prev) => -1 * prev);
+ }
+ );
+ } else {
+ Promise.all([reloadStudentClasses(), reloadStudentEvents()]).then(
+ () => {
+ setCurrentModal("view");
+ setMagic((prev) => -1 * prev);
+ }
+ );
+ }
}
+ };
- for (const key of keys1) {
- const val1 = object1[key];
- const val2 = object2[key];
- const areObjects = typeof val1 === "object" && typeof val2 === "object";
- if (
- (areObjects && !deepEquality(val1, val2)) ||
- (!areObjects && val1 !== val2)
- ) {
- return false;
+ const triggerRefresh = () => {
+ if (tabIndex === 0) {
+ if (role !== "student") {
+ Promise.all([reloadTeacherClasses()]).then(() => {
+ setRefresh((prev) => -1 * prev);
+ });
+ } else {
+ Promise.all([reloadStudentClasses()]).then(() => {
+ setRefresh((prev) => -1 * prev);
+ });
+ }
+ } else if (tabIndex === 1) {
+ if (role !== "student") {
+ Promise.all([reloadTeacherEvents()]).then(() => {
+ setRefresh((prev) => -1 * prev);
+ });
+ } else {
+ Promise.all([reloadStudentEvents()]).then(() => {
+ setRefresh((prev) => -1 * prev);
+ });
+ }
+ } else {
+ if (role !== "student") {
+ Promise.all([reloadTeacherClasses(), reloadTeacherEvents()]).then(
+ () => {
+ setRefresh((prev) => -1 * prev);
+ }
+ );
+ } else {
+ Promise.all([reloadStudentClasses(), reloadStudentEvents()]).then(
+ () => {
+ setRefresh((prev) => -1 * prev);
+ }
+ );
}
}
-
- return true;
};
- const updateModal = (item) => {
- const type =
- classes.some((e) => deepEquality(e, item)) ||
- draftClasses.some((e) => deepEquality(e, item))
- ? "class"
- : "event";
- console.log(
- "update",
- item,
- classes,
- draftClasses,
- type,
- deepEquality(classes[0], item)
- );
+ // https://dmitripavlutin.com/how-to-compare-objects-in-javascript/#4-deep-equality
+ // const deepequality = (object1, object2) => {
+ // if (object1 === null || object2 === null) return object1 === object2;
+ // const keys1 = Object.keys(object1);
+ // const keys2 = Object.keys(object2);
+
+ // if (keys1.length !== keys2.length) {
+ // return false;
+ // }
+
+ // for (const key of keys1) {
+ // const val1 = object1[key];
+ // const val2 = object2[key];
+ // const areObjects = typeof val1 === "object" && typeof val2 === "object";
+ // if (
+ // (areObjects && !deepEquality(val1, val2)) ||
+ // (!areObjects && val1 !== val2)
+ // ) {
+ // return false;
+ // }
+ // }
+
+ // return true;
+ // };
+
+ const updateModal = (item, type = "class") => {
if (type === "class") loadCorequisites(item.id);
setSelectedCard(item);
setCardType(type);
@@ -346,60 +523,84 @@ export const Bookings = () => {
}
};
- // const isFilterActive = Object.values(tagFilter).some(Boolean);
-
const loadCorequisites = async (classId) => {
try {
const response = await backend.get(`classes/corequisites/${classId}`);
if (response.status === 200) {
setCoEvents(response.data);
- console.log(coEvents);
}
} catch (error) {
console.error("Error fetching corequisite enrollment:", error);
}
};
-
const handleClassSearch = async (query) => {
- if (currentUser && role === "student") {
- // For students, search within their enrolled classes
- const enrolledRes = await backend.get(
- `/class-enrollments/student/${user_id}`
- );
- const allEnrolledClasses = enrolledRes.data;
+ try {
+ if (!query || query.trim() === "") {
+ // For empty search, reload the appropriate data based on role
+ if (role === "student") {
+ await reloadStudentClasses();
+ } else {
+ await reloadTeacherClasses();
+ }
+ return;
+ }
- // Client-side search filtering
- const filteredClasses = allEnrolledClasses.filter((cls) =>
- cls.title.toLowerCase().includes(query.toLowerCase())
- );
+ if (currentUser && role === "student") {
+ // For students, search within their enrolled classes
+ const enrolledRes = await backend.get(
+ `/class-enrollments/student/${user_id}`
+ );
+ const allEnrolledClasses = enrolledRes.data;
- setClasses(filteredClasses);
- } else {
- // For teachers/admin, keep the existing server-side search
- const searchRes = await backend.get(`/classes/search/${query}`);
- setClasses(searchRes.data);
+ // Client-side search filtering
+ const filteredClasses = allEnrolledClasses.filter((cls) =>
+ cls.title.toLowerCase().includes(query.trim().toLowerCase())
+ );
+
+ setClasses(filteredClasses);
+ } else {
+ // For teachers/admin, use server-side search
+ const searchRes = await backend.get(`/classes/search/${query.trim()}`);
+ setClasses(searchRes.data);
+ }
+ } catch (error) {
+ console.error("Error searching classes:", error);
}
};
const handleEventSearch = async (query) => {
- if (currentUser && role === "student") {
- // For students, search within their enrolled events
- const enrolledRes = await backend.get(
- `/event-enrollments/student/${user_id}`
- );
- const allEnrolledEvents = enrolledRes.data;
+ try {
+ if (!query || query.trim() === "") {
+ // For empty search, reload the appropriate data based on role
+ if (role === "student") {
+ await reloadStudentEvents();
+ } else {
+ await reloadTeacherEvents();
+ }
+ return;
+ }
- // Client-side search filtering
- const filteredEvents = allEnrolledEvents.filter((evt) =>
- evt.title.toLowerCase().includes(query.toLowerCase())
- );
+ if (currentUser && role === "student") {
+ // For students, search within their enrolled events
+ const enrolledRes = await backend.get(
+ `/event-enrollments/student/${user_id}`
+ );
+ const allEnrolledEvents = enrolledRes.data;
- setEvents(filteredEvents);
- } else {
- // For teachers/admin, keep the existing server-side search
- const searchRes = await backend.get(`/events/search/${query}`);
- setEvents(searchRes.data);
+ // Client-side search filtering
+ const filteredEvents = allEnrolledEvents.filter((evt) =>
+ evt.title.toLowerCase().includes(query.trim().toLowerCase())
+ );
+
+ setEvents(filteredEvents);
+ } else {
+ // For teachers/admin, use server-side search
+ const searchRes = await backend.get(`/events/search/${query.trim()}`);
+ setEvents(searchRes.data);
+ }
+ } catch (error) {
+ console.error("Error searching events:", error);
}
};
@@ -417,164 +618,61 @@ export const Bookings = () => {
setAttended([...attendedClasses, ...attendedEvents]);
setDrafts([...draftClasses, ...draftEvents]);
if (selectedCard) loadCorequisites(selectedCard.id);
+ // console.log(attended);
} catch (error) {
console.error("Error reloading classes:", error);
}
};
- const reloadClasses = async () => {
- await backend.get(`/classes/published`).then((res) => {
- setClasses(res.data);
- });
-
- const attendedClasses = classes.filter((c) => c.attendance !== null);
- const attendedEvents = events.filter((e) => e.attendance !== null);
- setAttended([...attendedClasses, ...attendedEvents]);
-
- if (selectedCard) {
- loadCorequisites(selectedCard.id);
- }
- };
-
- const reloadEvents = async () => {
- await backend.get(`/events/published`).then((res) => {
- setEvents(res.data);
- });
-
- const attendedClasses = classes.filter((c) => c.attendance !== null);
- const attendedEvents = events.filter((e) => e.attendance !== null);
- setAttended([...attendedClasses, ...attendedEvents]);
-
- if (selectedCard) {
- loadCorequisites(selectedCard.id);
- }
- };
-
- // useEffect(() => {
- // console.log("selectedCard", selectedCard);
- // }, [selectedCard]);
-
- const fetchClassData = async () => {
- try {
- const [classesResponse, classDataResponse] = await Promise.all([
- backend.get("/scheduled-classes"),
- backend.get("/classes"),
- ]);
-
- const classDataDict = new Map();
- classDataResponse.data.forEach((cls) => classDataDict.set(cls.id, cls));
-
- console.log("Fetching tags for class:", clsId);
- const response = await backend.get(`/class-tags/tags/${clsId}`);
- console.log("Raw tag data:", response.data);
-
- const formattedData = classesResponse.data
- .map((cls) => {
- const fullData = classDataDict.get(cls.classId);
-
- return fullData
- ? {
- classId: cls.classId,
- date: stringToDate(cls.date),
- startTime: stringToTime(cls.startTime),
- endTime: stringToTime(cls.endTime),
- title: fullData.title,
- description: fullData.description,
- location: fullData.location,
- capacity: fullData.capacity,
- level: fullData.level,
- costume: fullData.costume,
- isDraft: fullData.isDraft,
- }
- : null;
- })
- .filter(Boolean);
-
- setClasses(formattedData);
- } catch (error) {
- console.error("Error fetching class data:", error);
- }
- };
-
- const stringToDate = (date) => {
- return new Date(date);
- };
-
- const stringToTime = (time) => {
- const [hours, minutes] = time.split(":");
- const d = new Date();
- d.setHours(hours, minutes, 0);
-
- return d;
- };
-
return (
-
-
+
setTabIndex(index)}
>
-
-
- Classes
-
-
- Events
-
-
- {role !== "student" ? "Drafts" : "Attended"}
-
-
- {/*
-
-
-
-
- setSearchInput(e.target.value)}
- onKeyDown={handleKeyDown}
- />
-
- */}
+
+
+
+ Classes
+
+
+ Events
+
+
+ {role !== "student" ? "Drafts" : "Attended"}
+
+
+
+
{
>
{role !== "student" && (
{
setSelectedCard(null);
@@ -620,28 +719,43 @@ export const Bookings = () => {
)}
{role !== "student" ? (
classes.length > 0 ? (
- classes.map((classItem, index) => (
-
- ))
+ classes.map((classItem, index) => {
+ const isFilterActive =
+ Object.values(tagFilter).some(Boolean);
+ const classTags = classTagsMap[classItem.id] || [];
+
+ if (
+ !isFilterActive ||
+ classTags.some((tag) => tagFilter[tag.id])
+ ) {
+ return (
+ {
+ updateModal(classItem, "class");
+ }}
+ tags={classTags}
+ />
+ );
+ }
+ return null;
+ })
) : (
No classes available.
)
) : classes.length > 0 ? (
classes.map((classItem) => {
- const isFilterActive =
- Object.values(tagFilter).some(Boolean);
const classTags = classTagsMap[classItem.id] || [];
if (
- !isFilterActive ||
- classTags.some((tagId) => tagFilter[tagId])
+ !Object.values(tagFilter).some(Boolean) ||
+ classTags.some((tag) => tagFilter[tag.id])
) {
return (
{
>
updateModal(classItem)}
+ onClick={() => {
+ updateModal(classItem, "class");
+ }}
triggerRefresh={triggerRefresh}
onCloseModal={onCloseModal}
+ tags={classTags}
/>
);
@@ -683,7 +800,8 @@ export const Bookings = () => {
>
{role !== "student" && (
{
setSelectedCard(null);
@@ -708,24 +826,26 @@ export const Bookings = () => {
- )}
+ )}{" "}
{events.length > 0 ? (
events.map((eventItem) => {
- const isFilterActive =
- Object.values(tagFilter).some(Boolean);
const eventTags = eventTagsMap[eventItem.id] || [];
if (
- !isFilterActive ||
- eventTags.some((tagId) => tagFilter[tagId])
+ !Object.values(tagFilter).some(Boolean) ||
+ eventTags.some((tag) => tagFilter[tag.id])
) {
return (
updateModal(eventItem)}
+ onClick={() => {
+ updateModal(eventItem, "event");
+ }}
+ magic={refresh}
triggerRefresh={triggerRefresh}
onCloseModal={onCloseModal}
+ tags={eventTags}
/>
);
}
@@ -745,56 +865,78 @@ export const Bookings = () => {
mb={20}
justifyContent={"center"}
>
+ {" "}
{role !== "student" ? (
drafts.length > 0 ? (
- drafts.map((item) =>
- !item.callTime ? (
- updateModal(item)}
- />
- ) : (
- updateModal(item)}
- triggerRefresh={triggerRefresh}
- onCloseModal={onCloseModal}
- // setRefresh={reloadClassesAndDrafts}
- />
- )
- )
+ drafts.map((item) => {
+ const classTags = classTagsMap[item.id] || [];
+
+ if (!item.callTime) {
+ return (
+ updateModal(item, "class")}
+ setSelectedCard={setSelectedCard}
+ performance={coEvents}
+ onOpen={updateModal}
+ tags={classTags}
+ />
+ );
+ } else if (item.callTime) {
+ return (
+ {
+ updateModal(item, "event");
+ }}
+ magic={refresh}
+ triggerRefresh={triggerRefresh}
+ onCloseModal={onCloseModal}
+ tags={eventTagsMap[item.id] || []}
+ />
+ );
+ }
+ return null;
+ })
) : (
No draft events or classes
)
) : attended.length > 0 ? (
- attended.map((item) =>
- item.class_id ? (
+ attended.map((item) => {
+ return !item.callTime ? (
updateModal(item)}
+ onClick={() => {
+ updateModal(item, "class");
+ }}
+ tags={classTagsMap[item.id] || []}
/>
) : (
updateModal(item)}
+ onClick={() => {
+ updateModal(item, "event");
+ }}
+ magic={refresh}
triggerRefresh={triggerRefresh}
+ tags={eventTagsMap[item.id] || []}
/>
- )
- )
+ );
+ })
) : (
No attended classes or events.
)}
@@ -802,306 +944,114 @@ export const Bookings = () => {
-
- {role !== "student" ? (
+ {role !== "student" ? (
+ currentModal === "view" ? (
+ // this is always going to be the view for classes
+ // events view modal is handled in the event card component
+
+ ) : currentModal === "confirmation" ? (
+
+ ) : currentModal === "edit" ? (
+
+ ) : currentModal === "create" ? (
+
+
+
+
+
+ onCloseModal(3)} />
+
+ {tabIndex === 0 ? "New Class" : "New Event"}
+ {" "}
+ {/* Will add from prop */}
+
+
+
+
+ {tabIndex === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+ ) : (
+
+ )
+ ) : // STUDENT VIEW HERE
currentModal === "view" ? (
-
) : currentModal === "confirmation" ? (
-
- ) : currentModal === "edit" ? (
-
- ) : currentModal === "create" ? (
-
-
-
-
-
- onCloseModal(3)} />
-
- {tabIndex === 0 ? "New Class" : "New Event"}
- {" "}
- {/* Will add from prop */}
-
-
-
-
- {tabIndex === 0 ? (
-
- ) : (
-
- )}
-
-
-
) : (
- handleCancelEnrollment(selectedCard.id)}
+ type={cardType}
/>
- )
- ) : // STUDENT VIEW HERE
- currentModal === "view" ? (
-
- ) : currentModal === "confirmation" ? (
-
- ) : (
- handleCancelEnrollment(selectedCard.id)}
- type={cardType}
- />
- )}
+ )}
+
);
};
-
-const ClassTeacherCard = memo(
- ({
- id,
- title,
- location,
- date,
- description,
- capacity,
- level,
- costume,
- performance,
- rsvpCount,
- isDraft,
- recurrencePattern,
- isRecurring,
- startDate,
- endDate,
- startTime,
- endTime,
- navigate,
- setSelectedCard,
- tagId,
- onOpen,
- }) => {
- const formattedDate = date ? formatDate(date) : null;
- const formattedStartTime = startTime ? formatTime(startTime) : null;
- const formattedEndTime = endTime ? formatTime(endTime) : null;
- const getIcon = () => {
- const iconSize = 50;
- switch (tagId) {
- case 1:
- return ;
- case 2:
- return ;
- case 3:
- return ;
- case 4:
- return ;
- case 5:
- return ;
- case 6:
- return ;
- case 7:
- return ;
- default:
- return ;
- }
- };
- return (
-
- {
- const modalData = {
- id,
- title,
- location,
- date,
- description,
- capacity,
- level,
- costume,
- performances: performance,
- isRecurring,
- recurrencePattern,
- startDate,
- endDate,
- isDraft,
- rsvpCount,
- startTime,
- endTime,
- };
- setSelectedCard(modalData);
- onOpen({
- id,
- title,
- location,
- date,
- description,
- capacity,
- isRecurring,
- recurrencePattern,
- startDate,
- endDate,
- level,
- costume,
- isDraft,
- startTime,
- endTime,
- });
- }
- : () => {
- const modalData = {
- id,
- title,
- location,
- date,
- description,
- capacity,
- level,
- costume,
- isRecurring,
- recurrencePattern,
- performances: performance,
- isDraft,
- startDate,
- endDate,
- rsvpCount,
- startTime,
- endTime,
- };
- setSelectedCard(modalData);
- onOpen({
- id,
- title,
- location,
- date,
- description,
- capacity,
- level,
- isRecurring,
- recurrencePattern,
- costume,
- startDate,
- endDate,
- isDraft,
- startTime,
- endTime,
- });
- }
- // : () => navigate(`/dashboard/classes/${classId}`)
- }
- >
-
-
-
- {rsvpCount ?? 0} {(rsvpCount ?? 0) === 1 ? "Person" : "People"}{" "}
- Enrolled
-
-
-
- {getIcon()}
-
-
- {title}
-
-
-
-
- {location ? `${location}` : "No location"}
-
-
-
-
- {formattedDate
- ? `${formattedDate} · ${formattedStartTime} - ${formattedEndTime}`
- : "No date"}
-
-
-
-
-
-
-
- );
- }
-);
diff --git a/client/src/components/bookings/CancelModal.jsx b/client/src/components/bookings/CancelModal.jsx
index 06930e25..bf8d0039 100644
--- a/client/src/components/bookings/CancelModal.jsx
+++ b/client/src/components/bookings/CancelModal.jsx
@@ -1,51 +1,93 @@
-import { Button, Modal, ModalOverlay, ModalHeader, ModalContent, ModalBody, ModalFooter, Flex, Text } from "@chakra-ui/react";
+import {
+ Button,
+ Flex,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+} from "@chakra-ui/react";
-export const CancelModal = ({ isOpen, onClose, setCurrentModal, card, handleEvent, type }) => {
- const onGoBack = () => {
- setCurrentModal("view");
- };
- const onConfirm = () => {
- setCurrentModal("confirmation");
- handleEvent(); // sql query is dependent on card type (class or event)
- };
+export const CancelModal = ({
+ isOpen,
+ onClose,
+ setCurrentModal,
+ card,
+ handleEvent,
+ type,
+}) => {
+ const onGoBack = () => {
+ setCurrentModal("view");
+ };
+ const onConfirm = () => {
+ setCurrentModal("confirmation");
+ handleEvent(); // sql query is dependent on card type (class or event)
+ };
- return (
-
-
-
- Delete RSVP?
-
- You're cancelling {card ? card.title : "N/A"}. This action can't be undone.
-
+ return (
+
+
+
+
+ {" "}
+ Delete RSVP?
+
+
+ You're cancelling{" "}
+
+ {card ? card.title : "N/A"}
+
+ . This action can't be undone.
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
};
diff --git a/client/src/components/bookings/ClassTeacherCard.jsx b/client/src/components/bookings/ClassTeacherCard.jsx
new file mode 100644
index 00000000..bbf0dd29
--- /dev/null
+++ b/client/src/components/bookings/ClassTeacherCard.jsx
@@ -0,0 +1,185 @@
+import { memo, useEffect, useState } from "react";
+
+import {
+ Badge,
+ Box,
+ Card,
+ Flex,
+ Heading,
+ HStack,
+ Image,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+
+import { FaMicrophoneAlt, FaMusic } from "react-icons/fa";
+import {
+ GiAbstract001,
+ GiBallerinaShoes,
+ GiBoombox,
+ GiCartwheel,
+ GiTambourine,
+} from "react-icons/gi";
+
+import { useAuthContext } from "../../contexts/hooks/useAuthContext";
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
+import { formatDate, formatTime } from "../../utils/formatDateTime";
+
+export const ClassTeacherCard = memo(
+ ({
+ id,
+ title,
+ location,
+ date,
+ description,
+ capacity,
+ level,
+ costume,
+ performance,
+ attendeeCount = 0,
+ isDraft,
+ recurrencePattern,
+ isRecurring,
+ startDate,
+ endDate,
+ startTime,
+ endTime,
+ navigate,
+ setSelectedCard,
+ tags,
+ onOpen,
+ }) => {
+ const [openTeacherModal, setOpenTeacherModal] = useState(false);
+
+ // const closeTeacherModal = () => {
+ // setOpenTeacherModal(false);
+ // };
+
+ const handleClickModal = () => {
+ const modalData = {
+ id,
+ title,
+ location,
+ date,
+ description,
+ capacity,
+ level,
+ costume,
+ performances: performance,
+ isDraft,
+ recurrencePattern,
+ isRecurring,
+ startDate,
+ endDate,
+ attendeeCount,
+ startTime,
+ endTime,
+ };
+ setOpenTeacherModal(true);
+ setSelectedCard(modalData);
+ onOpen(modalData);
+ };
+
+ const formattedDate = date ? formatDate(date) : null;
+ const formattedStartTime = startTime ? formatTime(startTime) : null;
+ const formattedEndTime = endTime ? formatTime(endTime) : null;
+ const getIcon = () => {
+ const iconSize = 50;
+ switch (tags[0]?.id) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ case 5:
+ return ;
+ case 6:
+ return ;
+ case 7:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+ {attendeeCount} {attendeeCount === 1 ? "Person" : "People"} RSVP'd
+
+
+
+ {getIcon()}
+
+
+
+ {title}
+
+
+ {location}
+
+
+ {formattedDate} · {formattedStartTime} – {formattedEndTime}
+
+
+
+
+ );
+ }
+);
diff --git a/client/src/components/bookings/CompletedIndicator.png b/client/src/components/bookings/CompletedIndicator.png
deleted file mode 100644
index 9310fc72..00000000
Binary files a/client/src/components/bookings/CompletedIndicator.png and /dev/null differ
diff --git a/client/src/components/bookings/ConfirmationModal.jsx b/client/src/components/bookings/ConfirmationModal.jsx
index 38595707..c297b469 100644
--- a/client/src/components/bookings/ConfirmationModal.jsx
+++ b/client/src/components/bookings/ConfirmationModal.jsx
@@ -1,38 +1,68 @@
-import { Button, Modal, ModalOverlay, ModalHeader, ModalContent, ModalCloseButton, ModalBody, ModalFooter, Box, Icon, VStack } from "@chakra-ui/react";
-import CompletedIndicator from "./CompletedIndicator.png"
-
+import {
+ Box,
+ Button,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalOverlay,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+import { MdCheckCircle } from "react-icons/md";
export const ConfirmationModal = ({ isOpen, onClose, card }) => {
return (
-
-
-
-
-
-
-
-
-
+ {}}
+ >
+
+
+
+
+
+
+
+
+
+ Sorry to See You Go!
+
+
+ Your RSVP has been removed for {card ? card.title : "N/A"}
+
-
- You've successfully cancelled {card ? card.title : "N/A"}.
-
-
-
-
-
-
+
+
+
+
+
+
);
};
+
+export default ConfirmationModal;
diff --git a/client/src/components/bookings/InfoModal.jsx b/client/src/components/bookings/InfoModal.jsx
index 434ee57b..7e9b79d8 100644
--- a/client/src/components/bookings/InfoModal.jsx
+++ b/client/src/components/bookings/InfoModal.jsx
@@ -12,8 +12,8 @@ import {
ModalOverlay,
Text,
} from "@chakra-ui/react";
-import { formatDate } from "../../utils/formatDateTime";
+import { formatDate } from "../../utils/formatDateTime";
export const InfoModal = ({
isOpen,
diff --git a/client/src/components/bookings/TeacherCancelModal.jsx b/client/src/components/bookings/TeacherCancelModal.jsx
index 9949a6fd..fc23f363 100644
--- a/client/src/components/bookings/TeacherCancelModal.jsx
+++ b/client/src/components/bookings/TeacherCancelModal.jsx
@@ -38,6 +38,7 @@ export const TeacherCancelModal = ({
status: "error",
duration: 4000,
isClosable: true,
+ position: "top",
});
}
};
@@ -51,16 +52,36 @@ export const TeacherCancelModal = ({
Delete class?
- You are deleting {classData.title}. This action can't be undone.
+
+ You are deleting {classData.title}. This action can't be undone.
+
-
-
diff --git a/client/src/components/bookings/TeacherConfirmationModal.jsx b/client/src/components/bookings/TeacherConfirmationModal.jsx
index 8a5cd486..eab021ff 100644
--- a/client/src/components/bookings/TeacherConfirmationModal.jsx
+++ b/client/src/components/bookings/TeacherConfirmationModal.jsx
@@ -1,23 +1,57 @@
-import { Icon, Button, Modal, ModalOverlay, ModalContent, ModalFooter, Flex, Text, VStack } from "@chakra-ui/react";
+import {
+ Button,
+ Flex,
+ Icon,
+ Modal,
+ ModalContent,
+ ModalFooter,
+ ModalOverlay,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
import { BsCheck } from "react-icons/bs";
export const TeacherConfirmationModal = ({ isOpen, onClose }) => {
return (
-
+
-
-
+
+
- Class Deleted
-
-
- View Booked Events
-
+
+ Class Deleted
+
+
+
+ View Booked Events{" "}
+
+
);
-};
\ No newline at end of file
+};
diff --git a/client/src/components/bookings/TeacherEditModal.jsx b/client/src/components/bookings/TeacherEditModal.jsx
index 57017a61..f3778e06 100644
--- a/client/src/components/bookings/TeacherEditModal.jsx
+++ b/client/src/components/bookings/TeacherEditModal.jsx
@@ -66,6 +66,9 @@ export const TeacherEditModal = ({
performances,
setRefresh,
coreqId,
+ tags = [],
+ magic,
+ setMagic,
}) => {
const { backend } = useBackendContext();
const [isPublishing, setIsPublishing] = useState(false);
@@ -77,24 +80,41 @@ export const TeacherEditModal = ({
classData?.instructor ?? ""
);
const formRef = useRef(null);
- const [tags, setTags] = useState([]);
+ const [tagTypes, setTagTypes] = useState([]);
const toast = useToast();
- const [classType, setClassType] = useState(classData?.classType ?? "1");
+ console.log("type", tags[0]?.id);
+ const [classType, setClassType] = useState(tags[0]?.id ?? "-1");
- useMemo(() => {
- if (backend) {
- backend.get("/tags").then((response) => {
- setTags(response.data);
- });
- backend.get("/teachers/activated").then((response) => {
- setTeachers(response.data);
- });
+ useEffect(() => {
+ if (backend && classData?.id) {
+ Promise.all([
+ backend.get("/tags"),
+ backend.get("/teachers/activated"),
+ backend.get(`/classes-taught/instructor/${classData.id}`),
+ ])
+ .then(
+ ([tagResponse, activatedTeachersResponse, instructorResponse]) => {
+ setTagTypes(tagResponse.data);
+ setTeachers(activatedTeachersResponse.data);
+ const teacher = instructorResponse.data;
+ if (teacher && Array.isArray(teacher) && teacher[0]) {
+ setSelectedInstructor(teacher[0]?.id);
+ } else {
+ setSelectedInstructor("");
+ }
+ }
+ )
+ .catch((error) => {
+ console.log("Error fetching data:", error);
+ });
}
- }, [backend]);
+ }, [backend, classData?.id]);
+
const onBack = () => {
setCurrentModal("view");
};
+
const onSave = async (draft) => {
try {
// PUT request to save class data
@@ -111,26 +131,30 @@ export const TeacherEditModal = ({
is_recurring: recurrencePattern !== "none",
isDraft: draft,
};
- console.log(classData);
+ // console.log(classData);
await backend.put(`/classes/${classData.id}`, updatedData);
- console.log("Updating class", classData.id, updatedData);
+ // console.log("Updating class", classData.id, updatedData);
await backend.delete(`/corequisites/class/${classData.id}`);
- await backend.put(`/classes-taught`, {
- classId: classData.id,
- teacherId: selectedInstructor,
- });
- console.log("Updating instructor", classData.id, selectedInstructor);
- if (performanceId) {
+ if (selectedInstructor && parseInt(selectedInstructor) !== -1) {
+ await backend.put(`/classes-taught`, {
+ classId: classData.id,
+ teacherId: selectedInstructor,
+ });
+ } else {
+ await backend.delete(`/classes-taught/${classData.id}`);
+ }
+ // console.log("Updating instructor", classData.id, selectedInstructor);
+ if (performanceId && parseInt(performanceId) !== -1) {
await backend.put(
`/corequisites/${classData.id}/${performanceId}`,
updatedData
);
- console.log(
- "Updating corequisites",
- classData.id,
- performanceId,
- updatedData
- );
+ // console.log(
+ // "Updating corequisites",
+ // classData.id,
+ // performanceId,
+ // updatedData
+ // );
}
if (recurrencePattern !== "none") {
@@ -140,7 +164,7 @@ export const TeacherEditModal = ({
stringToDate(endDate),
recurrencePattern
);
- console.log("classdates", classDates);
+ // console.log("classdates", classDates);
for (const classDate of classDates) {
if (classDate && startTime && endTime) {
const scheduledClassBody = {
@@ -182,7 +206,14 @@ export const TeacherEditModal = ({
isDraft: draft,
}));
- if (classType !== "") {
+ if (classData?.id && classType !== "-1") {
+ tags.map(async (tag) => {
+ await backend
+ .delete(`/class-tags/${classData.id}/${tag.id}`)
+ .catch((err) => {
+ console.error(err);
+ });
+ });
await backend
.post("/class-tags", {
classId: classData.id,
@@ -230,7 +261,8 @@ export const TeacherEditModal = ({
const [classTitle, setClassTitle] = useState(classData?.title);
const [location, setLocation] = useState(classData?.location);
const [startDate, setStartDate] = useState(
- isDefaultDate(classData?.startDate) || isDefaultDate(classData?.date)
+ (isDefaultDate(classData?.startDate) && !classData?.date) ||
+ (isDefaultDate(classData?.date) && !classData?.startDate)
? ""
: classData?.startDate
? formatDate(classData.startDate)
@@ -270,9 +302,6 @@ export const TeacherEditModal = ({
const onStartTimeChange = (e) => setStartTime(e.target.value);
const onEndTimeChange = (e) => setEndTime(e.target.value);
- useEffect(() => {
- console.log();
- }, []);
return (
setSelectedInstructor(e.target.value)}
maxWidth="300px"
bg="white"
- color="black"
+ color={selectedInstructor === "" ? "gray.400" : "black"}
+ sx={{
+ "& option": {
+ color: "black",
+ backgroundColor: "white",
+ },
+ "& option[disabled]": {
+ color: "gray.400",
+ },
+ }}
>
{teachers.map((teacher) => (
+
+ ClassType
+
+
Performances
);
diff --git a/client/src/components/bookings/teacherView/DeleteConfirmModal.jsx b/client/src/components/bookings/teacherView/DeleteConfirmModal.jsx
index 2a607f5a..4bf1eab6 100644
--- a/client/src/components/bookings/teacherView/DeleteConfirmModal.jsx
+++ b/client/src/components/bookings/teacherView/DeleteConfirmModal.jsx
@@ -1,32 +1,84 @@
-import { Button, Modal, ModalOverlay, ModalHeader, ModalContent, ModalBody, ModalFooter, Flex, Text, useToast } from "@chakra-ui/react";
+import {
+ Button,
+ Flex,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
-export const DeleteConfirmModal = ({ isOpen, setIsDeleting, onConfirmDelete, title, id }) => {
+export const DeleteConfirmModal = ({
+ isOpen,
+ setIsDeleting,
+ onConfirmDelete,
+ title,
+ id,
+}) => {
+ const onClose = () => {
+ setIsDeleting(false);
+ };
- const onClose = () => {
- setIsDeleting(false);
- };
+ return (
+
+
+
+
+ Are you sure you want to delete this event?
+
+
+ You're cancelling{" "}
+
+ {title ? title : "N/A"}
+
+ . This action can't be undone.
+
- return (
-
-
-
- Are you sure you want to delete this event?
-
- You're cancelling {title ? title : "N/A"}. This action can't be undone.
-
-
-
-
-
- Close
-
-
- Confirm
-
-
-
-
-
-
+
+
+
+ Close
+
+
+ Confirm
+
+
+
+
+
);
-};
\ No newline at end of file
+};
diff --git a/client/src/components/bookings/teacherView/TeacherEventViewModal.jsx b/client/src/components/bookings/teacherView/TeacherEventViewModal.jsx
index d4c8c76a..4425a88f 100644
--- a/client/src/components/bookings/teacherView/TeacherEventViewModal.jsx
+++ b/client/src/components/bookings/teacherView/TeacherEventViewModal.jsx
@@ -30,7 +30,7 @@ import {
VStack,
} from "@chakra-ui/react";
-import { calcLength } from "framer-motion";
+import { AiOutlineArrowLeft } from "react-icons/ai";
import { BiSolidEdit } from "react-icons/bi";
import { BsChevronLeft } from "react-icons/bs";
import {
@@ -38,9 +38,7 @@ import {
FaCircleExclamation,
FaRegTrashCan,
} from "react-icons/fa6";
-import { MdArrowBackIosNew, MdMoreHoriz } from "react-icons/md";
-import { useNavigate } from "react-router-dom";
-import { createEmitAndSemanticDiagnosticsBuilderProgram } from "typescript";
+import { MdMoreHoriz } from "react-icons/md";
import { useAuthContext } from "../../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../../contexts/hooks/useBackendContext";
@@ -52,7 +50,6 @@ import { CreateEvent } from "../../forms/createEvent";
import { EventRSVP } from "../../rsvp/eventRsvp";
import { DeleteConfirmModal } from "./DeleteConfirmModal";
import { QRCode } from "./qrcode/QRCode.jsx";
-import { AiOutlineArrowLeft } from "react-icons/ai";
function TeacherEventViewModal({
isOpenProp,
@@ -73,6 +70,8 @@ function TeacherEventViewModal({
corequisites,
triggerRefresh,
handleResolveCoreq = () => {},
+ tags = [],
+ magic,
}) {
const { currentUser, role } = useAuthContext();
const { backend } = useBackendContext();
@@ -85,14 +84,10 @@ function TeacherEventViewModal({
const [openSuccessModal, setOpenSuccessModal] = useState(false);
- // temp for image
- const [imageSrc, setImageSrc] = useState("");
-
const [isEditing, setIsEditing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isConfirmDelete, setIsConfirmDelete] = useState(false);
-
// disclosure for rsvp
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -168,6 +163,7 @@ function TeacherEventViewModal({
status: "error",
duration: 5000,
isClosable: true,
+ position: "top",
});
}
};
@@ -178,15 +174,27 @@ function TeacherEventViewModal({
handleClose();
};
- useEffect(() => {
- if (isOpenProp && !imageSrc) {
- fetch("https://dog.ceo/api/breeds/image/random") // for fun
- .then((res) => res.json())
- .then((data) => setImageSrc(data.message));
- }
- }, [imageSrc, isOpenProp]);
+ const fetchTags = async () => {
+ if (!id) return;
+ try {
+ const response = await backend.get(`/event-tags/tags/${id}`);
+ if (response.data && response.data.length > 0) {
+ const responseTags = response.data.map((tagItem) => ({
+ id: tagItem.tag_id,
+ name: tagItem.tag,
+ }));
+ setTagData(responseTags);
+ } else {
+ setTagData([]);
+ }
+ } catch (error) {
+ console.error("Error fetching tags:", error);
+ setTagData([]);
+ }
+ };
+ const tagData = tags;
return (
<>
@@ -220,6 +228,7 @@ function TeacherEventViewModal({
{title}
@@ -240,6 +249,7 @@ function TeacherEventViewModal({
level: level,
capacity: capacity,
date: formFormattedDate,
+ tag: tags[0]?.id,
}}
onClose={handleSaveChanges}
triggerRefresh={triggerRefresh}
@@ -272,14 +282,17 @@ function TeacherEventViewModal({
>
-
+
-
+
- {/*
-
- Location:
- {location ? location : "N/A"}
-
-
- Date:
- {formattedDate ? formattedDate : "N/A"}
-
- */}
-
-
-
-
-
+
+
+
-
+
{" "}
{rsvpnum ? rsvpnum : 0} People Enrolled
-
+
+
+ {tagData[0]?.tag ? tagData[0].tag : "No Tags"}
+
+
+
+
-
{title}
-
-
-
+
-
- {description}
-
+ {description}
-
+
-
+
{formattedDate} ·{" "}
{formattedStartTime ? formattedStartTime : "TBD"} -{" "}
{formattedEndTime ? formattedEndTime : "TBD"}
- {location ? location : "N/A"}
+ {location ? location : "N/A"}
-
+
Call Time
{formattedCallTime ? formattedCallTime : "TBD"}
-
+
Capacity
@@ -437,20 +470,24 @@ function TeacherEventViewModal({
Level
{level ? level : "TBD"}
-
+
Costume
@@ -462,11 +499,14 @@ function TeacherEventViewModal({
Event Prerequisites
- We recommend taking these classes before enrolling in this event
+
+ We recommend taking these classes before enrolling in
+ this event
+
{!corequisites || corequisites.length === 0 ? (
No corequisites for this event
) : (
diff --git a/client/src/components/bookings/teacherView/qrcode/CheckInHandler.jsx b/client/src/components/bookings/teacherView/qrcode/CheckInHandler.jsx
index 52c48654..ad6cf76a 100644
--- a/client/src/components/bookings/teacherView/qrcode/CheckInHandler.jsx
+++ b/client/src/components/bookings/teacherView/qrcode/CheckInHandler.jsx
@@ -16,21 +16,6 @@ export const CheckInHandler = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
- // console.log("studentId", currentUser);
-
- // Print all localStorage data
- // console.log(
- // "LocalStorage contents:",
- // Object.entries(localStorage).reduce((obj, [key, value]) => {
- // try {
- // obj[key] = JSON.parse(value);
- // } catch {
- // obj[key] = value;
- // }
- // return obj;
- // }, {})
- // );
-
useEffect(() => {
const handleCheckIn = async () => {
try {
@@ -83,8 +68,6 @@ export const CheckInHandler = () => {
);
}
- console.log("title", title);
-
return (
{
const date = params.date;
// removed baseURL, was preventing the redirect to login from happening
- localStorage.setItem("qrcode_redirect", `/check-in/class/${id}/${date}`);
+ localStorage.setItem(
+ "qrcode_redirect",
+ `/check-in/class/${id}/${date}`
+ );
// throw new Error("No user ID found");
navigate("/login");
}
@@ -44,24 +47,24 @@ export const ClassCheckInHandler = () => {
// Format current date as YYYY-MM-DD
const today = new Date().toISOString().split("T")[0];
- // console.log(decodeURIComponent(date));
- // Class-specific endpoint
-
- const currentCheckIn = await backend.get(
- `/class-enrollments/test`, {
- params: {
- student_id: studentId,
- class_id: id,
- attendance: new Date(decodeURIComponent(date)).toISOString().split("T")[0],
- }
- }
- );
+
+ const currentCheckIn = await backend.get(`/class-enrollments/test`, {
+ params: {
+ student_id: studentId,
+ class_id: id,
+ attendance: new Date(decodeURIComponent(date))
+ .toISOString()
+ .split("T")[0],
+ },
+ });
if (!currentCheckIn.data.exists) {
await backend.post("/class-enrollments", {
studentId: studentId,
classId: id,
- attendance: new Date(decodeURIComponent(date)).toISOString().split("T")[0],
+ attendance: new Date(decodeURIComponent(date))
+ .toISOString()
+ .split("T")[0],
});
}
@@ -97,7 +100,9 @@ export const ClassCheckInHandler = () => {
return (
{
justify="center"
h="full"
p={4}
+ w="80%"
>
- ✓
+
{
>
You've checked in for
- {title}
+
+ {title}
+
navigate("/bookings")}
mt={4}
>
diff --git a/client/src/components/bookings/teacherView/qrcode/EventCheckInHandler.jsx b/client/src/components/bookings/teacherView/qrcode/EventCheckInHandler.jsx
index c7444ccb..ee75e3ee 100644
--- a/client/src/components/bookings/teacherView/qrcode/EventCheckInHandler.jsx
+++ b/client/src/components/bookings/teacherView/qrcode/EventCheckInHandler.jsx
@@ -41,7 +41,8 @@ export const EventCheckInHandler = () => {
);
const studentId = studentResponse.data.id;
- await backend.put(`/event-enrollments/${studentId}`, { // switched post to put?
+ await backend.put(`/event-enrollments/${studentId}`, {
+ // switched post to put?
student_id: studentId,
event_id: id,
// this statement has no effect since attendance defaults to false in backend route (event_enrollments.ts)
@@ -80,7 +81,9 @@ export const EventCheckInHandler = () => {
return (
{
justify="center"
h="full"
p={4}
+ w="80%"
>
- ✓
+
{
>
You've checked in for
- {title}
+
+ {title}
+
navigate("/bookings")}
mt={4}
>
diff --git a/client/src/components/bookings/teacherView/qrcode/QRCode.jsx b/client/src/components/bookings/teacherView/qrcode/QRCode.jsx
index 661d3080..a77d7bc8 100644
--- a/client/src/components/bookings/teacherView/qrcode/QRCode.jsx
+++ b/client/src/components/bookings/teacherView/qrcode/QRCode.jsx
@@ -2,10 +2,11 @@ import { Box, Text, VStack } from "@chakra-ui/react";
import QRCodeReact from "react-qr-code";
-export const QRCode = ({ id, type, date}) => {
+export const QRCode = ({ id, type, date }) => {
const baseUrl = window.location.origin;
- const qrUrl = `${baseUrl}/check-in/${type === 'Class' ? 'class' : 'event'}/${id}${date ? `/${encodeURIComponent(date)}` : ''}`;
- // console.log(qrUrl);
+ const localDate = date || new Date().toISOString().split("T")[0]; // Default to today's date if no date is provided
+ const qrUrl = `${baseUrl}/check-in/${type === "Class" ? "class" : "event"}/${id}${type === "Class" ? `/${encodeURIComponent(localDate)}` : ""}`;
+ console.log("QR URL:", qrUrl);
return (
{
justify={"space-between"}
>
Dashboard
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{
@@ -309,8 +320,6 @@ export const DashboardHome = () => {
};
export const Sidebar = () => {
-
-
const navigate = useNavigate();
return (
{
@@ -24,10 +32,15 @@ export const NavigationSidebar = () => {
position="sticky"
top={0}
>
-
-
+
{routes.map((route) => (
-
+
{
}}
transition="background-color 0.2s ease"
>
-
+
{route.name}
-
))}
diff --git a/client/src/components/dashboard/NotificationPanel.jsx b/client/src/components/dashboard/NotificationPanel.jsx
index 5f66c75b..5e883c76 100644
--- a/client/src/components/dashboard/NotificationPanel.jsx
+++ b/client/src/components/dashboard/NotificationPanel.jsx
@@ -1,70 +1,86 @@
-import { VStack, HStack, Text, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, DrawerCloseButton } from "@chakra-ui/react";
-
import { useEffect, useState } from "react";
-import { TeacherNotification } from "./TeacherNotification";
+
+import {
+ Drawer,
+ DrawerBody,
+ DrawerCloseButton,
+ DrawerContent,
+ DrawerHeader,
+ DrawerOverlay,
+ HStack,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
+import { TeacherNotification } from "./TeacherNotification";
export const NotificationPanel = ({ isOpen, onClose }) => {
- const [ activeTab, setActiveTab ] = useState("alerts");
- const [ teachers, setTeachers ] = useState([]);
- const { backend } = useBackendContext();
+ const [activeTab, setActiveTab] = useState("alerts");
+ const [teachers, setTeachers] = useState([]);
+ const { backend } = useBackendContext();
- useEffect(() => {
- const fetchData = async () => {
- try {
- const response = await backend.get("/teachers/notactivated");
- setTeachers(response.data);
- } catch (error) {
- console.error("Error fetching unapproved teachers:", error);
- }
- };
-
- fetchData();
- }, [backend]);
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const response = await backend.get("/teachers/notactivated");
+ setTeachers(response.data);
+ } catch (error) {
+ console.error("Error fetching unapproved teachers:", error);
+ }
+ };
+ fetchData();
+ }, [backend]);
- return (
-
-
-
-
- Notifications
+ return (
+
+
+
+
+ Notifications
-
-
- setActiveTab("alerts")}
- >
- Alerts
-
- setActiveTab("teachers")}
- >
- Teachers
-
-
+
+
+ setActiveTab("alerts")}
+ >
+ Alerts
+
+ setActiveTab("teachers")}
+ >
+ Teachers
+
+
-
- {(activeTab === "teachers" && teachers) ? teachers.map((teacher, ind) => (
- ))
- : null}
-
-
+
+ {activeTab === "teachers" && teachers
+ ? teachers.map((teacher, ind) => (
+
+ ))
+ : null}
+
+
- {/*
+ {/*
*/}
-
-
- );
-};
\ No newline at end of file
+
+
+ );
+};
diff --git a/client/src/components/dashboard/RoleSelect.tsx b/client/src/components/dashboard/RoleSelect.tsx
index 1c9c7a72..b93f240c 100644
--- a/client/src/components/dashboard/RoleSelect.tsx
+++ b/client/src/components/dashboard/RoleSelect.tsx
@@ -39,6 +39,7 @@ export const RoleSelect = ({ user, disabled = true }: RoleSelectProps) => {
title: "Role Updated",
description: `Updated role from ${previousRole} to ${updatedRole}`,
status: "success",
+ position: "top",
});
} catch (error) {
console.error("Error updating user role:", error);
@@ -47,6 +48,7 @@ export const RoleSelect = ({ user, disabled = true }: RoleSelectProps) => {
title: "An Error Occurred",
description: `Role was not updated`,
status: "error",
+ position: "top",
});
} finally {
setLoading(false);
diff --git a/client/src/components/dashboard/TeacherNotification.jsx b/client/src/components/dashboard/TeacherNotification.jsx
index e70c0306..e1e6192c 100644
--- a/client/src/components/dashboard/TeacherNotification.jsx
+++ b/client/src/components/dashboard/TeacherNotification.jsx
@@ -20,6 +20,7 @@ export const TeacherNotification = ({ id, firstName, lastName }) => {
status: "success",
duration: 3000,
isClosable: true,
+ position: "top",
});
setHandled(true);
} catch (error) {
@@ -28,6 +29,7 @@ export const TeacherNotification = ({ id, firstName, lastName }) => {
status: "error",
duration: 3000,
isClosable: true,
+ position: "top",
});
console.error("Error updating teacher's activation status: ", error);
}
@@ -44,6 +46,7 @@ export const TeacherNotification = ({ id, firstName, lastName }) => {
status: "error",
duration: 3000,
isClosable: true,
+ position: "top",
});
setHandled(true);
} catch (error) {
@@ -52,6 +55,7 @@ export const TeacherNotification = ({ id, firstName, lastName }) => {
status: "error",
duration: 3000,
isClosable: true,
+ position: "top",
});
console.error("Error updating teacher's activation status: ", error);
}
@@ -92,7 +96,7 @@ export const TeacherNotification = ({ id, firstName, lastName }) => {
Deny
Classes and Events
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{
- const { backend } = useBackendContext();
- const toast = useToast();
-
- const onGoBack = () => {
- setCurrentModal("view");
- };
+ Button,
+ Flex,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
- const onConfirm = async () => {
- try {
- await backend.delete(`/scheduled-classes/${classData.id}/${classData.date}`);
- setCurrentModal("confirmation");
- } catch (error) {
- console.error("Error deleting class:", error);
- console.log("Error deleting class:", error);
- toast({
- title: "Error deleting class",
- status: "error",
- duration: 4000,
- isClosable: true,
- });
- }
- };
- return (
-
-
-
- Delete class?
-
-
- You are deleting {classData.title}. This action can't be undone.
-
-
-
-
-
- Cancel
-
-
- Delete
-
-
-
-
-
- );
+import { useBackendContext } from "../../../contexts/hooks/useBackendContext";
+
+export const ClassDeleteConfirmationModal = ({
+ isOpen,
+ onClose,
+ setCurrentModal,
+ classData,
+}) => {
+ const { backend } = useBackendContext();
+ const toast = useToast();
+
+ const onGoBack = () => {
+ setCurrentModal("view");
};
+
+ const onConfirm = async () => {
+ try {
+ await backend.delete(
+ `/scheduled-classes/${classData.id}/${classData.date}`
+ );
+ setCurrentModal("confirmation");
+ } catch (error) {
+ console.error("Error deleting class:", error);
+ console.log("Error deleting class:", error);
+ toast({
+ title: "Error deleting class",
+ status: "error",
+ duration: 4000,
+ isClosable: true,
+ position: "top",
+ });
+ }
+ };
+ return (
+
+
+
+ Delete class?
+
+
+
+ You are deleting {classData.title}. This action can't be undone.
+
+
+
+
+
+
+ Cancel
+
+
+
+ Delete
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/components/dashboard/classDashboard/ConfirmDeleteModal.jsx b/client/src/components/dashboard/classDashboard/ConfirmDeleteModal.jsx
index ad893074..42ceec7c 100644
--- a/client/src/components/dashboard/classDashboard/ConfirmDeleteModal.jsx
+++ b/client/src/components/dashboard/classDashboard/ConfirmDeleteModal.jsx
@@ -1,23 +1,61 @@
-import { Icon, Button, Modal, ModalOverlay, ModalContent, ModalFooter, Flex, Text, VStack } from "@chakra-ui/react";
+import {
+ Button,
+ Flex,
+ Icon,
+ Modal,
+ ModalContent,
+ ModalFooter,
+ ModalOverlay,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
import { BsCheck } from "react-icons/bs";
-export const ConfirmDeleteModal = ({ isOpen, onClose, itemType}) => {
+export const ConfirmDeleteModal = ({ isOpen, onClose, itemType }) => {
return (
- {window.location.reload()}}>
+ {
+ window.location.reload();
+ }}
+ >
-
-
+
+
- {itemType} Deleted
-
- {window.location.reload()}}>
- Back to Dashboard
-
+
+ {itemType} Deleted
+
+
+ {
+ window.location.reload();
+ }}
+ >
+ Back to Dashboard{" "}
+
+
);
-};
\ No newline at end of file
+};
diff --git a/client/src/components/dashboard/classDashboard/EventDeleteConfirmationModal.jsx b/client/src/components/dashboard/classDashboard/EventDeleteConfirmationModal.jsx
index 3cf1f2b5..2910b92d 100644
--- a/client/src/components/dashboard/classDashboard/EventDeleteConfirmationModal.jsx
+++ b/client/src/components/dashboard/classDashboard/EventDeleteConfirmationModal.jsx
@@ -1,71 +1,92 @@
import {
- Button,
- Flex,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Text,
- useToast,
- } from "@chakra-ui/react";
-
- import { useBackendContext } from "../../../contexts/hooks/useBackendContext";
-
- export const EventDeleteConfirmationModal = ({
- isOpen,
- onClose,
- setCurrentModal,
- eventData,
- }) => {
- const { backend } = useBackendContext();
- const toast = useToast();
-
- const onGoBack = () => {
- setCurrentModal("view");
- };
+ Button,
+ Flex,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
- const onConfirm = async () => {
- try {
- await backend.delete(`/events/${eventData.id}`);
- setCurrentModal("confirmationEvent");
- } catch (error) {
- console.error("Error deleting event:", error);
- console.log("Error deleting event:", error);
- toast({
- title: "Error deleting event",
- status: "error",
- duration: 4000,
- isClosable: true,
- });
- }
- };
- return (
-
-
-
- Delete event?
-
-
- You are deleting {eventData.title}. This action can't be undone.
-
-
-
-
-
- Cancel
-
-
- Delete
-
-
-
-
-
- );
+import { useBackendContext } from "../../../contexts/hooks/useBackendContext";
+
+export const EventDeleteConfirmationModal = ({
+ isOpen,
+ onClose,
+ setCurrentModal,
+ eventData,
+}) => {
+ const { backend } = useBackendContext();
+ const toast = useToast();
+
+ const onGoBack = () => {
+ setCurrentModal("view");
};
+
+ const onConfirm = async () => {
+ try {
+ await backend.delete(`/events/${eventData.id}`);
+ setCurrentModal("confirmationEvent");
+ } catch (error) {
+ console.error("Error deleting event:", error);
+ console.log("Error deleting event:", error);
+ toast({
+ title: "Error deleting event",
+ status: "error",
+ duration: 4000,
+ isClosable: true,
+ position: "top",
+ });
+ }
+ };
+ return (
+
+
+
+ Delete event?
+
+
+
+ You are deleting {eventData.title}. This action can't be undone.
+
+
+
+
+
+
+ Cancel
+
+
+
+ Delete
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/components/dashboard/classInfoDashboard/ClassInfoDashboard.tsx b/client/src/components/dashboard/classInfoDashboard/ClassInfoDashboard.tsx
index f9ead013..9c8a9bdb 100644
--- a/client/src/components/dashboard/classInfoDashboard/ClassInfoDashboard.tsx
+++ b/client/src/components/dashboard/classInfoDashboard/ClassInfoDashboard.tsx
@@ -8,6 +8,7 @@ import {
FormLabel,
Heading,
HStack,
+ IconButton,
Image,
Input,
Table,
@@ -21,6 +22,7 @@ import {
VStack,
} from "@chakra-ui/react";
+import { FaRegBell } from "react-icons/fa";
import { SlArrowLeft } from "react-icons/sl";
import { useNavigate, useParams } from "react-router-dom";
@@ -109,12 +111,21 @@ export default function ClassInfoDashboard() {
{currentClass?.title}
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{
try {
// Fetch event info
- const response = await backend.get(
- `/events/${eventId}`
- );
+ const response = await backend.get(`/events/${eventId}`);
response.data[0].date = new Date(response.data[0].date || "");
setEvent(response.data[0]);
@@ -96,12 +96,21 @@ export default function EventInfoDashboard() {
{currentEvent?.title}
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
- {currentEvent ? formatTime(currentEvent.startTime) : ''}
+ {currentEvent ? formatTime(currentEvent.startTime) : ""}
- {currentEvent ? formatTime(currentEvent.endTime) : ''}
+ {currentEvent ? formatTime(currentEvent.endTime) : ""}
- {currentEvent ? formatTime(currentEvent.callTime) : ''}
+ {currentEvent ? formatTime(currentEvent.callTime) : ""}
{
>
- Settings
+ Settings
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{
Students
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{
{student?.firstName} {student?.lastName}
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{
Teachers
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{
{teacher?.firstName} {teacher?.lastName}
- */}
+ }
+ size="lg"
+ mt="-2"
+ onClick={onOpen}
+ ref={notifRef}
+ aria-label="Notifications"
+ bg="white"
/>
{},
-}) {
+ tags = [],
+}) => {
const { currentUser, role } = useAuthContext();
const { backend } = useBackendContext();
const [openSuccessModal, setOpenSuccessModal] = useState(false);
- // temp for image
- const [imageSrc, setImageSrc] = useState("");
+ const [teacherName, setTeacherName] = useState("");
+ const [startTime, setStartTime] = useState("");
+ const [endTime, setEndTime] = useState("");
+ const [description, setDescription] = useState("");
+ const [capacity, setCapacity] = useState("");
+ const [level, setLevel] = useState("");
+ const [coClasses, setCoClasses] = useState([]);
+
+ const getTeacherName = async () => {
+ const teacherName = await backend.get(`/classes-taught/instructor/${id}`);
+ setTeacherName(
+ teacherName.data[0].firstName + " " + teacherName.data[0].lastName
+ );
+ };
+
+ const getStartTime = async () => {
+ const data = await backend.get(`/scheduled-classes/${id}`);
+ setStartTime(data.data[0].startTime);
+ setEndTime(data.data[0].endTime);
+ };
const enrollInClass = async () => {
const users = await backend.get(`/users/${currentUser.uid}`);
@@ -80,20 +106,36 @@ function ClassInfoModal({
enrollInClass();
return;
}
+ // if there exisits a coreq and not enrolled in a coreq,
if (corequisites.some((coreq) => !coreq.enrolled)) {
+ // let coReqWarningModal know that it should programatically display an event info modal version
+ setModalIdentity("class");
handleResolveCoreq();
} else {
enrollInClass();
}
};
+ const initClass = async () => {
+ const classData = await backend.get(`/classes/${id}`);
+ setDescription(classData.data[0].description);
+ setCapacity(classData.data[0].capacity);
+ setLevel(classData.data[0].level);
+ };
+
+ const getCoClasses = async () => {
+ const response = await backend.get(`/corequisites/class/${id}`);
+ setCoClasses(response.data);
+ };
+
useEffect(() => {
- if (isOpenProp && !imageSrc) {
- fetch("https://dog.ceo/api/breeds/image/random") // for fun
- .then((res) => res.json())
- .then((data) => setImageSrc(data.message));
+ if (isOpenProp) {
+ getTeacherName();
+ getStartTime();
+ initClass();
+ getCoClasses();
}
- }, [imageSrc, isOpenProp]);
+ }, [isOpenProp]);
return (
<>
@@ -105,121 +147,175 @@ function ClassInfoModal({
/>
-
- {title}
-
+
+
+
+ }
+ onClick={handleClose}
+ aria-label="Back"
+ variant="ghost"
+ fontSize={"2xl"}
+ p={4}
+ ml={-4}
+ />
+
+ {tags.map((tag, index) => (
+
+ {tag.tag}
+
+ ))}
+
+
+ {title}
+
+
+
+ {/* */}
+ Taught by {teacherName ? teacherName : "Unknown"}
+
+ {description
+ ? `Description: ${description}`
+ : "No description available."}
+ {" "}
+
+
+
+ {formatDate(date)} · {formatTime(startTime)} –{" "}
+ {formatTime(endTime)}
+
+ Location: {location}
+
+
+
+ Level
+ {level}
+
+
+ Capacity
+ {capacity}
+
+
+
- {!isCorequisiteSignUp && (
-
-
-
-
-
- Recommended
-
-
- {!corequisites || corequisites.length === 0 ? (
- No corequisites for this class
- ) : (
-
- {corequisites.map((coreq, index) => (
-
-
- {coreq.title}
-
- ))}
-
- )}
-
+
+
+ Recommended Prerequisite(s)
+
+
+ We recommend taking these classes before enrolling in this
+ series.
+
+ {coClasses && coClasses.length > 0 ? (
+
+ {coClasses.map((prerequisite) => (
+
+ {prerequisite.title}
+
+ ))}
-
- )}
-
-
+ ) : (
+
+ No prerequisites for this class
+
+ )}
-
-
- Location
- {location}
-
-
- Date
- {date}
-
-
-
-
-
- Time
- pass in time prop and use it
-
- Description:
- {description}
+
+
+ Performance(s)
+
+
+ At the end of the class period, students will perform in a
+ final performance.
+
+ {corequisites && corequisites.length > 0 ? (
+
+ {corequisites.map((prerequisite) => (
+
+ {prerequisite.title}
+
+ ))}
+
+ ) : (
+
+ No performances for this class
+
+ )}
-
-
-
- Capacity
- {capacity}
-
-
- Level
- {level}
-
-
-
-
-
- Classes
- {costume}
-
-
-
+
{role === "student" && (
Sign up
+ width="100%"
+ p={7}
+ bg="purple.600"
+ color="white"
+ onClick={classSignUp}
+ >
+ Sign up
)}
@@ -237,6 +333,6 @@ function ClassInfoModal({
>
);
-}
+};
export default ClassInfoModal;
diff --git a/client/src/components/discovery/CoReqWarningModal.jsx b/client/src/components/discovery/CoReqWarningModal.jsx
index 60ee3703..b7d78ec4 100644
--- a/client/src/components/discovery/CoReqWarningModal.jsx
+++ b/client/src/components/discovery/CoReqWarningModal.jsx
@@ -3,119 +3,192 @@ import { useEffect, useState } from "react";
import {
Box,
Button,
+ Center,
List,
ListItem,
Modal,
ModalBody,
+ ModalCloseButton,
ModalContent,
ModalOverlay,
+ Stack,
Text,
VStack,
} from "@chakra-ui/react";
-import ClassInfoModal from "./ClassInfoModal";
-import EventInfoModal from "./EventInfoModal";
+import { useAuthContext } from "../../contexts/hooks/useAuthContext";
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
+import SuccessSignupModal from "./SuccessSignupModal";
function CoReqWarningModal({
- origin,
+ title,
+ user,
+ class_id = null,
+ event_id = null,
isOpenProp,
lstCorequisites,
+ modalIdentity,
+ setModalIdentity,
+ filteredCorequisites,
+ setFilteredCorequisites,
+ eventId,
handleClose = () => {},
killModal = () => {},
}) {
- const [openCoreq, setOpenCoreq] = useState(false);
- const [coreq, setCoreq] = useState(null);
+ const { backend } = useBackendContext();
+ const [openSuccessModal, setOpenSuccessModal] = useState(false);
+ const [withCoreqFlag, setWithCoreqFlag] = useState(false);
- const signup = () => {
- setOpenCoreq(true);
+ const enrollInClass = async (id) => {
+ if (user.data[0]) {
+ await backend.post(`/class-enrollments`, {
+ studentId: user.data[0].id,
+ classId: id,
+ attendance: null,
+ });
+ }
+ };
+
+ const enrollInEvent = async (id) => {
+ const currentCheckIn = await backend.get(`/event-enrollments/test`, {
+ params: {
+ student_id: user.data[0].id,
+ event_id: id,
+ },
+ });
+ if (user.data[0] && !currentCheckIn.data.exists) {
+ console.log("Enrolling in event:", id);
+ const req = await backend.post(`/event-enrollments/`, {
+ student_id: user.data[0].id,
+ event_id: id,
+ attendance: null,
+ });
+ if (req.status === 201) {
+ setOpenSuccessModal(true);
+ }
+ } else {
+ console.log("Already signed up for this event!");
+ }
+ };
+
+ const signupWithCoreq = async () => {
+ try {
+ setWithCoreqFlag(true);
+ if (class_id === null) {
+ await enrollInEvent(event_id);
+ for (const coreq of lstCorequisites) {
+ if (coreq.enrolled === false) {
+ await enrollInClass(coreq.id);
+ }
+ }
+ } else {
+ await enrollInClass(class_id);
+ for (const coreq of lstCorequisites) {
+ if (coreq.enrolled === false) {
+ console.log("Enrolling in corequisite class:", coreq.id);
+ await enrollInEvent(coreq.id);
+ }
+ }
+ // lstCorequisites.forEach((coreq) => {
+ // if (coreq.enrolled === false) {
+ // console.log("Enrolling in corequisite class:", coreq.id);
+ // await enrollInEvent(coreq.id);
+ // }
+ // });
+ }
+ setOpenSuccessModal(true);
+ killModal();
+ } catch (error) {
+ console.error("Error during signup:", error);
+ }
+ };
+
+ const signupWithoutCoreq = async () => {
+ // setOpenCoreq(true);
+ setWithCoreqFlag(false);
+ if (class_id === null) {
+ await enrollInEvent(event_id);
+ } else {
+ await enrollInClass(class_id);
+ }
+ setOpenSuccessModal(true);
killModal();
};
const cancelSignUp = () => {
- setOpenCoreq(false);
handleClose();
killModal();
};
- useEffect(() => {
- if (lstCorequisites.length > 0) {
- setCoreq(
- lstCorequisites.find((coreq) => coreq.enrolled === false) || null
- );
- }
- }, [lstCorequisites]);
-
- if (!coreq || !lstCorequisites || lstCorequisites.length === 0) {
+ if (!lstCorequisites || lstCorequisites.length === 0) {
return null;
}
return (
- {origin.toUpperCase() === "CLASS" ? (
-
- ) : (
-
- )}
-
+ setOpenSuccessModal(false)}
+ title={title}
+ corequisites={lstCorequisites}
+ withCoreqFlag={withCoreqFlag}
+ />
{}}
+ onClose={cancelSignUp}
>
+
+
- You still need to sign up for...
-
-
- {lstCorequisites && lstCorequisites.length > 0
- ? coreq?.title
- : ""}
+
+ Performance Participation Required
+
+
+ To enroll in {title}, it is recommended that you participate
+ in the end-of-session performance
+ {lstCorequisites.map((coreq, i) => (
+
+ {coreq.title}
+ {i < lstCorequisites.length - 1 ? ", " : ""}
+
+ ))}
+ Do you agree to take part in the performance?
-
+
- Sign up
+ Yes, Enroll & Join Performance
- Cancel
+ No, Enroll in Class Only
diff --git a/client/src/components/discovery/CompletedIndicator.png b/client/src/components/discovery/CompletedIndicator.png
deleted file mode 100644
index 9310fc72..00000000
Binary files a/client/src/components/discovery/CompletedIndicator.png and /dev/null differ
diff --git a/client/src/components/discovery/ConfirmationModal.jsx b/client/src/components/discovery/ConfirmationModal.jsx
index e92ab1b4..c2a0f550 100644
--- a/client/src/components/discovery/ConfirmationModal.jsx
+++ b/client/src/components/discovery/ConfirmationModal.jsx
@@ -1,39 +1,64 @@
-import { Button, Modal, ModalOverlay, ModalHeader, ModalContent, ModalBody, ModalFooter, Box, Icon, VStack } from "@chakra-ui/react";
-import { MdCheck } from "react-icons/md";
+import {
+ Box,
+ Button,
+ Icon,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ VStack,
+} from "@chakra-ui/react";
+import { MdCheck } from "react-icons/md";
export const ConfirmationModal = ({ isOpen, onClose, title }) => {
-
return (
-
-
-
-
-
-
-
-
-
-
-
-
- You've successfully cancelled {title ? title : "N/A"}.
-
+
+
+
+
+
+
+
+
+
+
+ You've successfully cancelled {title ? title : "N/A"}.
+
-
-
- Back to Discovery Page
-
-
-
+
+
+ Back to Discovery Page
+
+
+
);
-};
\ No newline at end of file
+};
diff --git a/client/src/components/discovery/Discovery.jsx b/client/src/components/discovery/Discovery.jsx
index 980ba6f3..d0c99755 100644
--- a/client/src/components/discovery/Discovery.jsx
+++ b/client/src/components/discovery/Discovery.jsx
@@ -1,72 +1,228 @@
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
-import { Box, Button, Flex, Input, InputGroup, InputLeftElement, VStack, Badge } from "@chakra-ui/react";
+import {
+ Box,
+ Button,
+ Center,
+ Flex,
+ Tab,
+ TabList,
+ TabPanel,
+ TabPanels,
+ Tabs,
+ VStack,
+} from "@chakra-ui/react";
+import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
import { Navbar } from "../navbar/Navbar";
+import { SearchBar } from "../searchbar/SearchBar";
import { ClassCard } from "../shared/ClassCard";
import { EventCard } from "../shared/EventCard";
-import { useAuthContext } from "../../contexts/hooks/useAuthContext";
-import { FaSearch } from "react-icons/fa";
-import { SearchBar } from "../searchbar/SearchBar";
export const Discovery = () => {
// Active Tab Logic
- const [activeTab, setActiveTab] = useState("classes"); // Default to showing classes
+ const [tabIndex, setTabIndex] = useState(0);
const [searchInput, setSearchInput] = useState("");
const [refresh, setRefresh] = useState(0);
const [lastToggledTag, setLastToggledTag] = useState(null);
+ const [classes, setClasses] = useState([]);
+ const [events, setEvents] = useState([]);
+ const [tags, setTags] = useState({});
+ const [tagFilter, setTagFilter] = useState({});
+ const [initialTagFilter, setInitialTagFilter] = useState({});
+ const [classTagsMap, setClassTagsMap] = useState({});
+ const [eventTagsMap, setEventTagsMap] = useState({});
+ const [user, setUser] = useState(null);
- const { currentUser } = useAuthContext();
+ const { currentUser, role } = useAuthContext();
+ const { backend } = useBackendContext();
- const toggleClasses = () => {
- setActiveTab("classes");
- };
+ // API endpoint configuration
+ const getApiEndpoints = useCallback(
+ () => ({
+ events: {
+ published: role === "student" ? "/events/published" : "/events",
+ search:
+ role === "student" ? "/events/search/published" : "/events/search",
+ },
+ classes: {
+ published:
+ role === "student" ? "/classes/published" : "/classes/scheduled",
+ search:
+ role === "student" ? "/classes/search/published" : "/classes/search",
+ },
+ }),
+ [role]
+ );
- const toggleEvents = () => {
- setActiveTab("events");
- };
+ // Fetch functions
+ const fetchAllEvents = useCallback(async () => {
+ try {
+ const endpoints = getApiEndpoints();
+ const res = await backend.get(endpoints.events.published);
+ setEvents(res.data);
+ } catch (error) {
+ console.error("Error fetching all events:", error);
+ }
+ }, [backend, getApiEndpoints]);
- // Fetching Class and Event Data
- const { backend } = useBackendContext();
+ const fetchAllClasses = useCallback(async () => {
+ try {
+ const endpoints = getApiEndpoints();
+ const res = await backend.get(endpoints.classes.published);
+ setClasses(res.data);
+ } catch (error) {
+ console.error("Error fetching all classes:", error);
+ }
+ }, [backend, getApiEndpoints]);
- const [classes, setClasses] = useState([]);
- const [events, setEvents] = useState([]);
+ const fetchEventsByTag = useCallback(
+ async (tagId) => {
+ try {
+ const res = await backend.get(`/event-tags/events/${tagId}`);
+ setEvents(res.data);
+ } catch (error) {
+ console.error("Error fetching events for specified tag:", error);
+ }
+ },
+ [backend]
+ );
- const [tags, setTags] = useState({});
- const [tagFilter, setTagFilter] = useState({});
+ const fetchClassesByTag = useCallback(
+ async (tagId) => {
+ try {
+ const res = await backend.get(`/class-tags/classes/${tagId}`);
+ setClasses(res.data);
+ } catch (error) {
+ console.error("Error fetching classes for specified tag:", error);
+ }
+ },
+ [backend]
+ );
+ // Search functions with proper empty query handling
+ const searchEvents = useCallback(
+ async (query) => {
+ try {
+ if (!query || query.trim() === "") {
+ await fetchAllEvents();
+ return;
+ }
- // this will be an array of users
- const [user, setUser] = useState(null);
- useEffect(() => {
- const fetchUserData = () => {backend.get(`/users/${currentUser.uid}`).then(res => setUser(res))};
- fetchUserData();
- }, [backend, currentUser])
+ const endpoints = getApiEndpoints();
+ const res = await backend.get(
+ `${endpoints.events.search}/${query.trim()}`
+ );
+ setEvents(res.data);
+ } catch (error) {
+ console.error("Error searching events:", error);
+ }
+ },
+ [backend, fetchAllEvents, getApiEndpoints]
+ );
+ const searchClasses = useCallback(
+ async (query) => {
+ try {
+ if (!query || query.trim() === "") {
+ await fetchAllClasses();
+ return;
+ }
+ const endpoints = getApiEndpoints();
+ const res = await backend.get(
+ `${endpoints.classes.search}/${query.trim()}`
+ );
+ setClasses(res.data);
+ } catch (error) {
+ console.error("Error searching classes:", error);
+ }
+ },
+ [backend, fetchAllClasses, getApiEndpoints]
+ );
+
+ // Tag filter handlers
+ const handleFilterToggle = useCallback(
+ (id) => () => {
+ setTagFilter((prev) => ({
+ ...prev,
+ [id]: !prev[id],
+ }));
+ setLastToggledTag(id);
+ },
+ []
+ );
+
+ const handleClassFilterToggle = useCallback(
+ (id) => () => {
+ setTagFilter((prev) => ({
+ ...prev,
+ [id]: !prev[id],
+ }));
+ setLastToggledTag(id);
+ },
+ []
+ );
+
+ // Fetch user data
useEffect(() => {
- const fetchData = async () => {
- // Fetch and Store Classes Information
+ const fetchUserData = async () => {
try {
- const response = await backend.get("/classes/scheduled");
- setClasses(response.data);
+ const res = await backend.get(`/users/${currentUser.uid}`);
+ setUser(res.data);
} catch (error) {
- console.error("Error fetching classes:", error);
+ console.error("Error fetching user data:", error);
}
+ };
+
+ if (currentUser?.uid) {
+ fetchUserData();
+ }
+ }, [backend, currentUser]);
- // Fetch and Store Events Information
+ // Initial data fetch
+ useEffect(() => {
+ const fetchData = async () => {
try {
- const response = await backend.get("/events");
- setEvents(response.data);
+ // Fetch classes with tags
+ const classResponse = await backend.get(
+ role !== "student" ? "/classes/scheduled" : "/classes/published"
+ );
+ setClasses(classResponse.data);
+
+ const classTagsResponse = await backend.get(
+ "/class-tags/all-class-tags"
+ );
+ const classTags = {};
+ classTagsResponse.data.forEach((tag) => {
+ classTags[tag.classId] = tag.tagArray;
+ });
+ setClassTagsMap(classTags);
+
+ // Fetch events with tags
+ const eventResponse = await backend.get(
+ role !== "student" ? "/events" : "/events/published"
+ );
+ setEvents(eventResponse.data);
+
+ const eventTagsResponse = await backend.get(
+ "/event-tags/all-event-tags"
+ );
+ const eventTags = {};
+ eventTagsResponse.data.forEach((tag) => {
+ eventTags[tag.eventId] = tag.tagArray;
+ });
+ setEventTagsMap(eventTags);
} catch (error) {
- console.error("Error fetching events:", error);
+ console.error("Error fetching data:", error);
}
};
fetchData();
- }, [backend]);
+ }, [backend, role]);
+ // Fetch tags
useEffect(() => {
const fetchTags = async () => {
try {
@@ -78,243 +234,194 @@ export const Discovery = () => {
initialTags[tag.id] =
tag.tag.charAt(0).toUpperCase() + tag.tag.slice(1).toLowerCase();
});
-
+
setTagFilter(initialTagFilter);
+ setInitialTagFilter(initialTagFilter);
setTags(initialTags);
} catch (error) {
console.error("Error fetching tags:", error);
}
};
-
- fetchTags();
- }, [backend]); // only run once or when `backend` changes
-
- // take string as search query
- const searchEvents = async (query) => {
- try {
- const res = await backend.get(
- query ? `/events/search/${query}` : "/events"
- );
- setEvents(res.data);
- } catch (error) {
- console.error("Error fetching events:", error);
- }
- };
-
- const searchClasses = async (query) => {
- try {
- const res = await backend.get(
- query ? `/classes/search/${query}` : "/classes/scheduled"
- );
- setClasses(res.data);
- } catch (error) {
- console.error("Error fetching classes:", error);
- }
- };
- const isFilterActive = Object.values(tagFilter).some(Boolean);
-
- const handleFilterToggle = (id) => {
- setTagFilter((prev) => ({
- ...prev,
- [id]: !prev[id],
- }));
- setLastToggledTag(id);
- };
+ fetchTags();
+ }, [backend]);
+ // Tag filtering effect
useEffect(() => {
if (lastToggledTag === null) {
return;
}
-
- const active = tagFilter[lastToggledTag];
-
- if (active) {
- fetchEventsByTag(lastToggledTag);
- } else {
- fetchAllEvents();
- }
- }, [tagFilter, lastToggledTag]);
-
- const handleClassFilterToggle = (id) => () => {
- setTagFilter((prev) => ({
- ...prev,
- [id]: !prev[id],
- }));
- setLastToggledTag(id);
- };
- useEffect(() => {
- if (lastToggledTag === null) {
- return;
- }
-
const active = tagFilter[lastToggledTag];
-
- if (active) {
- fetchClassesByTag(lastToggledTag);
- } else {
- fetchAllClasses();
- }
- }, [tagFilter, lastToggledTag]);
-
- const fetchEventsByTag = async (tagId) => {
- try {
- const res = await backend.get(`/event-tags/events/${tagId}`);
- const events = res.data;
- console.log("Fetched Events for Tag", tagId, res.data);
- setEvents(events);
- } catch (error) {
- console.error("Error fetching events for specified tag:", error);
- }
- };
- const fetchAllEvents = async () => {
- try {
- const res = await backend.get("/events/published");
- setEvents(res.data);
- } catch (error) {
- console.error("Error fetching all events:", error);
- }
- }
-
- const fetchClassesByTag = async (tagId) => {
- try {
- const res = await backend.get(`/class-tags/classes/${tagId}`);
- const classes = res.data;
- setClasses(classes);
- } catch (error) {
- console.error("Error fetching events for specified tag:", error);
- }
- };
-
- const fetchAllClasses = async () => {
- try {
- const res = await backend.get("/classes/published");
- setClasses(res.data);
- } catch (error) {
- console.error("Error fetching all events:", error);
+ if (tabIndex === 1) {
+ if (active) {
+ fetchEventsByTag(lastToggledTag);
+ } else {
+ fetchAllEvents();
+ }
+ } else {
+ if (active) {
+ fetchClassesByTag(lastToggledTag);
+ } else {
+ fetchAllClasses();
+ }
}
- }
+ }, [
+ tagFilter,
+ lastToggledTag,
+ fetchAllEvents,
+ fetchAllClasses,
+ fetchEventsByTag,
+ fetchClassesByTag,
+ tabIndex,
+ ]);
- // console.log(classes)
return (
-
-
-
- {
- setActiveTab("classes");
- toggleClasses();
- }}
- >
- Classes
- {
- setActiveTab("events");
- toggleEvents();
- }}
- >
- Events
-
-
-
-
- {
- if (activeTab === "events") {
- searchEvents(query);
- } else {
- searchClasses(query);
- }
- }}
- tags={tags}
- tagFilter={tagFilter}
- onTag={activeTab === "events" ? handleFilterToggle : handleClassFilterToggle}
- />
-
-
-
-
- {classes.map((classItem, index) => (
-
- ))}
-
-
-
- {events.map((eventItem, index) => (
-
- ))}
-
-
-
-
+ {
+ setTagFilter(initialTagFilter); // Reset tag filter when switching tabs
+ setTabIndex(index);
+ }}
+ >
+
+
+
+ Classes
+
+
+ Events
+
+
+
+
+
+
+ searchClasses(query)}
+ tags={tags}
+ tagFilter={tagFilter}
+ onTag={handleClassFilterToggle}
+ />
+
+ {classes.map((classItem, index) => {
+ const isFilterActive =
+ Object.values(tagFilter).some(Boolean);
+ const classTags = classTagsMap[classItem.id] || [];
+ if (
+ !isFilterActive ||
+ classTags.some((tag) => tagFilter[tag.id])
+ ) {
+ return (
+
+ );
+ }
+ return null;
+ })}
+
+
+
+
+
+ searchEvents(query)}
+ tags={tags}
+ tagFilter={tagFilter}
+ onTag={handleFilterToggle}
+ />
+
+ {events.map((eventItem, index) => {
+ const isFilterActive =
+ Object.values(tagFilter).some(Boolean);
+ const eventTags = eventTagsMap[eventItem.id] || [];
+ if (
+ !isFilterActive ||
+ eventTags.some((tag) => tagFilter[tag.id])
+ ) {
+ return (
+
+ );
+ }
+ return null;
+ })}
+
+
+
+
+
+
+
);
};
diff --git a/client/src/components/discovery/EventInfoModal.jsx b/client/src/components/discovery/EventInfoModal.jsx
index 69f46c74..0f863934 100644
--- a/client/src/components/discovery/EventInfoModal.jsx
+++ b/client/src/components/discovery/EventInfoModal.jsx
@@ -1,11 +1,14 @@
-import { useEffect, useState } from "react";
+import { memo, useEffect, useState } from "react";
+import { ArrowBackIcon } from "@chakra-ui/icons";
import {
Box,
Button,
+ Divider,
+ Flex,
HStack,
+ IconButton,
Image,
- Flex,
List,
ListIcon,
ListItem,
@@ -16,19 +19,20 @@ import {
ModalFooter,
ModalHeader,
ModalOverlay,
+ Tag,
Text,
VStack,
} from "@chakra-ui/react";
-import { FaTimesCircle } from "react-icons/fa";
-import { FaCheckCircle } from "react-icons/fa";
+
+import { FaCheckCircle, FaTimesCircle } from "react-icons/fa";
import { FaCircleCheck, FaCircleExclamation } from "react-icons/fa6";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
-import {formatDate} from "../../utils/formatDateTime";
+import { formatDate, formatTime } from "../../utils/formatDateTime";
import SuccessSignupModal from "./SuccessSignupModal";
-function EventInfoModal({
+const EventInfoModal = ({
user,
isOpenProp,
handleClose,
@@ -39,63 +43,47 @@ function EventInfoModal({
date,
id,
capacity,
- costume,
+ // costume,
isCorequisiteSignUp,
corequisites,
+ // modalIdentity,
+ setModalIdentity,
+ tags = [],
handleResolveCoreq = () => {},
-}) {
+}) => {
const { backend } = useBackendContext();
const [openSuccessModal, setOpenSuccessModal] = useState(false);
- // temp for image
- const [imageSrc, setImageSrc] = useState("");
- // const [enrollmentStatus, setEnrollmentStatus] = useState(false);
-
- // // console.log(user);
-
- // useEffect(() => {
- // const checkEventEnrollment = () => {
- // // Check if already checked into event
- // console.log(user.data[0].id, id);
- // const body = {student_id: user.data[0].id, event_id: id};
- // backend.get(
- // `/event-enrollments/test`, {body}
- // ).then(res => {
- // console.log(res);
- // setEnrollmentStatus(res.data.exists);
- // });
- // };
- // if (user?.data && user?.data[0]) {
- // checkEventEnrollment();
- // } else {
- // setEnrollmentStatus(false);
- // }
- // }, [backend, id, user?.data, isOpenProp]);
+ const [startTime, setStartTime] = useState("");
+ const [endTime, setEndTime] = useState("");
+ const [callTime, setCallTime] = useState("");
+ const getStartTime = async () => {
+ const data = await backend.get(`/events/${id}`);
+ setStartTime(data.data[0].startTime);
+ setEndTime(data.data[0].endTime);
+ setCallTime(data.data[0].callTime);
+ };
const enrollInEvent = async () => {
// Check if already checked into event
- const currentCheckIn = await backend.get(
- `/event-enrollments/test`,
- {
- params:{
- student_id: user.data[0].id,
- event_id: id
- }
- }
- );
+ const currentCheckIn = await backend.get(`/event-enrollments/test`, {
+ params: {
+ student_id: user.data[0].id,
+ event_id: id,
+ },
+ });
if (user.data[0] && !currentCheckIn.data.exists) {
const req = await backend.post(`/event-enrollments/`, {
student_id: user.data[0].id,
event_id: id,
- attendance: null
+ attendance: null,
});
if (req.status === 201) {
setOpenSuccessModal(true);
}
- }
- else {
+ } else {
console.log("Already signed up for this event!");
}
};
@@ -107,6 +95,8 @@ function EventInfoModal({
}
if (corequisites.some((coreq) => !coreq.enrolled)) {
+ // let coReqWarningModal know that it should programatically display an event info modal version
+ setModalIdentity("event");
handleResolveCoreq();
} else {
enrollInEvent();
@@ -118,15 +108,12 @@ function EventInfoModal({
handleClose();
};
-
-
useEffect(() => {
- if (isOpenProp && !imageSrc) {
- fetch("https://dog.ceo/api/breeds/image/random") // for fun
- .then((res) => res.json())
- .then((data) => setImageSrc(data.message));
+ if (!isOpenProp) {
+ return;
}
- }, [imageSrc, isOpenProp]);
+ getStartTime();
+ }, [isOpenProp]);
return (
<>
@@ -145,112 +132,128 @@ function EventInfoModal({
-
- {title}
-
+
+ }
+ onClick={handleClose}
+ aria-label="Back"
+ variant="ghost"
+ fontSize={"2xl"}
+ p={4}
+ ml={-4}
+ />
+
+ {tags.map((tag, index) => (
+
+ {tag.tag}
+
+ ))}
+
+
+ {title}
+
+
-
+
+ {description
+ ? `Description: ${description}`
+ : "No description available."}
+ {" "}
+
+
+
+ {formatDate(date)} · {formatTime(startTime)} –{" "}
+ {formatTime(endTime)}
+
+ Call Time: {formatTime(callTime)}
+ Location: {location}
+
-
- {!isCorequisiteSignUp && (
-
-
- Recommended
-
- {!corequisites || corequisites.length === 0 ? (
- No corequisites for this class
- ) : (
-
- {corequisites.map((coreq, index) => (
-
-
- {coreq.title}
-
- ))}
-
- )}
-
- )}
-
-
-
-
-
-
-
- Location:
- {location}
-
-
- Date
- {formatDate(date)}
-
-
-
-
-
- Time
- need to pass in time prop
-
- Description:
- {description}
-
-
-
- Capacity
- {capacity}
-
-
+
Level
{level}
+
+ Capacity
+ {capacity}
+
-
-
+
+
+
- Classes
- {costume}
+
+ Included Classes
+
+
+ All classes that will be participating in this performance.
+
+
+ {corequisites && corequisites.length > 0 ? (
+
+ {corequisites.map((prerequisite) => (
+
+ {prerequisite.title}
+
+ ))}
+
+ ) : (
+
+ No prerequisites for this class
+
+ )}
-
+
-
+
Sign Up
@@ -262,6 +265,6 @@ function EventInfoModal({
>
);
-}
+};
export default EventInfoModal;
diff --git a/client/src/components/discovery/SignUpController.jsx b/client/src/components/discovery/SignUpController.jsx
index 644bf77a..3c90584d 100644
--- a/client/src/components/discovery/SignUpController.jsx
+++ b/client/src/components/discovery/SignUpController.jsx
@@ -17,59 +17,36 @@ function SignUpController({
setOpenRootModal,
class_id = null,
event_id = null,
+ tags = [],
...infoProps
}) {
const { backend } = useBackendContext();
const { currentUser } = useAuthContext();
const [openCoreqModal, setOpenCoreqModal] = useState(false);
const [corequisites, setCorequisites] = useState([]);
+ // Is null as a initial state in this case okay? Semantically it makes sense to me.
+ const [modalIdentity, setModalIdentity] = useState(null);
+ const [coReqResponse, setCoreqResponse] = useState(null);
+ const [filteredCorequisites, setFilteredCorequisites] = useState([]);
+ const [tag, setTags] = useState([]);
+ const [teacherName, setTeacherName] = useState("");
const fetchCorequirements = useCallback(async () => {
- const id = class_id !== null ? class_id : event_id;
- const COREQUESITE_ROUTE =
- class_id !== null
- ? `/classes/corequisites/${id}`
- : `/events/corequisites/${id}`;
-
- const ENROLLMENT_ROUTE =
- class_id === null ? "/class-enrollments" : "/event-enrollments";
-
- const fetchEnrollments = async (coreq) => {
- try {
- const enrollment = await backend
- .get(ENROLLMENT_ROUTE)
- .then((res) => res.data);
-
- const user = await backend
- .get(`/users/${currentUser.uid}`)
- .then((res) => res.data[0]);
-
- const userEnrollments = enrollment
- .filter((event) => event.studentId === user.id)
- .map((event) => {
- if (class_id === null) {
- return event.classId;
- } else {
- return event.eventId;
- }
- });
-
- const corequisitesWithEnrollmentStatus = coreq.map((coreq) => {
- if (userEnrollments.includes(coreq.id)) {
- return { ...coreq, enrolled: true };
- }
- return coreq;
- });
- setCorequisites(corequisitesWithEnrollmentStatus);
- } catch (error) {
- console.error("Error fetching enrolled events or users:", error);
- }
- };
-
- const response = await backend.get(COREQUESITE_ROUTE);
- const coreq = response.data.map((coreq) => ({ ...coreq, enrolled: false }));
- setCorequisites(coreq);
- await fetchEnrollments(coreq);
+ const id = class_id ?? event_id;
+ const userId = user.data[0].id;
+
+ if (class_id !== null) {
+ // For classes, get associated event corequisite (max 1 per class)
+ const classCoReqResponse = await backend.get(
+ `/classes/corequisites/${id}/${userId}`
+ );
+ setCoreqResponse(classCoReqResponse.data);
+ } else {
+ const response = await backend.get(
+ `/events/corequisites/${id}/${userId}`
+ );
+ setCoreqResponse(response.data);
+ }
}, [backend, class_id, event_id, currentUser.uid]);
const toggleRootModal = () => {
@@ -80,15 +57,57 @@ function SignUpController({
setOpenCoreqModal(true);
};
+ useEffect(() => {
+ if (coReqResponse) {
+ // is this check to see an event okay? Will classes get call times in the future?
+ // console.log("CoReqResponse: ", coReqResponse);
+ const coreqs = coReqResponse.map((coreq) => {
+ const userId = user.data[0].id;
+ // No need to check userId anymore, coreq response will only return the rows for studentId
+ if (userId === coreq.studentId) {
+ return {
+ ...coreq,
+ isEvent: coreq.callTime ? true : false,
+ };
+ } else {
+ return {
+ ...coreq,
+ isEvent: coreq.callTime ? true : false,
+ };
+ }
+ });
+
+ // filter out the class associated with the card the user is currently on.
+ const postProcessedCoreqs = coreqs.filter((coreq) => {
+ if (class_id) {
+ return class_id !== coreq.id;
+ }
+
+ if (event_id) {
+ return event_id !== coreq.id;
+ }
+ });
+ // console.log("Filtered Coreqs: ", postProcessedCoreqs);
+ setCorequisites(coreqs);
+ setFilteredCorequisites(postProcessedCoreqs);
+ }
+ }, [coReqResponse]);
+
useEffect(() => {
if (openRootModal) {
fetchCorequirements();
}
}, [fetchCorequirements, openRootModal]);
+ useEffect(() => {
+ // console.log(modalIdentity);
+ }, [modalIdentity]);
+
if (class_id !== null && event_id !== null) {
throw new Error("Cannot have both class_id and event_id");
}
+
+ // console.log("SignUpController Rendered:", infoProps.title, class_id, event_id);
return (
<>
{class_id ? (
@@ -96,31 +115,46 @@ function SignUpController({
isOpenProp={openRootModal}
id={class_id}
{...infoProps}
- corequisites={corequisites}
+ corequisites={filteredCorequisites}
+ filteredCorequisites={filteredCorequisites}
isCorequisiteSignUp={false}
handleClose={toggleRootModal}
handleResolveCoreq={toggleCoreqModal}
user={user}
+ modalIdentity={modalIdentity}
+ setModalIdentity={setModalIdentity}
+ tags={tags}
/>
) : (
)}
setOpenCoreqModal(false)}
+ modalIdentity={modalIdentity}
+ setModalIdentity={setModalIdentity}
+ filteredCorequisites={filteredCorequisites}
/>
{/* setOpenRootModal(true)}>View Details */}
diff --git a/client/src/components/discovery/SuccessSignupModal.jsx b/client/src/components/discovery/SuccessSignupModal.jsx
index 2fb16649..7a59d203 100644
--- a/client/src/components/discovery/SuccessSignupModal.jsx
+++ b/client/src/components/discovery/SuccessSignupModal.jsx
@@ -3,33 +3,40 @@ import { useEffect } from "react";
import {
Box,
Button,
+ List,
+ ListItem,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
Text,
+ UnorderedList,
VStack,
} from "@chakra-ui/react";
-import { CiCircleCheck } from "react-icons/ci";
+import { MdCheckCircle } from "react-icons/md";
import { useNavigate } from "react-router-dom";
-import CompletedIndicator from "./CompletedIndicator.png"
-
function SuccessSignupModal({
isOpen,
title,
onClose,
- isCoreq: isCorequisiteSignUp,
+ isCoreq: isCorequisiteSignUp = false,
+ corequisites = [],
+ withCoreqFlag = false,
}) {
const navigate = useNavigate();
+ if (typeof title === "string") {
+ title = [title];
+ }
+
useEffect(() => {
if (isOpen) {
- setTimeout(() => {
- onClose();
- if (!isCorequisiteSignUp) navigate("/bookings");
- }, 2000);
+ // setTimeout(() => {
+ // onClose();
+ // if (!isCorequisiteSignUp) navigate("/bookings");
+ // }, 2000);
}
}, [isCorequisiteSignUp, isOpen, navigate, onClose]);
@@ -45,28 +52,57 @@ function SuccessSignupModal({
display={"flex"}
justifyContent={"center"}
>
-
-
-
+
+
+
+
+
+ You've successfully signed up for...
+
- You've successfully signed up for {title}
+ {title && title.length > 1 ? (
+
+ {title.map((t, index) => (
+ {t}
+ ))}
+
+ ) : (
+
+ {title ? title[0] : "No title detected"}
+
+ )}
+ {withCoreqFlag && corequisites.length > 0 && (
+
+ {corequisites.map((coreq) => (
+
+ {coreq.title}
+
+ ))}
+
+ )}
+
{!isCorequisiteSignUp && (
- View Booked Events
+ Find Upcoming Events
)}
diff --git a/client/src/components/discovery/confirmationModalStyle.js b/client/src/components/discovery/confirmationModalStyle.js
new file mode 100644
index 00000000..0a12ccd2
--- /dev/null
+++ b/client/src/components/discovery/confirmationModalStyle.js
@@ -0,0 +1,27 @@
+import { modalAnatomy as parts } from "@chakra-ui/anatomy";
+import {
+ createMultiStyleConfigHelpers,
+ defineStyle,
+} from "@chakra-ui/styled-system";
+
+const { definePartsStyle, defineMultiStyleConfig } =
+ createMultiStyleConfigHelpers(parts.keys);
+
+const xl = defineStyle({
+ px: "6",
+ py: "2",
+ fontSize: "xl",
+});
+
+const sm = defineStyle({
+ fontSize: "sm",
+ py: "6",
+});
+
+const sizes = {
+ xl: definePartsStyle({ header: sm, dialog: xl }),
+};
+
+export const modalTheme = defineMultiStyleConfig({
+ sizes,
+});
diff --git a/client/src/components/forms/createClasses.jsx b/client/src/components/forms/createClasses.jsx
index 1966be17..9a9c39d2 100644
--- a/client/src/components/forms/createClasses.jsx
+++ b/client/src/components/forms/createClasses.jsx
@@ -11,11 +11,8 @@ import {
Input,
NumberInput,
NumberInputField,
- Radio,
- RadioGroup,
Select,
Stack,
- Text,
Textarea,
useDisclosure,
useToast,
@@ -51,7 +48,7 @@ export const CreateClassForm = memo(
modalData?.end_date ?? modalData?.date ?? ""
);
const [recurrencePattern, setRecurrencePattern] = useState(
- modalData?.recurrence_pattern ?? "none"
+ modalData?.recurrence_pattern ?? ""
);
const [startTime, setStartTime] = useState(modalData?.startTime ?? "");
const [endTime, setEndTime] = useState(modalData?.endTime ?? "");
@@ -59,8 +56,8 @@ export const CreateClassForm = memo(
modalData?.description ?? ""
);
const [capacity, setCapacity] = useState(modalData?.capacity ?? 0);
- const [level, setLevel] = useState(modalData?.level ?? "beginner");
- const [classType, setClassType] = useState(modalData?.classType ?? "1");
+ const [level, setLevel] = useState(modalData?.level ?? "");
+ const [classType, setClassType] = useState(modalData?.classType ?? "");
const [performance, setPerformance] = useState(
modalData?.performance ?? -1
);
@@ -159,6 +156,12 @@ export const CreateClassForm = memo(
})
.catch((error) => console.log(error));
+ if (parseInt(performance) !== -1) {
+ await backend.put(
+ `/corequisites/${modalData.classId}` + `/${performance}`
+ );
+ }
+
// Add teacher to classes-taught on form post
const res = await backend.post(
`/classes-taught/`,
@@ -254,6 +257,11 @@ export const CreateClassForm = memo(
);
console.log("Added teacher to classes-taught:", res);
+ // Add prerequisite if performance is selected
+ if (parseInt(performance) !== -1) {
+ await backend.put(`/corequisites/${classId}` + `/${performance}`);
+ }
+
for (const classDate of classDates) {
try {
// const response = await backend.post("/classes", classBody);
@@ -292,12 +300,11 @@ export const CreateClassForm = memo(
onConfirmationClose();
onClose();
reloadCallback();
- closeModal(0); // 0 == "class"
};
useMemo(() => {
if (backend) {
- backend.get("/events").then((response) => {
+ backend.get("/events/all").then((response) => {
setEvents(response.data);
});
backend.get("/tags").then((response) => {
@@ -326,256 +333,349 @@ export const CreateClassForm = memo(
return (
-
+ ) : (
+
+
+
+ {isDraft ? "Draft" : "Class"}{" "}
+ {modalData ? "Updated" : "Submitted"}!
+ {" "}
+
{
- setIsDraft(false);
- displayToast();
- }}
+ colorScheme="blue"
+ onClick={closeModal}
>
- Publish
+ Return to Classes Page
-
-
+
+ )}
{
try {
const tagsResponse = await backend.get("/tags");
- // const initialTagFilter = {};
- const initialTags = {};
- tagsResponse.data.forEach((tag) => {
- // initialTagFilter[tag.id] = false;
- initialTags[tag.id] =
- tag.tag.charAt(0).toUpperCase() + tag.tag.slice(1).toLowerCase();
- });
-
- // setTagFilter(initialTagFilter);
- setTags(initialTags);
- console.log(initialTags);
+ setTags(tagsResponse.data);
} catch (error) {
console.error("Error fetching tags:", error);
}
@@ -224,209 +226,211 @@ export const CreateEvent = ({
}, []);
return (
-
- {!eventId ? : ""}
-
- Event Title
-
- {errors.title && {errors.title}}
-
-
-
- Location
-
- {errors.location && {errors.location}}
-
-
-
- Tags
-
- {/* {errors.level && {errors.level}} */}
-
-
-
- Date
-
- {errors.date && {errors.date}}
-
+
+
+ {!eventId ? : ""}
+
+ Event Title
+
+ {errors.title && {errors.title}}
+
-
-
- Start Time
+
+ Location
- {errors.startTime && {errors.startTime}}
-
+ {errors.location && {errors.location}}
+
-
- End Time
+
+ Tags
+
+ {/* {errors.level && {errors.level}} */}
+
+
+
+ Date
- {errors.endTime && {errors.endTime}}
-
-
+ {errors.date && {errors.date}}
+
+
+
+
+ Start Time
+
+ {errors.startTime && (
+ {errors.startTime}
+ )}
+
-
- Call Time
-
- {errors.callTime && {errors.callTime}}
-
+
+ End Time
+
+ {errors.endTime && {errors.endTime}}
+
+
-
- Capacity
+ Call Time
+ {errors.callTime && {errors.callTime}}
+
+
+ Capacity
+
+
+
+
+ Level
+
+ {errors.level && {errors.level}}
+
+
+
- Level
-
- {errors.level && {errors.level}}
+ isInvalid={errors.description}
+ />
+ {errors.description && (
+ {errors.description}
+ )}
-
-
-
- Description
-
- {errors.description && (
- {errors.description}
- )}
-
-
- handleSubmit(true)} // true = save draft
- isLoading={isSubmitting}
- flex="1"
- bg="gray.100"
+
- Save Draft
-
- handleSubmit(false)} // false = publish
- isLoading={isSubmitting}
- bg="purple.600"
- color="white"
- flex="1"
- >
- Publish
-
-
-
+ handleSubmit(true)} // true = save draft
+ isLoading={isSubmitting}
+ flex="1"
+ bg="gray.100"
+ >
+ Save Draft
+
+ handleSubmit(false)} // false = publish
+ isLoading={isSubmitting}
+ bg="purple.600"
+ color="white"
+ flex="1"
+ >
+ Publish
+
+
+
+
);
};
diff --git a/client/src/components/forms/editDraft.jsx b/client/src/components/forms/editDraft.jsx
index 32c931b5..5f6a0afb 100644
--- a/client/src/components/forms/editDraft.jsx
+++ b/client/src/components/forms/editDraft.jsx
@@ -1,21 +1,21 @@
import { useState } from "react";
import {
- Button,
- Center,
- Container,
- FormControl,
- FormLabel,
- Heading,
- Input,
- NumberInput,
- NumberInputField,
- Select,
- Stack,
- Text,
- Textarea,
- useDisclosure,
- VStack,
+ Button,
+ Center,
+ Container,
+ FormControl,
+ FormLabel,
+ Heading,
+ Input,
+ NumberInput,
+ NumberInputField,
+ Select,
+ Stack,
+ Text,
+ Textarea,
+ useDisclosure,
+ VStack,
} from "@chakra-ui/react";
import { IoIosCheckmarkCircle } from "react-icons/io";
@@ -24,183 +24,180 @@ import { useBackendContext } from "../../contexts/hooks/useBackendContext";
import SaveClassAsDraftModal from "./modals/saveClassAsDraft";
export const ClassEditingForm = ({ id }) => {
- const { backend } = useBackendContext();
- const postClass = async (e) => {
- e.preventDefault();
- console.log({
- location: location ? location : "in draft",
- date: date ? date : new Date(),
- description: description ? description : "in draft",
- level: level ? level : "in draft",
- capacity: capacity ? capacity : 0,
- costume: performances ? performances : "in draft",
- isDraft,
- title: title ? title : "in draft",
- });
- await backend
- .post("/classes", {
- location: location ? location : "in draft",
- date: date ? date : new Date(),
- description: description ? description : "in draft",
- level: level ? level : "in draft",
- capacity: capacity ? capacity : 0,
- costume: performances ? performances : "in draft",
- isDraft,
- title: title ? title : "in draft",
- })
- .then((response) => console.log(response))
- .catch((error) => console.log(error));
-
- setIsSubmitted(true);
- onClose();
- };
-
- const [title, setTitle] = useState("");
- const [location, setLocation] = useState("");
- const [date, setDate] = useState("");
- const [description, setDescription] = useState("");
- const [capacity, setCapacity] = useState("");
- const [level, setLevel] = useState("beginner");
- const [performances, setPerformances] = useState("");
- const { isOpen, onOpen, onClose } = useDisclosure();
- const [isSubmitted, setIsSubmitted] = useState(false);
- const [isDraft, setIsDraft] = useState(false);
-
- return (
-
- {
+ e.preventDefault();
+ console.log({
+ location: location ? location : "in draft",
+ date: date ? date : new Date(),
+ description: description ? description : "in draft",
+ level: level ? level : "in draft",
+ capacity: capacity ? capacity : 0,
+ costume: performances ? performances : "in draft",
+ isDraft,
+ title: title ? title : "in draft",
+ });
+ await backend
+ .post("/classes", {
+ location: location ? location : "in draft",
+ date: date ? date : new Date(),
+ description: description ? description : "in draft",
+ level: level ? level : "in draft",
+ capacity: capacity ? capacity : 0,
+ costume: performances ? performances : "in draft",
+ isDraft,
+ title: title ? title : "in draft",
+ })
+ .then((response) => console.log(response))
+ .catch((error) => console.log(error));
+
+ setIsSubmitted(true);
+ onClose();
+ };
+
+ const [title, setTitle] = useState("");
+ const [location, setLocation] = useState("");
+ const [date, setDate] = useState("");
+ const [description, setDescription] = useState("");
+ const [capacity, setCapacity] = useState("");
+ const [level, setLevel] = useState("beginner");
+ const [performances, setPerformances] = useState("");
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [isSubmitted, setIsSubmitted] = useState(false);
+ const [isDraft, setIsDraft] = useState(false);
+
+ return (
+
+
+ {!isSubmitted
+ ? "New Class"
+ : `${title} ${isDraft ? "Draft" : "Published"}`}
+
+ {!isSubmitted ? (
+
- {!isSubmitted
- ? (
-
- )
- : (
-
-
-
- Class Submitted!
- {" "}
-
-
- Return to Classes Page
-
-
- )}
-
- Beginner
+
+
+
+
+
+
+ Performances
+ setPerformances(e.target.value)}
/>
-
- );
+
+
+
+ {
+ onOpen();
+ setIsDraft(true);
+ }}
+ >
+ Save as Draft
+
+
+ Publish
+
+
+
+ ) : (
+
+
+
+ Class Submitted!
+ {" "}
+
+
+ Return to Classes Page
+
+
+ )}
+
+
+
+ );
};
export default ClassEditingForm;
diff --git a/client/src/components/forms/modals/saveClass.jsx b/client/src/components/forms/modals/saveClass.jsx
index b5c2cef5..f383b762 100644
--- a/client/src/components/forms/modals/saveClass.jsx
+++ b/client/src/components/forms/modals/saveClass.jsx
@@ -1,50 +1,55 @@
import {
- Button,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Stack,
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Stack,
} from "@chakra-ui/react";
const SaveClass = ({ isOpen, onClose, postClass }) => {
- return (
-
-
-
- Save Class
-
-
- Would you like to save this class?
-
-
-
-
- Close
-
- Save Class
-
-
-
-
- );
+ return (
+
+
+
+ Save Class
+
+
+ Would you like to save this class?
+
+
+
+
+ Close
+
+
+ Save Class
+
+
+
+
+
+ );
};
export default SaveClass;
diff --git a/client/src/components/login/Login.tsx b/client/src/components/login/Login.tsx
index f2dc45eb..94740756 100644
--- a/client/src/components/login/Login.tsx
+++ b/client/src/components/login/Login.tsx
@@ -1,34 +1,32 @@
import { useCallback, useEffect } from "react";
import {
+ Box,
Button,
Center,
Link as ChakraLink,
FormControl,
- FormLabel,
FormErrorMessage,
FormHelperText,
+ FormLabel,
Heading,
Image,
Input,
Stack,
+ Text,
useToast,
VStack,
- Text,
- Box,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
-import { FaGoogle } from "react-icons/fa6";
import { Link, useNavigate } from "react-router-dom";
import { z } from "zod";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
import { authenticateGoogleUser } from "../../utils/auth/providers";
-
-import logo from "./logo.png";
+import logo from "/logo.png";
const signinSchema = z.object({
email: z.string().email("Invalid email address"),
@@ -41,7 +39,7 @@ export const Login = () => {
const navigate = useNavigate();
const toast = useToast();
- const { login, handleRedirectResult } = useAuthContext();
+ const { login, handleRedirectResult, updateRole } = useAuthContext();
const { backend } = useBackendContext();
const {
@@ -60,6 +58,7 @@ export const Login = () => {
description: msg,
status: "error",
variant: "subtle",
+ position: "top",
});
},
[toast]
@@ -71,37 +70,32 @@ export const Login = () => {
email: data.email,
password: data.password,
});
- const response = await backend.get('/teachers');
- const teachers = response.data;
- const teacher = teachers.find(teach =>
- teach.email === data.email
- );
-
- if (teacher) {
- if (teacher.isActivated) {
- navigate('/bookings');
- }
- else {
- navigate('/teacher-signup/pending');
- }
+ const response = await backend.get("/teachers");
+ const teachers = response.data;
+ const teacher = teachers.find((teach) => teach.email === data.email);
+ updateRole();
+
+ if (teacher) {
+ if (teacher.isActivated) {
+ navigate("/bookings");
+ } else {
+ navigate("/teacher-signup/pending");
}
- else {
- console.log("In else clause!")
- const qrCodeRedirect = localStorage.getItem("qrcode_redirect");
- console.log(qrCodeRedirect)
- // return qrCodeRedirect ? navigate(qrCodeRedirect) : navigate('/discovery');
- if(qrCodeRedirect) {
- localStorage.removeItem("qrcode_redirect");
- navigate(qrCodeRedirect);
- } else {
- navigate('/bookings');
- }
+ } else {
+ console.log("In else clause!");
+ const qrCodeRedirect = localStorage.getItem("qrcode_redirect");
+ console.log(qrCodeRedirect);
+ // return qrCodeRedirect ? navigate(qrCodeRedirect) : navigate('/discovery');
+ if (qrCodeRedirect) {
+ localStorage.removeItem("qrcode_redirect");
+ navigate(qrCodeRedirect);
+ } else {
+ navigate("/bookings");
}
-
+ }
} catch (err) {
const errorCode = err.code;
const firebaseErrorMsg = err.message;
-
switch (errorCode) {
case "auth/wrong-password":
case "auth/invalid-credential":
@@ -138,29 +132,33 @@ export const Login = () => {
}, [backend, handleRedirectResult, navigate, toast]);
return (
-
-
-
+
-
+
{/* Account created! Continue to log in. */}
-
+
-
+
+ );
};
export default LinkOrImageModal;
diff --git a/client/src/components/playground/Playground.jsx b/client/src/components/playground/Playground.jsx
index 98cd4c60..3e944c18 100644
--- a/client/src/components/playground/Playground.jsx
+++ b/client/src/components/playground/Playground.jsx
@@ -1,23 +1,12 @@
-import { Button, useToast } from "@chakra-ui/react";
+import { Box, Button, Text, VStack } from "@chakra-ui/react";
+
+import { MdCheck, MdCheckCircle } from "react-icons/md";
+import { useNavigate } from "react-router-dom";
export const Playground = () => {
- const toast = useToast();
+ const title =
+ "classtitlethatisverylongandshouldbreakotherwiseiamgoingtohaveissueswiththelayout";
+ const navigate = useNavigate();
- return (
-
-
- toast({
- title: "Account created.",
- description: "We've created your account for you.",
- status: "success",
- duration: 9000,
- isClosable: true,
- })
- }
- >
- Show Toast
-
-
- );
+ return <>>;
};
diff --git a/client/src/components/profile/Profile.jsx b/client/src/components/profile/Profile.jsx
index a1d8574d..ab1a1129 100644
--- a/client/src/components/profile/Profile.jsx
+++ b/client/src/components/profile/Profile.jsx
@@ -1,29 +1,56 @@
+import { useEffect, useState } from "react";
+
import {
Box,
Button,
Flex,
Heading,
+ Icon,
Image,
Text,
VStack,
} from "@chakra-ui/react";
+import { FaUserCircle } from "react-icons/fa";
+import { useNavigate } from "react-router-dom";
+
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
import { L } from "../logout/Logout";
import { Navbar } from "../navbar/Navbar";
-import { useNavigate } from "react-router-dom";
-
export const Profile = () => {
- const { currentUser } = useAuthContext();
- const navigate = useNavigate()
+ const { currentUser, role } = useAuthContext();
+ const navigate = useNavigate();
+ const { backend } = useBackendContext();
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+
+ useEffect(() => {
+ const fetchUserDetails = async () => {
+ if (currentUser?.uid) {
+ try {
+ const response = await backend.get(`/users/${currentUser.uid}`);
+ if (response.data) {
+ const user = response.data[0];
+ setFirstName(user.firstName || "");
+ setLastName(user.lastName || "");
+ }
+ } catch (error) {
+ console.error("Error fetching user details:", error);
+ }
+ }
+ };
+
+ fetchUserDetails();
+ }, [backend, currentUser.uid]);
- const hardcodedProfilePic =
- "https://d1ef7ke0x2i9g8.cloudfront.net/hong-kong/_large700/2143830/LC-Sign-Tony-interview-Big-Hitter-header.webp";
- const hardcodedName =
- currentUser?.first_name && currentUser?.last_name
- ? `${currentUser.first_name} ${currentUser.last_name}`
- : "Homie Tony";
+ // const hardcodedProfilePic =
+ // "https://d1ef7ke0x2i9g8.cloudfront.net/hong-kong/_large700/2143830/LC-Sign-Tony-interview-Big-Hitter-header.webp";
+ // const hardcodedName =
+ // // currentUser?.first_name && currentUser?.last_name
+ // ? `${currentUser.first_name} ${currentUser.last_name}`
+ // : "Homie Tony";
return (
@@ -34,22 +61,27 @@ export const Profile = () => {
height="100vh"
padding={4}
>
-
- {hardcodedName}
+ {firstName && lastName
+ ? `${firstName} ${lastName}`
+ : firstName
+ ? `${firstName}`
+ : lastName
+ ? `${lastName}`
+ : "User"}
{currentUser?.email || "Not available"}
- Role: {currentUser?.user_role || "Not available"}
+ Role: {role || "Not available"}
{
target="_blank"
borderRadius="5px"
color="white"
- bg="#6B46C1"
+ bg="purple.600"
height="6.407vh"
// width calculated from figma hi-fi
w="82.33vw"
@@ -77,7 +109,7 @@ export const Profile = () => {
colorScheme="gray"
borderRadius="5px"
color="white"
- bg="#6B46C1"
+ bg="purple.600"
height="6.407vh"
// width calculated from figma hi-fi
w="82.33vw"
diff --git a/client/src/components/profile/Settings.jsx b/client/src/components/profile/Settings.jsx
index ad1f66da..4b0a4dd0 100644
--- a/client/src/components/profile/Settings.jsx
+++ b/client/src/components/profile/Settings.jsx
@@ -1,102 +1,254 @@
+import { useEffect, useState } from "react";
+
import {
Box,
Button,
- Center,
- Flex,
Heading,
- HStack,
- Image,
- SimpleGrid,
- Spacer,
+ Icon,
+ Input,
+ InputGroup,
+ InputRightElement,
Text,
+ useToast,
VStack,
} from "@chakra-ui/react";
-import { FaChevronRight } from "react-icons/fa";
+import { FaPencilAlt, FaUserCircle } from "react-icons/fa";
+import { useNavigate } from "react-router-dom";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
import { Navbar } from "../navbar/Navbar";
-import arrowImage from "./arrow.svg";
export const Settings = () => {
- const { currentUser, role } = useAuthContext();
+ const { currentUser } = useAuthContext();
+ const { backend } = useBackendContext();
+ const toast = useToast();
+
+ useEffect(() => {
+ const fetchUserDetails = async () => {
+ if (currentUser?.uid) {
+ try {
+ const response = await backend.get(`/users/${currentUser.uid}`);
+ if (response.data) {
+ const user = response.data[0];
+ setFirstName(user.firstName || "");
+ setLastName(user.lastName || "");
+ }
+ } catch (error) {
+ console.error("Error fetching user details:", error);
+ toast({
+ title: "Error",
+ description: "Could not fetch user details",
+ status: "error",
+ duration: 5000,
+ isClosable: true,
+ position: "top",
+ });
+ }
+ }
+ };
+
+ fetchUserDetails();
+ }, [currentUser, backend]);
+
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [isEditing, setIsEditing] = useState(false);
+
+ useEffect(() => {
+ if (currentUser?.displayName) {
+ const names = currentUser.displayName.split(" ", 2);
+ setFirstName(names[0] || "");
+ setLastName(names[1] || "");
+ } else if (currentUser?.firstName && currentUser?.lastName) {
+ setFirstName(currentUser.firstName);
+ setLastName(currentUser.lastName);
+ } else {
+ setFirstName("");
+ setLastName("");
+ }
+ }, [currentUser]);
- const hardcodedProfilePic =
- "https://d1ef7ke0x2i9g8.cloudfront.net/hong-kong/_large700/2143830/LC-Sign-Tony-interview-Big-Hitter-header.webp";
- const hardcodedName = currentUser?.displayName
- ? `${currentUser.displayName}`
- : "First Last";
+ const handleSave = async () => {
+ if (!currentUser?.uid) {
+ toast({
+ title: "Error",
+ description: "User not identified. Cannot save changes.",
+ status: "error",
+ duration: 5000,
+ isClosable: true,
+ position: "top",
+ });
+ return;
+ }
+
+ if (!firstName.trim() || !lastName.trim()) {
+ toast({
+ title: "Error",
+ description: "First and Last names cannot be empty.",
+ status: "error",
+ duration: 5000,
+ isClosable: true,
+ position: "top",
+ });
+ return;
+ }
+
+ try {
+ const response = await backend.put("/users/update", {
+ firebaseUid: currentUser.uid,
+ firstName: firstName.trim(),
+ lastName: lastName.trim(),
+ });
+
+ if (response.status === 200) {
+ toast({
+ title: "Success",
+ description: "Profile updated successfully.",
+ status: "success",
+ duration: 3000,
+ isClosable: true,
+ position: "top",
+ });
+ setIsEditing(false);
+ } else {
+ throw new Error("Failed to update profile");
+ }
+ } catch (error) {
+ console.error("Error updating profile:", error);
+ toast({
+ title: "Error",
+ description:
+ error.response?.data || "Could not update profile. Please try again.",
+ status: "error",
+ duration: 5000,
+ isClosable: true,
+ position: "top",
+ });
+ }
+ };
+
+ const navigate = useNavigate(); // Add this at the top with other hooks
return (
<>
-
-
-
-
-
- Account Details
-
-
+
+ Account Details
+
+
+
+
+
+
+ First Name
+
+
+ setFirstName(e.target.value)}
/>
-
-
-
-
-
+
+
+
+
+
+
+
+ Last Name
+
+
+ setLastName(e.target.value)}
+ />
+
+
+
+
+
+
+
+
- Name
- {currentUser?.displayName || "Not Available"}
- Role
- {role}
- Student Number
- {currentUser?.uid || "Not available"}
- Email
- {currentUser?.email || "Not available"}
- Password
-
-
- window.location.assign("/forgotPassword")
- }
- p={0}
- >
-
-
-
- Change Password
-
-
-
-
-
-
-
-
+ Email
+
+
+
+
+
+
+ Password
+
+ navigate("/forgotPassword")}
+ colorScheme="purple"
+ variant="outline"
+ >
+ Change Password
+
+
+
+
+ Save Changes
+
-
+
>
);
diff --git a/client/src/components/resources/LevelCard.jsx b/client/src/components/resources/LevelCard.jsx
index 7a42f89f..052b5d20 100644
--- a/client/src/components/resources/LevelCard.jsx
+++ b/client/src/components/resources/LevelCard.jsx
@@ -1,10 +1,14 @@
-import { Badge } from '@chakra-ui/react';
+import { Badge } from "@chakra-ui/react";
export const LevelCard = ({ level }) => (
-
{level}
diff --git a/client/src/components/resources/LevelCard.tsx b/client/src/components/resources/LevelCard.tsx
index de1802fd..67dffbc9 100644
--- a/client/src/components/resources/LevelCard.tsx
+++ b/client/src/components/resources/LevelCard.tsx
@@ -1,13 +1,17 @@
-import { Badge } from '@chakra-ui/react';
-import { Level } from '../types/booking';
+import { Badge } from "@chakra-ui/react";
+
+import { Level } from "../types/booking";
export const LevelCard = (level: Level) => (
-
{level}
);
-
diff --git a/client/src/components/resources/NewsCard.jsx b/client/src/components/resources/NewsCard.jsx
index b3e16e31..bd63f3c3 100644
--- a/client/src/components/resources/NewsCard.jsx
+++ b/client/src/components/resources/NewsCard.jsx
@@ -1,31 +1,77 @@
-import { Card, CardBody, Text, Stack, Link, Image, Badge, Flex } from "@chakra-ui/react";
+import {
+ Box,
+ Card,
+ CardBody,
+ Flex,
+ Image,
+ Link,
+ Stack,
+ Tag,
+ Text,
+} from "@chakra-ui/react";
-export const NewsCard = ({ id, S3Url, description, mediaUrl, tags }) => {
+export const NewsCard = ({
+ id,
+ S3Url,
+ description,
+ mediaUrl,
+ tags,
+ firstName,
+ lastName,
+}) => {
return (
-
-
-
- {description ?? "No description"}
-
-
-
-
-
- {tags?.length > 0 &&
- tags.map((tag, index) => (
-
- {tag}
-
- ))}
-
-
-
-
+
+
+
+
+
+ {description ?? "No description"}
+
+
+ Posted by{" "}
+ {firstName && lastName
+ ? `${firstName} ${lastName}`
+ : "Unknown Instructor"}
+
+
+
+
+
+
+ {tags?.length > 0 &&
+ tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+
);
};
diff --git a/client/src/components/resources/ResourceFlow/CardModal.jsx b/client/src/components/resources/ResourceFlow/CardModal.jsx
index b3e7a3b8..a83a6c45 100644
--- a/client/src/components/resources/ResourceFlow/CardModal.jsx
+++ b/client/src/components/resources/ResourceFlow/CardModal.jsx
@@ -1,3 +1,5 @@
+import { useEffect, useState } from "react";
+
import {
Badge,
Button,
@@ -25,6 +27,7 @@ import {
import { IoIosArrowBack } from "react-icons/io";
import { useAuthContext } from "../../../contexts/hooks/useAuthContext";
+import { useBackendContext } from "../../../contexts/hooks/useBackendContext";
import { ProgressBar } from "./ProgressBar";
export const CardModal = ({
@@ -40,7 +43,8 @@ export const CardModal = ({
allTags,
}) => {
const { currentUser } = useAuthContext();
- const user_name = currentUser.displayName || "Homie Tony";
+ const { backend } = useBackendContext();
+ const [user_name, setUserName] = useState(currentUser?.displayName || "");
const today = new Date();
const posted_date = today.toLocaleDateString();
@@ -48,6 +52,16 @@ export const CardModal = ({
setCurrentModal("form");
};
+ useEffect(() => {
+ backend.get(`/users/${currentUser?.uid}`).then((response) => {
+ setUserName(
+ response?.data[0]?.firstName && response?.data[0]?.lastName
+ ? `${response?.data[0]?.firstName} ${response?.data[0]?.lastName}`
+ : currentUser?.displayName || "current user"
+ );
+ });
+ }, [currentUser, backend]);
+
return (
-
{
top="10px"
zIndex={-1}
colorScheme="purple"
- sx={{ "--chakra-colors-purple-500": "#805AD5" }}
+ sx={{ "--chakra-colors-purple-500": "purple.600" }}
/>
diff --git a/client/src/components/resources/ResourceFlow/ResourceFlowController.jsx b/client/src/components/resources/ResourceFlow/ResourceFlowController.jsx
index b36ddb4a..7fcf0411 100644
--- a/client/src/components/resources/ResourceFlow/ResourceFlowController.jsx
+++ b/client/src/components/resources/ResourceFlow/ResourceFlowController.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useDisclosure } from "@chakra-ui/react";
+import { useAuthContext } from "../../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../../contexts/hooks/useBackendContext";
import { CardModal } from "./CardModal";
import { FormModal } from "./FormModal";
@@ -13,6 +14,7 @@ import { UploadFileModal } from "./UploadFileModal";
import { UploadLinkModal } from "./UploadLinkModal";
export const ControllerModal = ({ autoOpen = true }) => {
+ const { currentUser } = useAuthContext();
const { backend } = useBackendContext();
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -63,11 +65,14 @@ export const ControllerModal = ({ autoOpen = true }) => {
};
const ajax = async () => {
+ const teacherIdResponse = await backend.get(`/users/${currentUser?.uid}`);
+ const teacherId = teacherIdResponse.data[0].id;
+
const url = new URL(s3URL);
const urlBeforeQuery = url.origin + url.pathname;
let resourceId;
- if (link.includes("youtube")) {
+ if (link.includes("youtube") || link.includes("youtu.be")) {
// Create video resource
const videoResponse = await backend.post("/classes-videos", {
title: title,
@@ -75,6 +80,7 @@ export const ControllerModal = ({ autoOpen = true }) => {
description: description,
mediaUrl: link,
classId: clsId,
+ teacherId: teacherId,
});
console.log("Video Response:", videoResponse);
@@ -99,6 +105,7 @@ export const ControllerModal = ({ autoOpen = true }) => {
s3_url: urlBeforeQuery,
description: title,
media_url: link,
+ teacher_id: teacherId,
});
resourceId = articleResponse.data.id;
diff --git a/client/src/components/resources/ResourceFlow/SelectClassModal.jsx b/client/src/components/resources/ResourceFlow/SelectClassModal.jsx
index 8a7dd83e..aee1dbf8 100644
--- a/client/src/components/resources/ResourceFlow/SelectClassModal.jsx
+++ b/client/src/components/resources/ResourceFlow/SelectClassModal.jsx
@@ -142,7 +142,7 @@ export const SelectClassModal = ({
onChange={(e) =>
setClasses(
originalClasses.filter((obj) =>
- obj.name.includes(e.target.value)
+ obj.title.includes(e.target.value)
)
)
}
@@ -151,6 +151,8 @@ export const SelectClassModal = ({
spacing={4}
w="100%"
mt={4}
+ my={5}
+ mb={20}
>
{classes.length > 0 ? (
classes.map((cls, index) => (
diff --git a/client/src/components/resources/Resources.jsx b/client/src/components/resources/Resources.jsx
index 26446e82..47c066fc 100644
--- a/client/src/components/resources/Resources.jsx
+++ b/client/src/components/resources/Resources.jsx
@@ -5,10 +5,7 @@ import {
Box,
Center,
Flex,
- IconButton,
- Input,
- InputGroup,
- InputLeftAddon,
+ HStack,
Tab,
TabList,
TabPanel,
@@ -17,14 +14,12 @@ import {
Text,
} from "@chakra-ui/react";
-import { IoSearch } from "react-icons/io5";
-
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
import { Navbar } from "../navbar/Navbar";
+import { SearchBar } from "../searchbar/SearchBar";
import { NewsCard } from "./NewsCard";
import { ControllerModal } from "./ResourceFlow/ResourceFlowController";
-import { SearchBar } from "../searchbar/SearchBar";
import { UploadComponent } from "./UploadComponent";
import { VideoCard } from "./VideoCard";
@@ -37,7 +32,6 @@ export const Resources = () => {
const [tagFilter, setTagFilter] = useState({});
const [showModal, setShowModal] = useState(false);
const [tabIndex, setTabIndex] = useState(0);
- const [tab, setTab] = useState();
const { role } = useAuthContext();
@@ -143,17 +137,30 @@ export const Resources = () => {
const handleSearch = async (query) => {
try {
- if (tabIndex === 0) {
- const res = await backend.get(`/classes-videos/with-tags/search/${query}`);
- setVideos(res.data);
- } else if (tabIndex === 1) {
- const res = await backend.get(`/articles/with-tags/search/${query}`);
- setArticles(res.data);
+ if (!query) {
+ if (tabIndex === 0) {
+ const res = await backend.get("/classes-videos/with-tags");
+ setVideos(res.data);
+ }
+ if (tabIndex === 1) {
+ const res = await backend.get("/articles/with-tags");
+ setArticles(res.data);
+ }
+ } else {
+ if (tabIndex === 0) {
+ const res = await backend.get(
+ `/classes-videos/with-tags/search/${query}`
+ );
+ setVideos(res.data);
+ } else if (tabIndex === 1) {
+ const res = await backend.get(`/articles/with-tags/search/${query}`);
+ setArticles(res.data);
+ }
}
} catch (err) {
console.log("Error fetching search:", err);
}
- }
+ };
useEffect(() => {
if (tabIndex == 0) {
@@ -164,42 +171,90 @@ export const Resources = () => {
searchArticles();
}
fetchNews(); // Fetch news initially
- fetchTags(); // Fetch tags initially
+ fetchTags(); // Fetch tags initially
}, [tabIndex]);
return (
-
-
-
-
+
+
{/* place Videos and News cards into separate tabs */}
setTabIndex(index)}
>
- Videos
- Articles
+
+ Videos
+
+
+ Articles
+
-
- Videos
-
+
+
+
+ {role !== "student" && (
+
+
+ Add a Video
+
+
+ +
+
+
+ )}
+
{
classId={video.classId}
classTitle={video.classTitle}
mediaUrl={video.mediaUrl}
+ firstName={video.firstName}
+ lastName={video.lastName}
tags={video.tags?.map((tag) => tags[tag] || [])}
/>
);
@@ -229,13 +286,42 @@ export const Resources = () => {
-
- Articles
-
+
+
+
+ {role !== "student" && (
+
+
+ Add an Article
+
+
+ +
+
+
+ )}
{
S3Url={article.s3Url}
description={article.description}
mediaUrl={article.mediaUrl}
+ firstName={article.firstName}
+ lastName={article.lastName}
tags={article.tags?.map((tag) => tags[tag] || [])}
/>
);
@@ -266,21 +354,6 @@ export const Resources = () => {
{/* */}
- {role === "teacher" && (
- +}
- colorScheme="purple"
- size="lg"
- isRound
- position="fixed"
- bottom="95px"
- right="24px"
- zIndex={5}
- boxShadow="lg"
- aria-label="Add new item"
- onClick={handleAddButtonClick}
- />
- )}
{showModal && }
diff --git a/client/src/components/resources/StatusCard.jsx b/client/src/components/resources/StatusCard.jsx
index 6c4c1f77..01471bf7 100644
--- a/client/src/components/resources/StatusCard.jsx
+++ b/client/src/components/resources/StatusCard.jsx
@@ -1,8 +1,12 @@
-import { Badge } from '@chakra-ui/react';
+import { Badge } from "@chakra-ui/react";
export const StatusCard = ({ status }) => (
-
- {status ? 'verified' : 'not verified'}
+
+ {status ? "verified" : "not verified"}
);
-
diff --git a/client/src/components/resources/UploadComponent.jsx b/client/src/components/resources/UploadComponent.jsx
index 884b77f3..ecdfaae4 100644
--- a/client/src/components/resources/UploadComponent.jsx
+++ b/client/src/components/resources/UploadComponent.jsx
@@ -1,10 +1,10 @@
-import { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
+
+import { Box, Button, Input, Text } from "@chakra-ui/react";
-import { Text, Box, Button, Input } from "@chakra-ui/react";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
export const UploadComponent = () => {
-
const { backend } = useBackendContext();
const [file, setFile] = useState(null);
@@ -17,11 +17,11 @@ export const UploadComponent = () => {
const fetchS3URL = async () => {
try {
- const URLResponse = await backend.get('/s3/url');
+ const URLResponse = await backend.get("/s3/url");
console.log(URLResponse);
return URLResponse.data.url;
} catch (error) {
- console.error('Error fetching S3 URL:', error);
+ console.error("Error fetching S3 URL:", error);
}
};
@@ -63,11 +63,26 @@ export const UploadComponent = () => {
return (
Upload a File
-
-
+
+
{uploading ? "Uploading..." : "Upload"}
- {message && {message}}
+ {message && (
+
+ {message}
+
+ )}
);
};
diff --git a/client/src/components/resources/VideoCard.jsx b/client/src/components/resources/VideoCard.jsx
index ceba13b2..eaafda5b 100644
--- a/client/src/components/resources/VideoCard.jsx
+++ b/client/src/components/resources/VideoCard.jsx
@@ -1,33 +1,91 @@
-import { Card, CardHeader, CardBody, CardFooter, Text, Heading, Stack, Image, Link, Badge, Flex } from "@chakra-ui/react";
+import {
+ Box,
+ Card,
+ CardBody,
+ Flex,
+ Heading,
+ Image,
+ Link,
+ Stack,
+ Tag,
+ Text,
+} from "@chakra-ui/react";
-export const VideoCard = ({ id, title, description, S3Url, classId, classTitle, mediaUrl, tags }) => {
+export const VideoCard = ({
+ id,
+ title,
+ description,
+ S3Url,
+ classId,
+ classTitle,
+ mediaUrl,
+ tags,
+ firstName,
+ lastName,
+}) => {
return (
-
-
-
- {title}
- {description ?? "No description"}
- Posted by "Instructor Name" for {classTitle} {/* Implement Instructor Name at later task! - josh :D */}
-
-
-
-
-
- {tags?.length > 0 &&
- tags.map((tag, index) => (
-
- {tag}
-
- ))}
-
-
-
-
+
+
+
+
+
+ {title}
+
+
+ {description ?? "No description"}
+
+
+ Posted by{" "}
+ {firstName && lastName
+ ? `${firstName} ${lastName}`
+ : "Unknown Instructor"}{" "}
+ for Class: {classTitle}
+ {" "}
+ {/* Implement Instructor Name at later task! - josh :D */}
+
+
+
+
+ {tags?.length > 0 &&
+ tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+
);
};
diff --git a/client/src/components/resources/baseURL.jsx b/client/src/components/resources/baseURL.jsx
index 47fe29a4..bcc2b97c 100644
--- a/client/src/components/resources/baseURL.jsx
+++ b/client/src/components/resources/baseURL.jsx
@@ -1 +1 @@
-export const baseURL = "https://cse-images-dev.s3.us-west-1.amazonaws.com/"
+export const baseURL = "https://cse-images-dev.s3.us-west-1.amazonaws.com/";
diff --git a/client/src/components/reviewModals/reviewFailureModal.jsx b/client/src/components/reviewModals/reviewFailureModal.jsx
index 8949502a..59d7d06b 100644
--- a/client/src/components/reviewModals/reviewFailureModal.jsx
+++ b/client/src/components/reviewModals/reviewFailureModal.jsx
@@ -1,30 +1,40 @@
-import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton } from '@chakra-ui/react';
-import { Button } from '@chakra-ui/react';
-import { useDisclosure } from '@chakra-ui/react';
+import {
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ useDisclosure,
+} from "@chakra-ui/react";
const ReviewFailureModal = () => {
+ const { isOpen, onOpen, onClose } = useDisclosure();
- const {isOpen, onOpen, onClose} = useDisclosure();
+ return (
+ <>
+ Open Review Failure Modal
- return(
- <>
- Open Review Failure Modal
-
-
-
-
- Failure!
-
- An error occured while trying to submit that review... Please try again.
-
-
- Close
-
-
-
-
- >
- )
-}
+
+
+
+ Failure!
+
+
+ An error occured while trying to submit that review... Please
+ try again.
+
+
+
+ Close
+
+
+
+
+ >
+ );
+};
export default ReviewFailureModal;
diff --git a/client/src/components/reviewModals/reviewModal.jsx b/client/src/components/reviewModals/reviewModal.jsx
index 6988ebfb..dac113c9 100644
--- a/client/src/components/reviewModals/reviewModal.jsx
+++ b/client/src/components/reviewModals/reviewModal.jsx
@@ -1,91 +1,109 @@
-import { useState, useEffect} from 'react';
-import { useBackendContext } from '../../contexts/hooks/useBackendContext';
-import { useDisclosure } from '@chakra-ui/react';
-import { FormControl, FormLabel, FormErrorMessage, FormHelperText } from '@chakra-ui/react';
-import { Button, Radio, RadioGroup } from '@chakra-ui/react';
-import { HStack } from '@chakra-ui/react';
-import { Textarea } from '@chakra-ui/react';
-import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton } from '@chakra-ui/react';
+import { useEffect, useState } from "react";
-const ReviewModal = () => {
-
- const { backend } = useBackendContext();
+import {
+ Button,
+ FormControl,
+ FormErrorMessage,
+ FormHelperText,
+ FormLabel,
+ HStack,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Radio,
+ RadioGroup,
+ Textarea,
+ useDisclosure,
+} from "@chakra-ui/react";
- const {isOpen, onOpen, onClose} = useDisclosure();
- const [rating, setRating] = useState("");
- const [review, setReview] = useState("");
- const [data, setData] = useState();
- const [formSubmitted, setFormSubmitted] = useState(0);
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
- const handleRatingChange = (event) => {
- setRating(event);
- };
-
- const handleReviewChange = (event) => {
- setReview(event.target.value);
- };
+const ReviewModal = () => {
+ const { backend } = useBackendContext();
- const handleFormSubmission = async (event) => {
- // event.preventDefault();
- try {
- const response = await backend.post('/reviews',
- {
- class_id: 73,
- student_id: 153,
- rating: rating,
- review: review
- }
- );
- } catch(err) {
- alert(err);
- }
- };
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [rating, setRating] = useState("");
+ const [review, setReview] = useState("");
+ const [data, setData] = useState();
+ const [formSubmitted, setFormSubmitted] = useState(0);
- return(
- <>
+ const handleRatingChange = (event) => {
+ setRating(event);
+ };
- Open Review Modal
+ const handleReviewChange = (event) => {
+ setReview(event.target.value);
+ };
-
-
-
- Leave a review
-
+
+
+ >
+ );
};
export default ReviewModal;
diff --git a/client/src/components/reviewModals/reviewSubmittedModal.jsx b/client/src/components/reviewModals/reviewSubmittedModal.jsx
index a0afd4ae..9a649807 100644
--- a/client/src/components/reviewModals/reviewSubmittedModal.jsx
+++ b/client/src/components/reviewModals/reviewSubmittedModal.jsx
@@ -1,30 +1,37 @@
-import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton } from '@chakra-ui/react';
-import { Button } from '@chakra-ui/react';
-import { useDisclosure } from '@chakra-ui/react';
+import {
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ useDisclosure,
+} from "@chakra-ui/react";
const ReviewSubmittedModal = () => {
+ const { isOpen, onOpen, onClose } = useDisclosure();
- const {isOpen, onOpen, onClose} = useDisclosure();
+ return (
+ <>
+ Open Review Submitted Modal
- return(
- <>
- Open Review Submitted Modal
-
-
-
-
- Success!
-
- Thank you for taking the time to give us a review!
-
-
- Close
-
-
-
-
- >
- )
+
+
+
+ Success!
+
+ Thank you for taking the time to give us a review!
+
+
+ Close
+
+
+
+
+ >
+ );
};
-export default ReviewSubmittedModal;
\ No newline at end of file
+export default ReviewSubmittedModal;
diff --git a/client/src/components/reviews/Reviews.jsx b/client/src/components/reviews/Reviews.jsx
index db502b38..3e3db0f6 100644
--- a/client/src/components/reviews/Reviews.jsx
+++ b/client/src/components/reviews/Reviews.jsx
@@ -1,25 +1,23 @@
-import ReviewModal from '../reviewModals/reviewModal.jsx';
-import ReviewSubmittedModal from '../reviewModals/reviewSubmittedModal.jsx';
-import ReviewFailureModal from '../reviewModals/reviewSubmittedModal.jsx';
-import ClassInfoModal from '../reviewModals/classInfoModal.jsx';
+import ClassInfoModal from "../reviewModals/classInfoModal.jsx";
+import ReviewModal from "../reviewModals/reviewModal.jsx";
+import ReviewSubmittedModal from "../reviewModals/reviewSubmittedModal.jsx";
+import ReviewFailureModal from "../reviewModals/reviewSubmittedModal.jsx";
export const Reviews = () => {
- return(
- <>
-
-
-
-
-
-
- >
- )
+ return (
+ <>
+
+
+
+
+
+ >
+ );
};
-
diff --git a/client/src/components/reviews/classReview.jsx b/client/src/components/reviews/classReview.jsx
index f8d16678..14cccf16 100644
--- a/client/src/components/reviews/classReview.jsx
+++ b/client/src/components/reviews/classReview.jsx
@@ -10,7 +10,7 @@ import ReviewCard from "./reviewCard";
import ReviewCardController from "./ReviewCardController";
import StudentReview from "./studentReview";
-const PublishedReviews = ({ classId }) => {
+const PublishedReviews = ({ classId, isAttended = false }) => {
const { backend } = useBackendContext();
const { currentUser } = useAuthContext();
const [reviews, setReviews] = useState([]);
@@ -48,22 +48,24 @@ const PublishedReviews = ({ classId }) => {
Reviews
- {!reviews.some((review) => review.studentId === user?.id) && (
- <>
-
-
- >
- )}
+ {!reviews.some((review) => review.studentId === user?.id) &&
+ isAttended && (
+ <>
+
+
+ >
+ )}
+
{reviews.map((review, index) => (
{
@@ -43,12 +48,15 @@ const ReviewCard = ({
return (
<>
-
-
+
-
+
{student?.firstName} {student?.lastName}
diff --git a/client/src/components/reviews/studentReview.jsx b/client/src/components/reviews/studentReview.jsx
index bc43a056..f08543a2 100644
--- a/client/src/components/reviews/studentReview.jsx
+++ b/client/src/components/reviews/studentReview.jsx
@@ -11,15 +11,18 @@ import {
FormHelperText,
FormLabel,
HStack,
+ Icon,
Text,
Textarea,
+ useToken,
+ VStack,
} from "@chakra-ui/react";
+import { FaUserCircle } from "react-icons/fa";
import { FaStar } from "react-icons/fa6";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
-import { color } from "framer-motion";
const StudentReview = ({
rating,
@@ -35,6 +38,8 @@ const StudentReview = ({
const [starRating, setStarRating] = useState(rating ?? 0);
const [review, setReview] = useState(reviewText ?? "");
const [attended, setAttended] = useState(null);
+ const [purpleHex] = useToken("colors", ["purple.600"]);
+ const [greyHex] = useToken("colors", ["gray.400"]);
const [stars, setStars] = useState(Array(5).fill(0));
@@ -53,8 +58,8 @@ const StudentReview = ({
setStarRating(value);
};
const colors = {
- purple: "#6B46C1",
- grey: "#A9A9A9",
+ purple: purpleHex,
+ grey: greyHex,
};
const isError = review === "" || starRating === 0;
@@ -92,55 +97,71 @@ const StudentReview = ({
`/class-enrollments/student/${student_id}`
);
- const attendanceObject = attendance.data.find((a) => a.id === class_id)
+ const attendanceObject = attendance.data.find((a) => a.id === class_id);
setAttended(attendanceObject ? attendanceObject.attendance : null);
};
fetchAttendance();
}, [backend, class_id, student_id]);
-
+
return (
-
+
- */}
+
- {displayName}
-
-
- {stars.map((_, index) => (
- setStarRating(e.target.value)}
- color={
- (hoverValue || starRating) > index
- ? colors.purple
- : colors.grey
- }
- onClick={() => handleClickStar(index + 1)}
- onMouseOver={() => handleMouseOverStar(index + 1)}
- onMouseLeave={() => handleMouseLeaveStar}
- />
- ))}
+ {displayName}
-
diff --git a/client/src/components/rsvp/classRsvp.jsx b/client/src/components/rsvp/classRsvp.jsx
index 8ae51515..510c8109 100644
--- a/client/src/components/rsvp/classRsvp.jsx
+++ b/client/src/components/rsvp/classRsvp.jsx
@@ -1,13 +1,29 @@
-import {
- Box, Text, HStack, Heading, Flex, Avatar,
- Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay,
- Table, TableContainer, Thead, Tbody, Tr, Th, Td,
-} from "@chakra-ui/react"
+import { useEffect, useState } from "react";
+
+import {
+ Avatar,
+ Box,
+ Flex,
+ Heading,
+ HStack,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Table,
+ TableContainer,
+ Tbody,
+ Td,
+ Text,
+ Th,
+ Thead,
+ Tr,
+} from "@chakra-ui/react";
-import { MdArrowBackIosNew } from "react-icons/md";
import { FaCheckCircle, FaTimesCircle } from "react-icons/fa";
+import { MdArrowBackIosNew } from "react-icons/md";
-import { useEffect, useState } from "react";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
@@ -27,24 +43,29 @@ export const ClassRSVP = ({ isOpen, onClose, card }) => {
console.error("Error fetching user: ", error);
}
};
-
+
fetchTeacherId();
}, [backend, currentUser.uid]);
-
+
useEffect(() => {
- if (teacherId) {
+ // only fetch if card opened
+ if (teacherId && isOpen) {
const fetchClasses = async () => {
try {
// const response = await backend.get(`/class-enrollments/teacher/${teacherId}/${card.id}`);
- const date = new Date(decodeURIComponent(card?.date)).toISOString().split("T")[0];
- const response = await backend.get(`/class-enrollments/class/${card?.id}/${date}`);
- console.log(response)
+ const date = new Date(decodeURIComponent(card?.date))
+ .toISOString()
+ .split("T")[0];
+ const response = await backend.get(
+ `/class-enrollments/class/${card?.id}/${date}`
+ );
+ console.log(response);
setStudents(response.data);
} catch (error) {
console.error("Error fetching classes:", error);
}
};
-
+
fetchClasses();
}
}, [backend, teacherId, card, isOpen]);
@@ -55,45 +76,99 @@ export const ClassRSVP = ({ isOpen, onClose, card }) => {
onClose={onClose}
size="full"
>
-
+
-
-
- RSVP's for {card.name}
+
+
+
+
+
+ RSVP's for {card.name}
+
- {/* TODO: Rethink about using charkaUI table, idk if i want to use it */}
+
+ {" "}
+ {/* TODO: Rethink about using charkaUI table, idk if i want to use it */}
- | Student |
- Checked In |
+
+ Student
+ |
+
+ Checked In
+ |
- {students && students.length > 0 ? students.map((user, index) => (
-
-
-
-
-
- {user.firstName} {user.lastName}
- {user.email}
+ {students && students.length > 0 ? (
+ students.map((user, index) => (
+
+ |
+
+
+
+
+ {user.firstName} {user.lastName}
+
+ {user.email}
+
+
+ |
+
+
+ {user.attendance ? (
+
+ ) : (
+
+ )}
-
- |
-
-
- {user.attendance ? : }
-
- |
+
+
+ ))
+ ) : (
+
+ | No students have RSVP'd |
- ))
- :
- | No students have RSVP'd |
- }
+ )}
|
@@ -101,4 +176,4 @@ export const ClassRSVP = ({ isOpen, onClose, card }) => {
);
-};
\ No newline at end of file
+};
diff --git a/client/src/components/rsvp/eventRsvp.jsx b/client/src/components/rsvp/eventRsvp.jsx
index 73f24757..2b071544 100644
--- a/client/src/components/rsvp/eventRsvp.jsx
+++ b/client/src/components/rsvp/eventRsvp.jsx
@@ -1,28 +1,46 @@
-import {
- Box, Text, HStack, Heading, Flex, Avatar,
- Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay,
- Table, TableContainer, Thead, Tbody, Tr, Th, Td,
-} from "@chakra-ui/react"
+import { useEffect, useState } from "react";
+
+import {
+ Avatar,
+ Box,
+ Flex,
+ Heading,
+ HStack,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Table,
+ TableContainer,
+ Tbody,
+ Td,
+ Text,
+ Th,
+ Thead,
+ Tr,
+} from "@chakra-ui/react";
-import { MdArrowBackIosNew } from "react-icons/md";
import { FaCheckCircle, FaTimesCircle } from "react-icons/fa";
+import { MdArrowBackIosNew } from "react-icons/md";
-import { useEffect, useState } from "react";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
export const EventRSVP = ({ isOpen, onClose, card }) => {
const { backend } = useBackendContext();
const [students, setStudents] = useState([]);
-
+
useEffect(() => {
const fetchEvents = async () => {
- try {
- const response = await backend.get(`/event-enrollments/event/${card.id}`);
+ try {
+ const response = await backend.get(
+ `/event-enrollments/event/${card.id}`
+ );
setStudents(response.data);
// console.log(students);
- } catch (error) {
+ } catch (error) {
console.error("Error fetching events:", error);
- }
+ }
};
fetchEvents();
@@ -34,45 +52,99 @@ export const EventRSVP = ({ isOpen, onClose, card }) => {
onClose={onClose}
size="full"
>
-
+
-
-
- RSVP's for {card.name}
+
+
+
+
+
+ RSVP's for {card.name}
+
- {/* TODO: Rethink about using charkaUI table, idk if i want to use it */}
+
+ {" "}
+ {/* TODO: Rethink about using charkaUI table, idk if i want to use it */}
- | Student |
- Checked In |
+
+ Student
+ |
+
+ Checked In
+ |
- {students && students.length > 0 ? students.map((user, index) => (
-
-
-
-
-
- {user.firstName} {user.lastName}
- {user.email}
+ {students && students.length > 0 ? (
+ students.map((user, index) => (
+
+ |
+
+
+
+
+ {user.firstName} {user.lastName}
+
+ {user.email}
+
+
+ |
+
+
+ {user.attended ? (
+
+ ) : (
+
+ )}
-
- |
-
-
- {user.attended ? : }
-
- |
+
+
+ ))
+ ) : (
+
+ | No students have RSVP'd |
- ))
- :
- | No students have RSVP'd |
- }
+ )}
|
@@ -80,4 +152,4 @@ export const EventRSVP = ({ isOpen, onClose, card }) => {
);
-};
\ No newline at end of file
+};
diff --git a/client/src/components/searchbar/SearchBar.jsx b/client/src/components/searchbar/SearchBar.jsx
index 30aaa86d..0729b13c 100644
--- a/client/src/components/searchbar/SearchBar.jsx
+++ b/client/src/components/searchbar/SearchBar.jsx
@@ -1,77 +1,106 @@
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import {
- InputGroup,
- InputLeftElement,
- Input,
- Flex,
- Badge,
- VStack,
+ Badge,
+ Flex,
+ Input,
+ InputGroup,
+ InputLeftElement,
+ VStack,
} from "@chakra-ui/react";
import { FaSearch } from "react-icons/fa";
-export const SearchBar = ( { onSearch, tags = {}, tagFilter = {}, onTag }) => {
- const [searchInput, setSearchInput] = useState("");
+export const SearchBar = ({ onSearch, tags = {}, tagFilter = {}, onTag }) => {
+ const [searchInput, setSearchInput] = useState("");
+ const debounceTimeoutRef = useRef(null);
- const handleEnterKeyDown = async (e) => {
- if (e.key == "Enter") {
- onSearch(searchInput);
- }
+ // Debounced search effect
+ useEffect(() => {
+ // Clear previous timeout
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+
+ // Set new timeout for 200ms debounce
+ debounceTimeoutRef.current = setTimeout(() => {
+ onSearch(searchInput);
+ }, 200);
+
+ // Cleanup function to clear timeout on component unmount
+ return () => {
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
}
+ };
+ }, [searchInput, onSearch]);
+
+ const handleEnterKeyDown = async (e) => {
+ if (e.key === "Enter") {
+ // Clear debounce timeout and search immediately
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+ onSearch(searchInput);
+ }
+ };
return (
-
-
-
-
-
- setSearchInput(e.target.value)}
- onKeyDown={handleEnterKeyDown}
- />
-
+
+
+
+
+
+ setSearchInput(e.target.value)}
+ onKeyDown={handleEnterKeyDown}
+ />
+
- { tags && onTag && (
-
+ {Object.entries(tags).map(([id, name]) => (
+ {
+ onTag(id)();
}}
+ rounded="full"
+ px={4}
+ py={1}
+ border={"1px"}
+ borderColor="gray.300"
+ colorScheme={tagFilter[id] ? "gray" : "white"}
+ textTransform="none"
+ cursor="pointer"
>
- {Object.entries(tags).map(([id, name]) => (
- {
- onTag(id)();
- }}
- rounded="xl"
- px={4}
- py={1}
- border={"1px"}
- borderColor="gray.300"
- colorScheme={tagFilter[id] ? "gray" : "white"}
- textTransform="none"
- cursor="pointer"
- >
- {name}
+ {name}
- ))}
+ ))}
)}
- )
-}
\ No newline at end of file
+ );
+};
diff --git a/client/src/components/shared/ClassCard.jsx b/client/src/components/shared/ClassCard.jsx
index b79a2c91..2d0d4930 100644
--- a/client/src/components/shared/ClassCard.jsx
+++ b/client/src/components/shared/ClassCard.jsx
@@ -1,166 +1,189 @@
-import { useEffect, useState } from "react";
+import { memo, useEffect, useState } from "react";
import {
+ Badge,
Box,
- Card,
- CardBody,
- CardFooter,
- CardHeader,
+ Flex,
Heading,
HStack,
+ Image,
Text,
VStack,
} from "@chakra-ui/react";
-import { FaClock, FaMapMarkerAlt, FaUser } from "react-icons/fa";
+import { FaMicrophoneAlt, FaMusic } from "react-icons/fa";
+import {
+ GiAbstract001,
+ GiBallerinaShoes,
+ GiBoombox,
+ GiCartwheel,
+ GiTambourine,
+} from "react-icons/gi";
import { useLocation } from "react-router-dom";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
import { formatDate, formatTime } from "../../utils/formatDateTime";
import SignUpController from "../discovery/SignUpController";
-export const ClassCard = ({
- title,
- description,
- location,
- capacity,
- level,
- costume,
- date,
- startTime,
- endTime,
- attendeeCount = 0, // Default to 0 if not provided
- onClick,
- id,
- user = null
-}) => {
- const formattedDate = date ? formatDate(date) : null;
- const formattedStartTime = startTime ? formatTime(startTime) : null;
- const formattedEndTime = endTime ? formatTime(endTime) : null;
- const { backend } = useBackendContext();
- const [classDate, setClassDate] = useState(null);
- const { pathname } = useLocation();
- const [openRootModal, setOpenRootModal] = useState(false);
- // console.log({formattedDate, formattedStartTime, formattedEndTime})
- // const fetchClassDate = async () => {
- // if (!classDate) {
- // // console.log("id", id);
- // const response = await backend.get(`/scheduled-classes/${id}`);
- // if (response?.data[0]?.date) {
- // const formattedDate = new Date(
- // response.data[0].date
- // ).toLocaleDateString("en-US");
- // setClassDate(formattedDate);
- // }
- // }
- // };
- useEffect(() => {
- const fetchClassDate = async () => {
- if (!classDate && id) {
- // console.log("id", id);
- const response = await backend.get(`/scheduled-classes/${id}`);
- if (response?.data[0]?.date) {
- const newDate = new Date(
- response.data[0].date
- ).toLocaleDateString("en-US");
- setClassDate(newDate);
- }
+export const ClassCard = memo(
+ ({
+ title,
+ location,
+ date,
+ startTime,
+ endTime,
+ attendeeCount = 0,
+ id,
+ user = null,
+ onClick = null,
+ tags = [],
+ }) => {
+ const formattedDate = date ? formatDate(date) : null;
+ const formattedStartTime = startTime ? formatTime(startTime) : null;
+ const formattedEndTime = endTime ? formatTime(endTime) : null;
+ const { backend } = useBackendContext();
+ const [classDate, setClassDate] = useState(null);
+ const [openRootModal, setOpenRootModal] = useState(false);
+
+ const { pathname } = useLocation();
+
+ const getIcon = () => {
+ const iconSize = 50;
+ // console.log("tags", tags);
+ switch (tags[0]?.id) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ case 5:
+ return ;
+ case 6:
+ return ;
+ case 7:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const handleClick = () => {
+ if (pathname === "/bookings") {
+ if (onClick) onClick();
+ } else {
+ setOpenRootModal(true);
}
};
- fetchClassDate();
- }, [backend, classDate, id]);
- return (
- <>
- {
- if (pathname === "/bookings") {
- onClick();
- } else {
- setOpenRootModal(true);
+ useEffect(() => {
+ const fetchClassDate = async () => {
+ if (!classDate && id) {
+ const response = await backend.get(`/scheduled-classes/${id}`);
+ if (response?.data[0]?.date) {
+ const newDate = new Date(response.data[0].date).toLocaleDateString(
+ "en-US"
+ );
+ setClassDate(newDate);
+ }
}
- }}
- cursor="pointer">
-
-
-
-
- {attendeeCount} {parseInt(attendeeCount) === 1 ? "Person" : "People"} Enrolled
-
-
-
-
-
+ {attendeeCount} {attendeeCount === 1 ? "Person" : "People"} Enrolled
+
+
+
+
+ {getIcon()}
+
+
+ {/* text */}
-
{title}
+
+
+ {location}
+
+
+ {formattedDate} · {formattedStartTime} – {formattedEndTime}
-
-
- {location ? `${location}` : "No location"}
-
-
-
- {formattedDate
- ? `${formattedDate} · ${formattedStartTime} - ${formattedEndTime}`
- : "No date"}
-
-
-
-
+
-
- {/* 0/{capacity} spots left */}
-
-
-
-
- >
- );
-};
+
+
+ );
+ }
+);
diff --git a/client/src/components/shared/EventCard.jsx b/client/src/components/shared/EventCard.jsx
index 9566aa8f..f36fb85b 100644
--- a/client/src/components/shared/EventCard.jsx
+++ b/client/src/components/shared/EventCard.jsx
@@ -1,191 +1,218 @@
+import { memo, useState } from "react";
+
import {
+ Badge,
Box,
- Card,
- CardBody,
- CardHeader,
+ Flex,
Heading,
HStack,
+ Image,
Text,
VStack,
- CardFooter,
- useDisclosure
} from "@chakra-ui/react";
-import { FaClock, FaMapMarkerAlt, FaUser } from "react-icons/fa";
+import { FaMicrophoneAlt, FaMusic } from "react-icons/fa";
+import {
+ GiAbstract001,
+ GiBallerinaShoes,
+ GiBoombox,
+ GiCartwheel,
+ GiTambourine,
+} from "react-icons/gi";
import { useLocation } from "react-router-dom";
-import { useAuthContext } from "../../contexts/hooks/useAuthContext";
-
+import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { formatDate, formatTime } from "../../utils/formatDateTime";
-import SignUpController from "../discovery/SignUpController";
import TeacherEventViewModal from "../bookings/teacherView/TeacherEventViewModal";
-import { useState } from "react";
-
-export const EventCard = ({
- id,
- title,
- location,
- description,
- level,
- date,
- startTime,
- endTime,
- callTime,
- classId,
- costume,
- capacity,
- attendeeCount = 0, // Default to 0 if not provided
- onClick,
- isAttended = false,
- triggerRefresh,
- onCloseModal,
- user = null,
-
-}) => {
- const formattedDate = formatDate(date);
- const formattedStartTime = formatTime(startTime);
- const formattedEndTime = formatTime(endTime);
- const [openModal, setOpenModal] = useState(false);
- const { pathname } = useLocation();
- const [openRootModal, setOpenRootModal] = useState(false);
- const [openTeacherModal, setOpenTeacherModal] = useState(false);
-
- const { isOpen, onOpen, onClose } = useDisclosure();
- const [currentModal, setCurrentModal] = useState("view");
- const { role } = useAuthContext();
-
- const handleOpenModal = () => {
- setOpenModal(!openModal);
- };
- const handleCancel = () => {
- setOpenModal(false);
- };
+import SignUpController from "../discovery/SignUpController";
- // const onCloseModal = () => {
- // setCurrentModal("view");
- // onClose();
- // };
+export const EventCard = memo(
+ ({
+ id,
+ title,
+ location,
+ description,
+ level,
+ date,
+ startTime,
+ endTime,
+ callTime,
+ costume,
+ capacity,
+ attendeeCount = 0,
+ onClick,
+ triggerRefresh,
+ user = null,
+ tags = [],
+ magic,
+ }) => {
+ const formattedDate = formatDate(date);
+ const formattedStartTime = formatTime(startTime);
+ const formattedEndTime = formatTime(endTime);
+ const { pathname } = useLocation();
+ const [openRootModal, setOpenRootModal] = useState(false);
+ const [openTeacherModal, setOpenTeacherModal] = useState(false);
+
+ const [currentModal, setCurrentModal] = useState("view");
+ const { role } = useAuthContext();
+
+ const getIcon = () => {
+ const iconSize = 50;
+ switch (tags[0]?.id) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ case 5:
+ return ;
+ case 6:
+ return ;
+ case 7:
+ return ;
+ default:
+ return ;
+ }
+ };
- const closeTeacherModal = () => {
- setOpenTeacherModal(false);
- // onCloseModal();
- };
+ const closeTeacherModal = () => {
+ setOpenTeacherModal(false);
+ };
- const handleClickModal = () => {
+ const handleClickModal = () => {
if (pathname === "/bookings" && role !== "student") {
if (currentModal === "view") {
setOpenTeacherModal(true);
console.log("Open teacher view modal!");
}
} else if (pathname === "/bookings") {
- onClick();
- }
- else {
+ if (onClick) onClick();
+ } else {
setOpenRootModal(true);
}
};
+ const dateTimeString = formattedDate
+ ? `${formattedDate} @ ${formattedStartTime} - ${formattedEndTime}`
+ : "Date/Time not available";
- // console.log(user);
- return (
- <>
-
-
-
-
-
- {attendeeCount} {parseInt(attendeeCount) === 1 ? "Person" : "People"} Enrolled
-
-
-
-
+
+ {attendeeCount} {attendeeCount === 1 ? "Person" : "People"} RSVP'd
+
+
+ alignItems="center"
+ justifyContent="center"
+ >
+ {getIcon()}
+
+
+
+ {title}
+
- {title}
-
-
-
- {location ? `${location}` : "No location"}
-
-
-
- {formattedDate
- ? `${formattedDate} · ${formattedStartTime} - ${formattedEndTime}`
- : "No date"}
-
-
-
-
+ fontSize="sm"
+ color="grey.700"
+ wordBreak="break-word"
+ >
+ {location}
+
+
+ {formattedDate} · {formattedStartTime} – {formattedEndTime}
+
-
-
-
-
- {/* Required Class ID: {classId} */}
-
-
-
-
-
- >
- );
-};
+
+
+ {role && role !== "student" && (
+
+ )}
+
+ );
+ }
+);
diff --git a/client/src/components/signup/AuthorityModal.tsx b/client/src/components/signup/AuthorityModal.tsx
new file mode 100644
index 00000000..a58d9d85
--- /dev/null
+++ b/client/src/components/signup/AuthorityModal.tsx
@@ -0,0 +1,192 @@
+import React, { useState } from "react";
+
+import {
+ Box,
+ Button,
+ Image,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Stack,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+
+interface AuthorityModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSelectAuthority: (authority: "student" | "teacher") => void;
+}
+
+const AuthorityModal: React.FC = ({
+ isOpen,
+ onClose,
+ onSelectAuthority,
+}) => {
+ const [selected, setSelected] = useState<"student" | "teacher" | null>(null);
+
+ const cleanup = () => {
+ setSelected(null);
+ onClose();
+ };
+
+ const handleConfirm = () => {
+ if (selected) {
+ onSelectAuthority(selected);
+ cleanup();
+ }
+ };
+
+ const handleBack = () => {
+ cleanup();
+ };
+
+ const OptionButton = ({
+ authority,
+ icon,
+ label,
+ }: {
+ authority: "student" | "teacher";
+ icon: string;
+ label: string;
+ }) => {
+ const isActive = selected === authority;
+ return (
+ setSelected(authority)}
+ variant="outline"
+ borderColor={isActive ? "purple.600" : "gray.200"}
+ color={isActive ? "purple.600" : "gray.800"}
+ bg={isActive ? "purple.50" : "white"}
+ _hover={{ bg: isActive ? "purple.100" : "gray.100" }}
+ height="90px"
+ width="100%"
+ rounded="md"
+ justifyContent="flex-start"
+ leftIcon={
+
+ }
+ >
+
+ {label}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+ {/* HEADER */}
+
+ Are you a student or a teacher?
+
+
+
+ {/* OPTIONS */}
+
+
+
+
+
+
+
+ {/* FOOTER */}
+
+
+
+ Confirm
+
+
+
+ Back
+
+
+
+
+
+ );
+};
+
+export default AuthorityModal;
diff --git a/client/src/components/signup/Landing.tsx b/client/src/components/signup/Landing.tsx
index 713a7937..74bc5d60 100644
--- a/client/src/components/signup/Landing.tsx
+++ b/client/src/components/signup/Landing.tsx
@@ -1,51 +1,74 @@
+import React, { useState } from "react";
+
import {
Button,
- Image,
+ Divider,
+ Flex,
+ Text,
+ useDisclosure,
VStack,
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
-
-import logo from "./logo.png";
-
-
-
+import { Login } from "../login/Login";
+import AuthorityModal from "./AuthorityModal";
export const Landing = () => {
const navigate = useNavigate();
- const handleLogin = () => {
- navigate('/login')
- }
- const handleSignup = () => {
- navigate('/signup')
- }
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const handleSelectAuthority = (authority: "student" | "teacher") => {
+ onClose();
+ if (authority === "student") {
+ navigate("/signup");
+ } else if (authority === "teacher") {
+ navigate("/teacher-signup");
+ }
+ };
+ const handleSignupClick = onOpen;
return (
-
-
-
- Signup
-
+ <>
+
+
+
+
+
+ OR
+
+
+
- Login
+ type="button"
+ size={"lg"}
+ bg="#E2E8F0"
+ w={"48.2587vw"}
+ color="white"
+ mt={4}
+ onClick={handleSignupClick}
+ textColor={"#71717A"}
+ >
+ Signup
-
+
+
+
+ >
);
};
diff --git a/client/src/components/signup/Signup.tsx b/client/src/components/signup/Signup.tsx
index 91cd5889..bf16017d 100644
--- a/client/src/components/signup/Signup.tsx
+++ b/client/src/components/signup/Signup.tsx
@@ -1,49 +1,52 @@
import { useEffect } from "react";
import {
+ Box,
Button,
Center,
Link as ChakraLink,
FormControl,
- Box,
FormErrorMessage,
- Image,
FormHelperText,
FormLabel,
Heading,
+ Image,
Input,
Select,
Stack,
+ Text,
useToast,
VStack,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
-import { FaGoogle } from "react-icons/fa6";
-import { Link, useNavigate } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
-import { authenticateGoogleUser } from "../../utils/auth/providers";
-import logo from "./logo.png";
-
-const signupSchema = z.object({
- firstName: z.string().min(1, "Please include your first name."),
- lastName: z.string().min(1, "Please include your last name."),
- email: z.string().email("Invalid email address"),
- password: z.string().min(6, "Password must be at least 6 characters long"),
- level: z.string(),
-});
+const signupSchema = z
+ .object({
+ firstName: z.string().min(1, "Please include your first name."),
+ lastName: z.string().min(1, "Please include your last name."),
+ email: z.string().email("Invalid email address"),
+ password: z.string().min(8, "Password must be at least 8 characters long"),
+ confirmPassword: z.string().min(1, "Please confirm your password."),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords do not match",
+ path: ["confirmPassword"],
+ });
type SignupFormValues = z.infer;
export const Signup = () => {
const navigate = useNavigate();
const toast = useToast();
- const { studentSignup, handleRedirectResult } = useAuthContext();
+ const { studentSignup, handleRedirectResult, updateRole, login } =
+ useAuthContext();
const { backend } = useBackendContext();
const {
@@ -62,10 +65,15 @@ export const Signup = () => {
lastName: data.lastName,
email: data.email,
password: data.password,
- level: data.level,
+ level: "beginner",
});
if (user) {
+ login({
+ email: data.email,
+ password: data.password,
+ });
+ updateRole();
navigate("/discovery");
}
} catch (err) {
@@ -75,149 +83,129 @@ export const Signup = () => {
description: err.message,
status: "error",
variant: "subtle",
+ position: "top",
});
}
}
};
- const handleGoogleSignup = async () => {
- await authenticateGoogleUser();
- };
-
useEffect(() => {
handleRedirectResult(backend, navigate, toast);
}, [backend, handleRedirectResult, navigate, toast]);
- return (
-
+ const handleBack = () => {
+ navigate("/landing");
+ };
-
-
-
-
+ return (
+
-
+
+ Enter your details
+
-
-
+
- First Name
-
-
-
-
+ First Name
+
+
+ {errors.firstName?.message?.toString()}
+
+
Last Name
-
-
-
-
-
+
+
+ {errors.lastName?.message?.toString()}
+
+
+
Email Address
-
+
{errors.email?.message?.toString()}
-
-
-
- Password
-
-
-
- {errors.password?.message?.toString()}
-
-
-
-
-
-
-
-
-
-
-
+
+ Password
+
+
+ {errors.password?.message?.toString()}
+
-
-
- 0}
- mt={4}
- >
- Submit
-
-
-
+
+ Confirm Password Again
+
+
+ {errors.confirmPassword?.message?.toString()}
+
+
+ 0}
+ mt={4}
+ w="100%"
+ >
+ Confirm
+
+
+ Back
+
+
-
- {/* }
- variant={"solid"}
- size={"lg"}
- onClick={handleGoogleSignup}
- sx={{ width: "100%" }}
- >
- Signup with Google
- */}
);
diff --git a/client/src/components/signup/logo.png b/client/src/components/signup/logo.png
deleted file mode 100644
index 603c38ac..00000000
Binary files a/client/src/components/signup/logo.png and /dev/null differ
diff --git a/client/src/components/teacher-signup/TeacherSignup.tsx b/client/src/components/teacher-signup/TeacherSignup.tsx
index ba87749e..16554731 100644
--- a/client/src/components/teacher-signup/TeacherSignup.tsx
+++ b/client/src/components/teacher-signup/TeacherSignup.tsx
@@ -1,52 +1,47 @@
import { useEffect } from "react";
+import ReactDOMServer from "react-dom/server";
import {
+ Box,
Button,
- Center,
- Link as ChakraLink,
FormControl,
FormErrorMessage,
- FormHelperText,
- Heading,
- Image,
+ FormLabel,
Input,
- Select,
- Stack,
+ Text,
useToast,
VStack,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
-import { FaGoogle } from "react-icons/fa6";
-import { Link, useNavigate } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { useAuthContext } from "../../contexts/hooks/useAuthContext";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
-import { authenticateGoogleUser } from "../../utils/auth/providers";
import { EmailTemplate } from "../signup/EmailTemplate";
-import ReactDOMServer from "react-dom/server";
-import centerStageLogo from "./requests/cse-logo.png";
-const signupSchema = z.object({
- firstName: z.string().min(1, "Field Cannot Be Empty"),
- lastName: z.string().min(1, "Field Cannot Be Empty"),
- experience: z.string().min(1, "Must Select an Experience Value"),
- email: z.string().email("Invalid email address"),
- password: z.string().min(6, "Password must be at least 6 characters long"),
- repeatPassword: z.string().min(6)
-}).refine((data) => data.password == data.repeatPassword, {
- message: "Passwords must match.",
- path: ["repeatPassword"],
-});
+const signupSchema = z
+ .object({
+ firstName: z.string().min(1, "Field Cannot Be Empty"),
+ lastName: z.string().min(1, "Field Cannot Be Empty"),
+ email: z.string().email("Invalid email address"),
+ password: z.string().min(6, "Password must be at least 6 characters long"),
+ repeatPassword: z.string().min(6),
+ })
+ .refine((data) => data.password === data.repeatPassword, {
+ message: "Passwords must match.",
+ path: ["repeatPassword"],
+ });
type SignupFormValues = z.infer;
export const TeacherSignup = () => {
const navigate = useNavigate();
const toast = useToast();
- const { teacherSignup, handleRedirectResult, updateDisplayName } = useAuthContext();
+ const { teacherSignup, handleRedirectResult, updateDisplayName } =
+ useAuthContext();
const { backend } = useBackendContext();
const {
@@ -63,17 +58,22 @@ export const TeacherSignup = () => {
const user = await teacherSignup({
firstName: data.firstName,
lastName: data.lastName,
- experience: data.experience,
+ experience: "",
email: data.email,
password: data.password,
});
if (user) {
- updateDisplayName(user, data.firstName + " " + data.lastName)
- const templateEmail = EmailTemplate( {"firstName": data.firstName, "lastName":data.lastName, "email": data.email, "role": "teacher"});
+ updateDisplayName(user, data.firstName + " " + data.lastName);
+ const templateEmail = EmailTemplate({
+ firstName: data.firstName,
+ lastName: data.lastName,
+ email: data.email,
+ role: "teacher",
+ });
backend.post("/nodemailer/send", {
- "to": import.meta.env.VITE_ADMIN_EMAIL,
- "html": ReactDOMServer.renderToString(templateEmail)
+ to: import.meta.env.VITE_ADMIN_EMAIL,
+ html: ReactDOMServer.renderToString(templateEmail),
});
navigate("/teacher-signup/request");
}
@@ -84,184 +84,136 @@ export const TeacherSignup = () => {
description: err.message,
status: "error",
variant: "subtle",
+ position: "top",
});
}
}
};
- const handleGoogleSignup = async () => {
- await authenticateGoogleUser();
- };
-
useEffect(() => {
handleRedirectResult(backend, navigate, toast);
}, [backend, handleRedirectResult, navigate, toast]);
return (
-
- {/* Teacher Signup */}
-
-
-
+
-
- */}
+
+ Enter your details
+
+
+
- First Name
-
+
+ First Name
-
-
- {errors.firstName?.message?.toString()}
-
-
+
+ {errors.firstName?.message?.toString()}
+
+
-
- Last Name
-
+
+ Last Name
-
-
- {errors.lastName?.message?.toString()}
-
-
-
- {/*
- Experience Level
-
-
-
-
- {errors.experience?.message?.toString()}
-
- */}
+
+ {errors.lastName?.message?.toString()}
+
+
-
- Email
-
+
+ Email Address
-
-
- {errors.email?.message?.toString()}
-
-
+
+ {errors.email?.message?.toString()}
+
+
-
- Password
-
+
+ Password
-
-
- {errors.password?.message?.toString()}
-
+
+ {errors.password?.message?.toString()}
+
- Repeat Password
-
+ Confirm Password
-
-
- {errors.repeatPassword?.message?.toString()}
-
-
- {/*
- Click here to login
- */}
-
+
+ {errors.repeatPassword?.message?.toString()}
+
+
-
0}
- bg="#422e8d"
+ bg="#6A1B9A"
color="white"
- w="200px"
- h="55px"
- mt={10}
- >
+ _hover={{ bg: "#4A148C" }}
+ isDisabled={Object.keys(errors).length > 0}
+ mt={4}
+ w="100%"
+ >
Submit
-
- {/* Removed SelectRoot component as it was causing an error */}
-
-
- {/* }
- variant={"solid"}
- size={"lg"}
- onClick={handleGoogleSignup}
- // sx={{ width: "100%" }}
- w="200px"
- h="55px"
- >
- Signup with Google
- */}
-
+ navigate("/landing")}
+ w="100%"
+ mt={2}
+ >
+ Back
+
+
+
+
+
);
};
diff --git a/client/src/components/teacher-signup/requests/Request.jsx b/client/src/components/teacher-signup/requests/Request.jsx
index 7ee18e40..2334af6d 100644
--- a/client/src/components/teacher-signup/requests/Request.jsx
+++ b/client/src/components/teacher-signup/requests/Request.jsx
@@ -1,46 +1,57 @@
import { Box, Button, Image, Text, VStack } from "@chakra-ui/react";
-import {useNavigate, useLocation} from "react-router-dom";
-import centerStageLogo from "./cse-logo.png";
+
+import { useLocation, useNavigate } from "react-router-dom";
+
+import centerStageLogo from "/logo.png";
const Request = () => {
- const navigate = useNavigate();
- const location = useLocation();
-
- const pending = location.pathname.includes("pending");
-
-
- const message = pending
- ? 'Pending account verification. Once approved, check your email to log in.'
- : 'Request sent!\nOnce approved, check your email to log in.';
-
-
- return (
-
-
-
-
-
- {message}
-
- navigate("/login")}>
- OK
-
-
-
- );
-}
-
-export default Request
\ No newline at end of file
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const pending = location.pathname.includes("pending");
+
+ const message = pending
+ ? "Pending account verification. Once approved, check your email to log in."
+ : "Request sent!\nOnce approved, check your email to log in.";
+
+ return (
+
+
+
+
+
+ {message}
+
+
+ navigate("/login")}
+ >
+ OK
+
+
+
+ );
+};
+
+export default Request;
diff --git a/client/src/components/teacher-signup/requests/center-stage-logo.png b/client/src/components/teacher-signup/requests/center-stage-logo.png
deleted file mode 100644
index 1f76bd3c..00000000
Binary files a/client/src/components/teacher-signup/requests/center-stage-logo.png and /dev/null differ
diff --git a/client/src/components/teacher-signup/requests/cse-logo.png b/client/src/components/teacher-signup/requests/cse-logo.png
deleted file mode 100644
index 603c38ac..00000000
Binary files a/client/src/components/teacher-signup/requests/cse-logo.png and /dev/null differ
diff --git a/client/src/contexts/AuthContext.tsx b/client/src/contexts/AuthContext.tsx
index f54248fb..fd4cb97f 100644
--- a/client/src/contexts/AuthContext.tsx
+++ b/client/src/contexts/AuthContext.tsx
@@ -176,6 +176,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
} catch (e) {
await backend.delete(`/users/${result.user.uid}`);
toast({
+ position: "top",
title: "An error occurred",
description: `Account was not created: ${e.message}`,
status: "error",
@@ -189,7 +190,10 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}
};
- const updateDisplayName = async (userCredential: UserCredential, fullName: string) => {
+ const updateDisplayName = async (
+ userCredential: UserCredential,
+ fullName: string
+ ) => {
if (userCredential?.user) {
try {
await updateProfile(userCredential.user, { displayName: fullName });
@@ -215,7 +219,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
console.error("Error fetching user role:", error);
}
}
- }
+ };
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
@@ -244,7 +248,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
resetPassword,
handleRedirectResult,
updateDisplayName,
- updateRole
+ updateRole,
}}
>
{loading ? : children}
diff --git a/client/src/main.tsx b/client/src/main.tsx
index 406867fb..ace2f932 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -8,13 +8,15 @@ import App from "./App.tsx";
import "@fontsource-variable/inter";
const colors = {
- brand: {},
+ purple: {
+ 100: "#6B46C1",
+ },
};
const fonts = {
- body: `'inter', sans-serif`,
- heading: `'inter', sans-serif`,
-}
+ body: `'inter', sans-serif`,
+ heading: `'inter', sans-serif`,
+};
const theme = extendTheme({ colors, fonts });
diff --git a/client/src/types/attendance.ts b/client/src/types/attendance.ts
index 74a95c09..35dff711 100644
--- a/client/src/types/attendance.ts
+++ b/client/src/types/attendance.ts
@@ -1,5 +1,4 @@
export type Attendance = {
- month: number | string;
- count: number;
- };
-
\ No newline at end of file
+ month: number | string;
+ count: number;
+};
diff --git a/client/src/types/legacy/event.ts b/client/src/types/legacy/event.ts
index bf3cbe3c..10733183 100644
--- a/client/src/types/legacy/event.ts
+++ b/client/src/types/legacy/event.ts
@@ -1,14 +1,13 @@
export type Event = {
- id: number;
- location: string;
- title: string;
- description: string;
- level: string;
- date: string;
- start_time: string;
- end_time: string;
- call_time: string;
- // costume: string;
- is_draft: boolean;
+ id: number;
+ location: string;
+ title: string;
+ description: string;
+ level: string;
+ date: string;
+ start_time: string;
+ end_time: string;
+ call_time: string;
+ // costume: string;
+ is_draft: boolean;
};
-
diff --git a/client/src/utils/auth/ForgotPassword.tsx b/client/src/utils/auth/ForgotPassword.tsx
index dc9cf1ec..9893823c 100644
--- a/client/src/utils/auth/ForgotPassword.tsx
+++ b/client/src/utils/auth/ForgotPassword.tsx
@@ -1,123 +1,130 @@
-import { useState } from 'react';
-import { sendPasswordReset } from './firebase';
-import { FormControl, Input, Button, Center, Link, Box, Heading, Text, Alert, AlertDescription} from '@chakra-ui/react';
+import { useState } from "react";
+import {
+ Box,
+ Button,
+ Center,
+ FormControl,
+ Heading,
+ Image,
+ Input,
+ Text,
+ useToast,
+ VStack,
+} from "@chakra-ui/react";
+
+import { useNavigate } from "react-router-dom";
+
+import { sendPasswordReset } from "./firebase";
export const ForgotPassword = () => {
- const [email, setEmail] = useState('');
- const [hasError, setHasError] = useState(false);
- const [errorMessage, setErrorMessage] = useState();
+ const [email, setEmail] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const toast = useToast();
+ const navigate = useNavigate();
- const handleForgotPassword = async e => {
+ const handleForgotPassword = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
try {
- e.preventDefault();
await sendPasswordReset(email);
- setHasError(false);
- setErrorMessage('');
- window.location.replace("/forgotPasswordConfirmation");
+ toast({
+ title: "Password Reset Email Sent",
+ description: "Check your inbox for instructions.",
+ status: "success",
+ duration: 5000,
+ isClosable: true,
+ position: "top",
+ });
+ setEmail(""); // Clear email field
+ navigate("/forgotPasswordConfirmation");
} catch (err) {
- setHasError(true);
- if (err.code === 'auth/invalid-email') {
- setErrorMessage("Email could not be found. Please try again.");
- }
- else {
- setErrorMessage(err.message);
+ let message = "An unexpected error occurred. Please try again.";
+ if (err instanceof Error) {
+ if (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (err as any).code === "auth/invalid-email" ||
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (err as any).code === "auth/user-not-found"
+ ) {
+ message = "Email address not found. Please check and try again.";
+ } else {
+ message = err.message;
}
- console.log(err)
+ }
+ toast({
+ title: "Error",
+ description: message,
+ status: "error",
+ duration: 5000,
+ isClosable: true,
+ position: "top",
+ });
+ console.error(err);
+ } finally {
+ setIsLoading(false);
}
};
return (
-
-
- { hasError &&
-
- { errorMessage }
-
- }
-
+
+
+
-
-
+ Forgot Password
+
+
+ Enter your email to reset your password.
+
+
- Reset Password
- Enter email address associated with account
-
+
-
- setEmail(target.value)}
- placeholder="Email Address"
- borderColor={"#CBD5E0"}
- borderRadius= '3px'
- />
-
-
-
-
- Cancel
-
-
-
-
- Send Instructions
-
-
-
+ setEmail(target.value)}
+ placeholder="Enter email"
+ size="lg"
+ />
-
-
-
-
+
+ Submit
+
+
+
+
+
);
};
diff --git a/client/src/utils/auth/ForgotPasswordConfirmation.tsx b/client/src/utils/auth/ForgotPasswordConfirmation.tsx
index dce5b7e8..02f1e841 100644
--- a/client/src/utils/auth/ForgotPasswordConfirmation.tsx
+++ b/client/src/utils/auth/ForgotPasswordConfirmation.tsx
@@ -1,41 +1,50 @@
-import { Button, Center, Link, Box, Heading, Text} from '@chakra-ui/react';
+import { Box, Button, Center, Heading, Link, Text } from "@chakra-ui/react";
export const ForgotPasswordConfirmation = () => {
- return (
-
-
-
- Instructions Sent
- Please check inbox for password reset instructions.
-
-
- Return to Login
-
-
-
-
+ return (
+
+
+
+
+ Instructions Sent
+
+
+ Please check inbox for password reset instructions.
+
+
+
+
+ Return to Login
+
+
+
- );
+
+
+ );
};
-export default ForgotPasswordConfirmation;
\ No newline at end of file
+export default ForgotPasswordConfirmation;
diff --git a/client/src/utils/auth/authUtils.jsx b/client/src/utils/auth/authUtils.jsx
index a429bea9..79b47085 100644
--- a/client/src/utils/auth/authUtils.jsx
+++ b/client/src/utils/auth/authUtils.jsx
@@ -7,10 +7,9 @@ const logout = async (redirectPath, navigate, cookies) => {
await signOut(auth);
clearCookies(cookies);
navigate(redirectPath);
- window.location.reload(true);
+ // window.location.reload();
+ localStorage.clear();
+ sessionStorage.clear();
};
-
-
-
export { logout };
diff --git a/client/src/utils/auth/cookie.ts b/client/src/utils/auth/cookie.ts
index 2ef3f99f..4b1b3e3b 100644
--- a/client/src/utils/auth/cookie.ts
+++ b/client/src/utils/auth/cookie.ts
@@ -46,4 +46,4 @@ export const clearCookies = (cookies: Cookies) => {
Object.values(cookieKeys).forEach((value) => {
cookies.remove(value);
});
-};
\ No newline at end of file
+};
diff --git a/client/src/utils/auth/firebase.ts b/client/src/utils/auth/firebase.ts
index 065a2b89..cf063bd6 100644
--- a/client/src/utils/auth/firebase.ts
+++ b/client/src/utils/auth/firebase.ts
@@ -68,8 +68,8 @@ const refreshToken = async () => {
return null;
};
-const sendPasswordReset = async email => {
+const sendPasswordReset = async (email) => {
await sendPasswordResetEmail(auth, email);
};
-export {refreshToken, sendPasswordReset}
+export { refreshToken, sendPasswordReset };
diff --git a/client/src/utils/formFormatDateTime.ts b/client/src/utils/formFormatDateTime.ts
index 05e2e8f0..5cf7728b 100644
--- a/client/src/utils/formFormatDateTime.ts
+++ b/client/src/utils/formFormatDateTime.ts
@@ -1,9 +1,8 @@
export const formFormatDate = (dateString: string) => {
-
const date = new Date(dateString);
const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
- const day = String(date.getDate()).padStart(2, '0');
+ const month = String(date.getMonth() + 1).padStart(2, "0"); // Month is 0-indexed
+ const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
diff --git a/client/src/utils/formatDateTime.ts b/client/src/utils/formatDateTime.ts
index 13330340..8433dd6a 100644
--- a/client/src/utils/formatDateTime.ts
+++ b/client/src/utils/formatDateTime.ts
@@ -16,6 +16,7 @@ export const getDefaultDate = (dateString?: string) => {
};
export const isDefaultDate = (date: string) => {
+ if (!date) return true;
return date.split("T")[0] === "1970-01-01";
};
diff --git a/server/common/transporter.ts b/server/common/transporter.ts
index 1a171c5b..31518af0 100644
--- a/server/common/transporter.ts
+++ b/server/common/transporter.ts
@@ -18,7 +18,6 @@ const emailSender =
? `${process.env.DEV_EMAIL_FIRSTNAME} ${process.env.DEV_EMAIL_LASTNAME}`
: `${process.env.PROD_EMAIL_FIRSTNAMS} ${process.env.DEV_EMAIL_LASTNAME}`;
-
// sender information
const transport = {
host: "smtp.gmail.com", // e.g. smtp.gmail.com
diff --git a/server/db/schema/articles.sql b/server/db/schema/articles.sql
index bad36e6c..2ef38242 100644
--- a/server/db/schema/articles.sql
+++ b/server/db/schema/articles.sql
@@ -2,5 +2,7 @@ CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
s3_url VARCHAR(256) NOT NULL,
description TEXT NOT NULL,
- media_url VARCHAR(256) NOT NULL
+ media_url VARCHAR(256) NOT NULL,
+ teacher_id INTEGER NOT NULL,
+ FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE
);
diff --git a/server/db/schema/class_videos.sql b/server/db/schema/class_videos.sql
index 8318a580..7d5acada 100644
--- a/server/db/schema/class_videos.sql
+++ b/server/db/schema/class_videos.sql
@@ -7,5 +7,7 @@ CREATE TABLE class_videos (
description TEXT NOT NULL,
media_url TEXT NOT NULL,
class_id INTEGER NOT NULL,
- FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE
+ teacher_id INTEGER NOT NULL,
+ FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE,
+ FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE
);
diff --git a/server/routes/article_tags.js b/server/routes/article_tags.js
index 89f6a387..c88f93ee 100644
--- a/server/routes/article_tags.js
+++ b/server/routes/article_tags.js
@@ -8,29 +8,31 @@ const articleTagsRouter = express.Router();
articleTagsRouter.use(express.json());
articleTagsRouter.get("/articles/:id", async (req, res) => {
- try {
- const { id } = req.params;
- const tags = await db.query(
- `SELECT * FROM article_tags
+ try {
+ const { id } = req.params;
+ const tags = await db.query(
+ `SELECT * FROM article_tags
JOIN tags ON article_tags.tag_id = tags.id
- WHERE article_tags.article_id = $1;` [id]);
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ WHERE article_tags.article_id = $1;`[id]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
articleTagsRouter.get("/tags/:id", async (req, res) => {
- try {
- const { id } = req.params;
- const tags = await db.query(
- `SELECT * FROM article_tags
+ try {
+ const { id } = req.params;
+ const tags = await db.query(
+ `SELECT * FROM article_tags
JOIN tags ON article_tags.tag_id = tags.id
- WHERE article_tags.tag_id = $1;` [id]);
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ WHERE article_tags.tag_id = $1;`[id]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
articleTagsRouter.post("/", async (req, res) => {
diff --git a/server/routes/articles.ts b/server/routes/articles.ts
index 14678089..93df6f35 100644
--- a/server/routes/articles.ts
+++ b/server/routes/articles.ts
@@ -17,23 +17,25 @@ interface ArticleRequest {
s3_url?: string;
description?: string;
media_url?: string;
+ teacher_id?: number;
}
articlesRouter.get("/with-tags", async (req, res) => {
- try {
- const data = await db.query(`
- SELECT a.id, a.s3_url, a.description, a.media_url, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
+ try {
+ const data = await db.query(`
+ SELECT a.id, a.s3_url, a.description, a.media_url, u.first_name, u.last_name, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
FROM articles a
+ LEFT JOIN users u ON a.teacher_id = u.id
LEFT JOIN article_tags av ON av.article_id = a.id
LEFT JOIN tags t ON t.id = av.tag_id
- GROUP BY a.id, a.s3_url, a.description, a.media_url
+ GROUP BY a.id, a.s3_url, a.description, a.media_url, u.first_name, u.last_name
ORDER BY a.id;
`);
-
- res.status(200).json(keysToCamel(data));
- } catch (err) {
- res.status(500).send(err.message);
- }
+
+ res.status(200).json(keysToCamel(data));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// GET /articles/:id
@@ -68,38 +70,43 @@ articlesRouter.get("/", async (req, res) => {
// Articles search functionality
articlesRouter.get("/with-tags/search/:name", async (req, res) => {
- try {
- const { name } = req.params;
- const data = await db.query(`
- SELECT a.id, a.s3_url, a.description, a.media_url, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
+ try {
+ const { name } = req.params;
+ const data = await db.query(
+ `
+ SELECT a.id, a.s3_url, a.description, a.media_url, u.first_name, u.last_name, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
FROM articles a
+ LEFT JOIN users u ON a.teacher_id = u.id
LEFT JOIN article_tags g ON g.article_id = a.id
LEFT JOIN tags t ON t.id = g.tag_id
WHERE a.description ILIKE $1
- GROUP BY a.id, a.s3_url, a.description, a.media_url
+ GROUP BY a.id, a.s3_url, a.description, a.media_url, u.first_name, u.last_name
ORDER BY a.id;
- `, [`%${name}%`]);
-
- res.status(200).json(keysToCamel(data));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ `,
+ [`%${name}%`]
+ );
+
+ res.status(200).json(keysToCamel(data));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// POST /articles
articlesRouter.post("/", async (req, res) => {
try {
// Destructure the request body
- const { s3_url, description, media_url } = req.body as ArticleRequest;
+ const { s3_url, description, media_url, teacher_id } =
+ req.body as ArticleRequest;
// Since its required in the schema send an error
- if (!s3_url || !description || !media_url) {
+ if (!s3_url || !description || !media_url || !teacher_id) {
return res.status(400).json({ error: "Missing required parameters" });
}
// Insert the new article into the database
// Returning * will return the newly inserted row in the response
const rows = await db.query(
- "INSERT INTO articles (s3_url, description, media_url) VALUES ($1, $2, $3) RETURNING *",
- [s3_url, description, media_url]
+ "INSERT INTO articles (s3_url, description, media_url, teacher_id) VALUES ($1, $2, $3, $4) RETURNING *",
+ [s3_url, description, media_url, teacher_id]
);
// Convert the snake_case keys to camelCase and send the response with status 201 (Created)
res.status(201).json(keysToCamel(rows[0] as Article));
diff --git a/server/routes/class_enrollments.js b/server/routes/class_enrollments.js
index 3fbd1dd2..edfa6fae 100644
--- a/server/routes/class_enrollments.js
+++ b/server/routes/class_enrollments.js
@@ -76,6 +76,15 @@ classEnrollmentsRouter.get("/student/:student_id", async (req, res) => {
try {
const result = await db.query(
`
+ WITH ranked_enrollments AS (
+ SELECT *,
+ ROW_NUMBER() OVER (
+ PARTITION BY class_id, student_id
+ ORDER BY attendance IS NULL -- non-null first
+ ) AS rn
+ FROM class_enrollments
+ WHERE student_id = $1
+ )
SELECT DISTINCT ON (c.id, sc.date)
c.*,
sc.date,
@@ -84,7 +93,7 @@ classEnrollmentsRouter.get("/student/:student_id", async (req, res) => {
ce.attendance,
(SELECT COUNT(*) FROM class_enrollments WHERE class_id = c.id) AS attendee_count
FROM classes c
- JOIN class_enrollments ce ON c.id = ce.class_id AND ce.student_id = $1
+ JOIN ranked_enrollments ce ON c.id = ce.class_id AND ce.student_id = $1 AND ce.rn = 1
LEFT JOIN scheduled_classes sc ON c.id = sc.class_id
ORDER BY c.id, sc.date DESC;
`,
diff --git a/server/routes/class_tags.js b/server/routes/class_tags.js
index 64dc99cc..654fe898 100644
--- a/server/routes/class_tags.js
+++ b/server/routes/class_tags.js
@@ -7,49 +7,113 @@ const classTagsRouter = express.Router();
classTagsRouter.use(express.json());
+// tags for all classes
+classTagsRouter.get("/all-class-tags", async (req, res) => {
+ try {
+ const tags = await db.query(
+ `
+ SELECT class_tags.class_id, JSON_ARRAYAGG(tags.*) tag_array
+ FROM class_tags
+ JOIN tags ON class_tags.tag_id = tags.id
+ GROUP BY class_tags.class_id;`
+ );
+
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
+// tags for all enrolled classes
+classTagsRouter.get("/enrolled-class-tags/:userId", async (req, res) => {
+ try {
+ const { userId } = req.params;
+
+ const tags = await db.query(
+ `
+ SELECT class_tags.class_id, JSON_ARRAYAGG(tags.*) tag_array
+ FROM class_tags
+ JOIN tags ON class_tags.tag_id = tags.id
+ JOIN class_enrollments ON class_tags.class_id = class_enrollments.class_id
+ WHERE class_enrollments.student_id = $1 AND class_enrollments.attendance IS NULL
+ GROUP BY class_tags.class_id;`,
+ [userId]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
// tags for a single class
classTagsRouter.get("/tags/:id", async (req, res) => {
- try {
- const { id } = req.params;
+ try {
+ const { id } = req.params;
- const tags = await db.query(`SELECT * FROM class_tags JOIN tags on class_tags.tag_id = tags.id WHERE class_tags.class_id = $1;`, [id])
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ const tags = await db.query(
+ `SELECT * FROM class_tags JOIN tags on class_tags.tag_id = tags.id WHERE class_tags.class_id = $1;`,
+ [id]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// get all classes matching a tag
classTagsRouter.get("/classes/:id", async (req, res) => {
- try {
- const { id } = req.params;
- const classes = await db.query(`SELECT * FROM class_tags JOIN classes on class_tags.class_id = classes.id WHERE class_tags.tag_id = $1;`, [id]);
- res.status(200).json(keysToCamel(classes));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ try {
+ const { id } = req.params;
+ const classes = await db.query(
+ `SELECT * FROM class_tags JOIN classes on class_tags.class_id = classes.id WHERE class_tags.tag_id = $1;`,
+ [id]
+ );
+ res.status(200).json(keysToCamel(classes));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// post new tag/class relationship
classTagsRouter.post("/", async (req, res) => {
- try {
- const { classId, tagId } = req.body;
- const tags = await db.query(`INSERT INTO class_tags (class_id, tag_id) VALUES ($1, $2) RETURNING *;`, [classId, tagId]);
- res.status(201).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
+ try {
+ const { classId, tagId } = req.body;
+
+ const existingTag = await db.query(
+ `SELECT * FROM class_tags WHERE class_id = $1 AND tag_id = $2;`,
+ [classId, tagId]
+ );
+ if (existingTag.length > 0) {
+ return res.status(201).json(keysToCamel(existingTag));
}
+ if (!classId || !tagId) {
+ return res
+ .status(400)
+ .json({ error: "Class ID and Tag ID are required." });
+ }
+
+ const tags = await db.query(
+ `INSERT INTO class_tags (class_id, tag_id) VALUES ($1, $2) RETURNING *;`,
+ [classId, tagId]
+ );
+ res.status(201).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// delete class/tag relationship
-classTagsRouter.delete("/", async (req, res) => {
- try {
- const { classId, tagId } = req.body;
- const tags = await db.query(`DELETE FROM class_tags WHERE class_id = $1 AND tag_id = $2 RETURNING *;`, [classId, tagId]);
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+classTagsRouter.delete("/:classId/:tagId", async (req, res) => {
+ try {
+ const { classId, tagId } = req.params;
+ const tags = await db.query(
+ `DELETE FROM class_tags WHERE class_id = $1 AND tag_id = $2 RETURNING *;`,
+ [classId, tagId]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
-export { classTagsRouter };
\ No newline at end of file
+export { classTagsRouter };
diff --git a/server/routes/class_videos.js b/server/routes/class_videos.js
index a38aba51..7528faae 100644
--- a/server/routes/class_videos.js
+++ b/server/routes/class_videos.js
@@ -1,4 +1,5 @@
import express from "express";
+
import { keysToCamel } from "../common/utils";
import { db } from "../db/db-pgp";
@@ -18,12 +19,13 @@ classVideosRouter.get("/", async (req, res) => {
classVideosRouter.get("/with-tags", async (req, res) => {
try {
const data = await db.query(`
- SELECT c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id, cs.title AS class_title, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
+ SELECT c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id, cs.title AS class_title, u.first_name, u.last_name, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
FROM class_videos c
+ LEFT JOIN users u ON c.teacher_id = u.id
LEFT JOIN video_tags v ON v.video_id = c.id
LEFT JOIN tags t ON t.id = v.tag_id
LEFT JOIN classes cs ON cs.id = c.class_id
- GROUP BY c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id, cs.title
+ GROUP BY c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id, cs.title, u.first_name, u.last_name
ORDER BY c.id;
`);
@@ -34,29 +36,35 @@ classVideosRouter.get("/with-tags", async (req, res) => {
});
classVideosRouter.get("/with-tags/search/:name", async (req, res) => {
- try {
- const { name } = req.params;
- const data = await db.query(`
- SELECT c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
+ try {
+ const { name } = req.params;
+ const data = await db.query(
+ `
+ SELECT c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id, u.first_name, u.last_name, COALESCE(ARRAY_AGG(t.id) FILTER (WHERE t.id IS NOT NULL), '{}') AS tags
FROM class_videos c
+ LEFT JOIN users u ON c.teacher_id = u.id
LEFT JOIN video_tags v ON v.video_id = c.id
LEFT JOIN tags t ON t.id = v.tag_id
WHERE title ILIKE $1
- GROUP BY c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id
+ GROUP BY c.id, c.title, c.s3_url, c.description, c.media_url, c.class_id, u.first_name, u.last_name
ORDER BY c.id;
- `, [`%${name}%`]);
-
- res.status(200).json(keysToCamel(data));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ `,
+ [`%${name}%`]
+ );
+
+ res.status(200).json(keysToCamel(data));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
classVideosRouter.get("/:id", async (req, res) => {
try {
const { id } = req.params;
- const data = await db.query(`SELECT * FROM class_videos WHERE id = $1`, [id]);
+ const data = await db.query(`SELECT * FROM class_videos WHERE id = $1`, [
+ id,
+ ]);
res.status(200).json(keysToCamel(data));
} catch (err) {
@@ -66,12 +74,12 @@ classVideosRouter.get("/:id", async (req, res) => {
classVideosRouter.post("/", async (req, res) => {
try {
- const { title, s3Url, description, mediaUrl, classId } = req.body;
-
+ const { title, s3Url, description, mediaUrl, classId, teacherId } = req.body;
+
const postData = await db.query(
- `INSERT INTO class_videos (title, s3_url, description, media_url, class_id)
- VALUES ($1, $2, $3, $4, $5) RETURNING id, title, s3_url, description, media_url, class_id;`,
- [title, s3Url, description, mediaUrl, classId]
+ `INSERT INTO class_videos (title, s3_url, description, media_url, class_id, teacher_id)
+ VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, title, s3_url, description, media_url, class_id, teacher_id;`,
+ [title, s3Url, description, mediaUrl, classId, teacherId]
);
res.status(200).json(keysToCamel(postData)); //is this supposed to be .send instead?
@@ -97,29 +105,36 @@ classVideosRouter.put("/:id", async (req, res) => {
await db.query(`SELECT COUNT(*) FROM class_videos WHERE id = $1`, [id])
)[0];
- if(parseInt(count) === 1) {
- const updatedClassVideos = await db.query(query, [title, s3Url, description, mediaUrl, classId, id]);
- res.status(200).send(keysToCamel(updatedClassVideos));
+ if (parseInt(count) === 1) {
+ const updatedClassVideos = await db.query(query, [
+ title,
+ s3Url,
+ description,
+ mediaUrl,
+ classId,
+ id,
+ ]);
+ res.status(200).send(keysToCamel(updatedClassVideos));
} else {
res.status(400).send("Video with provided id not found");
}
- }
- catch (err) {
+ } catch (err) {
res.status(500).send(err.message);
}
});
-classVideosRouter.delete('/:id', async (req, res) => {
+classVideosRouter.delete("/:id", async (req, res) => {
try {
const { id } = req.params;
- const deletedClassVideos = await db.query(`DELETE FROM class_videos WHERE id = $1 RETURNING *;`, [id]);
+ const deletedClassVideos = await db.query(
+ `DELETE FROM class_videos WHERE id = $1 RETURNING *;`,
+ [id]
+ );
res.status(200).send(keysToCamel(deletedClassVideos));
} catch (err) {
res.status(500).send(err.message);
}
});
-
-
export { classVideosRouter };
diff --git a/server/routes/classes.js b/server/routes/classes.js
index 2f1e52db..a6ff50b4 100644
--- a/server/routes/classes.js
+++ b/server/routes/classes.js
@@ -127,18 +127,37 @@ classesRouter.get("/students/:id", async (req, res) => {
}
});
-classesRouter.get("/corequisites/:id", async (req, res) => {
+classesRouter.get("/corequisites/:classid/:userid", async (req, res) => {
try {
- const { id } = req.params;
- const events = await db.query(
+ const { classid, userid } = req.params;
+ const corequisites = await db.query(
+ `SELECT DISTINCT ON (e.id) e.*,
+ CASE WHEN ce.student_id IS NOT NULL THEN true ELSE false END AS enrolled
+ FROM events e
+ JOIN corequisites co ON e.id = co.event_id
+ FULL OUTER JOIN event_enrollments ce ON ce.event_id = e.id AND ce.student_id = $1
+ WHERE co.class_id = $2;`,
+ [userid, classid]
+ );
+
+ res.status(200).json(keysToCamel(corequisites));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
+classesRouter.get("/corequisites/:classid", async (req, res) => {
+ try {
+ const { classid } = req.params;
+ const corequisites = await db.query(
`SELECT e.*
- FROM events e
- JOIN corequisites co ON e.id = co.event_id
- WHERE co.class_id = $1;`,
- [id]
+ FROM events e
+ JOIN corequisites co ON e.id = co.event_id
+ WHERE co.class_id = $1;`,
+ [classid]
);
- res.status(200).json(keysToCamel(events));
+ res.status(200).json(keysToCamel(corequisites));
} catch (err) {
res.status(500).send(err.message);
}
@@ -218,18 +237,40 @@ classesRouter.get("/drafts", async (req, res) => {
});
// classesRouter.get("/search/:name", async (req, res) => {
- // try {
- // const { name } = req.params;
- // const data = await db.query(`SELECT * FROM classes WHERE title LIKE $1;`, [
- // `%${name}%`,
- // ]);
-//
- // res.status(200).json(keysToCamel(data));
- // } catch (err) {
- // res.status(500).send(err.message);
- // }
+// try {
+// const { name } = req.params;
+// const data = await db.query(`SELECT * FROM classes WHERE title LIKE $1;`, [
+// `%${name}%`,
+// ]);
+//
+// res.status(200).json(keysToCamel(data));
+// } catch (err) {
+// res.status(500).send(err.message);
+// }
// });
+classesRouter.get("/search/published/:name", async (req, res) => {
+ try {
+ const { name } = req.params;
+ const search = `%${name}%`;
+ const allClasses = await db.query(
+ `SELECT
+ c.*,
+ sc.date,
+ sc.start_time,
+ sc.end_time,
+ (SELECT COUNT(*) FROM class_enrollments WHERE class_id = c.id AND attendance IS NULL) as attendee_count
+ FROM classes c
+ LEFT JOIN scheduled_classes sc ON c.id = sc.class_id
+ WHERE c.is_draft = false AND c.title ILIKE $1
+ GROUP BY c.id, sc.date, sc.start_time, sc.end_time;`,
+ [search]
+ );
+ res.status(200).json(keysToCamel(allClasses));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
classesRouter.get("/search/:name", async (req, res) => {
try {
const { name } = req.params;
diff --git a/server/routes/classes_taught.js b/server/routes/classes_taught.js
index 2400593b..bad21365 100644
--- a/server/routes/classes_taught.js
+++ b/server/routes/classes_taught.js
@@ -19,7 +19,6 @@ classesTaughtRouter.get("/", async (req, res) => {
}
});
-
// Creates a new classes-taught entry in the classes taught table
classesTaughtRouter.post("/", async (req, res) => {
try {
@@ -43,18 +42,18 @@ classesTaughtRouter.get("/instructor/:classId", async (req, res) => {
try {
const classId = req.params.classId;
- const result = await db.any(`
- SELECT u.first_name, u.last_name
+ const result = await db.any(
+ `
+ SELECT u.first_name, u.last_name, u.id
FROM classes_taught ct
JOIN teachers t ON ct.teacher_id = t.id
JOIN users u ON u.id = t.id
WHERE ct.class_id = $1;
- `, [classId]);
+ `,
+ [classId]
+ );
res.status(200).json(keysToCamel(result));
-
-
-
} catch (err) {
console.error("Failed to fetch instructor:", err);
res.status(500).send(err.message);
@@ -82,5 +81,22 @@ classesTaughtRouter.put("/", async (req, res) => {
}
});
-export { classesTaughtRouter };
+// deletes the class taught entry
+classesTaughtRouter.delete("/:classId", async (req, res) => {
+ const classId = req.params.classId;
+ try {
+ // Delete the class taught entry
+ const deletedClassTaught = await db.query(
+ `DELETE FROM classes_taught WHERE class_id = $1 RETURNING *`,
+ [classId]
+ );
+
+ res.status(200).json(keysToCamel(deletedClassTaught));
+ } catch (error) {
+ console.error("Error deleting class taught entry:", error);
+ res.status(500).json({ error: error.message });
+ }
+});
+
+export { classesTaughtRouter };
diff --git a/server/routes/corequisites.js b/server/routes/corequisites.js
index 47b0864c..979eddce 100644
--- a/server/routes/corequisites.js
+++ b/server/routes/corequisites.js
@@ -7,61 +7,94 @@ const corequisitesRouter = express.Router();
corequisitesRouter.use(express.json());
+corequisitesRouter.get("/class/:classId", async (req, res) => {
+ const classId = req.params.classId;
+ try {
+ const result = await db.query(
+ ` SELECT * FROM corequisites WHERE class_id = $1;`,
+ [classId]
+ );
+ // use the event_id from result to get the coreqs of the event
+ if (result?.data) {
+ res.status(404).json({ error: "Error fetching coreqs for this class." });
+ return;
+ }
+ // there should only be one event per class
+ const coreqEvent = result[0]?.event_id;
+ const coreqResult = await db.query(
+ `SELECT * FROM corequisites JOIN classes on classes.id = corequisites.class_id WHERE class_id != $1 AND event_id = $2;`,
+ [classId, coreqEvent]
+ );
+
+ res.status(200).json(keysToCamel(coreqResult));
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ return;
+ }
+});
+
corequisitesRouter.get("/", async (req, res) => {
- try {
- const result = await db.query(`
+ try {
+ const result = await db.query(`
SELECT * FROM corequisites;
`);
- res.json(keysToCamel(result));
- } catch (err) {
- res.status(500).json({ error: err.message });
- }
+ res.json(keysToCamel(result));
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
});
corequisitesRouter.get("/:id", async (req, res) => {
- const classId = req.params.id;
+ const classId = req.params.id;
- try {
- const result = await db.query(`
+ try {
+ const result = await db.query(
+ `
SELECT * FROM corequisites WHERE class_id = $1;
- `, [classId]);
-
- res.json(keysToCamel(result));
- } catch (err) {
- res.status(500).json({ error: err.message });
- }
+ `,
+ [classId]
+ );
+
+ res.json(keysToCamel(result));
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
});
corequisitesRouter.put("/:class_id/:event_id", async (req, res) => {
- const { class_id, event_id } = req.params;
+ const { class_id, event_id } = req.params;
- try { // if class id doesnt exist yet, create new entry, else update
- const result = await db.query(`
+ try {
+ // if class id doesnt exist yet, create new entry, else update
+ const result = await db.query(
+ `
INSERT INTO corequisites (class_id, event_id)
VALUES ($1, $2)
ON CONFLICT (class_id, event_id) DO UPDATE
SET event_id = EXCLUDED.event_id
RETURNING *;
- `, [class_id, event_id]);
-
- res.json(keysToCamel(result));
- } catch (err) {
- res.status(500).json({ error: err.message });
- }
+ `,
+ [class_id, event_id]
+ );
+
+ res.json(keysToCamel(result));
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
});
corequisitesRouter.delete("/class/:id", async (req, res) => {
- const { id } = req.params;
- try {
- const result = await db.query(
- `DELETE FROM corequisites WHERE class_id = $1 RETURNING *;`, [id]
- );
- res.json(keysToCamel(result));
- } catch (err) {
- res.status(500).send(err.message);
- }
-
+ const { id } = req.params;
+ try {
+ const result = await db.query(
+ `DELETE FROM corequisites WHERE class_id = $1 RETURNING *;`,
+ [id]
+ );
+ res.json(keysToCamel(result));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
export { corequisitesRouter };
diff --git a/server/routes/event_enrollments.ts b/server/routes/event_enrollments.ts
index 6b914e02..c026bf03 100644
--- a/server/routes/event_enrollments.ts
+++ b/server/routes/event_enrollments.ts
@@ -24,17 +24,17 @@ eventEnrollmentRouter.get("/test", async (req, res) => {
const { student_id, event_id } = req.query;
try {
const result = await db.query(
- 'SELECT * FROM event_enrollments WHERE student_id = $1 AND event_id = $2;',
+ "SELECT * FROM event_enrollments WHERE student_id = $1 AND event_id = $2;",
[student_id, event_id]
- )
+ );
const exists = result.length > 0;
const enrollmentData = keysToCamel(result) as EventEnrollment[];
- res.status(200).send({enrollmentData, exists});
+ res.status(200).send({ enrollmentData, exists });
} catch (err) {
res.status(500).send(err.message);
}
-})
+});
// GET /:id
eventEnrollmentRouter.get("/:id", async (req, res) => {
@@ -66,7 +66,8 @@ eventEnrollmentRouter.get("/event/:event_id", async (req, res) => {
JOIN event_enrollments ee ON ee.student_id = s.id
JOIN events e ON e.id = ee.event_id
WHERE e.id = $1;
- `, [event_id]
+ `,
+ [event_id]
);
res.status(200).json(keysToCamel(result as EventEnrollment)); // Ensure .rows is used
@@ -75,7 +76,6 @@ eventEnrollmentRouter.get("/event/:event_id", async (req, res) => {
}
});
-
eventEnrollmentRouter.get("/student/:student_id", async (req, res) => {
try {
const result = await db.query(
@@ -110,7 +110,8 @@ eventEnrollmentRouter.get("/", async (req, res) => {
eventEnrollmentRouter.post("/", async (req, res) => {
try {
// Destructure the request body
- const { student_id, event_id, attendance } = req.body as EventEnrollmentRequest;
+ const { student_id, event_id, attendance } =
+ req.body as EventEnrollmentRequest;
// mathing sql schema
if (!student_id || !event_id) {
return res.status(400).json({ error: "Missing required parameters" });
diff --git a/server/routes/event_tags.js b/server/routes/event_tags.js
index 90615613..2a3d1e55 100644
--- a/server/routes/event_tags.js
+++ b/server/routes/event_tags.js
@@ -7,49 +7,110 @@ const eventTagsRouter = express.Router();
eventTagsRouter.use(express.json());
+eventTagsRouter.get("/all-event-tags", async (req, res) => {
+ try {
+ const tags = await db.query(
+ `
+ SELECT event_tags.event_id, JSON_ARRAYAGG(tags.*) tag_array
+ FROM event_tags
+ JOIN tags ON event_tags.tag_id = tags.id
+ GROUP BY event_tags.event_id;`
+ );
+
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
+eventTagsRouter.get("/enrolled-event-tags/:userId", async (req, res) => {
+ try {
+ const { userId } = req.params;
+
+ const tags = await db.query(
+ `
+ SELECT event_tags.event_id, JSON_ARRAYAGG(tags.*) tag_array
+ FROM event_tags
+ JOIN tags ON event_tags.tag_id = tags.id
+ JOIN event_enrollments ON event_tags.event_id = event_enrollments.event_id
+ WHERE event_enrollments.student_id = $1
+ GROUP BY event_tags.event_id;`,
+ [userId]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
// tags for a single event
eventTagsRouter.get("/tags/:id", async (req, res) => {
- try {
- const { id } = req.params;
+ try {
+ const { id } = req.params;
- const tags = await db.query(`SELECT * FROM event_tags JOIN tags on event_tags.tag_id = tags.id WHERE event_tags.event_id = $1;`, [id])
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ const tags = await db.query(
+ `SELECT * FROM event_tags JOIN tags on event_tags.tag_id = tags.id WHERE event_tags.event_id = $1;`,
+ [id]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// get all events matching a tag
eventTagsRouter.get("/events/:id", async (req, res) => {
- try {
- const { id } = req.params;
- const events = await db.query(`SELECT * FROM event_tags JOIN events on event_tags.event_id = events.id WHERE event_tags.tag_id = $1;`, [id]);
- res.status(200).json(keysToCamel(events));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ try {
+ const { id } = req.params;
+ const events = await db.query(
+ `SELECT * FROM event_tags JOIN events on event_tags.event_id = events.id WHERE event_tags.tag_id = $1;`,
+ [id]
+ );
+ res.status(200).json(keysToCamel(events));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// post new tag/event relationship
eventTagsRouter.post("/", async (req, res) => {
- try {
- const { eventId, tagId } = req.body;
- const tags = await db.query(`INSERT INTO event_tags (event_id, tag_id) VALUES ($1, $2) RETURNING *;`, [eventId, tagId]);
- res.status(201).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
+ try {
+ const { eventId, tagId } = req.body;
+
+ const existingTag = await db.query(
+ `SELECT * FROM event_tags WHERE event_id = $1 AND tag_id = $2;`,
+ [eventId, tagId]
+ );
+ if (existingTag.length > 0) {
+ return res.status(201).json(keysToCamel(existingTag));
+ }
+
+ if (!eventId || !tagId) {
+ return res.status(400).send("Event ID and Tag ID are required.");
}
+
+ const tags = await db.query(
+ `INSERT INTO event_tags (event_id, tag_id) VALUES ($1, $2) RETURNING *;`,
+ [eventId, tagId]
+ );
+ res.status(201).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
// delete event/tag relationship
-eventTagsRouter.delete("/", async (req, res) => {
- try {
- const { eventId, tagId } = req.body;
- const tags = await db.query(`DELETE FROM event_tags WHERE event_id = $1 AND tag_id = $2 RETURNING *;`, [eventId, tagId]);
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+eventTagsRouter.delete("/:eventId/:tagId", async (req, res) => {
+ try {
+ const { eventId, tagId } = req.params;
+ const tags = await db.query(
+ `DELETE FROM event_tags WHERE event_id = $1 AND tag_id = $2 RETURNING *;`,
+ [eventId, tagId]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
-export { eventTagsRouter };
\ No newline at end of file
+export { eventTagsRouter };
diff --git a/server/routes/events.js b/server/routes/events.js
index 1c9cf9e2..f44ce331 100644
--- a/server/routes/events.js
+++ b/server/routes/events.js
@@ -6,12 +6,35 @@ import { db } from "../db/db-pgp";
const eventsRouter = express.Router();
eventsRouter.use(express.json());
+eventsRouter.get("/search/published/:name", async (req, res) => {
+ try {
+ const { name } = req.params;
+ const search = `%${name}%`;
+ const allEvents = await db.query(
+ `SELECT
+ e.*,
+ (SELECT COUNT(*) FROM event_enrollments ee WHERE ee.event_id = e.id) AS attendee_count
+ FROM events e
+ WHERE e.title ILIKE $1 AND e.is_draft = false;`,
+ // "SELECT * FROM events WHERE title ILIKE $1 AND is_draft = false;",
+ [search]
+ );
+ res.status(200).json(keysToCamel(allEvents));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
eventsRouter.get("/search/:name", async (req, res) => {
try {
const { name } = req.params;
const search = `%${name}%`;
const allEvents = await db.query(
- "SELECT * FROM events WHERE title ILIKE $1;",
+ `SELECT
+ e.*,
+ (SELECT COUNT(*) FROM event_enrollments ee WHERE ee.event_id = e.id) AS attendee_count
+ FROM events e
+ WHERE e.title ILIKE $1;`,
+ // "SELECT * FROM events WHERE title ILIKE $1;",
[search]
);
res.status(200).json(keysToCamel(allEvents));
@@ -37,15 +60,17 @@ eventsRouter.get("/count", async (req, res) => {
}
});
-eventsRouter.get("/corequisites/:id", async (req, res) => {
+eventsRouter.get("/corequisites/:eventid/:userid", async (req, res) => {
try {
- const { id } = req.params;
+ const { eventid, userid } = req.params;
const corequisites = await db.query(
- `SELECT c.*
- FROM classes c
- JOIN corequisites co ON c.id = co.class_id
- WHERE co.event_id = $1;`,
- [id]
+ `SELECT DISTINCT ON (c.id) c.*,
+ CASE WHEN ce.student_id IS NOT NULL THEN true ELSE false END AS enrolled
+ FROM classes c
+ JOIN corequisites co ON c.id = co.class_id
+ FULL OUTER JOIN class_enrollments ce ON ce.class_id = c.id AND ce.student_id = $1
+ WHERE co.event_id = $2;`,
+ [userid, eventid]
);
res.status(200).json(keysToCamel(corequisites));
@@ -78,6 +103,16 @@ eventsRouter.get("/published", async (req, res) => {
}
});
+eventsRouter.get("/all", async (req, res) => {
+ try {
+ const eventID = await db.query("SELECT * FROM events;");
+
+ res.status(200).json(keysToCamel(eventID));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
eventsRouter.get("/:id", async (req, res) => {
try {
const { id } = req.params;
@@ -124,7 +159,7 @@ eventsRouter.post("/", async (req, res) => {
call_time,
costume,
capacity,
- is_draft
+ is_draft,
} = req.body;
const result = await db.query(
"INSERT INTO events (location, title, description, level, date, start_time, end_time, call_time, costume, capacity, is_draft) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *;",
@@ -139,7 +174,7 @@ eventsRouter.post("/", async (req, res) => {
call_time,
costume,
capacity,
- is_draft
+ is_draft,
]
);
diff --git a/server/routes/reviews.js b/server/routes/reviews.js
index 8cc96cde..0f12bf53 100644
--- a/server/routes/reviews.js
+++ b/server/routes/reviews.js
@@ -1,6 +1,7 @@
import express from "express";
+
import { keysToCamel } from "../common/utils";
-import { db } from "../db/db-pgp"
+import { db } from "../db/db-pgp";
const reviewsRouter = express.Router();
reviewsRouter.use(express.json());
@@ -28,7 +29,9 @@ reviewsRouter.get("/:id", async (req, res) => {
reviewsRouter.get("/class/:id", async (req, res) => {
try {
const { id } = req.params;
- const data = await db.query(`SELECT * FROM reviews WHERE class_id = $1;`, [id]);
+ const data = await db.query(`SELECT * FROM reviews WHERE class_id = $1;`, [
+ id,
+ ]);
res.status(200).json(keysToCamel(data));
} catch (err) {
@@ -39,7 +42,10 @@ reviewsRouter.get("/class/:id", async (req, res) => {
reviewsRouter.get("/student/:id", async (req, res) => {
try {
const { id } = req.params;
- const data = await db.query(`SELECT * FROM reviews WHERE student_id = $1;`, [id]);
+ const data = await db.query(
+ `SELECT * FROM reviews WHERE student_id = $1;`,
+ [id]
+ );
res.status(200).json(keysToCamel(data));
} catch (err) {
@@ -49,20 +55,19 @@ reviewsRouter.get("/student/:id", async (req, res) => {
reviewsRouter.post("/", async (req, res) => {
try {
-
- const {class_id, student_id, rating, review} = req.body;
+ const { class_id, student_id, rating, review } = req.body;
const data = await db.query(
`
INSERT INTO reviews(class_id, student_id, rating, review)
VALUES ($1, $2, $3, $4)
RETURNING *;
- `,[class_id, student_id, rating, review]
+ `,
+ [class_id, student_id, rating, review]
);
res.status(200).json(keysToCamel(data));
-
- } catch(err) {
+ } catch (err) {
res.status(500).send(err.message);
}
});
@@ -71,7 +76,11 @@ reviewsRouter.put("/", async (req, res) => {
try {
const { class_id, student_id, rating, review } = req.body;
if (!class_id || !student_id)
- res.status(500).send("Invalid PUT request, please enter required `student_id` and `class_id` parameters");
+ res
+ .status(500)
+ .send(
+ "Invalid PUT request, please enter required `student_id` and `class_id` parameters"
+ );
const query = `UPDATE reviews SET
rating = COALESCE($1, rating),
@@ -88,15 +97,17 @@ reviewsRouter.put("/", async (req, res) => {
reviewsRouter.delete("/", async (req, res) => {
try {
const { student_id, class_id } = req.body;
- const data = await db.query(`
+ const data = await db.query(
+ `
DELETE FROM reviews
WHERE student_id=$1 OR class_id=$2
RETURNING *;
- `, [student_id, class_id]
+ `,
+ [student_id, class_id]
);
res.status(200).json(keysToCamel(data));
- } catch(err) {
+ } catch (err) {
res.status(500).send(err.message);
}
});
diff --git a/server/routes/tags.js b/server/routes/tags.js
index 91a2afbb..cc25cea0 100644
--- a/server/routes/tags.js
+++ b/server/routes/tags.js
@@ -8,9 +8,7 @@ tagsRouter.use(express.json());
tagsRouter.get("/", async (req, res) => {
try {
- const tags = await db.query(
- `SELECT * FROM tags;`
- );
+ const tags = await db.query(`SELECT * FROM tags;`);
res.status(200).json(keysToCamel(tags));
} catch (err) {
console.log(err);
@@ -23,12 +21,11 @@ tagsRouter.get("/", async (req, res) => {
tagsRouter.get("/:tagName", async (req, res) => {
try {
-
const { tagName } = req.params;
- const tagId = await db.query(
- `SELECT id FROM tags WHERE tag = $1;`, [tagName]
- );
+ const tagId = await db.query(`SELECT id FROM tags WHERE tag = $1;`, [
+ tagName,
+ ]);
res.status(200).json(keysToCamel(tagId));
} catch (err) {
console.log(err);
@@ -37,4 +34,4 @@ tagsRouter.get("/:tagName", async (req, res) => {
msg: err.message,
});
}
-});
\ No newline at end of file
+});
diff --git a/server/routes/users.ts b/server/routes/users.ts
index 95b4cd36..f4fdb552 100644
--- a/server/routes/users.ts
+++ b/server/routes/users.ts
@@ -84,13 +84,17 @@ usersRouter.put("/hide", async (req, res) => {
// Update a user by ID
usersRouter.put("/update", async (req, res) => {
try {
- const { email, firebaseUid } = req.body;
+ const { email, firebaseUid, firstName, lastName } = req.body;
const user = await db.query(
- "UPDATE users SET email = $1 WHERE firebase_uid = $2 RETURNING *",
- [email, firebaseUid]
+ `UPDATE users
+ SET email = COALESCE($1, email),
+ first_name = COALESCE($2, first_name),
+ last_name = COALESCE($3, last_name)
+ WHERE firebase_uid = $4
+ RETURNING *`,
+ [email, firstName, lastName, firebaseUid]
);
-
res.status(200).json(keysToCamel(user));
} catch (err) {
res.status(400).send(err.message);
diff --git a/server/routes/video_tags.js b/server/routes/video_tags.js
index b42657af..8bd4a904 100644
--- a/server/routes/video_tags.js
+++ b/server/routes/video_tags.js
@@ -8,57 +8,58 @@ const videoTagsRouter = express.Router();
videoTagsRouter.use(express.json());
videoTagsRouter.get("/videos/:id", async (req, res) => {
- try {
- const { id } = req.params;
- const tags = await db.query(
- `SELECT * FROM video_tags
+ try {
+ const { id } = req.params;
+ const tags = await db.query(
+ `SELECT * FROM video_tags
JOIN tags ON video_tags.tag_id = tags.id
- WHERE video_tags.video_id = $1;` [id]);
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ WHERE video_tags.video_id = $1;`[id]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
videoTagsRouter.get("/tags/:id", async (req, res) => {
- try {
- const { id } = req.params;
- const tags = await db.query(
- `SELECT * FROM video_tags
+ try {
+ const { id } = req.params;
+ const tags = await db.query(
+ `SELECT * FROM video_tags
JOIN tags ON video_tags.tag_id = tags.id
- WHERE video_tags.tag_id = $1;` [id]);
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ WHERE video_tags.tag_id = $1;`[id]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
-
videoTagsRouter.post("/", async (req, res) => {
- try {
- const { videoId, tagId } = req.body;
- const tags = await db.query(
- `INSERT INTO video_tags (video_id, tag_id) VALUES
- ($1, $2) RETURNING *;`, [videoId, tagId]
- );
- res.status(201).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+ try {
+ const { videoId, tagId } = req.body;
+ const tags = await db.query(
+ `INSERT INTO video_tags (video_id, tag_id) VALUES
+ ($1, $2) RETURNING *;`,
+ [videoId, tagId]
+ );
+ res.status(201).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
-
-videoTagsRouter.delete('/', async (req, res) => {
- try {
- const { videoId, tagId } = req.body;
- const tags = await db.query(
- `DELETE FROM video_tags WHERE video_id = $1 AND tag_id = $2 RETURNING *;`, [videoId, tagId]
- );
- res.status(200).json(keysToCamel(tags));
- } catch (err) {
- res.status(500).send(err.message);
- }
+videoTagsRouter.delete("/", async (req, res) => {
+ try {
+ const { videoId, tagId } = req.body;
+ const tags = await db.query(
+ `DELETE FROM video_tags WHERE video_id = $1 AND tag_id = $2 RETURNING *;`,
+ [videoId, tagId]
+ );
+ res.status(200).json(keysToCamel(tags));
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
});
-
export { videoTagsRouter };
diff --git a/server/src/app.ts b/server/src/app.ts
index b6cd484d..a374b06b 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -4,27 +4,26 @@ import dotenv from "dotenv";
import express from "express";
import schedule from "node-schedule"; // TODO: Keep only if scheduling cronjobs
+import { articleTagsRouter } from "../routes/article_tags";
import { articlesRouter } from "../routes/articles";
-import { classesRouter } from "../routes/classes";
-import { scheduledClassesRouter } from "../routes/scheduled_classes";
-import { classVideosRouter } from "../routes/class_videos";
import { classEnrollmentsRouter } from "../routes/class_enrollments";
-import { usersRouter } from "../routes/users";
-import { studentsRouter } from "../routes/students";
+import { classTagsRouter } from "../routes/class_tags";
+import { classVideosRouter } from "../routes/class_videos";
+import { classesRouter } from "../routes/classes";
import { classesTaughtRouter } from "../routes/classes_taught";
+import { corequisitesRouter } from "../routes/corequisites";
import { eventEnrollmentRouter } from "../routes/event_enrollments";
-import { reviewsRouter } from "../routes/reviews";
-import { teachersRouter } from "../routes/teachers";
+import { eventTagsRouter } from "../routes/event_tags";
import { eventsRouter } from "../routes/events";
-import { s3Router } from "../routes/s3";
import emailRouter from "../routes/nodeMailer";
-import { articleTagsRouter } from "../routes/article_tags";
-import { videoTagsRouter } from "../routes/video_tags";
-import { classTagsRouter } from "../routes/class_tags";
-import { eventTagsRouter } from "../routes/event_tags";
+import { reviewsRouter } from "../routes/reviews";
+import { s3Router } from "../routes/s3";
+import { scheduledClassesRouter } from "../routes/scheduled_classes";
+import { studentsRouter } from "../routes/students";
import { tagsRouter } from "../routes/tags";
-import { corequisitesRouter } from "../routes/corequisites";
-
+import { teachersRouter } from "../routes/teachers";
+import { usersRouter } from "../routes/users";
+import { videoTagsRouter } from "../routes/video_tags";
import { verifyToken } from "./middleware";
dotenv.config();
@@ -63,7 +62,7 @@ app.use("/students", studentsRouter);
app.use("/events", eventsRouter);
app.use("/classes-taught", classesTaughtRouter);
app.use("/class-enrollments", classEnrollmentsRouter);
-app.use("/teachers", teachersRouter)
+app.use("/teachers", teachersRouter);
app.use("/reviews", reviewsRouter);
// connecting made router with the app
app.use("/articles", articlesRouter);