From a033bc0ac11770ba13f30100c7efdbb6fe27e1c6 Mon Sep 17 00:00:00 2001 From: Matthew Rowland Date: Fri, 12 Jul 2024 12:04:33 -0700 Subject: [PATCH] feat: Responsive catalog with dashboard --- frontend/package.json | 29 +- frontend/src/App.tsx | 134 +++--- frontend/src/app/Account/index.tsx | 7 + frontend/src/app/Catalog/Catalog.module.scss | 71 +++- .../src/app/Catalog/Class/Class.module.scss | 32 +- .../Class/Enrollment/Enrollment.module.scss | 2 +- .../Reservations/Reservations.module.scss | 2 +- .../Catalog/Class/Grades/Grades.module.scss | 2 +- .../Class/Overview/Overview.module.scss | 2 +- .../Class/Sections/Sections.module.scss | 4 +- frontend/src/app/Catalog/Class/index.tsx | 395 ++++++++++-------- .../Dashboard/Carousel/Carousel.module.scss | 93 +++++ .../app/Catalog/Dashboard/Carousel/index.tsx | 62 +++ .../Catalog/Dashboard/Dashboard.module.scss | 46 ++ frontend/src/app/Catalog/Dashboard/index.tsx | 74 +++- frontend/src/app/Catalog/index.tsx | 115 +++-- frontend/src/app/Dashboard/index.tsx | 30 ++ .../src/app/Discover/Placeholder/index.tsx | 2 +- frontend/src/app/Enrollment/index.tsx | 7 + frontend/src/app/Grades/Grades.module.scss | 18 + frontend/src/app/Grades/index.tsx | 83 ++++ .../src/app/Schedule/Manage/Map/index.tsx | 2 +- frontend/src/app/Schedule/Manage/index.tsx | 4 +- .../BaseLayout/BaseLayout.module.scss | 40 -- frontend/src/components/BaseLayout/index.tsx | 48 --- .../src/components/Button/Button.module.scss | 20 +- frontend/src/components/Button/index.tsx | 17 +- .../ClassBrowser/ClassBrowser.module.scss | 20 +- .../ClassBrowser/Filters/Filters.module.scss | 52 ++- .../components/ClassBrowser/Filters/index.tsx | 9 +- .../ClassBrowser/Header/Header.module.scss | 24 +- .../components/ClassBrowser/Header/index.tsx | 83 ++-- .../ClassBrowser/List/Class/Class.module.scss | 3 +- .../ClassBrowser/List/List.module.scss | 6 +- .../components/ClassBrowser/List/index.tsx | 17 +- .../components/ClassBrowser/browserContext.ts | 3 +- .../src/components/ClassBrowser/index.tsx | 19 +- .../Container/Container.module.scss | 14 +- frontend/src/components/Container/index.tsx | 30 +- .../components/CourseBrowser/List/index.tsx | 4 +- .../CourseDrawer/Course/Course.module.scss | 1 - .../components/CourseDrawer/Course/index.tsx | 2 +- .../IconButton/IconButton.module.scss | 22 +- frontend/src/components/IconButton/index.tsx | 17 +- .../src/components/Layout/Layout.module.scss | 37 +- frontend/src/components/Layout/index.tsx | 45 +- frontend/src/components/MenuItem/index.tsx | 35 +- .../src/components/NavigationBar/index.tsx | 12 +- frontend/src/lib/api.ts | 5 + frontend/src/main.scss | 5 +- 50 files changed, 1215 insertions(+), 591 deletions(-) create mode 100644 frontend/src/app/Account/index.tsx create mode 100644 frontend/src/app/Catalog/Dashboard/Carousel/Carousel.module.scss create mode 100644 frontend/src/app/Catalog/Dashboard/Carousel/index.tsx create mode 100644 frontend/src/app/Dashboard/index.tsx create mode 100644 frontend/src/app/Enrollment/index.tsx create mode 100644 frontend/src/app/Grades/Grades.module.scss create mode 100644 frontend/src/app/Grades/index.tsx delete mode 100644 frontend/src/components/BaseLayout/BaseLayout.module.scss delete mode 100644 frontend/src/components/BaseLayout/index.tsx diff --git a/frontend/package.json b/frontend/package.json index 5362646c5..82b0190e4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,46 +9,47 @@ "start": "serve -s dist -p 3000" }, "dependencies": { - "@apollo/client": "^3.10.6", - "@floating-ui/dom": "^1.6.6", + "@apollo/client": "^3.10.8", + "@floating-ui/dom": "^1.6.7", "@mapbox/mapbox-gl-directions": "^4.3.1", - "@radix-ui/react-checkbox": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-tooltip": "^1.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", "@shopify/draggable": "^1.1.3", - "@tanstack/react-virtual": "^3.7.0", + "@tanstack/react-virtual": "^3.8.2", "classnames": "^2.5.1", "fuse.js": "^7.0.0", "graphql": "^16.9.0", "iconoir-react": "^7.7.0", - "mapbox-gl": "^3.4.0", + "mapbox-gl": "^3.5.1", "moment": "^2.30.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.24.0", + "react-router-dom": "^6.24.1", "recharts": "^2.12.7" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/mapbox-gl": "^3.1.0", - "@types/node": "^20.14.8", + "@types/node": "^20.14.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.14.1", - "@typescript-eslint/parser": "^7.14.1", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@typescript-eslint/parser": "^7.16.0", "@vitejs/plugin-react": "^4.3.1", "eslint": "^8.56.0", "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-react-refresh": "^0.4.8", "prettier": "^3.3.2", - "sass": "^1.77.6", + "sass": "^1.77.7", "serve": "^14.2.3", - "typescript": "^5.5.2", - "vite": "^5.3.1" + "typescript": "^5.5.3", + "vite": "^5.3.3" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 738541105..31b53a947 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,15 +5,16 @@ import { IconoirProvider } from "iconoir-react"; import { RouterProvider, createBrowserRouter } from "react-router-dom"; import Catalog from "@/app/Catalog"; +import Enrollment from "@/app/Enrollment"; +import Grades from "@/app/Grades"; import Landing from "@/app/Landing"; -import BaseLayout from "@/components/BaseLayout"; import Layout from "@/components/Layout"; const About = lazy(() => import("@/app/About")); -const Enrollment = lazy(() => import("@/app/Catalog/Class/Enrollment")); -const Grades = lazy(() => import("@/app/Catalog/Class/Grades")); -const Overview = lazy(() => import("@/app/Catalog/Class/Overview")); -const Sections = lazy(() => import("@/app/Catalog/Class/Sections")); +const CatalogEnrollment = lazy(() => import("@/app/Catalog/Class/Enrollment")); +const CatalogGrades = lazy(() => import("@/app/Catalog/Class/Grades")); +const CatalogOverview = lazy(() => import("@/app/Catalog/Class/Overview")); +const CatalogSections = lazy(() => import("@/app/Catalog/Class/Sections")); const Discover = lazy(() => import("@/app/Discover")); const Plan = lazy(() => import("@/app/Plan")); const Schedule = lazy(() => import("@/app/Schedule")); @@ -21,86 +22,99 @@ const Compare = lazy(() => import("@/app/Schedule/Compare")); const Manage = lazy(() => import("@/app/Schedule/Manage")); const Schedules = lazy(() => import("@/app/Schedules")); const Map = lazy(() => import("@/app/Map")); +const Account = lazy(() => import("@/app/Account")); +const Dashboard = lazy(() => import("@/app/Dashboard")); const router = createBrowserRouter([ { - element: , + element: , + path: "dashboard", + }, + { + element: , children: [ { - element: , + element: , + index: true, + }, + { + element: , + path: "discover", + }, + { + element: , + path: "schedules/:scheduleId", children: [ { - element: , + element: , index: true, }, { - element: , - path: "explore", - }, - { - element: , - path: "schedules/:scheduleId", - children: [ - { - element: , - index: true, - }, - { - element: , - path: "compare/:comparedScheduleId?", - }, - ], - }, - { - element: , - path: "map", + element: , + path: "compare/:comparedScheduleId?", }, ], }, { - element: , - children: [ - { - element: , - path: "about", - }, - ], + element: , + path: "map", }, + ], + }, + { + element: , + children: [ { - element: , + element: , + path: "about", + }, + { + element: , + path: "account", + }, + ], + }, + { + element: , + children: [ + { + element: , + path: "grades", + }, + { + element: , + path: "enrollment", + }, + { + element: , + path: "catalog/:year?/:semester?/:subject?/:courseNumber?/:classNumber?", children: [ { - element: , - path: "catalog/:year?/:semester?/:subject?/:courseNumber?/:classNumber?", - children: [ - { - element: , - index: true, - }, - { - element: , - path: "sections", - }, - { - element: , - path: "enrollment", - }, - { - element: , - path: "grades", - }, - ], + element: , + index: true, + }, + { + element: , + path: "sections", }, { - element: , - path: "schedules", + element: , + path: "enrollment", }, { - element: , - path: "plan", + element: , + path: "grades", }, ], }, + { + element: , + path: "schedules", + }, + { + element: , + path: "plan", + }, ], }, ]); diff --git a/frontend/src/app/Account/index.tsx b/frontend/src/app/Account/index.tsx new file mode 100644 index 000000000..95a1e4f00 --- /dev/null +++ b/frontend/src/app/Account/index.tsx @@ -0,0 +1,7 @@ +export default function Account() { + return ( +
+

Grades

+
+ ); +} diff --git a/frontend/src/app/Catalog/Catalog.module.scss b/frontend/src/app/Catalog/Catalog.module.scss index 0b6b13d10..9ba2ab69d 100644 --- a/frontend/src/app/Catalog/Catalog.module.scss +++ b/frontend/src/app/Catalog/Catalog.module.scss @@ -1,7 +1,70 @@ .root { + flex: 1 1 0; display: flex; - flex-grow: 1; - overflow: clip; - height: 0; - position: relative; + min-height: 0; + + .view { + flex-grow: 1; + overflow: auto; + } + + .panel { + display: flex; + flex-direction: column; + + .header { + padding: 12px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border-color); + background-color: var(--foreground-color); + + .title { + font-size: 14px; + font-weight: 500; + color: var(--heading-color); + line-height: 1; + } + } + + .body { + flex: 1 1 0; + min-height: 0; + } + } + + @media (width > 992px) { + display: flex; + + &:not(.expanded) .panel { + display: none; + } + + .panel { + border-right: 1px solid var(--border-color); + } + + .panel .header { + display: none; + } + + .view { + flex-grow: 1; + } + } + + @media (width <= 992px) { + .panel { + flex-grow: 1; + } + + &.open .panel { + display: none; + } + + &:not(.open) .view { + display: none; + } + } } \ No newline at end of file diff --git a/frontend/src/app/Catalog/Class/Class.module.scss b/frontend/src/app/Catalog/Class/Class.module.scss index 93f986d42..3286ccc06 100644 --- a/frontend/src/app/Catalog/Class/Class.module.scss +++ b/frontend/src/app/Catalog/Class/Class.module.scss @@ -34,21 +34,21 @@ } .root { - flex-grow: 1; display: flex; flex-direction: column; - overflow: auto; - - @media (width <= 992px) { - position: absolute; - inset: 0; - background-color: var(--background-color); - z-index: 3; - } .header { - padding: 24px 24px 12px; + padding-top: 24px; background-color: var(--foreground-color); + border-bottom: 1px solid var(--border-color); + + .menu { + display: flex; + background-color: var(--foreground-color); + margin: 0 -12px; + padding: 24px 0 12px; + align-items: center; + } .row { display: flex; @@ -76,8 +76,8 @@ .heading { font-size: 24px; - line-height: 1.25; - font-weight: 700; + font-weight: 660; + font-feature-settings: "cv05" on, "cv13" on, "ss07" on, "cv12" on, "cv06" on; color: var(--heading-color); margin-bottom: 8px; } @@ -89,12 +89,4 @@ margin-bottom: 16px; } } - - .menu { - display: flex; - border-bottom: 1px solid var(--border-color); - background-color: var(--foreground-color); - padding: 12px; - align-items: center; - } } \ No newline at end of file diff --git a/frontend/src/app/Catalog/Class/Enrollment/Enrollment.module.scss b/frontend/src/app/Catalog/Class/Enrollment/Enrollment.module.scss index 5500e3906..541ef6363 100644 --- a/frontend/src/app/Catalog/Class/Enrollment/Enrollment.module.scss +++ b/frontend/src/app/Catalog/Class/Enrollment/Enrollment.module.scss @@ -1,5 +1,5 @@ .root { - padding: 24px; + padding: 24px 0; flex-grow: 1; display: flex; flex-direction: column; diff --git a/frontend/src/app/Catalog/Class/Enrollment/Reservations/Reservations.module.scss b/frontend/src/app/Catalog/Class/Enrollment/Reservations/Reservations.module.scss index 416d1359b..4d8a6facd 100644 --- a/frontend/src/app/Catalog/Class/Enrollment/Reservations/Reservations.module.scss +++ b/frontend/src/app/Catalog/Class/Enrollment/Reservations/Reservations.module.scss @@ -63,7 +63,7 @@ height: 16px; border-radius: 2px; overflow: hidden; - background-color: var(--button-color); + // background-color: var(--button-color); transition: all 100ms ease-in-out; min-width: 16px; diff --git a/frontend/src/app/Catalog/Class/Grades/Grades.module.scss b/frontend/src/app/Catalog/Class/Grades/Grades.module.scss index 64a8a3757..c2ff86407 100644 --- a/frontend/src/app/Catalog/Class/Grades/Grades.module.scss +++ b/frontend/src/app/Catalog/Class/Grades/Grades.module.scss @@ -1,4 +1,4 @@ .root { height: 384px; - padding: 24px; + padding: 24px 0; } \ No newline at end of file diff --git a/frontend/src/app/Catalog/Class/Overview/Overview.module.scss b/frontend/src/app/Catalog/Class/Overview/Overview.module.scss index 0ca3579a1..f92b7c2b9 100644 --- a/frontend/src/app/Catalog/Class/Overview/Overview.module.scss +++ b/frontend/src/app/Catalog/Class/Overview/Overview.module.scss @@ -1,5 +1,5 @@ .root { - padding: 24px; + padding: 24px 0; font-size: 14px; .label { diff --git a/frontend/src/app/Catalog/Class/Sections/Sections.module.scss b/frontend/src/app/Catalog/Class/Sections/Sections.module.scss index 0e21e418b..f53cd5dc4 100644 --- a/frontend/src/app/Catalog/Class/Sections/Sections.module.scss +++ b/frontend/src/app/Catalog/Class/Sections/Sections.module.scss @@ -7,7 +7,7 @@ height: 100%; font-size: 16px; line-height: 1.5; - padding: 24px; + padding: 24px 0; text-align: center; .heading { @@ -25,7 +25,7 @@ .root { background-color: var(--background-color); - padding: 24px 24px 24px 12px; + padding: 24px 0 0; display: flex; gap: 24px; diff --git a/frontend/src/app/Catalog/Class/index.tsx b/frontend/src/app/Catalog/Class/index.tsx index 04ef14d34..b962b96de 100644 --- a/frontend/src/app/Catalog/Class/index.tsx +++ b/frontend/src/app/Catalog/Class/index.tsx @@ -1,4 +1,4 @@ -import { Suspense, useMemo, useState } from "react"; +import { Dispatch, SetStateAction, Suspense, useMemo, useState } from "react"; import { useQuery } from "@apollo/client"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; @@ -7,6 +7,8 @@ import { Bookmark, BookmarkSolid, CalendarPlus, + Collapse, + Expand, GridPlus, Heart, HeartSolid, @@ -22,29 +24,46 @@ import Boundary from "@/components/Boundary"; import Button from "@/components/Button"; import CCN from "@/components/CCN"; import Capacity from "@/components/Capacity"; +import Container from "@/components/Container"; import CourseDrawer from "@/components/CourseDrawer"; import IconButton from "@/components/IconButton"; import LoadingIndicator from "@/components/LoadingIndicator"; import MenuItem from "@/components/MenuItem"; import Tooltip from "@/components/Tooltip"; import Units from "@/components/Units"; -import useCatalog from "@/hooks/useCatalog"; +import CatalogContext from "@/contexts/CatalogContext"; import useWindowDimensions from "@/hooks/useWindowDimensions"; -import { GET_CLASS, IClass } from "@/lib/api"; +import { GET_CLASS, IClass, Semester } from "@/lib/api"; import { getExternalLink } from "@/lib/section"; import styles from "./Class.module.scss"; -export default function Class() { - const [searchParams] = useSearchParams(); +interface ClassProps { + subject: string; + courseNumber: string; + classNumber: string; + partialClass: IClass | null; + year: number; + semester: Semester; + expanded: boolean; + setExpanded: Dispatch>; + onClose: () => void; +} +export default function Class({ + subject, + courseNumber, + classNumber, + partialClass, + year, + semester, + expanded, + setExpanded, + onClose, +}: ClassProps) { + const [searchParams] = useSearchParams(); const { width } = useWindowDimensions(); - const { subject, courseNumber, classNumber, semester, year, partialClass } = - useCatalog(); - - // Because Class will only be rendered when partialClass or _class has been set, - // we can render accordingly and not worry about loading or error states const { data, loading } = useQuery<{ class: IClass }>(GET_CLASS, { variables: { term: { @@ -111,172 +130,220 @@ export default function Class() { return (
-
-
- - - - - setBookmarked(!bookmarked)} + +
+
+ + + + - {bookmarked ? : } - - - - {width > 992 ? ( - - - - - - - - - setCourse(true)} + setBookmarked(!bookmarked)} + > + {bookmarked ? : } + + + + {width > 992 ? ( + + + + + + + + - - View course - - + setCourse(true)} + > + + View course + + + + Add class to schedule + + + + Add course to plan + + + + + ) : ( + <> + + - Add class to schedule - - + + + + - Add course to plan - - - - - ) : ( - <> - - - + + + + + + + + + + + )} +
+
+ {_class && ( + + + - + )} + + setExpanded(!expanded)}> + {expanded ? : } + + + + onClose()} + > - + - - - - - - - - - - )} + + +
+

+ {subject} {courseNumber} #{classNumber} +

+

+ {_class?.title ?? partialClass?.title ?? partialClass?.course.title} +

- {_class && ( - - - - - - )} - - - - - - - + + + + {_class && }
-
-

- {subject} {courseNumber} #{classNumber} -

-

- {_class?.title ?? partialClass?.title ?? partialClass?.course.title} -

-
- - - - {_class && } -
-
-
- - {({ isActive }) => Overview} - - - {({ isActive }) => Sections} - - - {({ isActive }) => Enrollment} - - - {({ isActive }) => Grades} - +
+ + {({ isActive }) => ( + Overview + )} + + + {({ isActive }) => ( + Sections + )} + + + {({ isActive }) => ( + Enrollment + )} + + + {({ isActive }) => Grades} + +
+
- {_class ? ( - - - - } - > - - - ) : loading ? ( - - - - ) : ( - <> - )} + + {_class ? ( + + + + + } + > + + + + ) : loading ? ( + <>{/* TODO: Loading */} + ) : ( + <>{/* TODO: Error */} + )} +
); } diff --git a/frontend/src/app/Catalog/Dashboard/Carousel/Carousel.module.scss b/frontend/src/app/Catalog/Dashboard/Carousel/Carousel.module.scss new file mode 100644 index 000000000..fcd60a525 --- /dev/null +++ b/frontend/src/app/Catalog/Dashboard/Carousel/Carousel.module.scss @@ -0,0 +1,93 @@ +.root { + display: flex; + flex-direction: column; + gap: 24px; + + .body { + margin: 0 -24px; + position: relative; + + &.start::before { + opacity: 1; + } + + &.end::after { + opacity: 1; + } + + &::before { + content: ""; + width: 48px; + position: absolute; + height: 100%; + top: 0; + left: 0; + opacity: 0; + background: linear-gradient(to right, var(--background-color), transparent); + transition: opacity 100ms ease-in-out; + } + + &::after { + content: ""; + width: 48px; + position: absolute; + height: 100%; + top: 0; + right: 0; + opacity: 0; + background: linear-gradient(to left, var(--background-color), transparent); + transition: opacity 100ms ease-in-out; + } + + .view { + overflow: auto; + padding: 0 24px; + display: flex; + gap: 24px; + } + } + + .header { + display: flex; + align-items: center; + gap: 12px; + margin-top: 32px; + + .link { + font-size: 14px; + margin-right: 12px; + font-weight: 500; + color: var(--blue-500); + line-height: 1; + display: flex; + gap: 8px; + align-items: center; + transition: color 100ms ease-in-out; + + &:hover { + color: var(--blue-600); + } + } + + .title { + display: flex; + gap: 12px; + align-items: center; + font-size: 14px; + font-weight: 500; + color: var(--orange-500); + line-height: 1; + flex-grow: 1; + + .icon { + height: 32px; + width: 32px; + border-radius: 50%; + display: grid; + place-items: center; + background-color: var(--orange-500); + color: white; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/Catalog/Dashboard/Carousel/index.tsx b/frontend/src/app/Catalog/Dashboard/Carousel/index.tsx new file mode 100644 index 000000000..620902fe7 --- /dev/null +++ b/frontend/src/app/Catalog/Dashboard/Carousel/index.tsx @@ -0,0 +1,62 @@ +import { ReactNode, UIEvent, useState } from "react"; + +import classNames from "classnames"; +import { ArrowRight, NavArrowLeft, NavArrowRight } from "iconoir-react"; +import { Link, To } from "react-router-dom"; + +import IconButton from "@/components/IconButton"; + +import styles from "./Carousel.module.scss"; + +interface CarouselProps { + title: string; + Icon: ReactNode; + children: ReactNode; + to?: To; +} + +export default function Carousel({ title, Icon, children, to }: CarouselProps) { + const [start, setStart] = useState(false); + const [end, setEnd] = useState(false); + + const handleScroll = (event: UIEvent) => { + const { scrollLeft, scrollWidth, clientWidth } = + event.target as HTMLDivElement; + + setStart(scrollLeft > 0); + setEnd(scrollLeft + clientWidth < scrollWidth); + }; + + return ( +
+
+
+
{Icon}
+ {title} +
+ {to && ( + + View all + + + )} + + + + + + +
+
+
+ {children} +
+
+
+ ); +} diff --git a/frontend/src/app/Catalog/Dashboard/Dashboard.module.scss b/frontend/src/app/Catalog/Dashboard/Dashboard.module.scss index e69de29bb..800e970f9 100644 --- a/frontend/src/app/Catalog/Dashboard/Dashboard.module.scss +++ b/frontend/src/app/Catalog/Dashboard/Dashboard.module.scss @@ -0,0 +1,46 @@ +.root { + padding: 24px 0; + + .card { + width: 320px; + border: 1px solid var(--border-color); + aspect-ratio: 3 / 1; + border-radius: 8px; + background-color: var(--foreground-color); + flex-shrink: 0; + } + + .header { + display: flex; + justify-content: space-between; + align-items: center; + + @media (width <= 992px) { + .toggle > :last-child { + display: none; + } + } + + @media (width > 992px) { + .toggle > :first-child { + display: none; + } + } + } + + .heading { + font-size: 24px; + line-height: 1.25; + font-weight: 660; + font-feature-settings: "cv05" on, "cv13" on, "ss07" on, "cv12" on, "cv06" on; + color: var(--heading-color); + margin-top: 32px; + } + + .paragraph { + font-size: 16px; + line-height: 1.5; + color: var(--paragraph-color); + margin-top: 8px; + } +} \ No newline at end of file diff --git a/frontend/src/app/Catalog/Dashboard/index.tsx b/frontend/src/app/Catalog/Dashboard/index.tsx index 9d57c0c22..dd4d35843 100644 --- a/frontend/src/app/Catalog/Dashboard/index.tsx +++ b/frontend/src/app/Catalog/Dashboard/index.tsx @@ -1,9 +1,71 @@ -export default function Dashboard() { +import { Dispatch, SetStateAction } from "react"; + +import { + ArrowSeparateVertical, + BookmarkSolid, + Calendar, + Collapse, + Expand, + Search, +} from "iconoir-react"; + +import Button from "@/components/Button"; +import Container from "@/components/Container"; +import IconButton from "@/components/IconButton"; +import Tooltip from "@/components/Tooltip"; + +import Carousel from "./Carousel"; +import styles from "./Dashboard.module.scss"; + +interface DashboardProps { + expanded: boolean; + setExpanded: Dispatch>; + setOpen: Dispatch>; +} + +export default function Dashboard({ + expanded, + setExpanded, + setOpen, +}: DashboardProps) { return ( - <> - {/* Possibly a calendar preview of upcoming events */} - {/* Any errors/warnings about invalid terms or classes */} - {/* Bookmarked courses/classes for this semester */} - +
+ +
+ +
+ + + setExpanded(!expanded)}> + {expanded ? : } + + +
+
+

Summer 2024

+

March 20th through August 9th

+ }> +
+
+
+
+ } to="/semesters"> +
+
+
+
+ } to="/account"> +
+
+
+
+
+
); } diff --git a/frontend/src/app/Catalog/index.tsx b/frontend/src/app/Catalog/index.tsx index 4562c84bb..bc9f24e5d 100644 --- a/frontend/src/app/Catalog/index.tsx +++ b/frontend/src/app/Catalog/index.tsx @@ -1,16 +1,24 @@ import { useCallback, useMemo, useState } from "react"; import { useQuery } from "@apollo/client"; +import classNames from "classnames"; +import { Xmark } from "iconoir-react"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import Boundary from "@/components/Boundary"; import ClassBrowser from "@/components/ClassBrowser"; -import LoadingIndicator from "@/components/LoadingIndicator"; -import { GET_CLASS, IClass, Semester } from "@/lib/api"; +import IconButton from "@/components/IconButton"; +import { + GET_CLASS, + GET_COURSE, + GetClassResponse, + GetCourseResponse, + IClass, + Semester, +} from "@/lib/api"; -import CatalogContext from "../../contexts/CatalogContext"; import styles from "./Catalog.module.scss"; import Class from "./Class"; +import Dashboard from "./Dashboard"; export default function Catalog() { const { @@ -24,6 +32,8 @@ export default function Catalog() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const [expanded, setExpanded] = useState(true); + const [open, setOpen] = useState(false); const [partialClass, setPartialClass] = useState(null); // TODO: Select year @@ -47,7 +57,11 @@ export default function Catalog() { [currentSubject] ); - const { data, loading, error } = useQuery<{ class: IClass }>(GET_CLASS, { + const { + data: classData, + loading: classLoading, + error: classError, + } = useQuery(GET_CLASS, { variables: { term: { semester, @@ -60,11 +74,22 @@ export default function Catalog() { skip: !subject || !courseNumber || !classNumber, }); - const _class = useMemo(() => data?.class, [data]); + // Fetch the course to for directing to the correct term + const { loading: courseLoading, error: courseError } = + useQuery(GET_COURSE, { + variables: { + subject, + courseNumber, + }, + skip: !subject || !courseNumber || !classNumber, + }); + + const _class = useMemo(() => classData?.class, [classData]); const handleClassSelect = useCallback( (selectedClass: IClass) => { setPartialClass(selectedClass); + setOpen(true); navigate({ pathname: `/catalog/${year}/${semester}/${selectedClass.course.subject}/${selectedClass.course.number}/${selectedClass.number}`, @@ -75,35 +100,55 @@ export default function Catalog() { ); return ( -
- - {courseNumber && classNumber && subject && (_class || partialClass) ? ( - - - - ) : loading ? ( - - - - ) : error ? ( - <> - ) : ( - <> - )} +
+
+
+

+ {semester} {year} catalog +

+ setOpen(true)}> + + +
+
+ +
+
+
+ {courseNumber && classNumber && subject && (_class || partialClass) ? ( + setOpen(false)} + /> + ) : classLoading || courseLoading ? ( + <>{/* Loading */} + ) : classError || courseError ? ( + <>{/* Error */} + ) : ( + + )} +
); } diff --git a/frontend/src/app/Dashboard/index.tsx b/frontend/src/app/Dashboard/index.tsx new file mode 100644 index 000000000..a4bf14900 --- /dev/null +++ b/frontend/src/app/Dashboard/index.tsx @@ -0,0 +1,30 @@ +import { useEffect, useMemo } from "react"; + +import { useQuery } from "@apollo/client"; +import { useNavigate } from "react-router-dom"; + +import Boundary from "@/components/Boundary"; +import LoadingIndicator from "@/components/LoadingIndicator"; +import { AccountResponse, GET_ACCOUNT } from "@/lib/api"; + +export default function Dashboard() { + const navigate = useNavigate(); + + const { data, loading } = useQuery(GET_ACCOUNT); + + const account = useMemo(() => data?.user, [data]); + + useEffect(() => { + if (loading || account?.staff) return; + + navigate("/account"); + }, [loading, account, navigate]); + + return account?.staff ? ( + <> + ) : ( + + + + ); +} diff --git a/frontend/src/app/Discover/Placeholder/index.tsx b/frontend/src/app/Discover/Placeholder/index.tsx index 6f55da168..e98e1663f 100644 --- a/frontend/src/app/Discover/Placeholder/index.tsx +++ b/frontend/src/app/Discover/Placeholder/index.tsx @@ -5,7 +5,7 @@ import classNames from "classnames"; import styles from "./Placeholder.module.scss"; const values = [ - "Explore courses...", + "Discover courses...", "Computational methods for science and engineering", "Environmental policy and resource management", "Bioinformatics and computational biology", diff --git a/frontend/src/app/Enrollment/index.tsx b/frontend/src/app/Enrollment/index.tsx new file mode 100644 index 000000000..3566c4167 --- /dev/null +++ b/frontend/src/app/Enrollment/index.tsx @@ -0,0 +1,7 @@ +export default function Enrollment() { + return ( +
+

Grades

+
+ ); +} diff --git a/frontend/src/app/Grades/Grades.module.scss b/frontend/src/app/Grades/Grades.module.scss new file mode 100644 index 000000000..a367dc1e6 --- /dev/null +++ b/frontend/src/app/Grades/Grades.module.scss @@ -0,0 +1,18 @@ +.root { + flex-grow: 1; + display: flex; + height: 0; + + .panel { + width: 384px; + border-right: 1px solid var(--border-color); + } + + .view { + padding: 24px; + flex-grow: 1; + display: flex; + flex-direction: column; + overflow: auto; + } +} \ No newline at end of file diff --git a/frontend/src/app/Grades/index.tsx b/frontend/src/app/Grades/index.tsx new file mode 100644 index 000000000..63ca31c43 --- /dev/null +++ b/frontend/src/app/Grades/index.tsx @@ -0,0 +1,83 @@ +import { + Bar, + BarChart, + CartesianGrid, + LabelList, + Legend, + ResponsiveContainer, + XAxis, +} from "recharts"; + +import styles from "./Grades.module.scss"; + +const data = [ + { + grade: "A", + percentage: 20, + average: 25, + }, + { + grade: "B", + percentage: 15, + average: 20, + }, + { + grade: "C", + percentage: 10, + average: 15, + }, + { + grade: "D", + percentage: 5, + average: 10, + }, + { + grade: "F", + percentage: 2.5, + average: 5, + }, + { + grade: "Pass", + percentage: 35, + average: 20, + }, + { + grade: "Not pass", + percentage: 17.5, + average: 5, + }, +]; + +export default function Grades() { + return ( +
+
+
+ + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/app/Schedule/Manage/Map/index.tsx b/frontend/src/app/Schedule/Manage/Map/index.tsx index 48917495e..aab61cf46 100644 --- a/frontend/src/app/Schedule/Manage/Map/index.tsx +++ b/frontend/src/app/Schedule/Manage/Map/index.tsx @@ -264,7 +264,7 @@ export default function Map({ selectedSections }: MapProps) { return (
- diff --git a/frontend/src/app/Schedule/Manage/index.tsx b/frontend/src/app/Schedule/Manage/index.tsx index 56b3cf9ec..2034a65e4 100644 --- a/frontend/src/app/Schedule/Manage/index.tsx +++ b/frontend/src/app/Schedule/Manage/index.tsx @@ -213,12 +213,12 @@ export default function Manage() {
- - diff --git a/frontend/src/components/BaseLayout/BaseLayout.module.scss b/frontend/src/components/BaseLayout/BaseLayout.module.scss deleted file mode 100644 index 5d7042987..000000000 --- a/frontend/src/components/BaseLayout/BaseLayout.module.scss +++ /dev/null @@ -1,40 +0,0 @@ -.root { - flex-grow: 1; - display: flex; - flex-direction: column; - - .view { - min-height: 100dvh; - display: flex; - flex-direction: column; - } - - .trigger { - position: fixed; - bottom: 128px; - right: 0; - border-radius: 4px 0 0 4px; - z-index: 979; - overflow: hidden; - pointer-events: none; - - .button { - padding: 0 16px; - border-radius: 4px 0 0 4px; - gap: 16px; - height: 48px; - transform: translateX(calc(100% - 48px)); - pointer-events: auto; - - &:hover, &:focus { - animation: slideOut 250ms ease-in-out forwards; - } - } - } -} - -@keyframes slideOut { - to { - transform: translateY(0); - } -} \ No newline at end of file diff --git a/frontend/src/components/BaseLayout/index.tsx b/frontend/src/components/BaseLayout/index.tsx deleted file mode 100644 index cc9704475..000000000 --- a/frontend/src/components/BaseLayout/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Suspense } from "react"; - -import { MessageText } from "iconoir-react"; -import { Outlet } from "react-router"; - -import Button from "@/components/Button"; -import Footer from "@/components/Footer"; -import NavigationBar from "@/components/NavigationBar"; - -import Boundary from "../Boundary"; -import LoadingIndicator from "../LoadingIndicator"; -import styles from "./BaseLayout.module.scss"; - -interface LayoutProps { - header?: boolean; - footer?: boolean; -} - -export default function BaseLayout({ - header = true, - footer = true, -}: LayoutProps) { - return ( -
-
- {header && } - - - - } - > - - -
- {footer &&
- ); -} diff --git a/frontend/src/components/Button/Button.module.scss b/frontend/src/components/Button/Button.module.scss index 11fbaf099..3dec9fb22 100644 --- a/frontend/src/components/Button/Button.module.scss +++ b/frontend/src/components/Button/Button.module.scss @@ -11,22 +11,24 @@ cursor: pointer; transition: all 100ms ease-in-out; gap: 8px; - background-color: var(--blue-500); - color: white; user-select: none; - &:hover { - background-color: var(--blue-600); - } + &.solid { + background-color: var(--blue-500); + color: white; - &:active { - background-color: var(--blue-700); + &:hover { + background-color: var(--blue-600); + } + + &:active { + background-color: var(--blue-700); + } } - &.secondary { + &.outline { border: 1px solid var(--border-color); color: var(--paragraph-color); - background-color: var(--button-color); &:hover { background-color: var(--button-hover-color); diff --git a/frontend/src/components/Button/index.tsx b/frontend/src/components/Button/index.tsx index 1dd73deac..c55fd0a73 100644 --- a/frontend/src/components/Button/index.tsx +++ b/frontend/src/components/Button/index.tsx @@ -10,10 +10,7 @@ import { import styles from "./Button.module.scss"; interface Props { - children: ReactNode; - className?: string; - secondary?: boolean; - active?: boolean; + variant?: "solid" | "outline"; } type ButtonProps = PolymorphicComponentPropsWithRef< @@ -23,7 +20,13 @@ type ButtonProps = PolymorphicComponentPropsWithRef< const Button = forwardRef( ( - { active, children, className, secondary, ...props }: ButtonProps, + { + active, + children, + className, + variant = "solid", + ...props + }: ButtonProps, ref: PolymorphicRef ) => { return ( @@ -32,8 +35,8 @@ const Button = forwardRef( className={classNames( styles.root, { - [styles.active]: active, - [styles.secondary]: secondary, + [styles.solid]: variant === "solid", + [styles.outline]: variant === "outline", }, className )} diff --git a/frontend/src/components/ClassBrowser/ClassBrowser.module.scss b/frontend/src/components/ClassBrowser/ClassBrowser.module.scss index e824c7faa..05ae500e7 100644 --- a/frontend/src/components/ClassBrowser/ClassBrowser.module.scss +++ b/frontend/src/components/ClassBrowser/ClassBrowser.module.scss @@ -3,7 +3,23 @@ height: 100%; display: flex; - &.block { - width: 100%; + @media (width <= 992px) { + &:not(.expanded) > :first-child { + display: none; + } + + &.expanded > :last-child { + display: none; + } + } + + @media (width <= 1400px) { + &:not(.expanded).responsive > :first-child { + display: none; + } + + &.expanded.responsive > :last-child { + display: none; + } } } \ No newline at end of file diff --git a/frontend/src/components/ClassBrowser/Filters/Filters.module.scss b/frontend/src/components/ClassBrowser/Filters/Filters.module.scss index 4aaf1ce77..7ccbf2e00 100644 --- a/frontend/src/components/ClassBrowser/Filters/Filters.module.scss +++ b/frontend/src/components/ClassBrowser/Filters/Filters.module.scss @@ -1,21 +1,9 @@ .root { - width: 384px; + width: 288px; flex-shrink: 0; overflow: auto; background-color: var(--foreground-color); - &:not(.block) { - border-right: 1px solid var(--border-color); - } - - &.block { - width: 100%; - } - - &.overlay .body { - padding-top: 12px; - } - .body { display: flex; flex-direction: column; @@ -42,7 +30,6 @@ font-size: 14px; color: var(--label-color); line-height: 1; - margin-bottom: 12px; &:not(:first-child) { margin-top: 24px; @@ -53,10 +40,7 @@ display: flex; align-items: center; gap: 12px; - - & + .filter { - margin-top: 12px; - } + margin-top: 12px; &:hover .text .value { color: var(--heading-color); @@ -133,4 +117,36 @@ } } } + + @media (width > 992px) { + border-right: 1px solid var(--border-color); + + &:not(.responsive) > :first-child { + display: none; + } + } + + @media (width > 1400px) { + > :first-child { + display: none; + } + } + + @media (width <= 992px) { + width: 100%; + + .body { + padding-top: 12px; + } + } + + @media (992px < width <= 1400px) { + &.responsive { + width: 384px; + + .body { + padding-top: 12px; + } + } + } } \ No newline at end of file diff --git a/frontend/src/components/ClassBrowser/Filters/index.tsx b/frontend/src/components/ClassBrowser/Filters/index.tsx index d0ce336c0..789673777 100644 --- a/frontend/src/components/ClassBrowser/Filters/index.tsx +++ b/frontend/src/components/ClassBrowser/Filters/index.tsx @@ -37,9 +37,7 @@ export default function Filters() { updateOnline, sortBy, updateSortBy, - overlay, - block, - expanded, + responsive, } = useBrowser(); const filteredLevels = useMemo(() => { @@ -243,11 +241,10 @@ export default function Filters() { return (
- {expanded && overlay &&
} +

Quick filters

diff --git a/frontend/src/components/ClassBrowser/Header/Header.module.scss b/frontend/src/components/ClassBrowser/Header/Header.module.scss index 4a5c0dd48..e1b291391 100644 --- a/frontend/src/components/ClassBrowser/Header/Header.module.scss +++ b/frontend/src/components/ClassBrowser/Header/Header.module.scss @@ -5,8 +5,28 @@ z-index: 1; background: linear-gradient(to bottom, var(--background-color), transparent); - &.overlay .group { - padding-right: 8px; + @media (width <= 992px) { + .group { + padding-right: 8px; + } + } + + @media (width <= 1400px) { + &.responsive .group { + padding-right: 8px; + } + } + + @media (width > 992px) { + &:not(.responsive) .group > :last-child { + display: none; + } + } + + @media (width > 1400px) { + .group > :last-child { + display: none; + } } .group { diff --git a/frontend/src/components/ClassBrowser/Header/index.tsx b/frontend/src/components/ClassBrowser/Header/index.tsx index 19e575f7e..df4e71e77 100644 --- a/frontend/src/components/ClassBrowser/Header/index.tsx +++ b/frontend/src/components/ClassBrowser/Header/index.tsx @@ -1,5 +1,3 @@ -import { ComponentPropsWithoutRef, forwardRef } from "react"; - import classNames from "classnames"; import { Filter, FilterSolid } from "iconoir-react"; @@ -8,52 +6,39 @@ import IconButton from "@/components/IconButton"; import useBrowser from "../useBrowser"; import styles from "./Header.module.scss"; -const Header = forwardRef>( - ({ className, ...props }, ref) => { - const { - overlay, - query, - updateQuery, - expanded, - setExpanded, - classes, - semester, - year, - } = useBrowser(); +export default function Header() { + const { + query, + updateQuery, + expanded, + setExpanded, + classes, + semester, + year, + responsive, + } = useBrowser(); - return ( -
-
- updateQuery(event.target.value)} - placeholder={`Search ${semester} ${year} classes...`} - onFocus={() => setExpanded(false)} - {...props} - /> -
{classes.length.toLocaleString()}
- {overlay && ( - setExpanded(!expanded)}> - {expanded ? : } - - )} -
+ return ( +
+
+ updateQuery(event.target.value)} + placeholder={`Search ${semester} ${year} classes...`} + onFocus={() => setExpanded(false)} + autoFocus + /> +
{classes.length.toLocaleString()}
+ setExpanded(!expanded)}> + {expanded ? : } +
- ); - } -); - -Header.displayName = "Header"; - -export default Header; +
+ ); +} diff --git a/frontend/src/components/ClassBrowser/List/Class/Class.module.scss b/frontend/src/components/ClassBrowser/List/Class/Class.module.scss index 911c356e7..de27312e7 100644 --- a/frontend/src/components/ClassBrowser/List/Class/Class.module.scss +++ b/frontend/src/components/ClassBrowser/List/Class/Class.module.scss @@ -37,7 +37,8 @@ } .heading { - font-weight: 700; + font-weight: 660; + font-feature-settings: "cv05" on, "cv13" on, "ss07" on, "cv12" on, "cv06" on; } .title { diff --git a/frontend/src/components/ClassBrowser/List/List.module.scss b/frontend/src/components/ClassBrowser/List/List.module.scss index 4d4e8dc1e..78fc0cba6 100644 --- a/frontend/src/components/ClassBrowser/List/List.module.scss +++ b/frontend/src/components/ClassBrowser/List/List.module.scss @@ -4,11 +4,7 @@ overflow: auto; background-color: var(--background-color); - &:not(.block) { - border-right: 1px solid var(--border-color); - } - - &.block { + @media (width <= 992px) { width: 100%; } diff --git a/frontend/src/components/ClassBrowser/List/index.tsx b/frontend/src/components/ClassBrowser/List/index.tsx index 366b5b307..27c25542c 100644 --- a/frontend/src/components/ClassBrowser/List/index.tsx +++ b/frontend/src/components/ClassBrowser/List/index.tsx @@ -1,7 +1,6 @@ import { useEffect, useRef } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; -import classNames from "classnames"; import { ArrowRight, FrameAltEmpty, Sparks } from "iconoir-react"; import { Link, useSearchParams } from "react-router-dom"; @@ -18,7 +17,7 @@ interface ListProps { } export default function List({ onClassSelect }: ListProps) { - const { classes, loading, block, overlay } = useBrowser(); + const { classes, loading } = useBrowser(); const rootRef = useRef(null); const [searchParams] = useSearchParams(); @@ -41,20 +40,14 @@ export default function List({ onClassSelect }: ListProps) { const totalSize = virtualizer.getTotalSize(); return ( -
+
-
+
{loading && items.length === 0 ? (
@@ -98,9 +91,9 @@ export default function List({ onClassSelect }: ListProps) {
)}
- + -

Try exploring courses

+

Try discovering courses

diff --git a/frontend/src/components/ClassBrowser/browserContext.ts b/frontend/src/components/ClassBrowser/browserContext.ts index e30bc3153..3ea520d32 100644 --- a/frontend/src/components/ClassBrowser/browserContext.ts +++ b/frontend/src/components/ClassBrowser/browserContext.ts @@ -5,8 +5,7 @@ import { Component, IClass, Semester } from "@/lib/api"; import { Day, Level, SortBy, Unit } from "./browser"; export interface BrowserContextType { - overlay: boolean; - block: boolean; + responsive: boolean; expanded: boolean; setExpanded: Dispatch>; classes: IClass[]; diff --git a/frontend/src/components/ClassBrowser/index.tsx b/frontend/src/components/ClassBrowser/index.tsx index 9e1d44138..2e9364e72 100644 --- a/frontend/src/components/ClassBrowser/index.tsx +++ b/frontend/src/components/ClassBrowser/index.tsx @@ -4,7 +4,6 @@ import { useQuery } from "@apollo/client"; import classNames from "classnames"; import { useSearchParams } from "react-router-dom"; -import useWindowDimensions from "@/hooks/useWindowDimensions"; import { Component, GET_CLASSES, @@ -43,7 +42,6 @@ export default function ClassBrowser({ }: ClassBrowserProps) { const [expanded, setExpanded] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); - const { width } = useWindowDimensions(); const [localQuery, setLocalQuery] = useState(""); const [localComponents, setLocalComponents] = useState([]); @@ -54,13 +52,6 @@ export default function ClassBrowser({ const [localOpen, setLocalOpen] = useState(false); const [localOnline, setLocalOnline] = useState(false); - const block = useMemo(() => width <= 992, [width]); - - const overlay = useMemo( - () => (responsive && width <= 1400) || block, - [width, responsive, block] - ); - const { data, loading } = useQuery(GET_CLASSES, { variables: { term: { @@ -301,9 +292,8 @@ export default function ClassBrowser({ return (
- {(expanded || !overlay) && } - {(!expanded || !overlay) && } + +
); diff --git a/frontend/src/components/Container/Container.module.scss b/frontend/src/components/Container/Container.module.scss index ea8390aea..a0cf76340 100644 --- a/frontend/src/components/Container/Container.module.scss +++ b/frontend/src/components/Container/Container.module.scss @@ -1,6 +1,18 @@ .root { - max-width: 1280px; width: 100%; margin: 0 auto; padding: 0 24px; + min-height: 100%; + + &.large { + max-width: 1280px; + } + + &.medium { + max-width: 1152px; + } + + &.small { + max-width: 1024px; + } } \ No newline at end of file diff --git a/frontend/src/components/Container/index.tsx b/frontend/src/components/Container/index.tsx index ba559594b..008f3f83d 100644 --- a/frontend/src/components/Container/index.tsx +++ b/frontend/src/components/Container/index.tsx @@ -1,14 +1,30 @@ -import { ReactNode } from "react"; +import { ComponentPropsWithoutRef, forwardRef } from "react"; import classNames from "classnames"; import styles from "./Container.module.scss"; -interface ContainerProps { - children?: ReactNode; - className?: string; +interface ContainerProps extends ComponentPropsWithoutRef<"div"> { + size?: "small" | "medium" | "large"; } -export default function Container({ children, className }: ContainerProps) { - return
{children}
; -} +const Container = forwardRef( + ({ children, className, size = "large", ...props }) => { + return ( +
+ {children} +
+ ); + } +); + +Container.displayName = "Container"; + +export default Container; diff --git a/frontend/src/components/CourseBrowser/List/index.tsx b/frontend/src/components/CourseBrowser/List/index.tsx index dd4566575..7f3b66c87 100644 --- a/frontend/src/components/CourseBrowser/List/index.tsx +++ b/frontend/src/components/CourseBrowser/List/index.tsx @@ -123,9 +123,9 @@ export default function List({
)}
- + -

Try exploring courses

+

Try discovering courses

diff --git a/frontend/src/components/CourseDrawer/Course/Course.module.scss b/frontend/src/components/CourseDrawer/Course/Course.module.scss index a947f7b04..b6339d8af 100644 --- a/frontend/src/components/CourseDrawer/Course/Course.module.scss +++ b/frontend/src/components/CourseDrawer/Course/Course.module.scss @@ -75,7 +75,6 @@ .heading { font-size: 24px; - line-height: 1.25; font-weight: 700; color: var(--heading-color); margin-bottom: 8px; diff --git a/frontend/src/components/CourseDrawer/Course/index.tsx b/frontend/src/components/CourseDrawer/Course/index.tsx index df2347569..47e66e5a9 100644 --- a/frontend/src/components/CourseDrawer/Course/index.tsx +++ b/frontend/src/components/CourseDrawer/Course/index.tsx @@ -48,7 +48,7 @@ export default function Course({ subject, number }: CourseProps) {
+
); } diff --git a/frontend/src/components/MenuItem/index.tsx b/frontend/src/components/MenuItem/index.tsx index 5355e3327..779cde714 100644 --- a/frontend/src/components/MenuItem/index.tsx +++ b/frontend/src/components/MenuItem/index.tsx @@ -1,32 +1,29 @@ -import { - ComponentPropsWithRef, - ElementType, - ReactNode, - forwardRef, -} from "react"; +import { ElementType, ReactElement, ReactNode, forwardRef } from "react"; import classNames from "classnames"; +import { + PolymorphicComponentPropsWithRef, + PolymorphicRef, +} from "@/lib/polymorphism"; + import styles from "./MenuItem.module.scss"; -interface MenuItemProps { +interface Props { active?: boolean; children: ReactNode; className?: string; - as?: T; } +type MenuItemProps = PolymorphicComponentPropsWithRef< + C, + Props +>; + const MenuItem = forwardRef( ( - { - active, - children, - className, - as, - ...props - }: MenuItemProps & - Omit, keyof MenuItemProps>, - ref: ComponentPropsWithRef["ref"] + { active, children, className, as, ...props }: MenuItemProps, + ref: PolymorphicRef ) => { const Component = as ?? "button"; @@ -50,4 +47,6 @@ const MenuItem = forwardRef( MenuItem.displayName = "MenuItem"; -export default MenuItem; +export default MenuItem as ( + props: MenuItemProps +) => ReactElement | null; diff --git a/frontend/src/components/NavigationBar/index.tsx b/frontend/src/components/NavigationBar/index.tsx index 880e93160..ba5f00f4f 100644 --- a/frontend/src/components/NavigationBar/index.tsx +++ b/frontend/src/components/NavigationBar/index.tsx @@ -36,18 +36,24 @@ export default function NavigationBar({ invert }: NavigationBarProps) { ) : ( <>
+ + Discover + Catalog + + Enrollment + + + Grades + My schedules My plan - - Explore -
{account ? (