diff --git a/apps/frontend/src/app/Catalog/Dashboard/index.tsx b/apps/frontend/src/app/Catalog/Dashboard/index.tsx index c60557856..5bd163b2b 100644 --- a/apps/frontend/src/app/Catalog/Dashboard/index.tsx +++ b/apps/frontend/src/app/Catalog/Dashboard/index.tsx @@ -1,5 +1,13 @@ -import { Dispatch, SetStateAction, useMemo } from "react"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { useApolloClient } from "@apollo/client"; import { ArrowSeparateVertical, BookmarkSolid, @@ -14,9 +22,11 @@ import { Button, Container, IconButton, Tooltip } from "@repo/theme"; import { DropdownMenu } from "@repo/theme"; import Carousel from "@/components/Carousel"; +import ClassCard from "@/components/ClassCard"; +import ClassDrawer from "@/components/ClassDrawer"; import { useReadUser } from "@/hooks/api"; -import { ITerm } from "@/lib/api"; -import { sortDescendingTerm } from "@/lib/classes"; +import { IClass, ITerm, READ_CLASS, ReadClassResponse } from "@/lib/api"; +import { sortByTermDescending } from "@/lib/classes"; import { getRecentClasses } from "@/lib/recent-classes"; import styles from "./Dashboard.module.scss"; @@ -37,18 +47,57 @@ export default function Dashboard({ setOpen, }: DashboardProps) { const navigate = useNavigate(); - const { data: user, loading: userLoading } = useReadUser(); + const client = useApolloClient(); + + const { data: user } = useReadUser(); - const recentClasses = useMemo( + const bookmarkedClasses = useMemo( () => - getRecentClasses().filter( - (recentClass) => - recentClass.semester === term.semester && - recentClass.year === term.year + user?.bookmarkedClasses.filter( + (bookmarkedClass) => + bookmarkedClass.year === term.year && + bookmarkedClass.semester === term.semester ), - [term] + [term, user] ); + const [recentClasses, setRecentClasses] = useState([]); + + const initialize = useCallback(async () => { + const recentClasses = getRecentClasses(); + + const responses = await Promise.all( + recentClasses.map(async (recentClass) => { + const { subject, year, semester, courseNumber, number } = recentClass; + + try { + const response = await client.query({ + query: READ_CLASS, + variables: { + subject, + year, + semester, + courseNumber, + number, + }, + }); + + return response.data.class; + } catch { + // TODO: Handle errors + + return; + } + }) + ); + + setRecentClasses(responses.filter((response) => !!response)); + }, [client]); + + useEffect(() => { + initialize(); + }, [initialize]); + return (
@@ -69,7 +118,7 @@ export default function Dashboard({ (term) => term.semester === semester && term.year === year ) ) - .toSorted(sortDescendingTerm((d) => d)) + .toSorted(sortByTermDescending) .map(({ year, semester }) => { return ( } to="/account" > - {userLoading || !user ? ( + {/* TODO: Better placeholder states */} + {!bookmarkedClasses ? (
Sign in to bookmark classes
- ) : user?.bookmarkedClasses.length == 0 ? ( + ) : bookmarkedClasses.length === 0 ? (
No bookmarked classes
) : ( - user?.bookmarkedClasses - .filter( - (bookmarkedClass) => - bookmarkedClass.year === term.year && - bookmarkedClass.semester === term.semester - ) - .map((bookmarkedClass, i) => { - return ( - - ); - }) + bookmarkedClasses.map((bookmarkedClass, index) => ( + + + + + + )) )} - {recentClasses.length !== 0 && ( + {recentClasses.length > 0 && ( }> - {recentClasses.map( - ({ subject, year, semester, courseNumber, number }, i) => { - return ( - - ); - } - )} + {recentClasses.map((recentClass, index) => ( + + + + + + ))} )}
diff --git a/apps/frontend/src/components/Capacity/index.tsx b/apps/frontend/src/components/Capacity/index.tsx index 1913fd04d..0aeca46ea 100644 --- a/apps/frontend/src/components/Capacity/index.tsx +++ b/apps/frontend/src/components/Capacity/index.tsx @@ -5,9 +5,10 @@ import { Tooltip } from "radix-ui"; import styles from "./Capacity.module.scss"; -const getColor = (count: number | undefined, capacity: number | undefined) => { - if (count === undefined || capacity === undefined) +const getColor = (count?: number, capacity?: number) => { + if (typeof count !== "number" || typeof capacity !== "number") return "var(--paragraph-color)"; + const percentage = count / capacity; return percentage >= 0.75 @@ -18,10 +19,10 @@ const getColor = (count: number | undefined, capacity: number | undefined) => { }; interface CapacityProps { - enrolledCount: number | undefined; - maxEnroll: number | undefined; - waitlistedCount: number | undefined; - maxWaitlist: number | undefined; + enrolledCount?: number; + maxEnroll?: number; + waitlistedCount?: number; + maxWaitlist?: number; } export default function Capacity({ diff --git a/apps/frontend/src/components/Carousel/Carousel.module.scss b/apps/frontend/src/components/Carousel/Carousel.module.scss index ad59ef258..44f0f22db 100644 --- a/apps/frontend/src/components/Carousel/Carousel.module.scss +++ b/apps/frontend/src/components/Carousel/Carousel.module.scss @@ -1,3 +1,8 @@ +.item { + flex-shrink: 0; + width: 288px; +} + .root { display: flex; flex-direction: column; diff --git a/apps/frontend/src/components/Carousel/Class/Class.module.scss b/apps/frontend/src/components/Carousel/Class/Class.module.scss deleted file mode 100644 index 92a853e6d..000000000 --- a/apps/frontend/src/components/Carousel/Class/Class.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -.cont { - flex-shrink: 0; - width: 320px; - gap: 16px; - cursor: pointer; -} \ No newline at end of file diff --git a/apps/frontend/src/components/Carousel/Class/index.tsx b/apps/frontend/src/components/Carousel/Class/index.tsx deleted file mode 100644 index d36dcee0f..000000000 --- a/apps/frontend/src/components/Carousel/Class/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import ClassCard from "@/components/ClassCard"; -import { Semester } from "@/lib/api"; - -import styles from "./Class.module.scss"; - -interface ClassProps { - year: number; - semester: Semester; - subject: string; - courseNumber: string; - number: string; -} - -export default function Class({ - year, - semester, - subject, - courseNumber, - number, -}: ClassProps) { - return ( -
- -
- ); - // return
- // - //
- // const { data, loading } = useReadClass( - // year as number, - // semester as Semester, - // subject as string, - // courseNumber as string, - // number as string - // ); - - // const _class = useMemo(() => data, [data]); - - // return loading || !_class ? ( - // <> - // ) : ( - // - //
- //
- //

- // {_class.subject} {_class.courseNumber} #{_class.number} - //

- //

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

- //
- // - // - // - //
- //
- //
- //
- // - //
- //
- //
- //
- // ); -} diff --git a/apps/frontend/src/components/Carousel/index.tsx b/apps/frontend/src/components/Carousel/index.tsx index 5c31476b6..28426b240 100644 --- a/apps/frontend/src/components/Carousel/index.tsx +++ b/apps/frontend/src/components/Carousel/index.tsx @@ -1,4 +1,10 @@ -import { ReactNode, UIEvent, useRef, useState } from "react"; +import { + ComponentPropsWithRef, + ReactNode, + UIEvent, + useRef, + useState, +} from "react"; import classNames from "classnames"; import { ArrowRight, NavArrowLeft, NavArrowRight } from "iconoir-react"; @@ -7,7 +13,10 @@ import { Link, To } from "react-router-dom"; import { IconButton } from "@repo/theme"; import styles from "./Carousel.module.scss"; -import Class from "./Class"; + +function Item({ className, ...props }: ComponentPropsWithRef<"div">) { + return
; +} interface RootProps { title: string; @@ -101,7 +110,7 @@ function Root({ title, Icon, children, to }: RootProps) { const Carousel = { Root, - Class, + Item, }; export default Carousel; diff --git a/apps/frontend/src/components/ClassBrowser/index.tsx b/apps/frontend/src/components/ClassBrowser/index.tsx index f80f3221b..1de706c79 100644 --- a/apps/frontend/src/components/ClassBrowser/index.tsx +++ b/apps/frontend/src/components/ClassBrowser/index.tsx @@ -183,9 +183,9 @@ export default function ClassBrowser({ if (sortBy === SortBy.OpenSeats) { const getOpenSeats = ({ primarySection: { enrollment } }: IClass) => - enrollment === undefined - ? 0 - : enrollment.latest.maxEnroll - enrollment.latest.enrolledCount; + enrollment + ? enrollment.latest.maxEnroll - enrollment.latest.enrolledCount + : 0; return getOpenSeats(b) - getOpenSeats(a); } @@ -194,11 +194,11 @@ export default function ClassBrowser({ const getPercentOpenSeats = ({ primarySection: { enrollment }, }: IClass) => - enrollment === undefined || enrollment.latest.maxEnroll === 0 - ? 0 - : (enrollment.latest.maxEnroll - + enrollment?.latest.maxEnroll + ? (enrollment.latest.maxEnroll - enrollment.latest.enrolledCount) / - enrollment.latest.maxEnroll; + enrollment.latest.maxEnroll + : 0; return getPercentOpenSeats(b) - getPercentOpenSeats(a); } diff --git a/apps/frontend/src/components/ClassBrowser/worker.ts b/apps/frontend/src/components/ClassBrowser/worker.ts index 8a23f1ddd..cc0e7639d 100644 --- a/apps/frontend/src/components/ClassBrowser/worker.ts +++ b/apps/frontend/src/components/ClassBrowser/worker.ts @@ -118,9 +118,9 @@ addEventListener( if (sortBy === SortBy.OpenSeats) { const getOpenSeats = ({ primarySection: { enrollment } }: IClass) => - enrollment === undefined - ? 0 - : enrollment.latest.maxEnroll - enrollment.latest.enrolledCount; + enrollment + ? enrollment.latest.maxEnroll - enrollment.latest.enrolledCount + : 0; return getOpenSeats(b) - getOpenSeats(a); } @@ -129,10 +129,10 @@ addEventListener( const getPercentOpenSeats = ({ primarySection: { enrollment }, }: IClass) => - enrollment === undefined || enrollment.latest.maxEnroll === 0 - ? 0 - : (enrollment.latest.maxEnroll - enrollment.latest.enrolledCount) / - enrollment.latest.maxEnroll; + enrollment?.latest.maxEnroll + ? (enrollment.latest.maxEnroll - enrollment.latest.enrolledCount) / + enrollment.latest.maxEnroll + : 0; return getPercentOpenSeats(b) - getPercentOpenSeats(a); } diff --git a/apps/frontend/src/components/ClassCard/ClassCard.module.scss b/apps/frontend/src/components/ClassCard/ClassCard.module.scss index 10138bb15..ede0079ba 100644 --- a/apps/frontend/src/components/ClassCard/ClassCard.module.scss +++ b/apps/frontend/src/components/ClassCard/ClassCard.module.scss @@ -1,65 +1,63 @@ - .root { - border: 1px solid var(--border-color); - border-radius: 8px; + border: 1px solid var(--border-color); + border-radius: 8px; + flex-shrink: 0; + background-color: var(--foreground-color); + position: relative; + padding: 16px; + display: flex; + gap: 16px; + align-items: flex-start; + cursor: pointer; + height: 100%; + + &:hover .column .icon { + color: var(--heading-color); + } + + .column { + display: flex; + flex-direction: column; + gap: 8px; flex-shrink: 0; - background-color: var(--foreground-color); - position: relative; - padding: 16px; + + .icon { + color: var(--paragraph-color); + transition: all 100ms ease-in-out; + } + } + + .body { + flex-grow: 1; + font-size: 14px; + justify-content: space-between; display: flex; - gap: 16px; - align-items: flex-start; - cursor: pointer; + flex-direction: column; height: 100%; - - &:hover .column .icon { + + .heading { color: var(--heading-color); + margin-bottom: 8px; + line-height: 1; + font-weight: 660; + font-feature-settings: + "cv05" on, + "cv13" on, + "ss07" on, + "cv12" on, + "cv06" on; } - - .column { - display: flex; - flex-direction: column; - gap: 8px; - flex-shrink: 0; - - .icon { - color: var(--paragraph-color); - transition: all 100ms ease-in-out; - } + + .description { + color: var(--paragraph-color); + line-height: 1.5; } - - .text { - flex-grow: 1; - font-size: 14px; - justify-content: space-between; + + .footer { display: flex; - flex-direction: column; - height: 100%; - - .heading { - color: var(--heading-color); - margin-bottom: 8px; - line-height: 1; - font-weight: 660; - font-feature-settings: - "cv05" on, - "cv13" on, - "ss07" on, - "cv12" on, - "cv06" on; - } - - .description { - color: var(--paragraph-color); - line-height: 1.5; - } - - .footer { - display: flex; - gap: 12px; - margin-top: 12px; - align-items: center; - } + gap: 12px; + margin-top: 12px; + align-items: center; } } - \ No newline at end of file +} diff --git a/apps/frontend/src/components/ClassCard/index.tsx b/apps/frontend/src/components/ClassCard/index.tsx index 7043949bb..200082a90 100644 --- a/apps/frontend/src/components/ClassCard/index.tsx +++ b/apps/frontend/src/components/ClassCard/index.tsx @@ -1,82 +1,42 @@ -import { useMemo } from "react"; - import { ArrowRight } from "iconoir-react"; import AverageGrade from "@/components/AverageGrade"; import Capacity from "@/components/Capacity"; import Units from "@/components/Units"; -import { useReadClass } from "@/hooks/api"; -import { Semester } from "@/lib/api"; +import { IClass } from "@/lib/api"; -import ClassDrawer from "../ClassDrawer"; import styles from "./ClassCard.module.scss"; interface ClassProps { - year: number; - semester: Semester; - subject: string; - courseNumber: string; - number: string; + class: IClass; } -export default function ClassCard({ - year, - semester, - subject, - courseNumber, - number, -}: ClassProps) { - const { data, loading } = useReadClass( - year as number, - semester as Semester, - subject as string, - courseNumber as string, - number as string - ); - - const _class = useMemo(() => data, [data]); - - return loading || !_class ? ( - <> - ) : ( - -
-
-
-

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

-

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

-
-
- - - -
+export default function ClassCard({ class: data }: ClassProps) { + return ( +
+
+

+ {data.subject} {data.courseNumber} #{data.number} +

+

{data.title ?? data.course.title}

+
+ + +
-
-
- -
+
+
+
+
- +
); } diff --git a/apps/frontend/src/components/Course/Classes/index.tsx b/apps/frontend/src/components/Course/Classes/index.tsx index 86b3cb8a0..d59f6ef0b 100644 --- a/apps/frontend/src/components/Course/Classes/index.tsx +++ b/apps/frontend/src/components/Course/Classes/index.tsx @@ -1,35 +1,24 @@ import ClassCard from "@/components/ClassCard"; import useCourse from "@/hooks/useCourse"; -import { sortDescendingTerm } from "@/lib/classes"; +import { sortByTermDescending } from "@/lib/classes"; import styles from "./Classes.module.scss"; export default function Classes() { const { course } = useCourse(); - console.log(course.classes); - + // TODO: UI and drawer vs. link for classes depending on dialog state return (
{course.classes && - course.classes - .toSorted(sortDescendingTerm((d) => d)) - .map((_class, index) => ( -
-
- {_class.semester} {_class.year} -
-
- -
+ course.classes.toSorted(sortByTermDescending).map((_class, index) => ( +
+
+ {_class.semester} {_class.year}
- ))} + +
+ ))}
); } diff --git a/apps/frontend/src/components/CourseCard/index.tsx b/apps/frontend/src/components/CourseCard/index.tsx index cf8e4aa96..abb75c788 100644 --- a/apps/frontend/src/components/CourseCard/index.tsx +++ b/apps/frontend/src/components/CourseCard/index.tsx @@ -1,43 +1,31 @@ -import { useMemo } from "react"; - import { ArrowRight } from "iconoir-react"; -import { useReadCourse } from "@/hooks/api"; +import { ICourse } from "@/lib/api"; import AverageGrade from "../AverageGrade"; -import CourseDrawer from "../CourseDrawer"; import styles from "./CourseCard.module.scss"; -interface CourseProps { - subject: string; - number: string; +interface CourseCardProps { + course: ICourse; } -export default function CourseCard({ subject, number }: CourseProps) { - const { data, loading } = useReadCourse(subject as string, number as string); - - const course = useMemo(() => data, [data]); - - return loading || !course ? ( - <> - ) : ( - -
-
-

- {subject} {number} -

-

{course.title}

-
- -
+export default function CourseCard({ course }: CourseCardProps) { + return ( +
+
+

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

+

{course.title}

+
+
-
-
- -
+
+
+
+
- +
); } diff --git a/apps/frontend/src/lib/classes.ts b/apps/frontend/src/lib/classes.ts index 9928c63db..f3529c374 100644 --- a/apps/frontend/src/lib/classes.ts +++ b/apps/frontend/src/lib/classes.ts @@ -1,12 +1,12 @@ const SEMESTER_ORDER = ["Spring", "Summer", "Fall"]; -export function sortDescendingTerm(objFetch: (data: any) => any) { - return (a: any, b: any) => { - const objA = objFetch(a); - const objB = objFetch(b); - return objA.year === objB.year - ? SEMESTER_ORDER.indexOf(objB.semester) - - SEMESTER_ORDER.indexOf(objA.semester) - : objB.year - objA.year; - }; +interface PartialTerm { + year: number; + semester: string; } + +export const sortByTermDescending = (a: T, b: T) => { + return a.year === b.year + ? SEMESTER_ORDER.indexOf(b.semester) - SEMESTER_ORDER.indexOf(a.semester) + : b.year - a.year; +};