From 7c2f065c0721e48d26f9931c07456b83c0ca2a45 Mon Sep 17 00:00:00 2001 From: Matthew Rowland Date: Fri, 12 Apr 2024 17:02:01 -0700 Subject: [PATCH] feat: Location, Time, Details, CCN, Tooltip; sections, etc. --- .../src/app/Catalog/Class/Class.module.scss | 46 +-- .../Class/Overview/Overview.module.scss | 18 + .../src/app/Catalog/Class/Overview/index.tsx | 29 +- .../Class/Sections/Sections.module.scss | 96 +++++- .../src/app/Catalog/Class/Sections/index.tsx | 59 +++- frontend/src/app/Catalog/Class/index.tsx | 48 ++- frontend/src/app/Catalog/List/index.tsx | 4 +- frontend/src/app/Catalog/index.tsx | 2 +- frontend/src/app/Schedules/Map/index.tsx | 13 +- frontend/src/components/CCN/CCN.module.scss | 45 +++ frontend/src/components/CCN/index.tsx | 54 +++ .../components/Details/Details.module.scss | 44 +++ frontend/src/components/Details/index.tsx | 53 +++ .../components/Location/Location.module.scss | 60 ++++ frontend/src/components/Location/index.tsx | 55 +++ frontend/src/components/Time/Time.module.scss | 74 ++++ frontend/src/components/Time/index.tsx | 132 +++++++ .../components/Tooltip/Tooltip.module.scss | 26 ++ frontend/src/components/Tooltip/index.tsx | 46 +++ frontend/src/lib/api.ts | 73 ++-- frontend/src/lib/course.ts | 323 ++++++++++++++---- 21 files changed, 1089 insertions(+), 211 deletions(-) create mode 100644 frontend/src/app/Catalog/Class/Overview/Overview.module.scss create mode 100644 frontend/src/components/CCN/CCN.module.scss create mode 100644 frontend/src/components/CCN/index.tsx create mode 100644 frontend/src/components/Details/Details.module.scss create mode 100644 frontend/src/components/Details/index.tsx create mode 100644 frontend/src/components/Location/Location.module.scss create mode 100644 frontend/src/components/Location/index.tsx create mode 100644 frontend/src/components/Time/Time.module.scss create mode 100644 frontend/src/components/Time/index.tsx create mode 100644 frontend/src/components/Tooltip/Tooltip.module.scss create mode 100644 frontend/src/components/Tooltip/index.tsx diff --git a/frontend/src/app/Catalog/Class/Class.module.scss b/frontend/src/app/Catalog/Class/Class.module.scss index 5eb30f141..568d9acfb 100644 --- a/frontend/src/app/Catalog/Class/Class.module.scss +++ b/frontend/src/app/Catalog/Class/Class.module.scss @@ -1,11 +1,11 @@ .root { flex-grow: 1; - overflow: auto; display: flex; flex-direction: column; .view { flex-grow: 1; + overflow: auto; } .header { @@ -15,51 +15,13 @@ display: flex; align-items: center; gap: 12px; - padding: 16px 24px 0; + padding: 16px 24px 12px; + font-size: 14px; + line-height: 1; .units { - font-size: 14px; color: var(--slate-500); user-select: none; - line-height: 1; - } - } - - .row { - display: flex; - padding: 24px 24px 12px; - - .detail { - flex: 1; - font-size: 14px; - - .title { - color: var(--slate-500); - line-height: 1; - margin-bottom: 8px; - } - - .description { - color: var(--slate-600); - line-height: 1.5; - font-weight: 500; - } - - &:not(:last-child) { - border-right: 1px solid var(--slate-200); - } - - &:first-child { - padding-right: 16px; - } - - &:nth-child(2) { - padding: 0 16px; - } - - &:last-child { - padding-left: 16px; - } } } diff --git a/frontend/src/app/Catalog/Class/Overview/Overview.module.scss b/frontend/src/app/Catalog/Class/Overview/Overview.module.scss new file mode 100644 index 000000000..3a797db3c --- /dev/null +++ b/frontend/src/app/Catalog/Class/Overview/Overview.module.scss @@ -0,0 +1,18 @@ +.root { + padding: 24px; + background-color: var(--slate-50); + min-height: 100%; + font-size: 14px; + + .label { + margin-top: 24px; + color: var(--slate-400); + line-height: 1; + } + + .description { + color: var(--slate-500); + margin-top: 8px; + line-height: 1.5; + } +} \ No newline at end of file diff --git a/frontend/src/app/Catalog/Class/Overview/index.tsx b/frontend/src/app/Catalog/Class/Overview/index.tsx index eccd72ce7..e0e01e0f5 100644 --- a/frontend/src/app/Catalog/Class/Overview/index.tsx +++ b/frontend/src/app/Catalog/Class/Overview/index.tsx @@ -1,3 +1,28 @@ -export default function Overview() { - return
Overview
; +import Details from "@/components/Details"; +import { IClass } from "@/lib/api"; + +import styles from "./Overview.module.scss"; + +interface OverviewProps { + currentClass?: IClass; +} + +export default function Overview({ currentClass }: OverviewProps) { + if (!currentClass) return null; + + return ( +
+
+

Description

+

+ {currentClass.description ?? currentClass.course.description} +

+ {currentClass.course.prereqs && ( + <> +

Prerequisites

+

{currentClass.course.prereqs}

+ + )} +
+ ); } diff --git a/frontend/src/app/Catalog/Class/Sections/Sections.module.scss b/frontend/src/app/Catalog/Class/Sections/Sections.module.scss index 4d4000292..65fa2696d 100644 --- a/frontend/src/app/Catalog/Class/Sections/Sections.module.scss +++ b/frontend/src/app/Catalog/Class/Sections/Sections.module.scss @@ -1,19 +1,89 @@ .root { background-color: var(--slate-50); - height: 100%; - padding: 24px; - - .section { - padding: 24px; - background-color: white; - border-radius: 8px; - box-shadow: 0 1px 2px rgb(0 0 0 / 5%); - margin-bottom: 24px; - border: 1px solid var(--slate-200); - - .header { + min-height: 100%; + padding: 24px 24px 24px 12px; + display: flex; + align-items: flex-start; + gap: 24px; + + .view { + flex-grow: 1; + height: 200vh; + + .section { + padding: 24px; + background-color: white; + border-radius: 8px; + box-shadow: 0 1px 2px rgb(0 0 0 / 5%); + margin-bottom: 24px; + border: 1px solid var(--slate-200); + + .header { + display: flex; + margin-bottom: 24px; + gap: 12px; + + .lock { + height: 32px; + width: 32px; + border-radius: 16px; + display: grid; + place-items: center; + background-color: var(--red-50); + color: var(--red-500); + } + + .text { + flex-grow: 1; + + .title { + color: var(--slate-900); + font-size: 14px; + margin-bottom: 8px; + line-height: 1; + font-weight: 500; + } + } + } + } + } + + .menu { + position: sticky; + top: 24px; + width: 128px; + display: flex; + flex-direction: column; + gap: 8px; + + .item { + height: 32px; + border-radius: 4px; display: flex; - justify-content: space-between; + align-items: center; + padding: 0 12px; + font-size: 14px; + font-weight: 500; + color: var(--slate-500); + width: fit-content; + + &:hover { + background-color: var(--slate-100); + } + + &:first-child { + color: var(--slate-900); + position: relative; + + &::after { + content: ''; + width: 2px; + height: 100%; + background-color: var(--blue-500); + position: absolute; + left: -12px; + } + } } } } \ No newline at end of file diff --git a/frontend/src/app/Catalog/Class/Sections/index.tsx b/frontend/src/app/Catalog/Class/Sections/index.tsx index 4688f8edf..9cfe710c1 100644 --- a/frontend/src/app/Catalog/Class/Sections/index.tsx +++ b/frontend/src/app/Catalog/Class/Sections/index.tsx @@ -1,25 +1,54 @@ +import { Lock } from "iconoir-react"; + +import CCN from "@/components/CCN"; +import Details from "@/components/Details"; +import { IClass } from "@/lib/api"; + import Capacity from "../../../../components/Capacity"; import styles from "./Sections.module.scss"; -export default function Sections() { +interface SectionsProps { + currentClass?: IClass; +} + +export default function Sections({ currentClass }: SectionsProps) { return (
-
-
-
-
Section 1
-
Instructor: John Doe
+
+
Discussions
+
Labs
+
+
+ {currentClass?.sections.map((section) => ( +
+
+
+

+ {section.kind === "Laboratory" ? "Lab" : section.kind}{" "} + {section.number} +

+ +
+
+ +
+ +
+
- -
+ ))}
-
-
); } diff --git a/frontend/src/app/Catalog/Class/index.tsx b/frontend/src/app/Catalog/Class/index.tsx index a01562b88..4ca4b7d7e 100644 --- a/frontend/src/app/Catalog/Class/index.tsx +++ b/frontend/src/app/Catalog/Class/index.tsx @@ -1,10 +1,11 @@ import { useMemo, useState } from "react"; import { useQuery } from "@apollo/client"; -import { Bookmark, Plus, Xmark } from "iconoir-react"; +import { Bookmark, CalendarPlus, Xmark } from "iconoir-react"; import { Link, useSearchParams } from "react-router-dom"; import AverageGrade from "@/components/AverageGrade"; +import CCN from "@/components/CCN"; import Capacity from "@/components/Capacity"; import IconButton from "@/components/IconButton"; import MenuItem from "@/components/MenuItem"; @@ -29,6 +30,18 @@ const views = [ text: "Sections", Component: Sections, }, + { + text: "Grades", + Component: () => null, + }, + { + text: "Enrollment", + Component: () => null, + }, + { + text: "Discussion", + Component: () => null, + }, ]; interface ClassProps { @@ -51,6 +64,8 @@ export default function Class({ const [searchParams] = useSearchParams(); const [view, setView] = useState(0); + // TODO: Query for enrollment and grades data in the background + const { data } = useQuery<{ class: IClass }>(GET_CLASS, { variables: { term: { @@ -84,6 +99,8 @@ export default function Class({ const Component = useMemo(() => views[view].Component, [view]); + console.log(currentClass?.primarySection); + return (
@@ -98,7 +115,7 @@ export default function Class({
- + @@ -116,10 +133,6 @@ export default function Class({
- {/**/}
{units}
-
-
-
-
Time
-
TuTh 6:30 PM - 7:59 PM
-
-
-
Location
-
- Anthro/Art Practice Bldg 160 -
-
-
-
Instructor
- {/*currentClass?.primarySection.instructors.map((instructor) => ( -
- {instructor.givenName} {instructor.familyName} -
- ))*/} -
+ {currentClass && }
{views.map(({ text }, index) => ( @@ -170,7 +164,7 @@ export default function Class({
- +
); diff --git a/frontend/src/app/Catalog/List/index.tsx b/frontend/src/app/Catalog/List/index.tsx index 60aa53c11..ce6a833c3 100644 --- a/frontend/src/app/Catalog/List/index.tsx +++ b/frontend/src/app/Catalog/List/index.tsx @@ -6,7 +6,7 @@ import { Wind } from "iconoir-react"; import { useSearchParams } from "react-router-dom"; import { ICatalogCourse, Semester } from "@/lib/api"; -import { abbreviations } from "@/lib/course"; +import { subjects } from "@/lib/course"; import Course from "./Course"; import styles from "./List.module.scss"; @@ -21,7 +21,7 @@ const initializeFuse = (courses: ICatalogCourse[]) => { const term = subject.toLowerCase(); - const alternateNames = abbreviations[term]?.reduce( + const alternateNames = subjects[term]?.abbreviations.reduce( (acc, abbreviation) => { // Add alternate names for abbreviations const abbreviations = [ diff --git a/frontend/src/app/Catalog/index.tsx b/frontend/src/app/Catalog/index.tsx index 718068596..50f125374 100644 --- a/frontend/src/app/Catalog/index.tsx +++ b/frontend/src/app/Catalog/index.tsx @@ -24,7 +24,7 @@ export default function Catalog() { } = useParams(); // TODO: Fetch available years - const currentYear = useMemo(() => (year && parseInt(year)) || 2022, [year]); + const currentYear = useMemo(() => (year && parseInt(year)) || 2024, [year]); // TODO: Fetch available semesters const currentSemester = useMemo( diff --git a/frontend/src/app/Schedules/Map/index.tsx b/frontend/src/app/Schedules/Map/index.tsx index 16aad3d6f..1e5e7c98e 100644 --- a/frontend/src/app/Schedules/Map/index.tsx +++ b/frontend/src/app/Schedules/Map/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { autoUpdate, @@ -43,6 +43,7 @@ interface MapProps { export default function Map({ boundary }: MapProps) { const [zoom, setZoom] = useState(DEFAULT_ZOOM); const [map, setMap] = useState(null); + const mapRef = useRef(null); const waypoints = useMemo(() => { return Object.keys(buildings) @@ -63,12 +64,12 @@ export default function Map({ boundary }: MapProps) { }; useEffect(() => { - if (!boundary) return; + if (!boundary || !mapRef.current) return; let destructor: (() => void) | null = null; const map = new mapboxgl.Map({ - container: "map", + container: mapRef.current, style: "mapbox://styles/mathhulk/clbznbvgs000314k8gtwa9q60", center: [-122.2592173, 37.8721508], zoom: DEFAULT_ZOOM, @@ -241,6 +242,10 @@ export default function Map({ boundary }: MapProps) { }); setMap(map); + + return () => { + map.remove(); + }; }, [boundary, waypoints]); return ( @@ -260,7 +265,7 @@ export default function Map({ boundary }: MapProps) { -
+
1
diff --git a/frontend/src/components/CCN/CCN.module.scss b/frontend/src/components/CCN/CCN.module.scss new file mode 100644 index 000000000..a714b79d1 --- /dev/null +++ b/frontend/src/components/CCN/CCN.module.scss @@ -0,0 +1,45 @@ +.trigger { + display: flex; + gap: 4px; + align-items: center; + color: var(--slate-400); + font-size: 14px; + line-height: 1; + cursor: pointer; + max-width: fit-content; +} + +.content { + border-radius: 4px; + padding: 12px; + background-color: var(--neutral-900); + box-shadow: 0 4px 64px var(--gray-200); + color: var(--neutral-400); + max-width: 256px; + font-size: 14px; + animation: fadeIn 100ms ease-in; + + .arrow { + fill: var(--neutral-900); + } + + .title { + font-weight: 500; + color: white; + margin-bottom: 8px; + line-height: 1; + } + + .description { + line-height: 1.5; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} \ No newline at end of file diff --git a/frontend/src/components/CCN/index.tsx b/frontend/src/components/CCN/index.tsx new file mode 100644 index 000000000..ba58d5254 --- /dev/null +++ b/frontend/src/components/CCN/index.tsx @@ -0,0 +1,54 @@ +import { useRef } from "react"; + +import * as Tooltip from "@radix-ui/react-tooltip"; +import { Hashtag } from "iconoir-react"; + +import styles from "./CCN.module.scss"; + +interface CCNProps { + ccn: string; +} + +export default function CCN({ ccn }: CCNProps) { + const textRef = useRef(null); + + const copy = () => { + if (!textRef.current) return; + + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(textRef.current); + selection?.removeAllRanges(); + selection?.addRange(range); + + navigator.clipboard.writeText(ccn); + }; + + return ( + + +
+ + {ccn} +
+
+ + +
+ +

Class number

+

+ Use this number to search for and enroll in this class within the + CalCentral Enrollment Center. +

+
+
+
+
+ ); +} diff --git a/frontend/src/components/Details/Details.module.scss b/frontend/src/components/Details/Details.module.scss new file mode 100644 index 000000000..9a79520b5 --- /dev/null +++ b/frontend/src/components/Details/Details.module.scss @@ -0,0 +1,44 @@ +.root { + display: flex; + + .column { + flex: 1; + font-size: 14px; + + .title { + color: var(--slate-400); + line-height: 1; + margin-bottom: 8px; + } + + .description { + color: var(--slate-500); + line-height: 1.5; + + a { + color: var(--blue-500); + font-weight: 500; + + &:hover { + color: var(--blue-600); + } + } + } + + &:not(:last-child) { + border-right: 1px solid var(--slate-200); + } + + &:first-child { + padding-right: 16px; + } + + &:nth-child(2) { + padding: 0 16px; + } + + &:last-child { + padding-left: 16px; + } + } +} \ No newline at end of file diff --git a/frontend/src/components/Details/index.tsx b/frontend/src/components/Details/index.tsx new file mode 100644 index 000000000..96d4b8928 --- /dev/null +++ b/frontend/src/components/Details/index.tsx @@ -0,0 +1,53 @@ +import { IInstructor } from "@/lib/api"; + +import Location from "../Location"; +import Time from "../Time"; +import styles from "./Details.module.scss"; + +interface DetailsProps { + days: boolean[]; + timeStart: string; + timeEnd: string; + location: string; + instructors: IInstructor[]; +} + +export default function Details({ + days, + timeStart, + timeEnd, + location, + instructors, +}: DetailsProps) { + return ( +
+
+

Time

+
+
+

Location

+ +
+
+

+ {instructors?.length > 1 ? "Instructors" : "Instructor"} +

+

+ {instructors?.[0] + ? instructors.length === 1 + ? `${instructors[0].givenName} ${instructors[0].familyName}` + : `${instructors[0].givenName} ${instructors[0].familyName}, et al.` + : "To be determined"} +

+
+
+ ); +} diff --git a/frontend/src/components/Location/Location.module.scss b/frontend/src/components/Location/Location.module.scss new file mode 100644 index 000000000..c838d4531 --- /dev/null +++ b/frontend/src/components/Location/Location.module.scss @@ -0,0 +1,60 @@ +.trigger { + color: var(--slate-500); +} + +.root { + color: var(--blue-500); + transition: all 100ms ease-in-out; + font-weight: 500; + + &:hover { + color: var(--blue-600); + } +} + +.root, .trigger { + font-size: 14px; + line-height: 1.5; + width: fit-content; +} + +.content { + width: 256px; + border-radius: 4px; + padding: 12px; + background-color: var(--neutral-900); + box-shadow: 0 4px 64px var(--gray-200); + animation: fadeIn 100ms ease-in; + color: var(--neutral-400); + font-size: 14px; + + .arrow { + fill: var(--neutral-900); + } + + .map { + aspect-ratio: 3 / 2; + border-radius: 2px; + margin-bottom: 16px; + } + + .title { + font-weight: 500; + color: white; + margin-bottom: 8px; + line-height: 1; + } + + .description { + line-height: 1.5; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} \ No newline at end of file diff --git a/frontend/src/components/Location/index.tsx b/frontend/src/components/Location/index.tsx new file mode 100644 index 000000000..18fd65ec3 --- /dev/null +++ b/frontend/src/components/Location/index.tsx @@ -0,0 +1,55 @@ +import { useMemo } from "react"; + +import * as Tooltip from "@radix-ui/react-tooltip"; + +import { buildings } from "@/lib/location"; + +import styles from "./Location.module.scss"; + +interface LocationProps { + location: string; +} + +export default function Location({ location }: LocationProps) { + const building = useMemo(() => { + if (!location) return; + + const building = location.split(" ").slice(0, -1).join(" "); + + return buildings[building]; + }, [location]); + + const room = useMemo(() => { + if (!location) return; + + return location.split(" ").pop(); + }, [location]); + + return building?.location && building?.link && room ? ( + + {building.name + " " + room} + + ) : ( + + +

To be determined

+
+ + +
+ +

Location

+

+ The location for this class has not been determined yet. +

+
+
+
+
+ ); +} diff --git a/frontend/src/components/Time/Time.module.scss b/frontend/src/components/Time/Time.module.scss new file mode 100644 index 000000000..aeee0c072 --- /dev/null +++ b/frontend/src/components/Time/Time.module.scss @@ -0,0 +1,74 @@ +.trigger { + font-size: 14px; + line-height: 1.5; + color: var(--slate-500); + width: fit-content; +} + +.calendar, .content { + border-radius: 4px; + padding: 12px; + background-color: var(--neutral-900); + box-shadow: 0 4px 64px var(--gray-200); + animation: fadeIn 100ms ease-in; + color: var(--neutral-400); + font-size: 14px; +} + +.calendar { + display: flex; + gap: 4px; + + .day { + width: 24px; + height: 188px; + position: relative; + + .label { + text-align: center; + line-height: 1; + margin-bottom: 4px; + } + + .event { + width: 100%; + border-radius: 1px; + background-color: white; + position: absolute; + left: 0; + } + } + + .divider { + width: 1px; + background-color: var(--neutral-700); + } +} + +.content { + max-width: 256px; + + .arrow { + fill: var(--neutral-900); + } + + .title { + font-weight: 500; + color: white; + margin-bottom: 8px; + line-height: 1; + } + + .description { + line-height: 1.5; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} \ No newline at end of file diff --git a/frontend/src/components/Time/index.tsx b/frontend/src/components/Time/index.tsx new file mode 100644 index 000000000..02a11ded0 --- /dev/null +++ b/frontend/src/components/Time/index.tsx @@ -0,0 +1,132 @@ +import { useMemo } from "react"; + +import * as Tooltip from "@radix-ui/react-tooltip"; + +import styles from "./Time.module.scss"; + +const getTime = (start: string, end: string) => { + if (!start || !end) return "To be determined"; + + const [startHours, startMinutes] = start + .split(":") + .map((value) => parseInt(value)); + + const [endHours, endMinutes] = end.split(":").map((value) => parseInt(value)); + + if (startHours === 0 && endHours === 0) return "To be determined"; + + let time = `${startHours % 12 || 12}`; + if (startMinutes > 0) time += `:${startMinutes.toString().padStart(2, "0")}`; + + time += ` - ${endHours % 12 || 12}`; + if (endMinutes > 0) time += `:${endMinutes.toString().padStart(2, "0")}`; + time += endHours < 12 ? " AM" : " PM"; + + return time; +}; + +interface TimeProps { + days: boolean[]; + start: string; + end: string; +} + +export default function Time({ days, start, end }: TimeProps) { + const bottom = useMemo(() => { + if (!end) return; + + const [hours, minutes] = end.split(":").map((value) => parseInt(value)); + + return 170 - ((hours - 6) * 10 + minutes / 6); + }, [end]); + + const height = useMemo(() => { + if (!start || !end) return; + + const [startHours, startMinutes] = start + .split(":") + .map((value) => parseInt(value)); + + const [endHours, endMinutes] = end + .split(":") + .map((value) => parseInt(value)); + + return (endHours - startHours) * 10 + (endMinutes - startMinutes) / 6; + }, [start, end]); + + const time = useMemo(() => { + if (!days?.some((day) => day)) return; + + return ( + days + .reduce( + (time, day, index) => + day + ? [...time, ["Su", "M", "Tu", "W", "Th", "F", "Sa"][index]] + : time, + [] as string[] + ) + .join("") + + ", " + + getTime(start, end) + ); + }, [days, start, end]); + + return time ? ( + + +

{time}

+
+ + +
+ + {days.map((day, index) => ( + <> + {index > 0 &&
} +
+
+ {["Su", "M", "Tu", "W", "Th", "F", "Sa"][index]} +
+ {day && ( +
+ )} +
+ + ))} +
+ + + + ) : ( + + +

To be determined

+
+ + +
+ +

Time

+

+ The time for this class has not been determined yet. +

+
+
+
+
+ ); +} diff --git a/frontend/src/components/Tooltip/Tooltip.module.scss b/frontend/src/components/Tooltip/Tooltip.module.scss new file mode 100644 index 000000000..5e395de79 --- /dev/null +++ b/frontend/src/components/Tooltip/Tooltip.module.scss @@ -0,0 +1,26 @@ +.content { + border-radius: 4px; + height: 32px; + padding: 0 12px; + background-color: var(--neutral-900); + box-shadow: 0 4px 64px var(--gray-200); + animation: fadeIn 100ms ease-in; + font-size: 14px; + line-height: 1; + color: white; + display: flex; + align-items: center; + + .arrow { + fill: var(--neutral-900); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} \ No newline at end of file diff --git a/frontend/src/components/Tooltip/index.tsx b/frontend/src/components/Tooltip/index.tsx new file mode 100644 index 000000000..46dc7344e --- /dev/null +++ b/frontend/src/components/Tooltip/index.tsx @@ -0,0 +1,46 @@ +import { ReactNode } from "react"; + +import { + Arrow, + Content, + Portal, + Root, + TooltipContentProps, + Trigger, +} from "@radix-ui/react-tooltip"; + +import styles from "./Tooltip.module.scss"; + +interface TooltipProps { + children: ReactNode; + content: string; +} + +export default function Tooltip({ + content, + children, + sideOffset = 8, + collisionPadding = 8, + side = "bottom", + ...props +}: TooltipContentProps & Omit) { + return ( + + {children} + + +
+ + {content} +
+
+
+
+ ); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 75d7226f8..48701aa6e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -23,7 +23,7 @@ export interface IInstructor { } export interface ISection { - ccn: number; + ccn: string; dateEnd: string; dateStart: string; days: boolean[]; @@ -45,6 +45,8 @@ export interface IClass extends ICatalogClass { course: ICourse; primarySection: ISection; sections: ISection[]; + description: string; + notes: string; } export interface ICatalogCourse { @@ -55,19 +57,47 @@ export interface ICatalogCourse { classes: ICatalogClass[]; } -export interface ICourse extends Omit { +export interface ICourse extends Omit { subjectName: string; prereqs?: string; gradingBasis: string; + description: string; } export interface IAccount { email: string; } -/* - - +export const GET_CLASS = gql` + query GetClass( + $term: TermInput! + $subject: String! + $courseNumber: String! + $classNumber: String! + ) { + class( + term: $term + subject: $subject + courseNumber: $courseNumber + classNumber: $classNumber + ) { + title + description + enrollCount + enrollMax + unitsMax + unitsMin + waitlistCount + waitlistMax + number + course { + title + description + gradeAverage + gradingBasis + subjectName + prereqs + } primarySection { ccn dateEnd @@ -110,39 +140,6 @@ export interface IAccount { waitlistMax number } - -*/ - -export const GET_CLASS = gql` - query GetClass( - $term: TermInput! - $subject: String! - $courseNumber: String! - $classNumber: String! - ) { - class( - term: $term - subject: $subject - courseNumber: $courseNumber - classNumber: $classNumber - ) { - title - description - enrollCount - enrollMax - unitsMax - unitsMin - waitlistCount - waitlistMax - number - course { - title - description - gradeAverage - gradingBasis - subjectName - prereqs - } } } `; diff --git a/frontend/src/lib/course.ts b/frontend/src/lib/course.ts index 13a7dd22a..28229e304 100644 --- a/frontend/src/lib/course.ts +++ b/frontend/src/lib/course.ts @@ -1,68 +1,257 @@ -export const abbreviations: Record = { - astron: ["astro"], - compsci: ["cs", "comp sci", "computer science"], - mcellbi: ["mcb"], - nusctx: ["nutrisci"], - "bio eng": ["bioe", "bio e", "bio p", "bioeng"], - biology: ["bio"], - "civ eng": ["cive", "civ e", "civeng"], - "chm eng": ["cheme"], - classic: ["classics"], - "cog sci": ["cogsci"], - colwrit: ["college writing"], - "com lit": ["complit", "comlit"], - "cy plan": ["cyplan", "cp"], - "des inv": ["desinv", "design"], - "dev eng": ["eveng"], - "dev std": ["devstd"], - datasci: ["ds", "data"], - data: ["ds", "data"], - "ea lang": ["ealang"], - "env des": ["ed"], - "el eng": ["ee", "electrical engineering"], - eecs: ["eecs"], - "ene,res": ["erg", "er", "eneres"], - engin: ["e", "engineering"], - "env sci": ["envsci"], - "eth std": ["ethstd"], - "eura st": ["eurast"], - geog: ["geology", "geo"], - "hin-urd": ["hinurd"], - "hum bio": ["humbio"], - integbi: ["ib"], - "ind eng": ["ie", "ieor"], - linguis: ["ling"], - "l & s": ["l&s", "ls", "lns"], - "malay/i": ["malayi"], - "mat sci": ["matsci", "ms", "mse"], - "mec eng": ["meceng", "meche", "mech e", "me"], - "med st": ["medst"], - "me stu": ["mestu", "middle eastern studies"], - "mil aff": ["milaff"], - "mil sci": ["milsci"], - natamst: ["native american studies", "nat am st"], - neurosc: ["neurosci"], - "nuc en": ["ne"], - "ne stud": ["nestud"], - mediast: ["media"], - music: ["mus"], - "pb hlth": ["pbhlth", "ph", "pub hlth", "public health"], - "phys end": ["pe", "physed"], - philos: ["philo", "phil"], - polecon: ["poliecon"], - philo: ["philosophy"], - plantbi: ["pmb"], - "pol sci": ["poli", "polsci", "polisci", "poli sci", "ps"], - "pub pol": ["pubpol", "pp", "public policy"], - "pub aff": ["pubaff", "public affaris"], - psych: ["psychology", "psych"], - rhetor: ["rhetoric"], - "s asian": ["sasian"], - "s,seasn": ["sseasn"], - stat: ["stats"], - theater: ["tdps"], - ugba: ["haas"], - vietnms: ["vietnamese"], - "vis sci": ["vissci"], - "vis std": ["visstd"], +interface Subject { + abbreviations: string[]; + name: string; +} + +// TODO: https://guide.berkeley.edu/courses/#C + +export const subjects: Record = { + astron: { + abbreviations: ["astro"], + name: "Astronomy", + }, + compsci: { + abbreviations: ["cs", "comp sci", "computer science"], + name: "Computer Science", + }, + mcellbi: { + abbreviations: ["mcb"], + name: "Molecular and Cell Biology", + }, + nusctx: { + abbreviations: ["nutrisci"], + name: "Nutritional Sciences and Toxicology", + }, + bioeng: { + abbreviations: ["bioe", "bio e", "bio p", "bio eng"], + name: "Bioengineering", + }, + biology: { + abbreviations: ["bio"], + name: "Biology", + }, + civeng: { + abbreviations: ["cive", "civ e", "civ eng"], + name: "Civil and Environmental Engineering", + }, + chmeng: { + abbreviations: ["cheme", "chm eng"], + name: "Chemical Engineering", + }, + classic: { + abbreviations: ["classics"], + name: "Classics", + }, + cogsci: { + abbreviations: ["cogsci"], + name: "Cognitive Science", + }, + colwrit: { + abbreviations: ["college writing", "col writ"], + name: "College Writing", + }, + comlit: { + abbreviations: ["complit", "com lit"], + name: "Comparative Literature", + }, + cyplan: { + abbreviations: ["cy plan", "cp"], + name: "City and Regional Planning", + }, + desinv: { + abbreviations: ["des inv", "design"], + name: "Design Innovation", + }, + deveng: { + abbreviations: ["dev eng"], + name: "Development Engineering", + }, + devstd: { + abbreviations: ["dev std"], + name: "Development Studies", + }, + datasci: { + abbreviations: ["ds", "data", "data sci"], + name: "Data Science", + }, + data: { + abbreviations: ["ds", "data", "data sci"], + name: "Data Science, Undergraduate", + }, + ealang: { + abbreviations: ["ea lang"], + name: "East Asian Languages and Cultures", + }, + envdes: { + abbreviations: ["ed", "env des"], + name: "Environmental Design", + }, + eleng: { + abbreviations: ["ee", "electrical engineering", "el eng"], + name: "Electrical Engineering", + }, + eecs: { + abbreviations: ["eecs"], + name: "Electrical Engineering and Computer Sciences", + }, + eneres: { + abbreviations: ["erg", "er", "ene,res"], + name: "Energy and Resources Group", + }, + engin: { + abbreviations: ["e", "engineering"], + name: "Engineering", + }, + envsci: { + abbreviations: ["env sci"], + name: "Environmental Sciences", + }, + ethstd: { + abbreviations: ["eth std"], + name: "Ethnic Studies", + }, + geog: { + abbreviations: ["geology", "geo"], + name: "Geography", + }, + hinurd: { + abbreviations: ["hin urd", "hin-urd"], + name: "Hindi-Urdu", + }, + integbi: { + abbreviations: ["ib"], + name: "Integrative Biology", + }, + indeng: { + abbreviations: ["ie", "ieor", "ind eng"], + name: "Industrial Engineering and Operations Research", + }, + linguis: { + abbreviations: ["ling"], + name: "Linguistics", + }, + "l&s": { + abbreviations: ["l & s", "ls", "lns"], + name: "Letters and Science", + }, + indones: { + abbreviations: ["indonesian"], + name: "Indonesian", + }, + matsci: { + abbreviations: ["mat sci", "ms", "mse"], + name: "Materials Science and Engineering", + }, + meceng: { + abbreviations: ["mec eng", "meche", "mech e", "me"], + name: "Mechanical Engineering", + }, + medst: { + abbreviations: ["med st"], + name: "Medical Studies", + }, + mestu: { + abbreviations: ["me stu", "middle eastern studies"], + name: "Middle Eastern Studies", + }, + milaff: { + abbreviations: ["mil aff"], + name: "Military Affairs", + }, + milsci: { + abbreviations: ["mil sci"], + name: "Military Science", + }, + natamst: { + abbreviations: ["native american studies", "nat am st"], + name: "Native American Studies", + }, + neurosc: { + abbreviations: ["neurosci"], + name: "Neuroscience", + }, + nuceng: { + abbreviations: ["ne", "nuc eng"], + name: "Nuclear Engineering", + }, + mediast: { + abbreviations: ["media", "media st"], + name: "Media Studies", + }, + music: { + abbreviations: ["mus"], + name: "Music", + }, + pbhlth: { + abbreviations: ["pb hlth", "ph", "pub hlth", "public health"], + name: "Public Health", + }, + physed: { + abbreviations: ["pe", "phys ed"], + name: "Physical Education", + }, + polecon: { + abbreviations: ["poliecon"], + name: "Political Economy", + }, + philo: { + abbreviations: ["philosophy", "philos", "phil"], + name: "Philosophy", + }, + plantbi: { + abbreviations: ["pmb"], + name: "Plant and Microbial Biology", + }, + polsci: { + abbreviations: ["poli", "pol sci", "polisci", "poli sci", "ps"], + name: "Political Science", + }, + pubpol: { + abbreviations: ["pub pol", "pp", "public policy"], + name: "Public Policy", + }, + pubaff: { + abbreviations: ["pubaff", "public affaris"], + name: "Public Affairs", + }, + psych: { + abbreviations: ["psychology"], + name: "Psychology", + }, + rhetor: { + abbreviations: ["rhetoric"], + name: "Rhetoric", + }, + sasian: { + abbreviations: ["s asian"], + name: "South Asian Studies", + }, + seasian: { + abbreviations: ["se asian"], + name: "Southeast Asian Studies", + }, + stat: { + abbreviations: ["stats"], + name: "Statistics", + }, + theater: { + abbreviations: ["tdps"], + name: "Theater, Dance, and Performance Studies", + }, + ugba: { + abbreviations: ["haas"], + name: "Undergraduate Business Administration", + }, + vietnms: { + abbreviations: ["vietnamese"], + name: "Vietnamese", + }, + vissci: { + abbreviations: ["vis sci"], + name: "Vision Science", + }, + visstd: { + abbreviations: ["vis std"], + name: "Visual Studies", + }, };