diff --git a/.husky/pre-commit b/.husky/pre-commit index 3723623..8c0fe37 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ -yarn lint-staged +#!/bin/sh +yarn lint-staged \ No newline at end of file diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 346d39c..253932b 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -22,6 +22,10 @@ const PAGES = [ label: 'Transfer Courses', href: '/transfer-courses', }, + { + label: 'Classrooms', + href: '/classroom-schedules', + }, { label: 'About', href: '/about', @@ -36,7 +40,7 @@ const getTermDisplayName = (term: IPotentialFutureTerm) => { return `${SEMESTER_DISPLAY_MAPPING[term.semester]} ${term.year}`; }; -const PATHS_THAT_REQUIRE_TERM_SELECTOR = new Set(['/', '/help/registration-script']); +const PATHS_THAT_REQUIRE_TERM_SELECTOR = new Set(['/', '/help/registration-script', '/classroom-schedules']); const Navbar = observer(() => { const router = useRouter(); @@ -62,7 +66,7 @@ const Navbar = observer(() => { return ( - + @@ -87,6 +91,7 @@ const Navbar = observer(() => { mr={6} mt={{base: 4, md: 0}} color='inherit' + whiteSpace={'nowrap'} > {page.label} @@ -100,7 +105,7 @@ const Navbar = observer(() => { width={{base: 'full', md: 'auto'}} mt={{base: 4, md: 0}} alignItems='center' - flex={{lg: 1}} + flexGrow={1} visibility={(shouldShowCourseSearch || shouldShowTransferSearch) ? 'visible' : 'hidden'} > { @@ -121,8 +126,10 @@ const Navbar = observer(() => { { diff --git a/src/pages/classroom-schedules.tsx b/src/pages/classroom-schedules.tsx new file mode 100644 index 0000000..7ae69c0 --- /dev/null +++ b/src/pages/classroom-schedules.tsx @@ -0,0 +1,221 @@ +import React, {useCallback, useState, useEffect, useMemo} from 'react'; +import {Select, Skeleton, Table, HStack, Flex} from '@chakra-ui/react'; +import {format, add} from 'date-fns'; +import Head from 'next/head'; +import {observer} from 'mobx-react-lite'; +import {NextSeo} from 'next-seo'; +import useCalendar, {CalendarViewType} from '@veccu/react-calendar'; +import useStore from 'src/lib/state/context'; +import CalendarToolbar from 'src/components/basket/calendar/toolbar'; +import MonthView from 'src/components/basket/calendar/views/month'; +import {type ICourseFromAPI, type ISectionFromAPIWithSchedule} from 'src/lib/api-types'; +import occurrenceGeneratorCache from 'src/lib/occurrence-generator-cache'; +import WeekView from '../components/basket/calendar/views/week'; +import styles from '../components/basket/calendar/styles/calendar.module.scss'; + +const isFirstRender = typeof window === 'undefined'; + +const ClassroomSchedules = observer(() => { + const {apiState} = useStore(); + const calendar = useCalendar({defaultViewType: CalendarViewType.Week}); + const [rooms, setRooms] = useState([]); + const [sectionsInRoom, setSectionsInRoom] = useState>([]); + + useEffect(() => { + apiState.setSingleFetchEndpoints(['buildings']); + + if (apiState.selectedTerm?.isFuture) { + apiState.setRecurringFetchEndpoints(['courses']); + } else { + apiState.setRecurringFetchEndpoints(['courses', 'sections']); + } + + setSectionsInRoom([]); + + return () => { + apiState.setSingleFetchEndpoints([]); + apiState.setRecurringFetchEndpoints([]); + }; + }, [apiState.selectedTerm, apiState]); + + let sectionsInBuilding: ISectionFromAPIWithSchedule[] = []; + + const buildings = apiState.buildings; + let selectedBuilding: string; + let selectedRoom: string; + + const handleBuildingSelect = useCallback(async (event: React.ChangeEvent) => { + selectedBuilding = event.target.value; + sectionsInBuilding = apiState.sectionsWithParsedSchedules.filter(section => section.buildingName === selectedBuilding); + + const availableRooms: string[] = []; + for (const section of sectionsInBuilding) { + if (section.room !== null && !availableRooms.includes(section.room)) { + availableRooms.push(section.room); + } + } + + availableRooms.sort(); + setRooms(availableRooms); + setSectionsInRoom([]); + }, []); + + const handleRoomSelect = useCallback(async (event: React.ChangeEvent) => { + selectedRoom = event.target.value; + + const sections = sectionsInBuilding.filter(section => section.room === selectedRoom) + .map(section => ({...section, course: apiState.courseById.get(section.courseId)!})); + setSectionsInRoom(sections); + }, []); + + const firstDate = useMemo(() => { + let start = new Date(); + if (sectionsInRoom.length > 0 && sectionsInRoom[0].parsedTime?.firstDate?.date) { + start = sectionsInRoom[0].parsedTime?.firstDate?.date; + } + + for (const section of sectionsInRoom) { + if (section.parsedTime?.firstDate?.date && section.parsedTime?.lastDate?.date) { + if (section.parsedTime?.firstDate?.date <= new Date() && section.parsedTime?.lastDate?.date >= new Date()) { + return new Date(); + } + + start = section.parsedTime?.firstDate?.date; + } + } + + return start; + }, [sectionsInRoom]); + + useEffect(() => { + if (firstDate) { + calendar.navigation.setDate(firstDate); + } + }, [firstDate]); + + const bodyWithEvents = useMemo(() => ({ + ...calendar.body, + value: calendar.body.value.map(week => ({ + ...week, + value: week.value.map(day => { + const events = []; + + const start = day.value; + const end = add(day.value, {days: 1}); + + for (const section of sectionsInRoom ?? []) { + if (section.parsedTime) { + for (const occurrence of occurrenceGeneratorCache(JSON.stringify(section.time), start, end, section.parsedTime)) { + if (events.filter(event => event.start.toISOString() === occurrence.date.toISOString()).length > 3) { + break; + } + + events.push({ + section, + start: occurrence.date as Date, + end: occurrence.end as Date ?? new Date(), + }); + } + } + } + + return { + ...day, + events: events.sort((a, b) => a.start.getTime() - b.start.getTime()).map(event => ({ + ...event, + key: `${event.section.id}-${event.start.toISOString()}-${event.end.toISOString()}`, + label: `${event.section.course.title} ${event.section.section} (${event.section.course.subject}${event.section.course.crse})`, + })), + }; + }), + })), + }), [calendar.body, sectionsInRoom]); + + return ( + <> + + + + {isFirstRender && ( + <> + + + + )} + + + + + + + + + + + + + + + + + + { + calendar.view.isMonthView && ( + undefined} + /> + ) + } + + { + calendar.view.isWeekView && ( + undefined} + /> + ) + } +
+
+ +
+ + ); +}); + +export default ClassroomSchedules;