From 29d05974079b9faa694d8e96f975f58cbeb39559 Mon Sep 17 00:00:00 2001 From: Loc Mai Date: Sun, 15 Dec 2024 01:56:01 -0500 Subject: [PATCH 1/7] sidebar done --- src/app/components/Sidebar.tsx | 116 ++++++++++++++++++ src/app/components/buttons/MenuButton.tsx | 27 ++++ src/app/context/SidebarContext.tsx | 49 ++++++++ src/app/layout.tsx | 7 +- src/app/private/course/[courseId]/page.tsx | 10 +- .../course/[courseId]/profile/page.tsx | 10 +- .../private/course/[courseId]/queue/page.tsx | 10 +- src/app/private/course/page.tsx | 13 +- 8 files changed, 210 insertions(+), 32 deletions(-) create mode 100644 src/app/components/Sidebar.tsx create mode 100644 src/app/components/buttons/MenuButton.tsx create mode 100644 src/app/context/SidebarContext.tsx diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx new file mode 100644 index 0000000..428dcbd --- /dev/null +++ b/src/app/components/Sidebar.tsx @@ -0,0 +1,116 @@ +"use client"; + +import React from "react"; +import { + useSidebarStateContext, + useSidebarActionsContext, +} from "@context/SidebarContext"; +import { Drawer, Box, Typography, List, ListItemButton } from "@mui/material"; +import theme from "theme"; +import { getCourses } from "@services/client/course"; +import { useUserSession } from "@context/UserSessionContext"; +import { useRouter } from "next/navigation"; +import { usePathname } from "next/navigation"; +import { IdentifiableCourse } from "@interfaces/type"; +import MenuButton from "./buttons/MenuButton"; + +const Sidebar = React.memo(() => { + const [courses, setCourses] = React.useState([]); + const { isOpen } = useSidebarStateContext(); + const { closeSidebar } = useSidebarActionsContext(); + const { user } = useUserSession(); + const { courses: courseIds = [] } = user || {}; + const router = useRouter(); + const pathname = usePathname(); + + React.useEffect(() => { + const loadCourses = async () => { + const allCourses = await getCourses(); + const filteredCourses = allCourses + .filter((course) => courseIds.includes(course.id)) + .map((course) => course); + setCourses(filteredCourses); + }; + loadCourses(); + }, [user]); + + const handleCourseClick = (course: IdentifiableCourse) => { + const isCourseRoute = /\/[a-zA-Z]+\d+/.test(pathname); + + const basePath = isCourseRoute + ? pathname.replace(/\/[a-zA-Z]+\d+.*$/, "") + : pathname; + + router.push(`${basePath}/${course.id}`); + closeSidebar(); + }; + + const formattedCourseName = (course: IdentifiableCourse) => { + const formattedCourseId = course.id + .toUpperCase() + .replace(/([a-zA-Z]+)(\d+)/, "$1 $2"); + + return `${formattedCourseId} ${course.name}`; + }; + + return ( + + + + + My Classes + + + + + {courses.map((course, i) => ( + handleCourseClick(course)} + > + + {formattedCourseName(course)} + + + ))} + + + + ); +}); + +export default Sidebar; diff --git a/src/app/components/buttons/MenuButton.tsx b/src/app/components/buttons/MenuButton.tsx new file mode 100644 index 0000000..9edbbb5 --- /dev/null +++ b/src/app/components/buttons/MenuButton.tsx @@ -0,0 +1,27 @@ +"use client"; + +import React from "react"; +import { IconButton } from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import { useSidebarActionsContext } from "@context/SidebarContext"; + +interface MenuButtonProps { + isEdge?: boolean; + isOpen?: boolean; +} + +const MenuButton = React.memo((props: MenuButtonProps) => { + const { openSidebar, closeSidebar } = useSidebarActionsContext(); + const { isEdge = false, isOpen = true } = props; + + return ( + + + + ); +}); + +export default MenuButton; diff --git a/src/app/context/SidebarContext.tsx b/src/app/context/SidebarContext.tsx new file mode 100644 index 0000000..9f7a0a6 --- /dev/null +++ b/src/app/context/SidebarContext.tsx @@ -0,0 +1,49 @@ +"use client"; + +import React from "react"; + +interface SidebarContextState { + isOpen: boolean; +} + +interface SidebarContextActions { + openSidebar: () => void; + closeSidebar: () => void; +} + +interface SidebarContextProps { + children: React.ReactNode; +} + +const SidebarStateContext = React.createContext({ + isOpen: false, +}); + +const SidebarActionsContext = React.createContext({ + openSidebar: () => {}, + closeSidebar: () => {}, +}); + +export const SidebarProvider: React.FC = ( + props: SidebarContextProps +) => { + const { children } = props; + const [isSidebarOpen, setIsSidebarOpen] = React.useState(false); + + const openSidebar = React.useCallback(() => setIsSidebarOpen(true), []); + const closeSidebar = React.useCallback(() => setIsSidebarOpen(false), []); + + return ( + + + {children} + + + ); +}; + +export const useSidebarStateContext = () => + React.useContext(SidebarStateContext); + +export const useSidebarActionsContext = () => + React.useContext(SidebarActionsContext); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a68bc19..dafc657 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,8 @@ import { Inter } from "next/font/google"; import "./globals.css"; import ThemeProviderWrapper from "@context/ThemeProviderWrapper"; import { UserSessionContextProvider } from "@context/UserSessionContext"; +import { SidebarProvider } from "@context/SidebarContext"; +import Sidebar from "@components/Sidebar"; const inter = Inter({ subsets: ["latin"] }); @@ -20,7 +22,10 @@ export default function RootLayout({
- {children} + + + {children} +
diff --git a/src/app/private/course/[courseId]/page.tsx b/src/app/private/course/[courseId]/page.tsx index e84c1b2..a1d5517 100644 --- a/src/app/private/course/[courseId]/page.tsx +++ b/src/app/private/course/[courseId]/page.tsx @@ -1,9 +1,9 @@ import Header from "@components/Header"; -import { IconButton } from "@mui/material"; -import MenuIcon from "@mui/icons-material/Menu"; import { getCourse } from "@services/client/course"; import { getUsers } from "@services/client/user"; import DisplayCourse from "@components/DisplayCourse"; +import MenuButton from "@components/buttons/MenuButton"; + interface PageProps { params: { courseId: string; @@ -19,11 +19,7 @@ const Page = async (props: PageProps) => { return (
- - - } + leftIcon={} title={courseId.toUpperCase()} alignCenter /> diff --git a/src/app/private/course/[courseId]/profile/page.tsx b/src/app/private/course/[courseId]/profile/page.tsx index 1680475..dacfd7e 100644 --- a/src/app/private/course/[courseId]/profile/page.tsx +++ b/src/app/private/course/[courseId]/profile/page.tsx @@ -1,19 +1,15 @@ "use client"; +import MenuButton from "@components/buttons/MenuButton"; import Header from "@components/Header"; import { useUserSession } from "@context/UserSessionContext"; -import MenuIcon from "@mui/icons-material/Menu"; -import { Button, IconButton } from "@mui/material"; +import { Button } from "@mui/material"; const Page = () => { const { onSignOut } = useUserSession(); return (
- - - } + leftIcon={} title="Profile" alignCenter rightIcon={} diff --git a/src/app/private/course/[courseId]/queue/page.tsx b/src/app/private/course/[courseId]/queue/page.tsx index 08342b0..bfc3f54 100644 --- a/src/app/private/course/[courseId]/queue/page.tsx +++ b/src/app/private/course/[courseId]/queue/page.tsx @@ -1,10 +1,10 @@ "use client"; +import MenuButton from "@components/buttons/MenuButton"; import Header from "@components/Header"; import Queue from "@components/queue"; import { useOfficeHour } from "@hooks/oh/useOfficeHour"; -import MenuIcon from "@mui/icons-material/Menu"; -import { Box, IconButton } from "@mui/material"; +import { Box } from "@mui/material"; const Page = () => { const { course } = useOfficeHour(); @@ -17,11 +17,7 @@ const Page = () => { height="100%" >
- - - } + leftIcon={} title={course.name} alignCenter /> diff --git a/src/app/private/course/page.tsx b/src/app/private/course/page.tsx index c41c0c9..e913b70 100644 --- a/src/app/private/course/page.tsx +++ b/src/app/private/course/page.tsx @@ -1,9 +1,9 @@ "use client"; import Header from "@components/Header"; -import { Button, IconButton, Typography } from "@mui/material"; -import MenuIcon from "@mui/icons-material/Menu"; +import { Button, Typography } from "@mui/material"; import { useRouter } from "next/navigation"; +import MenuButton from "@components/buttons/MenuButton"; const Page = async () => { const router = useRouter(); @@ -11,14 +11,7 @@ const Page = async () => { // fetch courses and render return (
-
- - - } - title="Classes" - /> +
} title="Classes" /> {/* NOTE: Currently only displays the case where a user has no classes */}
Add a class to get started
From 02b36f9ba170ca0036002014acc11c95851c309d Mon Sep 17 00:00:00 2001 From: Loc Mai Date: Sun, 15 Dec 2024 02:29:50 -0500 Subject: [PATCH 2/7] add manage classes button --- src/app/components/Sidebar.tsx | 49 +++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx index 428dcbd..47714f0 100644 --- a/src/app/components/Sidebar.tsx +++ b/src/app/components/Sidebar.tsx @@ -5,7 +5,14 @@ import { useSidebarStateContext, useSidebarActionsContext, } from "@context/SidebarContext"; -import { Drawer, Box, Typography, List, ListItemButton } from "@mui/material"; +import { + Drawer, + Box, + Typography, + List, + ListItemButton, + Divider, +} from "@mui/material"; import theme from "theme"; import { getCourses } from "@services/client/course"; import { useUserSession } from "@context/UserSessionContext"; @@ -13,6 +20,7 @@ import { useRouter } from "next/navigation"; import { usePathname } from "next/navigation"; import { IdentifiableCourse } from "@interfaces/type"; import MenuButton from "./buttons/MenuButton"; +import ModeIcon from "@mui/icons-material/Mode"; const Sidebar = React.memo(() => { const [courses, setCourses] = React.useState([]); @@ -45,6 +53,17 @@ const Sidebar = React.memo(() => { closeSidebar(); }; + const handleManageCoursesClick = () => { + const isCourseRoute = /\/[a-zA-Z]+\d+/.test(pathname); + + const basePath = isCourseRoute + ? pathname.match(/(.*\/[a-zA-Z]+\d+)/)?.[0] + : `${pathname}/${courses[0].id}`; + + router.push(`${basePath}/profile/edit`); + closeSidebar(); + }; + const formattedCourseName = (course: IdentifiableCourse) => { const formattedCourseId = course.id .toUpperCase() @@ -67,8 +86,7 @@ const Sidebar = React.memo(() => { sx={{ display: "flex", flexDirection: "column", - height: "100%", - padding: "30px", + padding: "30px 30px 0", }} > { ))} + + + + + Manage Classes + + ); }); From 64bb2574e30f02b0153ce3abedf93b175c7a7c3c Mon Sep 17 00:00:00 2001 From: Loc Mai Date: Sun, 15 Dec 2024 13:40:29 -0500 Subject: [PATCH 3/7] better edit icon --- src/app/components/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx index 47714f0..76fe9ad 100644 --- a/src/app/components/Sidebar.tsx +++ b/src/app/components/Sidebar.tsx @@ -20,7 +20,7 @@ import { useRouter } from "next/navigation"; import { usePathname } from "next/navigation"; import { IdentifiableCourse } from "@interfaces/type"; import MenuButton from "./buttons/MenuButton"; -import ModeIcon from "@mui/icons-material/Mode"; +import ModeEditOutlinedIcon from "@mui/icons-material/ModeEditOutlined"; const Sidebar = React.memo(() => { const [courses, setCourses] = React.useState([]); @@ -148,7 +148,7 @@ const Sidebar = React.memo(() => { }, }} > - + Manage Classes From 7fedf4df56d5406abd8319bc3a2cbcb472bbecca Mon Sep 17 00:00:00 2001 From: Loc Mai Date: Sun, 15 Dec 2024 14:02:35 -0500 Subject: [PATCH 4/7] minor style changes --- src/app/components/Sidebar.tsx | 12 +++++------- src/app/layout.tsx | 10 ++++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx index 76fe9ad..864047e 100644 --- a/src/app/components/Sidebar.tsx +++ b/src/app/components/Sidebar.tsx @@ -97,11 +97,7 @@ const Sidebar = React.memo(() => { alignItems: "center", }} > - + My Classes @@ -120,7 +116,7 @@ const Sidebar = React.memo(() => { }} onClick={() => handleCourseClick(course)} > - + {formattedCourseName(course)} @@ -149,7 +145,9 @@ const Sidebar = React.memo(() => { }} > - Manage Classes + + Manage Classes + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dafc657..765adfe 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,10 +22,12 @@ export default function RootLayout({
- - - {children} - + + + + {children} + +
From 88e99938d806e490ae6b12e7d44144d20a1f4e04 Mon Sep 17 00:00:00 2001 From: Loc Mai Date: Sun, 15 Dec 2024 20:12:31 -0500 Subject: [PATCH 5/7] get courses by ids, static routing --- src/app/components/Sidebar.tsx | 29 +++++++---------------------- src/app/services/client/course.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx index 864047e..f89881f 100644 --- a/src/app/components/Sidebar.tsx +++ b/src/app/components/Sidebar.tsx @@ -14,10 +14,9 @@ import { Divider, } from "@mui/material"; import theme from "theme"; -import { getCourses } from "@services/client/course"; +import { getCoursesByIds } from "@services/client/course"; import { useUserSession } from "@context/UserSessionContext"; import { useRouter } from "next/navigation"; -import { usePathname } from "next/navigation"; import { IdentifiableCourse } from "@interfaces/type"; import MenuButton from "./buttons/MenuButton"; import ModeEditOutlinedIcon from "@mui/icons-material/ModeEditOutlined"; @@ -29,38 +28,24 @@ const Sidebar = React.memo(() => { const { user } = useUserSession(); const { courses: courseIds = [] } = user || {}; const router = useRouter(); - const pathname = usePathname(); React.useEffect(() => { const loadCourses = async () => { - const allCourses = await getCourses(); - const filteredCourses = allCourses - .filter((course) => courseIds.includes(course.id)) - .map((course) => course); - setCourses(filteredCourses); + if (courseIds.length > 0) { + const courses = await getCoursesByIds(courseIds); + setCourses(courses); + } }; loadCourses(); }, [user]); const handleCourseClick = (course: IdentifiableCourse) => { - const isCourseRoute = /\/[a-zA-Z]+\d+/.test(pathname); - - const basePath = isCourseRoute - ? pathname.replace(/\/[a-zA-Z]+\d+.*$/, "") - : pathname; - - router.push(`${basePath}/${course.id}`); + router.push(`/private/course/${course.id}`); closeSidebar(); }; const handleManageCoursesClick = () => { - const isCourseRoute = /\/[a-zA-Z]+\d+/.test(pathname); - - const basePath = isCourseRoute - ? pathname.match(/(.*\/[a-zA-Z]+\d+)/)?.[0] - : `${pathname}/${courses[0].id}`; - - router.push(`${basePath}/profile/edit`); + router.push(`/private/course/${courses[0].id}/profile/edit`); closeSidebar(); }; diff --git a/src/app/services/client/course.ts b/src/app/services/client/course.ts index 966d596..f721b73 100644 --- a/src/app/services/client/course.ts +++ b/src/app/services/client/course.ts @@ -57,6 +57,15 @@ export const getCourses = async () => { return coursesDocs; }; +export const getCoursesByIds = async ( + courseIds: String[] +): Promise => { + const courses = courseIds.map((id) => getCourse(id)); + const snapshots = await Promise.all(courses); + + return snapshots; +}; + export const deleteCourse = async (courseID: String) => { const courseDoc = doc(db, `courses/${courseID}`).withConverter( courseConverter From da72f13d8796e808db30a748613dc51c6edeb736 Mon Sep 17 00:00:00 2001 From: Loc Mai Date: Mon, 16 Dec 2024 22:23:50 -0500 Subject: [PATCH 6/7] filter on query --- src/app/services/client/course.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/app/services/client/course.ts b/src/app/services/client/course.ts index f721b73..b4b52f2 100644 --- a/src/app/services/client/course.ts +++ b/src/app/services/client/course.ts @@ -10,6 +10,9 @@ import { getDocs, setDoc, updateDoc, + query, + where, + documentId, } from "firebase/firestore"; export const addCourse = async (course: IdentifiableCourse) => { @@ -58,12 +61,21 @@ export const getCourses = async () => { }; export const getCoursesByIds = async ( - courseIds: String[] + courseIds: string[] ): Promise => { - const courses = courseIds.map((id) => getCourse(id)); - const snapshots = await Promise.all(courses); + const coursesQuery = query( + collection(db, "courses"), + where(documentId(), "in", courseIds) + ); + + const snapshot = await getDocs(coursesQuery.withConverter(courseConverter)); - return snapshots; + const coursesDocs: IdentifiableCourse[] = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); + + return coursesDocs; }; export const deleteCourse = async (courseID: String) => { From f8795ee906d77faa529c5900f9ad7c28527cdc73 Mon Sep 17 00:00:00 2001 From: Loc Mai Date: Mon, 16 Dec 2024 22:24:34 -0500 Subject: [PATCH 7/7] handle 0 course users --- src/app/components/Sidebar.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx index f89881f..495286f 100644 --- a/src/app/components/Sidebar.tsx +++ b/src/app/components/Sidebar.tsx @@ -37,7 +37,7 @@ const Sidebar = React.memo(() => { } }; loadCourses(); - }, [user]); + }, [courseIds]); const handleCourseClick = (course: IdentifiableCourse) => { router.push(`/private/course/${course.id}`); @@ -45,7 +45,11 @@ const Sidebar = React.memo(() => { }; const handleManageCoursesClick = () => { - router.push(`/private/course/${courses[0].id}/profile/edit`); + if (courses.length > 0) { + router.push(`/private/course/${courses[0].id}/profile/edit`); + } else { + router.push(`/private/course`); + } closeSidebar(); };