From d78107861cef1edadf39e52db922edead404b4ee Mon Sep 17 00:00:00 2001 From: Matthew Rowland Date: Sun, 9 Jun 2024 11:50:25 -0700 Subject: [PATCH] feat: Manage --- frontend/src/App.tsx | 20 +- .../Schedule/Calendar/Event/Event.module.scss | 50 ---- .../src/app/Schedule/Calendar/Event/index.tsx | 61 ----- .../Compare/Compare.module.scss | 0 frontend/src/app/Schedule/Compare/index.tsx | 101 +++++++ .../Calendar/Calendar.module.scss | 29 +- .../Calendar/Week/Day/Day.module.scss | 28 +- .../{ => Manage}/Calendar/Week/Day/index.tsx | 43 ++- .../Calendar/Week/Week.module.scss | 0 .../{ => Manage}/Calendar/Week/index.tsx | 0 .../{ => Manage}/Calendar/calendar.ts | 0 .../Schedule/{ => Manage}/Calendar/index.tsx | 56 ++-- .../Manage.module.scss} | 0 .../Schedule/{ => Manage}/Map/Map.module.scss | 0 .../app/Schedule/{ => Manage}/Map/index.tsx | 0 .../SideBar/Catalog/Catalog.module.scss | 0 .../{ => Manage}/SideBar/Catalog/index.tsx | 0 .../SideBar/Class/Class.module.scss | 0 .../SideBar/Class/Section/Section.module.scss | 0 .../SideBar/Class/Section/index.tsx | 0 .../{ => Manage}/SideBar/Class/index.tsx | 2 +- .../{ => Manage}/SideBar/SideBar.module.scss | 0 .../Schedule/{ => Manage}/SideBar/index.tsx | 0 frontend/src/app/Schedule/Manage/index.tsx | 250 +++++++++++++++++ frontend/src/app/Schedule/index.tsx | 252 ++---------------- frontend/src/app/Schedule/schedule.ts | 12 + frontend/src/app/Schedules/Compare/index.tsx | 114 -------- frontend/src/components/Week/Event/index.tsx | 2 +- frontend/src/lib/api.ts | 7 + frontend/src/lib/section.ts | 17 ++ 30 files changed, 527 insertions(+), 517 deletions(-) delete mode 100644 frontend/src/app/Schedule/Calendar/Event/Event.module.scss delete mode 100644 frontend/src/app/Schedule/Calendar/Event/index.tsx rename frontend/src/app/{Schedules => Schedule}/Compare/Compare.module.scss (100%) create mode 100644 frontend/src/app/Schedule/Compare/index.tsx rename frontend/src/app/Schedule/{ => Manage}/Calendar/Calendar.module.scss (60%) rename frontend/src/app/Schedule/{ => Manage}/Calendar/Week/Day/Day.module.scss (69%) rename frontend/src/app/Schedule/{ => Manage}/Calendar/Week/Day/index.tsx (53%) rename frontend/src/app/Schedule/{ => Manage}/Calendar/Week/Week.module.scss (100%) rename frontend/src/app/Schedule/{ => Manage}/Calendar/Week/index.tsx (100%) rename frontend/src/app/Schedule/{ => Manage}/Calendar/calendar.ts (100%) rename frontend/src/app/Schedule/{ => Manage}/Calendar/index.tsx (74%) rename frontend/src/app/Schedule/{Schedule.module.scss => Manage/Manage.module.scss} (100%) rename frontend/src/app/Schedule/{ => Manage}/Map/Map.module.scss (100%) rename frontend/src/app/Schedule/{ => Manage}/Map/index.tsx (100%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/Catalog/Catalog.module.scss (100%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/Catalog/index.tsx (100%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/Class/Class.module.scss (100%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/Class/Section/Section.module.scss (100%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/Class/Section/index.tsx (100%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/Class/index.tsx (98%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/SideBar.module.scss (100%) rename frontend/src/app/Schedule/{ => Manage}/SideBar/index.tsx (100%) create mode 100644 frontend/src/app/Schedule/Manage/index.tsx create mode 100644 frontend/src/app/Schedule/schedule.ts delete mode 100644 frontend/src/app/Schedules/Compare/index.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 379813352..da0e9c9f0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,12 +7,14 @@ import Catalog from "@/app/Catalog"; import Explore from "@/app/Explore"; import Landing from "@/app/Landing"; import Plan from "@/app/Plan"; -import Schedule from "@/app/Schedule"; +import Compare from "@/app/Schedule/Compare"; +import Manage from "@/app/Schedule/Manage"; import Schedules from "@/app/Schedules"; -import Compare from "@/app/Schedules/Compare"; import AccountProvider from "@/components/AccountProvider"; import Layout from "@/components/Layout"; +import Schedule from "./app/Schedule"; + const router = createBrowserRouter([ { element: , @@ -42,10 +44,16 @@ const router = createBrowserRouter([ { element: , path: "schedules/:scheduleId", - }, - { - element: , - path: "schedules/compare/:leftScheduleId?/:rightScheduleId?", + children: [ + { + element: , + index: true, + }, + { + element: , + path: "compare/:comparedScheduleId?", + }, + ], }, ], }, diff --git a/frontend/src/app/Schedule/Calendar/Event/Event.module.scss b/frontend/src/app/Schedule/Calendar/Event/Event.module.scss deleted file mode 100644 index fbb8078f9..000000000 --- a/frontend/src/app/Schedule/Calendar/Event/Event.module.scss +++ /dev/null @@ -1,50 +0,0 @@ -.trigger { - position: absolute; - z-index: 2; - padding: 8px; - border-radius: 4px; - background-color: var(--blue-500); - font-size: 12px; - opacity: 0.5; - - &.active { - opacity: 1; - } - - .heading { - font-weight: 500; - color: white; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .description { - margin-top: 4px; - color: rgb(255 255 255 / 75%); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } -} - -.content { - height: 128px; - width: 320px; - background-color: var(--foreground-color); - border-radius: 8px; - box-shadow: 0 0 16px rgb(0 0 0 / 10%); - z-index: 989; - opacity: 0; - animation: fadeIn 100ms ease-in-out forwards; - - .arrow { - fill: var(--foreground-color); - } -} - -@keyframes fadeIn { - to { - opacity: 1; - } -} \ No newline at end of file diff --git a/frontend/src/app/Schedule/Calendar/Event/index.tsx b/frontend/src/app/Schedule/Calendar/Event/index.tsx deleted file mode 100644 index d08ed8b4e..000000000 --- a/frontend/src/app/Schedule/Calendar/Event/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useMemo } from "react"; - -import * as HoverCard from "@radix-ui/react-hover-card"; -import classNames from "classnames"; - -import { ISection } from "@/lib/api"; -import { getY } from "@/lib/schedule"; - -import styles from "./Event.module.scss"; - -interface EventProps { - columns: number; - position: number; - active: boolean; -} - -export default function Event({ - columns, - position, - meetings: [{ startTime, endTime }], - course, - component, - number, - active, -}: EventProps & ISection) { - const top = useMemo(() => getY(startTime!), [startTime]); - - const height = useMemo(() => getY(endTime!) - top + 1, [top, endTime]); - - // TODO: Hover card content - return ( - - -
-
- {course.subject} {course.number} -
-
- {component} {number} -
-
-
- - -
- -
-
-
-
- ); -} diff --git a/frontend/src/app/Schedules/Compare/Compare.module.scss b/frontend/src/app/Schedule/Compare/Compare.module.scss similarity index 100% rename from frontend/src/app/Schedules/Compare/Compare.module.scss rename to frontend/src/app/Schedule/Compare/Compare.module.scss diff --git a/frontend/src/app/Schedule/Compare/index.tsx b/frontend/src/app/Schedule/Compare/index.tsx new file mode 100644 index 000000000..1f58eafbc --- /dev/null +++ b/frontend/src/app/Schedule/Compare/index.tsx @@ -0,0 +1,101 @@ +import { useEffect, useRef, useState } from "react"; + +import { DataTransferBoth, Xmark } from "iconoir-react"; +import { Link, useOutletContext } from "react-router-dom"; + +import IconButton from "@/components/IconButton"; +import Week from "@/components/Week"; + +import { ScheduleContextType } from "../schedule"; +import styles from "./Compare.module.scss"; + +export default function Compare() { + const { selectedSections } = useOutletContext(); + + const [y, setY] = useState(null); + const leftRef = useRef(null); + const rightRef = useRef(null); + const [current, setCurrent] = useState(null); + + useEffect(() => { + if (!leftRef.current || !rightRef.current) return; + + const left = leftRef.current; + const right = rightRef.current; + + const handleScroll = (left?: boolean) => { + if (!leftRef.current || !rightRef.current) return; + + if (left) { + if (current === 1) return; + + rightRef.current.scrollTo({ + top: leftRef.current.scrollTop, + left: leftRef.current.scrollLeft, + }); + + return; + } + + if (current === 0) return; + + leftRef.current.scrollTo({ + top: rightRef.current.scrollTop, + left: rightRef.current.scrollLeft, + }); + }; + + const handleLeftScroll = () => handleScroll(true); + left.addEventListener("scroll", handleLeftScroll); + + const handleRightScroll = () => handleScroll(); + right.addEventListener("scroll", handleRightScroll); + + return () => { + right.removeEventListener("scroll", handleLeftScroll); + left.removeEventListener("scroll", handleRightScroll); + }; + }, [current]); + + return ( +
+
+
+

Untitled Spring 2024 schedule

+

Spring 2024

+
+
+

No schedule selected

+ + + + + + +
+
+
+
setCurrent(0)} + onMouseLeave={() => + setCurrent((previous) => (previous === 0 ? null : previous)) + } + > + +
+
setCurrent(1)} + onMouseLeave={() => + setCurrent((previous) => (previous === 1 ? null : previous)) + } + > + +
+
+
+ ); +} diff --git a/frontend/src/app/Schedule/Calendar/Calendar.module.scss b/frontend/src/app/Schedule/Manage/Calendar/Calendar.module.scss similarity index 60% rename from frontend/src/app/Schedule/Calendar/Calendar.module.scss rename to frontend/src/app/Schedule/Manage/Calendar/Calendar.module.scss index d98f2f811..fa59735c7 100644 --- a/frontend/src/app/Schedule/Calendar/Calendar.module.scss +++ b/frontend/src/app/Schedule/Manage/Calendar/Calendar.module.scss @@ -6,26 +6,47 @@ .header { display: flex; + flex-direction: column; border-bottom: 1px solid var(--border-color); position: sticky; top: 0; background-color: var(--background-color); z-index: 1; font-size: 12px; - color: var(--label-color); line-height: 1; z-index: 2; + .legend { + display: flex; + padding: 0 12px; + height: 32px; + gap: 12px; + align-items: center; + background-color: var(--slate-100); + border-bottom: 1px solid var(--border-color); + color: var(--paragraph-color); + + .timezone { + margin-left: auto; + } + + .category { + display: flex; + gap: 8px; + align-items: center; + } + } + .week { display: grid; grid-template-columns: repeat(7, 1fr); + color: var(--label-color); flex-grow: 1; .day { height: 32px; - display: flex; - align-items: center; - justify-content: center; + display: grid; + place-items: center; &:not(:last-child) { border-right: 1px solid var(--border-color); diff --git a/frontend/src/app/Schedule/Calendar/Week/Day/Day.module.scss b/frontend/src/app/Schedule/Manage/Calendar/Week/Day/Day.module.scss similarity index 69% rename from frontend/src/app/Schedule/Calendar/Week/Day/Day.module.scss rename to frontend/src/app/Schedule/Manage/Calendar/Week/Day/Day.module.scss index 5bf0825cd..25a77fdab 100644 --- a/frontend/src/app/Schedule/Calendar/Week/Day/Day.module.scss +++ b/frontend/src/app/Schedule/Manage/Calendar/Week/Day/Day.module.scss @@ -20,35 +20,47 @@ gap: 8px; height: 24px; white-space: nowrap; + color: white; + + &:not(.exam) { + border: 1px dashed var(--color); + } + + &.exam { + background-color: var(--color); + + .time { + color: rgb(255 255 255 / 75%); + } + + .course { + color: white; + } + } &.active { opacity: 0.5; } .time { - color: rgb(255 255 255 / 75%); font-size: 8px; + color: var(--label-color); } .course { - color: white; font-weight: 500; text-overflow: ellipsis; overflow: hidden; + color: var(--color); } } .date { display: flex; - - .month { - color: var(--heading-color); - font-weight: 500; - } + color: var(--label-color); .number { margin-left: auto; - color: var(--label-color); } } } \ No newline at end of file diff --git a/frontend/src/app/Schedule/Calendar/Week/Day/index.tsx b/frontend/src/app/Schedule/Manage/Calendar/Week/Day/index.tsx similarity index 53% rename from frontend/src/app/Schedule/Calendar/Week/Day/index.tsx rename to frontend/src/app/Schedule/Manage/Calendar/Week/Day/index.tsx index 2e3c60e27..3d9177c0d 100644 --- a/frontend/src/app/Schedule/Calendar/Week/Day/index.tsx +++ b/frontend/src/app/Schedule/Manage/Calendar/Week/Day/index.tsx @@ -1,3 +1,5 @@ +import { CSSProperties } from "react"; + import classNames from "classnames"; import { getColor } from "@/lib/section"; @@ -24,32 +26,29 @@ export default function Day({ date, events, active }: DayProps) {
{active && (
- {date.format("D") === "1" && ( -

{date.format("MMMM")}

- )} + {date.format("D") === "1" && date.format("MMMM")}

{date.format("D")}

)} - {events.map((event) => ( -
-
- {parseTime(event.meetings[0].startTime)} -
-
- {event.course.subject} {event.course.number} + {events.map((event, index) => { + const color = getColor(event.subject, event.number); + + return ( +
+
{parseTime(event.startTime)}
+
+ {event.subject} {event.number} +
-
- ))} + ); + })}
); } diff --git a/frontend/src/app/Schedule/Calendar/Week/Week.module.scss b/frontend/src/app/Schedule/Manage/Calendar/Week/Week.module.scss similarity index 100% rename from frontend/src/app/Schedule/Calendar/Week/Week.module.scss rename to frontend/src/app/Schedule/Manage/Calendar/Week/Week.module.scss diff --git a/frontend/src/app/Schedule/Calendar/Week/index.tsx b/frontend/src/app/Schedule/Manage/Calendar/Week/index.tsx similarity index 100% rename from frontend/src/app/Schedule/Calendar/Week/index.tsx rename to frontend/src/app/Schedule/Manage/Calendar/Week/index.tsx diff --git a/frontend/src/app/Schedule/Calendar/calendar.ts b/frontend/src/app/Schedule/Manage/Calendar/calendar.ts similarity index 100% rename from frontend/src/app/Schedule/Calendar/calendar.ts rename to frontend/src/app/Schedule/Manage/Calendar/calendar.ts diff --git a/frontend/src/app/Schedule/Calendar/index.tsx b/frontend/src/app/Schedule/Manage/Calendar/index.tsx similarity index 74% rename from frontend/src/app/Schedule/Calendar/index.tsx rename to frontend/src/app/Schedule/Manage/Calendar/index.tsx index 444fa04f9..f41411811 100644 --- a/frontend/src/app/Schedule/Calendar/index.tsx +++ b/frontend/src/app/Schedule/Manage/Calendar/index.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from "react"; +import { MinusSquareDashed, MinusSquareSolid } from "iconoir-react"; import moment from "moment"; import { ISection } from "@/lib/api"; @@ -47,33 +48,44 @@ export default function Calendar({ ccn, } = section; - for (const exam of exams) { - const { date, startTime, endTime } = exam; + for (const meeting of meetings) { + const { days, startTime, endTime } = meeting; events.push({ - date, + startDate, + endDate, subject: subject, number: number, active: currentSection?.ccn === ccn, + days, startTime, endTime, - startDate, - endDate, }); } - for (const meeting of meetings) { - const { days, startTime, endTime } = meeting; + const filteredExams = exams.filter(function (exam, index) { + return ( + exams.findIndex( + ({ date, startTime, endTime }) => + date === exam.date && + exam.startTime === startTime && + exam.endTime === endTime + ) == index + ); + }); + + for (const exam of filteredExams) { + const { date, startTime, endTime } = exam; events.push({ - startDate, - endDate, + date, subject: subject, number: number, active: currentSection?.ccn === ccn, - days, startTime, endTime, + startDate, + endDate, }); } @@ -94,11 +106,12 @@ export default function Calendar({ const day = { date: moment(current), events: events - .filter( - ({ startDate, endDate, date, days }) => - current.isSameOrAfter(startDate) && - current.isSameOrBefore(endDate) && - (moment(date).isSame(current, "day") || days?.[current.day()]) + .filter(({ startDate, endDate, date, days }) => + date + ? moment(parseInt(date)).isSame(current, "day") + : current.isSameOrAfter(startDate) && + current.isSameOrBefore(endDate) && + days?.[current.day()] ) .sort((a, b) => a.startTime.localeCompare(b.startTime)), }; @@ -117,6 +130,19 @@ export default function Calendar({ return (
+
+
+ + Section +
+
+ + Exam +
+
+ All times are listed in Pacific Standard Time (PST). +
+
Sunday
Monday
diff --git a/frontend/src/app/Schedule/Schedule.module.scss b/frontend/src/app/Schedule/Manage/Manage.module.scss similarity index 100% rename from frontend/src/app/Schedule/Schedule.module.scss rename to frontend/src/app/Schedule/Manage/Manage.module.scss diff --git a/frontend/src/app/Schedule/Map/Map.module.scss b/frontend/src/app/Schedule/Manage/Map/Map.module.scss similarity index 100% rename from frontend/src/app/Schedule/Map/Map.module.scss rename to frontend/src/app/Schedule/Manage/Map/Map.module.scss diff --git a/frontend/src/app/Schedule/Map/index.tsx b/frontend/src/app/Schedule/Manage/Map/index.tsx similarity index 100% rename from frontend/src/app/Schedule/Map/index.tsx rename to frontend/src/app/Schedule/Manage/Map/index.tsx diff --git a/frontend/src/app/Schedule/SideBar/Catalog/Catalog.module.scss b/frontend/src/app/Schedule/Manage/SideBar/Catalog/Catalog.module.scss similarity index 100% rename from frontend/src/app/Schedule/SideBar/Catalog/Catalog.module.scss rename to frontend/src/app/Schedule/Manage/SideBar/Catalog/Catalog.module.scss diff --git a/frontend/src/app/Schedule/SideBar/Catalog/index.tsx b/frontend/src/app/Schedule/Manage/SideBar/Catalog/index.tsx similarity index 100% rename from frontend/src/app/Schedule/SideBar/Catalog/index.tsx rename to frontend/src/app/Schedule/Manage/SideBar/Catalog/index.tsx diff --git a/frontend/src/app/Schedule/SideBar/Class/Class.module.scss b/frontend/src/app/Schedule/Manage/SideBar/Class/Class.module.scss similarity index 100% rename from frontend/src/app/Schedule/SideBar/Class/Class.module.scss rename to frontend/src/app/Schedule/Manage/SideBar/Class/Class.module.scss diff --git a/frontend/src/app/Schedule/SideBar/Class/Section/Section.module.scss b/frontend/src/app/Schedule/Manage/SideBar/Class/Section/Section.module.scss similarity index 100% rename from frontend/src/app/Schedule/SideBar/Class/Section/Section.module.scss rename to frontend/src/app/Schedule/Manage/SideBar/Class/Section/Section.module.scss diff --git a/frontend/src/app/Schedule/SideBar/Class/Section/index.tsx b/frontend/src/app/Schedule/Manage/SideBar/Class/Section/index.tsx similarity index 100% rename from frontend/src/app/Schedule/SideBar/Class/Section/index.tsx rename to frontend/src/app/Schedule/Manage/SideBar/Class/Section/index.tsx diff --git a/frontend/src/app/Schedule/SideBar/Class/index.tsx b/frontend/src/app/Schedule/Manage/SideBar/Class/index.tsx similarity index 98% rename from frontend/src/app/Schedule/SideBar/Class/index.tsx rename to frontend/src/app/Schedule/Manage/SideBar/Class/index.tsx index 1e75d736e..049f33661 100644 --- a/frontend/src/app/Schedule/SideBar/Class/index.tsx +++ b/frontend/src/app/Schedule/Manage/SideBar/Class/index.tsx @@ -64,7 +64,7 @@ export default function Class({
diff --git a/frontend/src/app/Schedule/SideBar/SideBar.module.scss b/frontend/src/app/Schedule/Manage/SideBar/SideBar.module.scss similarity index 100% rename from frontend/src/app/Schedule/SideBar/SideBar.module.scss rename to frontend/src/app/Schedule/Manage/SideBar/SideBar.module.scss diff --git a/frontend/src/app/Schedule/SideBar/index.tsx b/frontend/src/app/Schedule/Manage/SideBar/index.tsx similarity index 100% rename from frontend/src/app/Schedule/SideBar/index.tsx rename to frontend/src/app/Schedule/Manage/SideBar/index.tsx diff --git a/frontend/src/app/Schedule/Manage/index.tsx b/frontend/src/app/Schedule/Manage/index.tsx new file mode 100644 index 000000000..ba99f38b0 --- /dev/null +++ b/frontend/src/app/Schedule/Manage/index.tsx @@ -0,0 +1,250 @@ +import { useCallback, useRef, useState } from "react"; + +import { useApolloClient } from "@apollo/client"; +import { + ArrowLeft, + Copy, + EditPencil, + ShareIos, + ViewColumns2, +} from "iconoir-react"; +import { Link, useOutletContext } from "react-router-dom"; + +import Button from "@/components/Button"; +import IconButton from "@/components/IconButton"; +import MenuItem from "@/components/MenuItem"; +import Week from "@/components/Week"; +import { GET_CLASS, IClass, ICourse, ISection } from "@/lib/api"; +import { getY } from "@/lib/schedule"; + +import { ScheduleContextType } from "../schedule"; +import Calendar from "./Calendar"; +import styles from "./Manage.module.scss"; +import Map from "./Map"; +import SideBar from "./SideBar"; + +export default function Manage() { + const { + selectedSections, + setSelectedSections, + classes, + setClasses, + expanded, + setExpanded, + } = useOutletContext(); + + const [currentSection, setCurrentSection] = useState(null); + const apolloClient = useApolloClient(); + const [tab, setTab] = useState(0); + + // Radix and Floating UI reference the boundary by id + const bodyRef = useRef(null); + + const handleSectionSelect = useCallback( + (section: ISection) => { + // Clear the current section + setCurrentSection(null); + + setSelectedSections((selectedSections) => { + // Ignore selected sections + const ignored = selectedSections.some( + (selectedSection) => selectedSection.ccn === section.ccn + ); + + if (ignored) return selectedSections; + + return [ + ...selectedSections.filter( + (selectedSection) => + !( + selectedSection.course.subject === section.course.subject && + selectedSection.course.number === section.course.number && + selectedSection.class.number === section.class.number && + selectedSection.component == section.component + ) + ), + section, + ]; + }); + }, + [setSelectedSections, setCurrentSection] + ); + + const handleSectionMouseOver = useCallback( + (section: ISection) => { + // Jump to the section + if ( + tab === 0 && + section.meetings[0].startTime && + section.meetings[0].endTime + ) { + const top = getY(section.meetings[0].startTime); + + const offset = (getY(section.meetings[0].endTime) - top) / 2; + + bodyRef.current?.scrollTo({ + top: top + offset - bodyRef.current.clientHeight / 2, + behavior: "smooth", + }); + } + + // Ignore selected sections + if ( + selectedSections.some( + (selectedSection) => selectedSection.ccn === section.ccn + ) + ) + return; + + setCurrentSection(section); + }, + [setCurrentSection, selectedSections, tab] + ); + + const handleClassSelect = useCallback( + async (course: ICourse, number: string) => { + // Fetch the selected class + const { data } = await apolloClient.query<{ class: IClass }>({ + query: GET_CLASS, + variables: { + term: { + semester: "Spring", + year: 2024, + }, + subject: course.subject, + courseNumber: course.number, + classNumber: number, + }, + }); + + if (!data) return; + + // Move existing classes to the top rather than duplicating them + const index = classes.findIndex( + (class_) => + class_.course.subject === course.subject && + class_.course.number === course.number && + class_.number === number + ); + + setExpanded((expandedClasses) => { + const _expandedClasses = structuredClone(expandedClasses); + if (index !== -1) _expandedClasses.splice(index, 1); + return [true, ...expandedClasses]; + }); + + setClasses((classes) => { + const _classes = structuredClone(classes); + if (index !== -1) _classes.splice(index, 1); + return [data.class, ...classes]; + }); + + // Add new classes to the top expanded + if (index !== -1) return; + + const { primarySection, sections } = data.class; + + const clonedPrimarySection = structuredClone(primarySection); + + // @ts-expect-error - Hack to set the class number + clonedPrimarySection.class = { number }; + + // Add the primary section to selected sections + setSelectedSections((sections) => [...sections, clonedPrimarySection]); + + const kinds = Array.from( + new Set(sections.map((section) => section.component)) + ); + + // Add the first section of each kind to selected sections + for (const kind of kinds) { + const section = sections + .filter((section) => section.component === kind) + .sort((a, b) => a.number.localeCompare(b.number))[0]; + + const clonedSection = structuredClone(section); + + // @ts-expect-error - Hack to set the class number + clonedSection.class = { number }; + + setSelectedSections((sections) => [...sections, clonedSection]); + } + }, + [apolloClient, setClasses, classes, setSelectedSections, setExpanded] + ); + + const handleExpandedChange = (index: number, expanded: boolean) => { + setExpanded((expandedClasses) => { + const _expandedClasses = structuredClone(expandedClasses); + _expandedClasses[index] = expanded; + return _expandedClasses; + }); + }; + + return ( +
+
+
+ + + +

Untitled Spring 2024 schedule

+ + + +
+
+
+ setTab(0)}> + Schedule + + setTab(1)}> + Calendar + + setTab(2)}> + Map + +
+ + + +
+
+ setCurrentSection(null)} + /> +
+ {tab === 0 ? ( + + ) : tab === 1 ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/frontend/src/app/Schedule/index.tsx b/frontend/src/app/Schedule/index.tsx index 4bb9f3340..a2aa867fa 100644 --- a/frontend/src/app/Schedule/index.tsx +++ b/frontend/src/app/Schedule/index.tsx @@ -1,246 +1,28 @@ -import { useCallback, useRef, useState } from "react"; +import { useState } from "react"; -import { useApolloClient } from "@apollo/client"; -import { - ArrowLeft, - Copy, - EditPencil, - ShareIos, - ViewColumns2, -} from "iconoir-react"; +import { Outlet } from "react-router"; -import Button from "@/components/Button"; -import IconButton from "@/components/IconButton"; -import MenuItem from "@/components/MenuItem"; -import Week from "@/components/Week"; -import { GET_CLASS, IClass, ICourse, ISection } from "@/lib/api"; -import { getY } from "@/lib/schedule"; +import { IClass, ISection } from "@/lib/api"; -import Calendar from "./Calendar"; -import Map from "./Map"; -import styles from "./Schedule.module.scss"; -import SideBar from "./SideBar"; +import { ScheduleContextType } from "./schedule"; export default function Schedule() { - const apolloClient = useApolloClient(); - const [tab, setTab] = useState(0); - - // Radix and Floating UI reference the boundary by id - const bodyRef = useRef(null); - + const [selectedSections, setSelectedSections] = useState([]); const [classes, setClasses] = useState([]); const [expanded, setExpanded] = useState([]); - const [selectedSections, setSelectedSections] = useState([]); - const [currentSection, setCurrentSection] = useState(null); - - console.log(JSON.stringify(selectedSections)); - - const handleSectionSelect = useCallback( - (section: ISection) => { - // Clear the current section - setCurrentSection(null); - - setSelectedSections((selectedSections) => { - // Ignore selected sections - const ignored = selectedSections.some( - (selectedSection) => selectedSection.ccn === section.ccn - ); - - if (ignored) return selectedSections; - - return [ - ...selectedSections.filter( - (selectedSection) => - !( - selectedSection.course.subject === section.course.subject && - selectedSection.course.number === section.course.number && - selectedSection.class.number === section.class.number && - selectedSection.component == section.component - ) - ), - section, - ]; - }); - }, - [setSelectedSections] - ); - - const handleSectionMouseOver = useCallback( - (section: ISection) => { - // Jump to the section - if ( - tab === 0 && - section.meetings[0].startTime && - section.meetings[0].endTime - ) { - const top = getY(section.meetings[0].startTime); - - const offset = (getY(section.meetings[0].endTime) - top) / 2; - - bodyRef.current?.scrollTo({ - top: top + offset - bodyRef.current.clientHeight / 2, - behavior: "smooth", - }); - } - - // Ignore selected sections - if ( - selectedSections.some( - (selectedSection) => selectedSection.ccn === section.ccn - ) - ) - return; - - setCurrentSection(section); - }, - [setCurrentSection, selectedSections, tab] - ); - - const handleClassSelect = useCallback( - async (course: ICourse, number: string) => { - // Fetch the selected class - const { data } = await apolloClient.query<{ class: IClass }>({ - query: GET_CLASS, - variables: { - term: { - semester: "Spring", - year: 2024, - }, - subject: course.subject, - courseNumber: course.number, - classNumber: number, - }, - }); - - if (!data) return; - - // Move existing classes to the top rather than duplicating them - const index = classes.findIndex( - (class_) => - class_.course.subject === course.subject && - class_.course.number === course.number && - class_.number === number - ); - - setExpanded((expandedClasses) => { - const _expandedClasses = structuredClone(expandedClasses); - if (index !== -1) _expandedClasses.splice(index, 1); - return [true, ...expandedClasses]; - }); - - setClasses((classes) => { - const _classes = structuredClone(classes); - if (index !== -1) _classes.splice(index, 1); - return [data.class, ...classes]; - }); - - // Add new classes to the top expanded - if (index !== -1) return; - - const { primarySection, sections } = data.class; - - const clonedPrimarySection = structuredClone(primarySection); - - // @ts-expect-error - Hack to set the class number - clonedPrimarySection.class = { number }; - - // Add the primary section to selected sections - setSelectedSections((sections) => [...sections, clonedPrimarySection]); - - const kinds = Array.from( - new Set(sections.map((section) => section.component)) - ); - - // Add the first section of each kind to selected sections - for (const kind of kinds) { - const section = sections - .filter((section) => section.component === kind) - .sort((a, b) => a.number.localeCompare(b.number))[0]; - - const clonedSection = structuredClone(section); - - // @ts-expect-error - Hack to set the class number - clonedSection.class = { number }; - - setSelectedSections((sections) => [...sections, clonedSection]); - } - }, - [apolloClient, setClasses, classes, setSelectedSections] - ); - - const handleExpandedChange = (index: number, expanded: boolean) => { - setExpanded((expandedClasses) => { - const _expandedClasses = structuredClone(expandedClasses); - _expandedClasses[index] = expanded; - return _expandedClasses; - }); - }; - return ( -
-
-
- - - -

Untitled Spring 2024 schedule

- - - -
-
-
- setTab(0)}> - Schedule - - setTab(1)}> - Calendar - - setTab(2)}> - Map - -
- - - -
-
- setCurrentSection(null)} - /> -
- {tab === 0 ? ( - - ) : tab === 1 ? ( - - ) : ( - - )} -
-
-
+ ); } diff --git a/frontend/src/app/Schedule/schedule.ts b/frontend/src/app/Schedule/schedule.ts new file mode 100644 index 000000000..394870cf0 --- /dev/null +++ b/frontend/src/app/Schedule/schedule.ts @@ -0,0 +1,12 @@ +import { Dispatch, SetStateAction } from "react"; + +import { IClass, ISection } from "@/lib/api"; + +export interface ScheduleContextType { + selectedSections: ISection[]; + setSelectedSections: Dispatch>; + classes: IClass[]; + setClasses: Dispatch>; + expanded: boolean[]; + setExpanded: Dispatch>; +} diff --git a/frontend/src/app/Schedules/Compare/index.tsx b/frontend/src/app/Schedules/Compare/index.tsx deleted file mode 100644 index fcdffaf67..000000000 --- a/frontend/src/app/Schedules/Compare/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -import { ArrowLeft, DataTransferBoth, Xmark } from "iconoir-react"; - -import IconButton from "@/components/IconButton"; -import Week from "@/components/Week"; - -import styles from "./Compare.module.scss"; - -const leftSelectedSections = JSON.parse( - `[{"__typename":"Section","course":{"__typename":"Course","subject":"COMPSCI","number":"61B"},"reservations":[{"__typename":"Reservation","enrollCount":301,"enrollMax":1,"group":"Electrical Engineering & Computer Science, EECS/Materials Science & Engineering, and EECS/Nuclear Engineering Majors; and Undeclared Students in the College of Engineering"},{"__typename":"Reservation","enrollCount":32,"enrollMax":1,"group":"Non-EECS Declared Engineering Majors"},{"__typename":"Reservation","enrollCount":16,"enrollMax":1,"group":"Students declared in the Computer Science BA major"},{"__typename":"Reservation","enrollCount":0,"enrollMax":1,"group":"Intended L&S Computer Science Undergraduates with 1-2 Terms in Attendance"},{"__typename":"Reservation","enrollCount":617,"enrollMax":1,"group":"Undeclared Students in the College of Letters & Science"}],"ccn":15660,"enrollCount":1393,"enrollMax":1500,"meetings":[{"__typename":"Meeting","days":[false,true,false,true,false,true,false],"location":"Dwinelle 155","endTime":"13:59:00","startTime":"13:00:00","instructors":[{"__typename":"Instructor","familyName":"Yokota","givenName":"Justin"},{"__typename":"Instructor","familyName":"Kao","givenName":"Peyrin"}]}],"component":"LEC","primary":true,"waitlistCount":0,"waitlistMax":700,"number":"001","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"COMPSCI","number":"61B"},"ccn":16493,"enrollCount":0,"enrollMax":1,"meetings":[{"__typename":"Meeting","days":[false,false,false,true,false,false,false],"endTime":"15:59:00","startTime":"14:00:00","location":"Soda 271","instructors":[]}],"primary":false,"component":"LAB","waitlistCount":0,"waitlistMax":0,"number":"101L","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"COMPSCI","number":"61B"},"ccn":16492,"enrollCount":0,"enrollMax":1,"meetings":[{"__typename":"Meeting","days":[false,false,true,false,false,false,false],"endTime":"09:59:00","startTime":"09:00:00","location":"Soda 320","instructors":[]}],"primary":false,"component":"DIS","waitlistCount":0,"waitlistMax":0,"number":"101","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"EPS","number":"C20"},"reservations":[],"ccn":20537,"enrollCount":455,"enrollMax":453,"meetings":[{"__typename":"Meeting","days":[false,false,true,false,true,false,false],"location":"Internet/Online","endTime":"15:29:00","startTime":"14:00:00","instructors":[{"__typename":"Instructor","familyName":"Burgmann","givenName":"Roland"}]}],"component":"LEC","primary":true,"waitlistCount":0,"waitlistMax":100,"number":"001","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}}]` -); - -const rightSelectedSections = JSON.parse( - `[{"__typename":"Section","course":{"__typename":"Course","subject":"COMPSCI","number":"61B"},"reservations":[{"__typename":"Reservation","enrollCount":301,"enrollMax":1,"group":"Electrical Engineering & Computer Science, EECS/Materials Science & Engineering, and EECS/Nuclear Engineering Majors; and Undeclared Students in the College of Engineering"},{"__typename":"Reservation","enrollCount":32,"enrollMax":1,"group":"Non-EECS Declared Engineering Majors"},{"__typename":"Reservation","enrollCount":16,"enrollMax":1,"group":"Students declared in the Computer Science BA major"},{"__typename":"Reservation","enrollCount":0,"enrollMax":1,"group":"Intended L&S Computer Science Undergraduates with 1-2 Terms in Attendance"},{"__typename":"Reservation","enrollCount":617,"enrollMax":1,"group":"Undeclared Students in the College of Letters & Science"}],"ccn":15660,"enrollCount":1393,"enrollMax":1500,"meetings":[{"__typename":"Meeting","days":[false,true,false,true,false,true,false],"location":"Dwinelle 155","endTime":"13:59:00","startTime":"13:00:00","instructors":[{"__typename":"Instructor","familyName":"Yokota","givenName":"Justin"},{"__typename":"Instructor","familyName":"Kao","givenName":"Peyrin"}]}],"component":"LEC","primary":true,"waitlistCount":0,"waitlistMax":700,"number":"001","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"COMPSCI","number":"61B"},"ccn":16493,"enrollCount":0,"enrollMax":1,"meetings":[{"__typename":"Meeting","days":[false,false,false,true,false,false,false],"endTime":"15:59:00","startTime":"14:00:00","location":"Soda 271","instructors":[]}],"primary":false,"component":"LAB","waitlistCount":0,"waitlistMax":0,"number":"101L","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"COMPSCI","number":"61B"},"ccn":16492,"enrollCount":0,"enrollMax":1,"meetings":[{"__typename":"Meeting","days":[false,false,true,false,false,false,false],"endTime":"09:59:00","startTime":"09:00:00","location":"Soda 320","instructors":[]}],"primary":false,"component":"DIS","waitlistCount":0,"waitlistMax":0,"number":"101","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"EPS","number":"C20"},"reservations":[],"ccn":20537,"enrollCount":455,"enrollMax":453,"meetings":[{"__typename":"Meeting","days":[false,false,true,false,true,false,false],"location":"Internet/Online","endTime":"15:29:00","startTime":"14:00:00","instructors":[{"__typename":"Instructor","familyName":"Burgmann","givenName":"Roland"}]}],"component":"LEC","primary":true,"waitlistCount":0,"waitlistMax":100,"number":"001","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"PHILOS","number":"100"},"reservations":[{"__typename":"Reservation","enrollCount":49,"enrollMax":50,"group":"Philosophy Majors"}],"ccn":21697,"enrollCount":50,"enrollMax":50,"meetings":[{"__typename":"Meeting","days":[false,false,false,true,false,false,false],"location":"Dwinelle 88","endTime":"17:59:00","startTime":"16:00:00","instructors":[{"__typename":"Instructor","familyName":"Dasgupta","givenName":"Shamik"},{"__typename":"Instructor","familyName":"DeBrine","givenName":"Hannah"},{"__typename":"Instructor","familyName":"Huang","givenName":"Anhui"},{"__typename":"Instructor","familyName":"McIntosh","givenName":"Russell"},{"__typename":"Instructor","familyName":"Strelau","givenName":"Klaus"},{"__typename":"Instructor","familyName":"von Gotz","givenName":"Aglaia"}]}],"component":"LEC","primary":true,"waitlistCount":0,"waitlistMax":0,"number":"001","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}},{"__typename":"Section","course":{"__typename":"Course","subject":"PHILOS","number":"100"},"ccn":21698,"enrollCount":50,"enrollMax":50,"meetings":[{"__typename":"Meeting","days":[false,false,false,false,false,false,false],"endTime":"00:00:00","startTime":"00:00:00","location":null,"instructors":[]}],"primary":false,"component":"TUT","waitlistCount":0,"waitlistMax":2,"number":"101","startDate":"2024-01-16T00:00:00.000Z","endDate":"2024-05-03T00:00:00.000Z","class":{"number":"001"}}]` -); - -export default function Compare() { - const [y, setY] = useState(null); - const leftRef = useRef(null); - const rightRef = useRef(null); - const [current, setCurrent] = useState(null); - - useEffect(() => { - if (!leftRef.current || !rightRef.current) return; - - const left = leftRef.current; - const right = rightRef.current; - - const handleScroll = (left?: boolean) => { - if (!leftRef.current || !rightRef.current) return; - - if (left) { - if (current === 1) return; - - rightRef.current.scrollTo({ - top: leftRef.current.scrollTop, - left: leftRef.current.scrollLeft, - }); - - return; - } - - if (current === 0) return; - - leftRef.current.scrollTo({ - top: rightRef.current.scrollTop, - left: rightRef.current.scrollLeft, - }); - }; - - const handleLeftScroll = () => handleScroll(true); - left.addEventListener("scroll", handleLeftScroll); - - const handleRightScroll = () => handleScroll(); - right.addEventListener("scroll", handleRightScroll); - - return () => { - right.removeEventListener("scroll", handleLeftScroll); - left.removeEventListener("scroll", handleRightScroll); - }; - }, [current]); - - return ( -
-
-
- - - -

Untitled Spring 2024 schedule

-

Spring 2024

- - - - - - -
-
-

No schedule selected

- - - - - - -
-
-
-
setCurrent(0)} - onMouseLeave={() => - setCurrent((previous) => (previous === 0 ? null : previous)) - } - > - -
-
setCurrent(1)} - onMouseLeave={() => - setCurrent((previous) => (previous === 1 ? null : previous)) - } - > - -
-
-
- ); -} diff --git a/frontend/src/components/Week/Event/index.tsx b/frontend/src/components/Week/Event/index.tsx index 423ce3663..7da4785a2 100644 --- a/frontend/src/components/Week/Event/index.tsx +++ b/frontend/src/components/Week/Event/index.tsx @@ -36,7 +36,7 @@ export default function Event({ className={classNames(styles.trigger, { [styles.active]: active })} style={{ top: `${top + 1}px`, - backgroundColor: getColor(`${course.subject} ${course.number}`), + backgroundColor: getColor(course.subject, course.number), height: `${height - 2}px`, width: `calc((100% - 4px - ${columns - 1} * 2px) / ${columns})`, left: `calc(2px + calc((100% - 4px - ${columns - 1} * 2px) / ${columns}) * ${position} + 2px * ${position})`, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 44ae9712e..1e3ad4249 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -247,6 +247,13 @@ export const GET_CLASS = gql` givenName } } + exams { + date + final + location + startTime + endTime + } component primary waitlistCount diff --git a/frontend/src/lib/section.ts b/frontend/src/lib/section.ts index 5ec5ecae3..b18f8aa04 100644 --- a/frontend/src/lib/section.ts +++ b/frontend/src/lib/section.ts @@ -29,6 +29,23 @@ const colors = [ "var(--violet-500)", "var(--fuchsia-500)", "var(--rose-500)", + "var(--red-700)", + "var(--orange-700)", + "var(--yellow-700)", + "var(--green-700)", + "var(--teal-700)", + "var(--blue-700)", + "var(--indigo-700)", + "var(--purple-700)", + "var(--pink-700)", + "var(--amber-700)", + "var(--lime-700)", + "var(--emerald-700)", + "var(--cyan-700)", + "var(--sky-700)", + "var(--violet-700)", + "var(--fuchsia-700)", + "var(--rose-700)", ]; export const getColor = (subject: string, number: string) => {