diff --git a/apps/backend/src/bootstrap/loaders/passport.ts b/apps/backend/src/bootstrap/loaders/passport.ts index 11d78e3fb..d2e3d966e 100644 --- a/apps/backend/src/bootstrap/loaders/passport.ts +++ b/apps/backend/src/bootstrap/loaders/passport.ts @@ -1,9 +1,3 @@ -/** - * package.json override for oauth is to resolve package dependency issues in passport-google-oauth20 and - * passport-aouth2. Once these packages are updated, this override can be removed. - * - * Opened pull request: https://github.com/jaredhanson/passport-oauth2/pull/165 - */ import RedisStore from "connect-redis"; import type { Application } from "express"; import session from "express-session"; @@ -144,7 +138,6 @@ export default async (app: Application, redis: RedisClientType) => { async (_, __, profile, done) => { const email = profile.emails?.[0].value; - // null check for type safety if (!email) { return done(null, false, { message: "No email found" }); } diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 8b7e9a77c..c5334c769 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -11,11 +11,22 @@ import Grades from "@/app/Grades"; import Landing from "@/app/Landing"; import Layout from "@/components/Layout"; +const Class = { + Enrollment: lazy(() => import("@/components/Class/Enrollment")), + Grades: lazy(() => import("@/components/Class/Grades")), + Overview: lazy(() => import("@/components/Class/Overview")), + Sections: lazy(() => import("@/components/Class/Sections")), +}; + +const Course = { + Root: lazy(() => import("@/app/Course")), + Enrollment: lazy(() => import("@/components/Course/Enrollment")), + Grades: lazy(() => import("@/components/Course/Grades")), + Overview: lazy(() => import("@/components/Course/Overview")), + Classes: lazy(() => import("@/components/Course/Classes")), +}; + const About = lazy(() => import("@/app/About")); -const CatalogEnrollment = lazy(() => import("@/components/Class/Enrollment")); -const CatalogGrades = lazy(() => import("@/components/Class/Grades")); -const CatalogOverview = lazy(() => import("@/components/Class/Overview")); -const CatalogSections = lazy(() => import("@/components/Class/Sections")); const Discover = lazy(() => import("@/app/Discover")); const Plan = lazy(() => import("@/app/Plan")); const Schedule = lazy(() => import("@/app/Schedule")); @@ -26,17 +37,12 @@ const Map = lazy(() => import("@/app/Map")); const router = createBrowserRouter([ { - element: , + element: , children: [ { element: , path: "discover", }, - ], - }, - { - element: , - children: [ { element: , index: true, @@ -81,24 +87,46 @@ const router = createBrowserRouter([ element: , path: "enrollment", }, + { + element: , + path: "courses/:subject/:number", + children: [ + { + element: , + index: true, + }, + { + element: , + path: "classes", + }, + { + element: , + path: "enrollment", + }, + { + element: , + path: "grades", + }, + ], + }, { element: , - path: "catalog/:year?/:semester?/:subject?/:courseNumber?/:classNumber?", + path: "catalog/:year?/:semester?/:subject?/:courseNumber?/:number?", children: [ { - element: , + element: , index: true, }, { - element: , + element: , path: "sections", }, { - element: , + element: , path: "enrollment", }, { - element: , + element: , path: "grades", }, ], diff --git a/apps/frontend/src/app/Catalog/index.tsx b/apps/frontend/src/app/Catalog/index.tsx index feb13a6bd..ab993237b 100644 --- a/apps/frontend/src/app/Catalog/index.tsx +++ b/apps/frontend/src/app/Catalog/index.tsx @@ -1,121 +1,105 @@ 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 { useLocation, useNavigate, useParams } from "react-router-dom"; -import { Boundary, IconButton, LoadingIndicator } from "@repo/theme"; +import { IconButton } from "@repo/theme"; import Class from "@/components/Class"; import ClassBrowser from "@/components/ClassBrowser"; -import { - GET_CLASS, - GET_COURSE, - GET_TERMS, - GetClassResponse, - GetCourseResponse, - GetTermsResponse, - IClass, - TemporalPosition, -} from "@/lib/api"; +import { useReadTerms } from "@/hooks/api"; +import { useReadClass } from "@/hooks/api/classes/useReadClass"; +import { Semester, TemporalPosition } from "@/lib/api"; import styles from "./Catalog.module.scss"; import Dashboard from "./Dashboard"; export default function Catalog() { const { - year: currentYear, - semester: currentSemester, - subject: currentSubject, + year: providedYear, + semester: providedSemester, + subject: providedSubject, courseNumber, - classNumber, + number, } = useParams(); const navigate = useNavigate(); - const [searchParams] = useSearchParams(); + const location = useLocation(); const [expanded, setExpanded] = useState(true); const [open, setOpen] = useState(false); - const [partialClass, setPartialClass] = useState(null); - const { data, loading } = useQuery(GET_TERMS); + const { data: terms, loading: termsLoading } = useReadTerms(); - const terms = useMemo(() => data?.terms, [data]); + const semester = useMemo(() => { + if (!providedSemester) return null; - const selectedTerm = useMemo(() => { - if (!currentYear || !currentSemester) return; + return providedSemester[0].toUpperCase() + providedSemester.slice(1); + }, [providedSemester]); - const semester = - currentSemester[0].toUpperCase() + currentSemester.slice(1); + const year = useMemo(() => { + if (!providedYear) return null; - return terms?.find( - (term) => - term.year === parseInt(currentYear) && term.semester === semester + return parseInt(providedYear) || null; + }, [providedYear]); + + const term = useMemo(() => { + if (!terms) return null; + + const currentTerm = terms?.find( + (term) => term.temporalPosition === TemporalPosition.Current ); - }, [terms, currentYear, currentSemester]); - const currentTerm = useMemo( - () => - selectedTerm ?? - terms?.find((term) => term.temporalPosition === TemporalPosition.Current), - [terms] - ); + // Default to the current term + return ( + terms?.find((term) => term.year === year && term.semester === semester) ?? + currentTerm + ); + }, [terms, year, semester]); const subject = useMemo( - () => currentSubject?.toUpperCase(), - [currentSubject] + () => providedSubject?.toUpperCase(), + [providedSubject] ); - const { - data: classData, - loading: classLoading, - error: classError, - } = useQuery(GET_CLASS, { - variables: { - term: { - semester: currentTerm?.semester, - year: currentTerm?.year, - }, - subject, - courseNumber, - classNumber, - }, - skip: !subject || !courseNumber || !classNumber || !currentTerm, - }); - - // Fetch the course to for directing to the correct term - const { loading: courseLoading, error: courseError } = - useQuery(GET_COURSE, { - variables: { - subject, - courseNumber, - }, - skip: !subject || !courseNumber, - }); - - const _class = useMemo(() => classData?.class, [classData]); - - const handleClassSelect = useCallback( - (selectedClass: IClass) => { - if (!currentTerm) return; - - setPartialClass(selectedClass); + const { data: _class, loading: classLoading } = useReadClass( + term?.year as number, + term?.semester as Semester, + subject as string, + courseNumber as string, + number as string, + { + skip: !subject || !courseNumber || !number || !term, + } + ); + + const handleSelect = useCallback( + (subject: string, courseNumber: string, number: string) => { + if (!term) return; + setOpen(true); navigate({ - pathname: `/catalog/${currentTerm.year}/${currentTerm.semester}/${selectedClass.course.subject}/${selectedClass.course.number}/${selectedClass.number}`, - search: searchParams.toString(), + ...location, + pathname: `/catalog/${term.year}/${term.semester}/${subject}/${courseNumber}/${number}`, }); }, - [navigate, currentYear, currentSemester, searchParams, currentTerm] + [navigate, year, semester, location, term] ); - return loading ? ( - - - - ) : currentTerm ? ( + // TODO: Loading state + if (termsLoading) { + return <>; + } + + // TODO: Error state + if (!term) { + return <>; + } + + // TODO: Class error state, class loading state + return (

- {currentTerm.semester} {currentTerm.year} + {term.semester} {term.year}

setOpen(true)}> @@ -133,30 +117,23 @@ export default function Catalog() {
- {courseNumber && classNumber && subject && (_class || partialClass) ? ( + {classLoading ? ( + <> + ) : _class ? ( setOpen(false)} /> - ) : classLoading || courseLoading ? ( - <>{/* Loading */} - ) : classError || courseError ? ( - <>{/* Error */} ) : (
- ) : ( - <>{/* Error */} ); } diff --git a/apps/frontend/src/app/Course/index.tsx b/apps/frontend/src/app/Course/index.tsx new file mode 100644 index 000000000..4cc3498a9 --- /dev/null +++ b/apps/frontend/src/app/Course/index.tsx @@ -0,0 +1,9 @@ +import { useParams } from "react-router-dom"; + +import Component from "@/components/Course"; + +export default function Course() { + const { subject, number } = useParams(); + + return ; +} diff --git a/apps/frontend/src/app/Discover/Discover.module.scss b/apps/frontend/src/app/Discover/Discover.module.scss index d1f58ec77..7926a2849 100644 --- a/apps/frontend/src/app/Discover/Discover.module.scss +++ b/apps/frontend/src/app/Discover/Discover.module.scss @@ -1,19 +1,20 @@ .root { + height: 100dvh; + .body { display: flex; - gap: 48px; - padding: 48px 0; - - .sideBar { - width: 288px; - flex-shrink: 0; - } + overflow: auto; - .view { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)); + .column { + display: flex; + flex-direction: column; gap: 12px; - flex-grow: 1; + padding: 12px; + width: 320px; + + &:not(:last-child) { + border-right: 1px solid var(--border-color); + } .course { border-radius: 8px; @@ -36,27 +37,6 @@ font-size: 14px; color: var(--paragraph-color); line-height: 1.5; - margin-bottom: 12px; - } - - .row { - display: flex; - align-items: center; - gap: 12px; - margin-top: auto; - - .badge { - font-size: 14px; - line-height: 1; - height: 32px; - display: flex; - align-items: center; - gap: 8px; - color: var(--gray-500); - background-color: var(--background-color); - padding: 0 12px; - border-radius: 16px; - } } } } @@ -102,4 +82,4 @@ } } } -} \ No newline at end of file +} diff --git a/apps/frontend/src/app/Discover/index.tsx b/apps/frontend/src/app/Discover/index.tsx index 1296b56e3..97f01f1c4 100644 --- a/apps/frontend/src/app/Discover/index.tsx +++ b/apps/frontend/src/app/Discover/index.tsx @@ -1,68 +1,85 @@ -import { FormEvent, useState } from "react"; +import { FormEvent, useCallback, useMemo, useState } from "react"; -import { useApolloClient } from "@apollo/client"; -import { ArrowRight, Calendar } from "iconoir-react"; +import { useQuery } from "@apollo/client"; +import { ArrowRight } from "iconoir-react"; -import { Button, Container } from "@repo/theme"; +import { Button } from "@repo/theme"; -import AverageGrade from "@/components/AverageGrade"; import CourseDrawer from "@/components/CourseDrawer"; -import Footer from "@/components/Footer"; import NavigationBar from "@/components/NavigationBar"; -import Units from "@/components/Units"; -import { GET_COURSE, ICourse, Semester } from "@/lib/api"; +import { GET_COURSES, GetCoursesResponse, ICourse } from "@/lib/api"; import styles from "./Discover.module.scss"; import Placeholder from "./Placeholder"; -const score = { - [Semester.Fall]: 4, - [Semester.Summer]: 3, - [Semester.Spring]: 2, - [Semester.Winter]: 1, -}; +interface RawResult { + model: string; + courses: [ + { + subject: string; + number: string; + score?: number; + }, + ]; +} + +interface Result { + model: string; + courses: ICourse[]; +} export default function Discover() { const [input, setInput] = useState(""); - const [courses, setCourses] = useState([]); - const apolloClient = useApolloClient(); - const getCourse = async (name: string) => { - const [subject, courseNumber] = name.split(" "); + const { data } = useQuery(GET_COURSES); - const { data } = await apolloClient.query<{ course: ICourse }>({ - query: GET_COURSE, - variables: { subject, courseNumber }, - }); + const courses = useMemo(() => data?.courses ?? [], [data]); - if (!data) return; + const [results, setResults] = useState([]); - return data.course; - }; + const handleSubmit = useCallback( + async (event: FormEvent) => { + event.preventDefault(); - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); + try { + const response = await fetch( + `http://localhost:8000/courses?query=${encodeURIComponent(input)}` + ); - const response = await fetch( - `http://localhost:3002/query?input=${encodeURIComponent(input)}&topK=24` - ); + const data = (await response.json()) as RawResult[]; - if (!response.ok) return; + const results = data.map((result) => { + return { + ...result, + courses: result.courses + .toSorted((a, b) => { + if (a.score && b.score) { + return b.score - a.score; + } - const { matches } = (await response.json()) as { - matches: { id: string }[]; - }; + return 0; + }) + .reduce((acc, c) => { + const course = courses.find( + ({ subject, number }) => + subject.replaceAll(" ", "") === c.subject && + number === c.number + ); + + if (course) return [...acc, course]; - const courses = await Promise.all( - matches.map(({ id: name }) => getCourse(name)) - ); + return acc; + }, [] as ICourse[]), + }; + }); - setCourses( - courses.filter( - (course) => course && course.classes.length !== 0 - ) as ICourse[] - ); - }; + setResults(results); + } catch (error) { + console.error(error); + } + }, + [input, courses] + ); return (
@@ -85,65 +102,27 @@ export default function Discover() {
- -
-
-
- {courses - .sort((a, b) => { - const getTerm = (course: ICourse) => { - return [...course.classes].sort( - (a, b) => - a.year - b.year || score[a.semester] - score[b.semester] - )[0]; - }; - - return ( - getTerm(b).year - getTerm(a).year || - score[getTerm(b).semester] - score[getTerm(a).semester] - ); - }) - .map((course) => { - const { year, semester } = [...course.classes].sort( - (a, b) => - a.year - b.year || score[a.semester] - score[b.semester] - )[0]; - - const { unitsMax, unitsMin } = course.classes.reduce( - (acc, { unitsMax, unitsMin }) => ({ - unitsMax: Math.max(acc.unitsMax, unitsMax), - unitsMin: Math.min(acc.unitsMin, unitsMin), - }), - { unitsMax: 0, unitsMin: 0 } - ); - - return ( - -
-

- {course.subject} {course.number} -

-

{course.title}

-
- -
- - {semester} {year} -
- -
-
-
- ); - })} +
+ {results.map((result) => ( +
+

{result.model}

+ {result.courses.map((course, index) => ( + +
+

+ {course.subject} {course.number} +

+

{course.title}

+
+
+ ))}
-
- -
+ ))} +
); } diff --git a/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss b/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss index 55b361825..e32621815 100644 --- a/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss +++ b/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss @@ -1,16 +1,3 @@ -.overlay { - background-color: rgb(255 255 255 / 80%); - inset: 0; - position: fixed; - z-index: 988; - opacity: 0; - animation: fadeIn 250ms ease-in-out forwards; - - @media (prefers-color-scheme: dark) { - background-color: rgb(0 0 0 / 80%); - } -} - .content { height: 100dvh; position: fixed; @@ -19,8 +6,14 @@ z-index: 989; display: flex; flex-direction: column; - transform: translateX(-100%); - animation: slideIn 250ms ease-in-out forwards; + + &[data-state="open"] { + animation: slideIn 250ms ease-out; + } + + &[data-state="closed"] { + animation: slideOut 250ms ease-in; + } @media (width <= 992px) { width: 100%; @@ -52,14 +45,22 @@ } } -@keyframes fadeIn { - 100% { - opacity: 1; +@keyframes slideIn { + from { + transform: translateX(-100%); + } + + to { + transform: translateX(0); } } -@keyframes slideIn { - 100% { +@keyframes slideOut { + from { transform: translateX(0); } -} \ No newline at end of file + + to { + transform: translateX(-100%); + } +} diff --git a/apps/frontend/src/app/Plan/Term/Catalog/index.tsx b/apps/frontend/src/app/Plan/Term/Catalog/index.tsx index 504697f0a..c873db783 100644 --- a/apps/frontend/src/app/Plan/Term/Catalog/index.tsx +++ b/apps/frontend/src/app/Plan/Term/Catalog/index.tsx @@ -1,10 +1,9 @@ import { ReactNode, useState } from "react"; -import * as Dialog from "@radix-ui/react-dialog"; import { Xmark } from "iconoir-react"; import { useSearchParams } from "react-router-dom"; -import { IconButton } from "@repo/theme"; +import { Dialog, IconButton } from "@repo/theme"; import CourseBrowser from "@/components/CourseBrowser"; import { ICourse, Semester } from "@/lib/api"; @@ -48,30 +47,27 @@ export default function Catalog({ return ( {children} - - - -
- -

- Add a course to {semester} {year} -

-
- - - - - -
-
- -
-
-
+ +
+ +

+ Add a course to {semester} {year} +

+
+ + + + + +
+
+ +
+
); } diff --git a/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss b/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss index 60aa65c33..6bb641259 100644 --- a/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss +++ b/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss @@ -1,12 +1,3 @@ -.overlay { - background-color: var(--background-color); - inset: 0; - position: fixed; - z-index: 988; - opacity: 0; - animation: fadeIn 250ms ease-in-out forwards; -} - .content { height: 100dvh; position: fixed; @@ -15,14 +6,19 @@ z-index: 989; display: flex; flex-direction: column; - transform: translateX(-100%); - animation: slideIn 250ms ease-in-out forwards; + + &[data-state="open"] { + animation: slideIn 250ms ease-out; + } + + &[data-state="closed"] { + animation: slideOut 250ms ease-in; + } @media (width <= 992px) { width: 100%; } - .body { flex-grow: 1; height: 0; @@ -50,14 +46,22 @@ } } -@keyframes fadeIn { +@keyframes slideIn { + from { + transform: translateX(-100%); + } + to { - opacity: 0.75; + transform: translateX(0); } } -@keyframes slideIn { - to { +@keyframes slideOut { + from { transform: translateX(0); } -} \ No newline at end of file + + to { + transform: translateX(-100%); + } +} diff --git a/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/index.tsx b/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/index.tsx index 168071b0c..1264aff0e 100644 --- a/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/index.tsx +++ b/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/index.tsx @@ -1,13 +1,12 @@ import { ReactNode, useState } from "react"; -import * as Dialog from "@radix-ui/react-dialog"; import { Xmark } from "iconoir-react"; import { useSearchParams } from "react-router-dom"; -import { IconButton } from "@repo/theme"; +import { Dialog, IconButton } from "@repo/theme"; import ClassBrowser from "@/components/ClassBrowser"; -import { IClass, Semester } from "@/lib/api"; +import { Semester } from "@/lib/api"; import styles from "./Catalog.module.scss"; @@ -35,8 +34,12 @@ export default function Catalog({ onClassSelect, children }: CatalogProps) { setSearchParams(searchParams); }; - const handleClassSelect = (_class: IClass) => { - onClassSelect(_class.subject, _class.courseNumber, _class.number); + const handleSelect = ( + subject: string, + courseNumber: string, + number: string + ) => { + onClassSelect(subject, courseNumber, number); setOpen(false); @@ -47,27 +50,24 @@ export default function Catalog({ onClassSelect, children }: CatalogProps) { return ( {children} - - - -
- Add a course to this schedule - - - - - -
-
- -
-
-
+ +
+ Add a course to this schedule + + + + + +
+
+ +
+
); } diff --git a/apps/frontend/src/app/Schedule/Editor/index.tsx b/apps/frontend/src/app/Schedule/Editor/index.tsx index 701da7648..13b7eaf1c 100644 --- a/apps/frontend/src/app/Schedule/Editor/index.tsx +++ b/apps/frontend/src/app/Schedule/Editor/index.tsx @@ -15,7 +15,7 @@ import { Button, IconButton, MenuItem, Tooltip } from "@repo/theme"; import Week from "@/app/Schedule/Week"; import { useUpdateSchedule } from "@/hooks/api"; import useSchedule from "@/hooks/useSchedule"; -import { GET_CLASS, GetClassResponse, ISection } from "@/lib/api"; +import { ISection, READ_CLASS, ReadClassResponse } from "@/lib/api"; import { getY } from "../schedule"; import { getSelectedSections } from "../schedule"; @@ -223,8 +223,8 @@ export default function Editor() { } // Fetch the selected class - const { data } = await apolloClient.query({ - query: GET_CLASS, + const { data } = await apolloClient.query({ + query: READ_CLASS, variables: { year: schedule.year, semester: schedule.semester, diff --git a/apps/frontend/src/components/Class/Enrollment/index.tsx b/apps/frontend/src/components/Class/Enrollment/index.tsx index 11ff8bf07..69d18c80b 100644 --- a/apps/frontend/src/components/Class/Enrollment/index.tsx +++ b/apps/frontend/src/components/Class/Enrollment/index.tsx @@ -1,6 +1,3 @@ -import { useMemo } from "react"; - -import { useQuery } from "@apollo/client"; import { CartesianGrid, Line, @@ -12,9 +9,8 @@ import { YAxis, } from "recharts"; -import { GET_CLASS, IClass } from "@/lib/api"; +import useClass from "@/hooks/useClass"; -import useClass from "../useClass"; import styles from "./Enrollment.module.scss"; import Reservations from "./Reservations"; @@ -64,24 +60,7 @@ const series = [ ]; export default function Enrollment() { - const { subject, courseNumber, classNumber, semester, year } = useClass(); - - // TODO: Handle loading state - const { data } = useQuery<{ class: IClass }>(GET_CLASS, { - variables: { - term: { - semester, - year, - }, - subject, - courseNumber, - classNumber, - }, - }); - - // Because Sections will only be rendered when data loaded, we do - // not need to worry about loading or error states for right now - const _class = useMemo(() => data?.class as IClass, [data]); + const { class: _class } = useClass(); return (
diff --git a/apps/frontend/src/components/Class/Overview/index.tsx b/apps/frontend/src/components/Class/Overview/index.tsx index dc589b6e5..c731dc8ed 100644 --- a/apps/frontend/src/components/Class/Overview/index.tsx +++ b/apps/frontend/src/components/Class/Overview/index.tsx @@ -1,32 +1,10 @@ -import { useMemo } from "react"; - -import { useQuery } from "@apollo/client"; - import Details from "@/components/Details"; -import { GET_CLASS, IClass } from "@/lib/api"; +import useClass from "@/hooks/useClass"; -import useClass from "../useClass"; import styles from "./Overview.module.scss"; export default function Overview() { - const { subject, courseNumber, classNumber, semester, year } = useClass(); - - // TODO: Handle loading state - const { data } = useQuery<{ class: IClass }>(GET_CLASS, { - variables: { - term: { - semester, - year, - }, - subject, - courseNumber, - classNumber, - }, - }); - - // Because Overview will only be rendered when data loaded, we do - // not need to worry about loading or error states for right now - const _class = useMemo(() => data?.class as IClass, [data]); + const { class: _class } = useClass(); return (
diff --git a/apps/frontend/src/components/Class/Sections/index.tsx b/apps/frontend/src/components/Class/Sections/index.tsx index 09f34e748..08ec4d2f1 100644 --- a/apps/frontend/src/components/Class/Sections/index.tsx +++ b/apps/frontend/src/components/Class/Sections/index.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { useQuery } from "@apollo/client"; import classNames from "classnames"; import { FrameAltEmpty, OpenNewWindow } from "iconoir-react"; @@ -9,35 +8,18 @@ import { IconButton, Tooltip } from "@repo/theme"; import CCN from "@/components/CCN"; import Capacity from "@/components/Capacity"; import Details from "@/components/Details"; -import { Component, GET_CLASS, IClass, componentMap } from "@/lib/api"; +import useClass from "@/hooks/useClass"; +import { Component, componentMap } from "@/lib/api"; import { getExternalLink } from "@/lib/section"; -import useClass from "../useClass"; import styles from "./Sections.module.scss"; export default function Sections() { - const { subject, courseNumber, classNumber, semester, year } = useClass(); + const { class: _class } = useClass(); const viewRef = useRef(null); const [group, setGroup] = useState(null); - // TODO: Handle loading state - const { data } = useQuery<{ class: IClass }>(GET_CLASS, { - variables: { - term: { - semester, - year, - }, - subject, - courseNumber, - classNumber, - }, - }); - - // Because Sections will only be rendered when data loaded, we do - // not need to worry about loading or error states for right now - const _class = useMemo(() => data?.class as IClass, [data]); - // TODO: Include primarySection const groups = useMemo(() => { const sortedSections = _class.sections.toSorted((a, b) => @@ -152,10 +134,10 @@ export default function Sections() { (null); - -export default ClassContext; diff --git a/apps/frontend/src/components/Class/index.tsx b/apps/frontend/src/components/Class/index.tsx index ec35ccf64..c3603f56f 100644 --- a/apps/frontend/src/components/Class/index.tsx +++ b/apps/frontend/src/components/Class/index.tsx @@ -1,6 +1,5 @@ import { ReactNode, Suspense, useMemo, useState } from "react"; -import { useQuery } from "@apollo/client"; import { DialogClose } from "@radix-ui/react-dialog"; import * as Tabs from "@radix-ui/react-tabs"; import classNames from "classnames"; @@ -8,13 +7,13 @@ import { Bookmark, BookmarkSolid, CalendarPlus, - Collapse, - Expand, - OpenInWindow, + OpenBook, OpenNewWindow, + SidebarCollapse, + SidebarExpand, Xmark, } from "iconoir-react"; -import { Link, NavLink, Outlet, useSearchParams } from "react-router-dom"; +import { Link, NavLink, Outlet, useLocation } from "react-router-dom"; import { Boundary, @@ -30,7 +29,9 @@ import CCN from "@/components/CCN"; import Capacity from "@/components/Capacity"; import CourseDrawer from "@/components/CourseDrawer"; import Units from "@/components/Units"; -import { GET_CLASS, GetClassResponse, IClass, Semester } from "@/lib/api"; +import ClassContext from "@/contexts/ClassContext"; +import { useReadClass } from "@/hooks/api/classes/useReadClass"; +import { IClass, Semester } from "@/lib/api"; import { getExternalLink } from "@/lib/section"; import styles from "./Class.module.scss"; @@ -38,7 +39,6 @@ import Enrollment from "./Enrollment"; import Grades from "./Grades"; import Overview from "./Overview"; import Sections from "./Sections"; -import ClassContext from "./context"; interface BodyProps { children: ReactNode; @@ -76,62 +76,79 @@ function Root({ dialog, children }: RootProps) { ); } -interface BaseClassProps { +interface ControlledProps { + class: IClass; + year?: never; + semester?: never; + subject?: never; + courseNumber?: never; + number?: never; +} + +interface UncontrolledProps { + class?: never; year: number; semester: Semester; subject: string; courseNumber: string; - classNumber: string; - partialClass?: IClass | null; + number: string; } -interface CatalogClassProps extends BaseClassProps { +interface CatalogClassProps { dialog?: never; expanded: boolean; onExpandedChange: (expanded: boolean) => void; onClose: () => void; } -interface DialogClassProps extends BaseClassProps { +interface DialogClassProps { dialog: true; expanded?: never; onExpandedChange?: never; onClose?: never; } -type ClassProps = CatalogClassProps | DialogClassProps; +type ClassProps = (CatalogClassProps | DialogClassProps) & + (ControlledProps | UncontrolledProps); export default function Class({ year, semester, subject, courseNumber, - classNumber, - partialClass, + number, + class: providedClass, expanded, onExpandedChange, onClose, dialog, }: ClassProps) { - const [searchParams] = useSearchParams(); + const location = useLocation(); + + // TODO: Bookmarks const [bookmarked, setBookmarked] = useState(false); - const { data, loading } = useQuery(GET_CLASS, { - variables: { - term: { - semester, - year, - }, - subject, - courseNumber, - classNumber, - }, - }); + const { data, loading } = useReadClass( + year as number, + semester as Semester, + subject as string, + courseNumber as string, + number as string, + { + // Allow class to be provided + skip: !!providedClass, + } + ); + + const _class = useMemo(() => providedClass ?? data, [data, providedClass]); - // TODO: Properly type a partial IClass - const _class = useMemo(() => (data?.class ?? partialClass) as IClass, [data]); + if (loading) { + return <>; + } - const search = useMemo(() => searchParams.toString(), [searchParams]); + if (!_class) { + return <>; + } return ( @@ -142,7 +159,7 @@ export default function Class({ {!dialog && ( onExpandedChange(!expanded)}> - {expanded ? : } + {expanded ? : } )} @@ -165,21 +182,35 @@ export default function Class({
- - - - + {dialog ? ( + + + - + ) : ( + + + + + + + + )} + {dialog && ( + + + + + + + + )} {dialog ? ( @@ -199,8 +242,8 @@ export default function Class({ onClose()} > @@ -211,7 +254,7 @@ export default function Class({

- {subject} {courseNumber} #{classNumber} + {_class.subject} {_class.courseNumber} {_class.number}

{_class.title || _class.course.title} @@ -244,22 +287,22 @@ export default function Class({ ) : (

- + {({ isActive }) => ( Overview )} - + {({ isActive }) => ( Sections )} - + {({ isActive }) => ( Enrollment )} - + {({ isActive }) => ( Grades )} @@ -269,38 +312,26 @@ export default function Class({
- {data ? ( - - - - - - - - - - - - - - - - - ) : loading ? ( - <>{/* TODO: Loading */} - ) : ( - <>{/* TODO: Error */} - )} + + + + + + + + + + + + + + + + ); diff --git a/apps/frontend/src/components/ClassBrowser/List/index.tsx b/apps/frontend/src/components/ClassBrowser/List/index.tsx index 3657d7f8c..7687ab1bd 100644 --- a/apps/frontend/src/components/ClassBrowser/List/index.tsx +++ b/apps/frontend/src/components/ClassBrowser/List/index.tsx @@ -6,18 +6,16 @@ import { Link, useSearchParams } from "react-router-dom"; import { LoadingIndicator } from "@repo/theme"; -import { IClass } from "@/lib/api"; - import Header from "../Header"; import useBrowser from "../useBrowser"; import Class from "./Class"; import styles from "./List.module.scss"; interface ListProps { - onClassSelect: (_class: IClass) => void; + onSelect: (subject: string, courseNumber: string, number: string) => void; } -export default function List({ onClassSelect }: ListProps) { +export default function List({ onSelect }: ListProps) { const { classes, loading } = useBrowser(); const rootRef = useRef(null); @@ -85,7 +83,9 @@ export default function List({ onClassSelect }: ListProps) { index={index} key={key} ref={virtualizer.measureElement} - onClick={() => onClassSelect(_class)} + onClick={() => + onSelect(_class.subject, _class.courseNumber, _class.number) + } /> ); })} diff --git a/apps/frontend/src/components/ClassBrowser/index.tsx b/apps/frontend/src/components/ClassBrowser/index.tsx index 2682c71af..25222805c 100644 --- a/apps/frontend/src/components/ClassBrowser/index.tsx +++ b/apps/frontend/src/components/ClassBrowser/index.tsx @@ -26,7 +26,7 @@ import { import BrowserContext from "./browserContext"; interface ClassBrowserProps { - onClassSelect: (_class: IClass) => void; + onSelect: (subject: string, courseNumber: string, number: string) => void; responsive?: boolean; semester: Semester; year: number; @@ -34,7 +34,7 @@ interface ClassBrowserProps { } export default function ClassBrowser({ - onClassSelect, + onSelect, responsive = true, semester: currentSemester, year: currentYear, @@ -328,7 +328,7 @@ export default function ClassBrowser({ })} > - + ); diff --git a/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss b/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss index 3ff1108a2..bd6fbf9ce 100644 --- a/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss +++ b/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss @@ -1,16 +1,3 @@ -.overlay { - background-color: rgb(255 255 255 / 80%); - inset: 0; - position: fixed; - z-index: 988; - opacity: 0; - animation: fadeIn 250ms ease-in-out forwards; - - @media (prefers-color-scheme: dark) { - background-color: rgb(0 0 0 / 80%); - } -} - .content { height: 100dvh; position: fixed; @@ -22,47 +9,36 @@ z-index: 989; display: flex; flex-direction: column; - transform: translateX(100%); - animation: slideIn 250ms ease-in-out forwards; - @media (width <= 992px) { - width: 100%; + &[data-state="open"] { + animation: slideIn 250ms ease-out; } - .body { - flex-grow: 1; - height: 0; - overflow: clip; + &[data-state="closed"] { + animation: slideOut 250ms ease-in; } - .header { - border-width: 0 1px 1px 0; - border-color: var(--border-color); - border-style: solid; - padding: 12px; - background-color: var(--foreground-color); - display: flex; - align-items: center; - flex-shrink: 0; - justify-content: space-between; - - .title { - font-size: 14px; - line-height: 1; - color: var(--heading-color); - font-weight: 500; - } + @media (width <= 992px) { + width: 100%; } } -@keyframes fadeIn { - 100% { - opacity: 1; +@keyframes slideIn { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); } } -@keyframes slideIn { - 100% { +@keyframes slideOut { + from { transform: translateX(0); } -} \ No newline at end of file + + to { + transform: translateX(100%); + } +} diff --git a/apps/frontend/src/components/ClassDrawer/index.tsx b/apps/frontend/src/components/ClassDrawer/index.tsx index 0b9509d02..083dfe1c1 100644 --- a/apps/frontend/src/components/ClassDrawer/index.tsx +++ b/apps/frontend/src/components/ClassDrawer/index.tsx @@ -1,131 +1,65 @@ -import { ReactNode, forwardRef } from "react"; +import { ReactNode } from "react"; -import { useQuery } from "@apollo/client"; -import { - Content, - DialogTriggerProps, - Overlay, - Portal, - Root, - Trigger, -} from "@radix-ui/react-dialog"; +import { Dialog } from "@repo/theme"; import Class from "@/components/Class"; -import { GET_CLASS, GetClassResponse, IClass, Semester } from "@/lib/api"; +import { Semester } from "@/lib/api"; import styles from "./ClassDrawer.module.scss"; interface Props { subject: string; courseNumber: string; - classNumber: string; + number: string; semester: Semester; year: number; - partialClass?: IClass | null; + dialog?: boolean; } -function Body({ - subject, - courseNumber, - classNumber, - semester, - year, - partialClass, -}: Props) { - const { data, loading } = useQuery(GET_CLASS, { - variables: { - subject, - courseNumber, - classNumber, - term: { - semester, - year, - }, - }, - }); - - return data ? ( - - ) : loading ? ( - <> - ) : ( - <> - ); -} - -interface BaseClassDrawerProps extends Props { +interface Props { onOpenChange?: (open: boolean) => void; } -export interface DefaultClassDrawerProps extends BaseClassDrawerProps { +interface ControlledProps extends Props { open?: boolean; children: ReactNode; } -export interface ControlledClassDrawerProps extends BaseClassDrawerProps { +interface UncontrolledProps extends Props { open: boolean; children?: never; } -export type ClassDrawerProps = - | ControlledClassDrawerProps - | DefaultClassDrawerProps; - -// TODO: Determine if this component would be better suited in the global context -const ClassDrawer = forwardRef< - HTMLButtonElement, - ClassDrawerProps & Omit ->( - ( - { - subject, - children, - courseNumber, - classNumber, - semester, - year, - partialClass, - open, - onOpenChange, - ...props - }, - ref - ) => { - return ( - - {children && ( - - {children} - - )} - - - event.preventDefault()} - > - - - - - ); - } -); +export type ClassDrawerProps = ControlledProps | UncontrolledProps; -export default ClassDrawer; +export default function ClassDrawer({ + subject, + children, + courseNumber, + number, + semester, + year, + open, + onOpenChange, + ...props +}: ClassDrawerProps) { + return ( + + {children && ( + + {children} + + )} + + + + + ); +} diff --git a/apps/frontend/src/components/Course/Classes/index.tsx b/apps/frontend/src/components/Course/Classes/index.tsx index e4d0404cd..db77d5669 100644 --- a/apps/frontend/src/components/Course/Classes/index.tsx +++ b/apps/frontend/src/components/Course/Classes/index.tsx @@ -1,3 +1,18 @@ +import useCourse from "@/hooks/useCourse"; + export default function Classes() { - return <>; + const { course } = useCourse(); + + return ( +
+ {course.classes.map((_class, index) => ( +
+

+ {_class.year} {_class.semester} +

+

{_class.title}

+
+ ))} +
+ ); } diff --git a/apps/frontend/src/components/Course/Overview/index.tsx b/apps/frontend/src/components/Course/Overview/index.tsx index 0bf1204e9..9c4e51835 100644 --- a/apps/frontend/src/components/Course/Overview/index.tsx +++ b/apps/frontend/src/components/Course/Overview/index.tsx @@ -1,3 +1,12 @@ +import useCourse from "@/hooks/useCourse"; + export default function Overview() { - return <>; + const { course } = useCourse(); + + return ( +
+

{course.title}

+

{course.description}

+
+ ); } diff --git a/apps/frontend/src/components/Course/index.tsx b/apps/frontend/src/components/Course/index.tsx index 21a23a4a1..aa6986729 100644 --- a/apps/frontend/src/components/Course/index.tsx +++ b/apps/frontend/src/components/Course/index.tsx @@ -1,15 +1,21 @@ import { ReactNode, Suspense, useMemo, useState } from "react"; import { useQuery } from "@apollo/client"; -import { DialogClose } from "@radix-ui/react-dialog"; import * as Tabs from "@radix-ui/react-tabs"; import classNames from "classnames"; -import { Bookmark, BookmarkSolid, CalendarPlus, Xmark } from "iconoir-react"; -import { NavLink, Outlet, useSearchParams } from "react-router-dom"; +import { + Bookmark, + BookmarkSolid, + Expand, + GridPlus, + Xmark, +} from "iconoir-react"; +import { Link, NavLink, Outlet, useLocation } from "react-router-dom"; import { Boundary, Container, + Dialog, IconButton, LoadingIndicator, MenuItem, @@ -17,6 +23,7 @@ import { } from "@repo/theme"; import AverageGrade from "@/components/AverageGrade"; +import CourseContext from "@/contexts/CourseContext"; import { GET_COURSE, GetCourseResponse, ICourse } from "@/lib/api"; import styles from "./Class.module.scss"; @@ -24,7 +31,6 @@ import Classes from "./Classes"; import Enrollment from "./Enrollment"; import Grades from "./Grades"; import Overview from "./Overview"; -import ClassContext from "./context"; interface BodyProps { children: ReactNode; @@ -62,20 +68,33 @@ function Root({ dialog, children }: RootProps) { ); } -interface CourseProps { +interface BaseProps { + dialog?: boolean; +} + +interface ControlledProps extends BaseProps { + course: ICourse; + subject?: never; + number?: never; +} + +interface UncontrolledProps extends BaseProps { + course?: never; subject: string; number: string; - partialCourse?: ICourse | null; - dialog?: boolean; } +export type CourseProps = ControlledProps | UncontrolledProps; + export default function Course({ subject, number, - partialCourse, dialog, + course: providedCourse, }: CourseProps) { - const [searchParams] = useSearchParams(); + const location = useLocation(); + + // TODO: Bookmarks const [bookmarked, setBookmarked] = useState(false); const { data, loading } = useQuery(GET_COURSE, { @@ -83,16 +102,23 @@ export default function Course({ subject, number, }, + // Allow course to be provided + skip: !!providedCourse, }); - // TODO: Properly type a partial ICourse - const course = useMemo( - () => (data?.course ?? partialCourse) as ICourse, - [data] - ); + const course = useMemo(() => providedCourse ?? data?.course, [data]); + + // TODO: Loading state + if (loading) { + return <>; + } - const search = useMemo(() => searchParams.toString(), [searchParams]); + // TODO: Error state + if (!course) { + return <>; + } + // TODO: Differentiate between class and course return (
@@ -111,36 +137,27 @@ export default function Course({ {bookmarked ? : } - + - +
- {/* - - - - */} + {dialog && ( + + + + + + )} {dialog && ( - + - + )}
@@ -169,22 +186,22 @@ export default function Course({ ) : (
- + {({ isActive }) => ( Overview )} - + {({ isActive }) => ( Classes )} - + {({ isActive }) => ( Enrollment )} - + {({ isActive }) => ( Grades )} @@ -194,35 +211,26 @@ export default function Course({
- {data ? ( - - - - - - - - - - - - - - - - - ) : loading ? ( - <>{/* TODO: Loading */} - ) : ( - <>{/* TODO: Error */} - )} + + + + + + + + + + + + + + + +
); diff --git a/apps/frontend/src/components/CourseBrowser/index.tsx b/apps/frontend/src/components/CourseBrowser/index.tsx index e3e9391a7..25ae2dd1b 100644 --- a/apps/frontend/src/components/CourseBrowser/index.tsx +++ b/apps/frontend/src/components/CourseBrowser/index.tsx @@ -54,7 +54,7 @@ export default function CourseBrowser({ const { data, loading } = useQuery(GET_COURSES); - const courses = useMemo(() => data?.courseList ?? [], [data?.courseList]); + const courses = useMemo(() => data?.courses ?? [], [data?.courses]); const currentQuery = useMemo( () => (persistent ? (searchParams.get("query") ?? "") : localQuery), @@ -191,7 +191,7 @@ export default function CourseBrowser({ onOpenChange={setOpen} persistent={persistent} // API response - loading={loading && !data?.courseList} + loading={loading && courses.length === 0} // Manage courses onSelect={onSelect} currentCourses={currentCourses} diff --git a/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss b/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss index 3ff1108a2..f4189d55c 100644 --- a/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss +++ b/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss @@ -1,16 +1,3 @@ -.overlay { - background-color: rgb(255 255 255 / 80%); - inset: 0; - position: fixed; - z-index: 988; - opacity: 0; - animation: fadeIn 250ms ease-in-out forwards; - - @media (prefers-color-scheme: dark) { - background-color: rgb(0 0 0 / 80%); - } -} - .content { height: 100dvh; position: fixed; @@ -22,47 +9,36 @@ z-index: 989; display: flex; flex-direction: column; - transform: translateX(100%); - animation: slideIn 250ms ease-in-out forwards; - @media (width <= 992px) { - width: 100%; + &[data-state="open"] { + animation: slideIn 250ms ease-in; } - .body { - flex-grow: 1; - height: 0; - overflow: clip; + &[data-state="closed"] { + animation: slideOut 250ms ease-out; } - .header { - border-width: 0 1px 1px 0; - border-color: var(--border-color); - border-style: solid; - padding: 12px; - background-color: var(--foreground-color); - display: flex; - align-items: center; - flex-shrink: 0; - justify-content: space-between; - - .title { - font-size: 14px; - line-height: 1; - color: var(--heading-color); - font-weight: 500; - } + @media (width <= 992px) { + width: 100%; } } -@keyframes fadeIn { - 100% { - opacity: 1; +@keyframes slideIn { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); } } -@keyframes slideIn { - 100% { +@keyframes slideOut { + from { transform: translateX(0); } -} \ No newline at end of file + + to { + transform: translateX(100%); + } +} diff --git a/apps/frontend/src/components/CourseDrawer/index.tsx b/apps/frontend/src/components/CourseDrawer/index.tsx index d8ab7e5df..30ac34872 100644 --- a/apps/frontend/src/components/CourseDrawer/index.tsx +++ b/apps/frontend/src/components/CourseDrawer/index.tsx @@ -1,99 +1,41 @@ -import { ReactNode, forwardRef } from "react"; +import { ReactNode } from "react"; -import { useQuery } from "@apollo/client"; -import { - Content, - DialogTriggerProps, - Overlay, - Portal, - Root, - Trigger, -} from "@radix-ui/react-dialog"; - -import Course from "@/components/Course"; -import { GET_COURSE, GetCourseResponse, ICourse } from "@/lib/api"; +import { Dialog } from "@repo/theme"; +import Course from "../Course"; import styles from "./CourseDrawer.module.scss"; interface Props { subject: string; number: string; - partialCourse?: ICourse | null; -} - -function Body({ subject, number }: Props) { - const { data, loading } = useQuery(GET_COURSE, { - variables: { - subject, - courseNumber: number, - }, - }); - - return data ? ( - - ) : loading ? ( - <> - ) : ( - <> - ); -} - -interface BaseCourseDrawerProps extends Props { onOpenChange?: (open: boolean) => void; } -export interface ControlledCourseDrawerProps extends BaseCourseDrawerProps { +export interface ControlledProps extends Props { open: boolean; children?: never; } -export interface DefaultCourseDrawerProps extends BaseCourseDrawerProps { +export interface UncontrolledProps extends Props { open?: boolean; children: ReactNode; } -export type CourseDrawerProps = - | ControlledCourseDrawerProps - | DefaultCourseDrawerProps; - -// TODO: Determine if this component would be better suited in the global context -const CourseDrawer = forwardRef< - HTMLButtonElement, - CourseDrawerProps & Omit ->( - ( - { subject, children, number, partialCourse, open, onOpenChange, ...props }, - ref - ) => { - return ( - - {children && ( - - {children} - - )} - - - event.preventDefault()} - > - - - - - ); - } -); - -export default CourseDrawer; +export type CourseDrawerProps = ControlledProps | UncontrolledProps; + +export default function CourseDrawer({ + subject, + children, + number, + open, + onOpenChange, +}: CourseDrawerProps) { + return ( + + {children && {children}} + + + + + ); +} diff --git a/apps/frontend/src/contexts/ClassContext.ts b/apps/frontend/src/contexts/ClassContext.ts new file mode 100644 index 000000000..6cb8122b3 --- /dev/null +++ b/apps/frontend/src/contexts/ClassContext.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; + +import { IClass } from "@/lib/api"; + +export interface ClassContextType { + class: IClass; +} + +const ClassContext = createContext(null); + +export default ClassContext; diff --git a/apps/frontend/src/components/Course/context.ts b/apps/frontend/src/contexts/CourseContext.ts similarity index 70% rename from apps/frontend/src/components/Course/context.ts rename to apps/frontend/src/contexts/CourseContext.ts index 9ae88d4fa..a3c84f14f 100644 --- a/apps/frontend/src/components/Course/context.ts +++ b/apps/frontend/src/contexts/CourseContext.ts @@ -3,10 +3,7 @@ import { createContext } from "react"; import { ICourse } from "@/lib/api"; export interface CourseContextType { - subject: string; - number: string; - partialCourse?: ICourse | null; - dialog?: boolean; + course: ICourse; } const CourseContext = createContext(null); diff --git a/apps/frontend/src/hooks/api/classes/useReadClass.ts b/apps/frontend/src/hooks/api/classes/useReadClass.ts new file mode 100644 index 000000000..f6e44a4ce --- /dev/null +++ b/apps/frontend/src/hooks/api/classes/useReadClass.ts @@ -0,0 +1,28 @@ +import { QueryHookOptions, useQuery } from "@apollo/client"; + +import { READ_CLASS, ReadClassResponse, Semester } from "@/lib/api"; + +export const useReadClass = ( + year: number, + semester: Semester, + subject: string, + courseNumber: string, + number: string, + options?: Omit, "variables"> +) => { + const query = useQuery(READ_CLASS, { + ...options, + variables: { + year, + semester, + subject, + courseNumber, + number, + }, + }); + + return { + ...query, + data: query.data?.class, + }; +}; diff --git a/apps/frontend/src/hooks/api/index.ts b/apps/frontend/src/hooks/api/index.ts index 7e6e7b459..b0a50d4eb 100644 --- a/apps/frontend/src/hooks/api/index.ts +++ b/apps/frontend/src/hooks/api/index.ts @@ -1 +1,2 @@ export * from "./schedules"; +export * from "./terms"; diff --git a/apps/frontend/src/hooks/api/terms/index.ts b/apps/frontend/src/hooks/api/terms/index.ts new file mode 100644 index 000000000..dde85958f --- /dev/null +++ b/apps/frontend/src/hooks/api/terms/index.ts @@ -0,0 +1 @@ +export * from "./useReadTerms"; diff --git a/apps/frontend/src/hooks/api/terms/useReadTerms.ts b/apps/frontend/src/hooks/api/terms/useReadTerms.ts new file mode 100644 index 000000000..72765daa6 --- /dev/null +++ b/apps/frontend/src/hooks/api/terms/useReadTerms.ts @@ -0,0 +1,14 @@ +import { QueryHookOptions, useQuery } from "@apollo/client"; + +import { READ_TERMS, ReadTermsResponse } from "@/lib/api"; + +export const useReadTerms = ( + options?: Omit, "variables"> +) => { + const query = useQuery(READ_TERMS, options); + + return { + ...query, + data: query.data?.terms, + }; +}; diff --git a/apps/frontend/src/components/Class/useClass.ts b/apps/frontend/src/hooks/useClass.ts similarity index 83% rename from apps/frontend/src/components/Class/useClass.ts rename to apps/frontend/src/hooks/useClass.ts index 456dc9229..75a7d5b11 100644 --- a/apps/frontend/src/components/Class/useClass.ts +++ b/apps/frontend/src/hooks/useClass.ts @@ -1,6 +1,6 @@ import { useContext } from "react"; -import ClassContext from "./context"; +import ClassContext from "@/contexts/ClassContext"; const useClass = () => { const classContext = useContext(ClassContext); diff --git a/apps/frontend/src/components/Course/useCourse.ts b/apps/frontend/src/hooks/useCourse.ts similarity index 58% rename from apps/frontend/src/components/Course/useCourse.ts rename to apps/frontend/src/hooks/useCourse.ts index d37291901..771f91483 100644 --- a/apps/frontend/src/components/Course/useCourse.ts +++ b/apps/frontend/src/hooks/useCourse.ts @@ -1,12 +1,13 @@ import { useContext } from "react"; -import CourseContext from "./context"; +import CoursePageContext from "@/contexts/CourseContext"; const useCourse = () => { - const courseContext = useContext(CourseContext); + const courseContext = useContext(CoursePageContext); - if (!courseContext) + if (!courseContext) { throw new Error("useCourse must be used within a CourseContext.Provider"); + } return courseContext; }; diff --git a/apps/frontend/src/lib/api/class.ts b/apps/frontend/src/lib/api/classes.ts similarity index 97% rename from apps/frontend/src/lib/api/class.ts rename to apps/frontend/src/lib/api/classes.ts index c1e32dcfb..8d84070ac 100644 --- a/apps/frontend/src/lib/api/class.ts +++ b/apps/frontend/src/lib/api/classes.ts @@ -1,7 +1,7 @@ import { gql } from "@apollo/client"; -import { ICourse } from "../api"; -import { ITerm, Semester } from "./term"; +import { ICourse } from "."; +import { ITerm, Semester } from "./terms"; export enum InstructionMethod { Unknown = "UNK", @@ -205,11 +205,11 @@ export interface IClass { unitsMin: number; } -export interface GetClassResponse { +export interface ReadClassResponse { class: IClass; } -export const GET_CLASS = gql` +export const READ_CLASS = gql` query GetClass( $year: Int! $semester: Semester! diff --git a/apps/frontend/src/lib/api/course.ts b/apps/frontend/src/lib/api/courses.ts similarity index 90% rename from apps/frontend/src/lib/api/course.ts rename to apps/frontend/src/lib/api/courses.ts index 37f6f3bcb..1681b66a3 100644 --- a/apps/frontend/src/lib/api/course.ts +++ b/apps/frontend/src/lib/api/courses.ts @@ -1,7 +1,7 @@ import { gql } from "@apollo/client"; -import { AcademicCareer, IClass, InstructionMethod } from "../api"; -import { Semester } from "./term"; +import { AcademicCareer, IClass, InstructionMethod } from "."; +import { Semester } from "./terms"; export interface ICourse { // Identifiers @@ -33,7 +33,7 @@ export interface GetCourseResponse { export const GET_COURSE = gql` query GetCourse($subject: String!, $number: String!) { - course(subject: $subject, number: $courseNumber) { + course(subject: $subject, number: $number) { subject number title @@ -57,12 +57,12 @@ export const GET_COURSE = gql` `; export interface GetCoursesResponse { - courseList: ICourse[]; + courses: ICourse[]; } export const GET_COURSES = gql` query GetCourses { - courseList { + courses { subject number title diff --git a/apps/frontend/src/lib/api/index.ts b/apps/frontend/src/lib/api/index.ts index d1a1b079a..d00fc5c80 100644 --- a/apps/frontend/src/lib/api/index.ts +++ b/apps/frontend/src/lib/api/index.ts @@ -1,5 +1,5 @@ -export * from "./class"; -export * from "./user"; -export * from "./course"; -export * from "./term"; -export * from "./schedule"; +export * from "./classes"; +export * from "./users"; +export * from "./courses"; +export * from "./terms"; +export * from "./schedules"; diff --git a/apps/frontend/src/lib/api/schedule.ts b/apps/frontend/src/lib/api/schedules.ts similarity index 98% rename from apps/frontend/src/lib/api/schedule.ts rename to apps/frontend/src/lib/api/schedules.ts index 02ad0fede..f91eba960 100644 --- a/apps/frontend/src/lib/api/schedule.ts +++ b/apps/frontend/src/lib/api/schedules.ts @@ -1,7 +1,7 @@ import { gql } from "@apollo/client"; import { IClass } from "../api"; -import { ITerm, Semester } from "./term"; +import { ITerm, Semester } from "./terms"; export type ScheduleIdentifier = string & { readonly __brand: unique symbol; diff --git a/apps/frontend/src/lib/api/term.ts b/apps/frontend/src/lib/api/terms.ts similarity index 88% rename from apps/frontend/src/lib/api/term.ts rename to apps/frontend/src/lib/api/terms.ts index f36436472..d01c66fb2 100644 --- a/apps/frontend/src/lib/api/term.ts +++ b/apps/frontend/src/lib/api/terms.ts @@ -29,11 +29,11 @@ export interface ITerm { endDate?: string; } -export interface GetTermsResponse { +export interface ReadTermsResponse { terms: ITerm[]; } -export const GET_TERMS = gql` +export const READ_TERMS = gql` query GetTerms { terms { year @@ -51,11 +51,11 @@ export const GET_TERMS = gql` } `; -export interface GetTermResponse { +export interface ReadTermResponse { term: ITerm; } -export const GET_TERM = gql` +export const READ_TERM = gql` query GetTerm($year: Int!, $semester: Semester!) { term(year: $year, semester: $semester) { year diff --git a/apps/frontend/src/lib/api/user.ts b/apps/frontend/src/lib/api/users.ts similarity index 100% rename from apps/frontend/src/lib/api/user.ts rename to apps/frontend/src/lib/api/users.ts diff --git a/docker-compose.yml b/docker-compose.yml index 760d8ca5e..7b086f350 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: restart: always volumes: - ./apps/frontend/src:/frontend/apps/frontend/src + - ./packages/theme/src:/frontend/packages/theme/src - ./apps/frontend/public:/frontend/apps/frontend/public mongodb: image: mongo:5 diff --git a/packages/theme/src/components/Dialog/Dialog.module.scss b/packages/theme/src/components/Dialog/Dialog.module.scss index c01cb24df..5cf7821a2 100644 --- a/packages/theme/src/components/Dialog/Dialog.module.scss +++ b/packages/theme/src/components/Dialog/Dialog.module.scss @@ -34,11 +34,11 @@ body:not([data-theme]) { z-index: 988; &[data-state="open"] { - animation: fadeIn 250ms ease-in-out forwards; + animation: fadeIn 250ms ease-in; } &[data-state="closed"] { - animation: fadeOut 250ms ease-in-out forwards; + animation: fadeOut 250ms ease-out; } } @@ -48,21 +48,21 @@ body:not([data-theme]) { } @keyframes fadeIn { - 0% { + from { opacity: 0; } - 100% { + to { opacity: 1; } } @keyframes fadeOut { - 0% { + from { opacity: 1; } - 100% { + to { opacity: 0; } } diff --git a/packages/theme/src/components/ThemeProvider/index.tsx b/packages/theme/src/components/ThemeProvider/index.tsx index 158578c67..6db3c337a 100644 --- a/packages/theme/src/components/ThemeProvider/index.tsx +++ b/packages/theme/src/components/ThemeProvider/index.tsx @@ -32,12 +32,12 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
- + {children} diff --git a/packages/theme/src/components/index.ts b/packages/theme/src/components/index.ts index ab5880d31..35311bded 100644 --- a/packages/theme/src/components/index.ts +++ b/packages/theme/src/components/index.ts @@ -6,3 +6,4 @@ export * from "./LoadingIndicator"; export * from "./MenuItem"; export * from "./ThemeProvider"; export * from "./Tooltip"; +export * from "./Dialog";