Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Course navigation sidebar #31

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions src/app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"use client";

import React from "react";
import {
useSidebarStateContext,
useSidebarActionsContext,
} from "@context/SidebarContext";
import {
Drawer,
Box,
Typography,
List,
ListItemButton,
Divider,
} from "@mui/material";
import theme from "theme";
import { getCoursesByIds } from "@services/client/course";
import { useUserSession } from "@context/UserSessionContext";
import { useRouter } from "next/navigation";
import { IdentifiableCourse } from "@interfaces/type";
import MenuButton from "./buttons/MenuButton";
import ModeEditOutlinedIcon from "@mui/icons-material/ModeEditOutlined";

const Sidebar = React.memo(() => {
locmai1 marked this conversation as resolved.
Show resolved Hide resolved
const [courses, setCourses] = React.useState<IdentifiableCourse[]>([]);
const { isOpen } = useSidebarStateContext();
const { closeSidebar } = useSidebarActionsContext();
const { user } = useUserSession();
const { courses: courseIds = [] } = user || {};
const router = useRouter();

React.useEffect(() => {
const loadCourses = async () => {
if (courseIds.length > 0) {
const courses = await getCoursesByIds(courseIds);
setCourses(courses);
}
};
loadCourses();
}, [courseIds]);

const handleCourseClick = (course: IdentifiableCourse) => {
router.push(`/private/course/${course.id}`);
closeSidebar();
};

const handleManageCoursesClick = () => {
if (courses.length > 0) {
router.push(`/private/course/${courses[0].id}/profile/edit`);
} else {
router.push(`/private/course`);
}
closeSidebar();
};

const formattedCourseName = (course: IdentifiableCourse) => {
const formattedCourseId = course.id
.toUpperCase()
.replace(/([a-zA-Z]+)(\d+)/, "$1 $2");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking - should there be a space?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think so, according to Figma at least:

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see some wireframe has it together - might be worth confirming with Fa and Sarah
image


return `${formattedCourseId} ${course.name}`;
};

return (
<Drawer
anchor="left"
open={isOpen}
onClose={closeSidebar}
variant="temporary"
sx={{
"& .MuiDrawer-paper": { width: "100%" },
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "30px 30px 0",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography variant="h6" color={theme.palette.text.primary}>
My Classes
</Typography>
<MenuButton isOpen={false} />
</Box>
<List>
{courses.map((course, i) => (
<ListItemButton
key={i}
sx={{
padding: "16px 12px",
borderRadius: "9999px",
"&:active": {
backgroundColor: theme.palette.primary.main,
color: "white",
},
}}
onClick={() => handleCourseClick(course)}
>
<Typography variant="body1">
{formattedCourseName(course)}
</Typography>
</ListItemButton>
))}
</List>
</Box>
<Box
sx={{
paddingX: "30px",
}}
>
<Divider sx={{ marginBottom: "12px" }} />
<ListItemButton
onClick={handleManageCoursesClick}
sx={{
padding: "12px 0 12px 4px",
borderRadius: "9999px",
gap: "8px",
justifyContent: "flex-start",
"&:hover": {
background: "transparent",
},
"&:active": {
background: "transparent",
},
}}
>
<ModeEditOutlinedIcon />
<Typography variant="body1" fontWeight="light">
Manage Classes
</Typography>
</ListItemButton>
</Box>
</Drawer>
);
});

export default Sidebar;
27 changes: 27 additions & 0 deletions src/app/components/buttons/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<IconButton
edge={isEdge ? "start" : undefined}
onClick={isOpen ? openSidebar : closeSidebar}
>
<MenuIcon />
</IconButton>
);
});

export default MenuButton;
49 changes: 49 additions & 0 deletions src/app/context/SidebarContext.tsx
Original file line number Diff line number Diff line change
@@ -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<SidebarContextState>({
isOpen: false,
});

const SidebarActionsContext = React.createContext<SidebarContextActions>({
openSidebar: () => {},
closeSidebar: () => {},
});

export const SidebarProvider: React.FC<SidebarContextProps> = (
props: SidebarContextProps
) => {
const { children } = props;
const [isSidebarOpen, setIsSidebarOpen] = React.useState<boolean>(false);

const openSidebar = React.useCallback(() => setIsSidebarOpen(true), []);
const closeSidebar = React.useCallback(() => setIsSidebarOpen(false), []);

return (
<SidebarStateContext.Provider value={{ isOpen: isSidebarOpen }}>
<SidebarActionsContext.Provider value={{ openSidebar, closeSidebar }}>
{children}
</SidebarActionsContext.Provider>
</SidebarStateContext.Provider>
);
};

export const useSidebarStateContext = () =>
React.useContext(SidebarStateContext);

export const useSidebarActionsContext = () =>
React.useContext(SidebarActionsContext);
9 changes: 8 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"] });

Expand All @@ -20,7 +22,12 @@ export default function RootLayout({
<body className={inter.className} id="root">
<div id="__next">
<UserSessionContextProvider>
<ThemeProviderWrapper>{children}</ThemeProviderWrapper>
<ThemeProviderWrapper>
<SidebarProvider>
<Sidebar />
{children}
</SidebarProvider>
</ThemeProviderWrapper>
</UserSessionContextProvider>
</div>
</body>
Expand Down
10 changes: 3 additions & 7 deletions src/app/private/course/[courseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,11 +19,7 @@ const Page = async (props: PageProps) => {
return (
<div>
<Header
leftIcon={
<IconButton edge="start">
<MenuIcon />
</IconButton>
}
leftIcon={<MenuButton isEdge={true} />}
title={courseId.toUpperCase()}
alignCenter
/>
Expand Down
10 changes: 3 additions & 7 deletions src/app/private/course/[courseId]/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<Header
leftIcon={
<IconButton edge="start">
<MenuIcon />
</IconButton>
}
leftIcon={<MenuButton isEdge={true} />}
title="Profile"
alignCenter
rightIcon={<Button onClick={onSignOut}>Sign Out</Button>}
Expand Down
10 changes: 3 additions & 7 deletions src/app/private/course/[courseId]/queue/page.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -17,11 +17,7 @@ const Page = () => {
height="100%"
>
<Header
leftIcon={
<IconButton edge="start">
<MenuIcon />
</IconButton>
}
leftIcon={<MenuButton isEdge={true} />}
title={course.name}
alignCenter
/>
Expand Down
13 changes: 3 additions & 10 deletions src/app/private/course/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
"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();

// fetch courses and render
return (
<div className="flex flex-col min-h-screen">
<Header
leftIcon={
<IconButton>
<MenuIcon />
</IconButton>
}
title="Classes"
/>
<Header leftIcon={<MenuButton />} title="Classes" />
{/* NOTE: Currently only displays the case where a user has no classes */}
<div className="flex flex-col items-center justify-center flex-grow gap-4">
<div>Add a class to get started</div>
Expand Down
21 changes: 21 additions & 0 deletions src/app/services/client/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
getDocs,
setDoc,
updateDoc,
query,
where,
documentId,
} from "firebase/firestore";

export const addCourse = async (course: IdentifiableCourse) => {
Expand Down Expand Up @@ -57,6 +60,24 @@ export const getCourses = async () => {
return coursesDocs;
};

export const getCoursesByIds = async (
courseIds: string[]
): Promise<IdentifiableCourse[]> => {
const coursesQuery = query(
collection(db, "courses"),
where(documentId(), "in", courseIds)
);

const snapshot = await getDocs(coursesQuery.withConverter(courseConverter));

const coursesDocs: IdentifiableCourse[] = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));

return coursesDocs;
};

export const deleteCourse = async (courseID: String) => {
const courseDoc = doc(db, `courses/${courseID}`).withConverter(
courseConverter
Expand Down