|
| 1 | +import React, {useCallback, useState, useEffect, useMemo} from 'react'; |
| 2 | +import {Select, Skeleton, Table, HStack, Flex} from '@chakra-ui/react'; |
| 3 | +import {format, add} from 'date-fns'; |
| 4 | +import Head from 'next/head'; |
| 5 | +import {observer} from 'mobx-react-lite'; |
| 6 | +import {NextSeo} from 'next-seo'; |
| 7 | +import useCalendar, {CalendarViewType} from '@veccu/react-calendar'; |
| 8 | +import useStore from 'src/lib/state/context'; |
| 9 | +import CalendarToolbar from 'src/components/basket/calendar/toolbar'; |
| 10 | +import MonthView from 'src/components/basket/calendar/views/month'; |
| 11 | +import {type ICourseFromAPI, type ISectionFromAPIWithSchedule} from 'src/lib/api-types'; |
| 12 | +import occurrenceGeneratorCache from 'src/lib/occurrence-generator-cache'; |
| 13 | +import WeekView from '../components/basket/calendar/views/week'; |
| 14 | +import styles from '../components/basket/calendar/styles/calendar.module.scss'; |
| 15 | + |
| 16 | +const isFirstRender = typeof window === 'undefined'; |
| 17 | + |
| 18 | +const ClassroomSchedules = observer(() => { |
| 19 | + const {apiState} = useStore(); |
| 20 | + const calendar = useCalendar({defaultViewType: CalendarViewType.Week}); |
| 21 | + const [rooms, setRooms] = useState<string[]>([]); |
| 22 | + const [sectionsInRoom, setSectionsInRoom] = useState<Array<ISectionFromAPIWithSchedule & {course: ICourseFromAPI}>>([]); |
| 23 | + |
| 24 | + useEffect(() => { |
| 25 | + apiState.setSingleFetchEndpoints(['buildings']); |
| 26 | + |
| 27 | + if (apiState.selectedTerm?.isFuture) { |
| 28 | + apiState.setRecurringFetchEndpoints(['courses']); |
| 29 | + } else { |
| 30 | + apiState.setRecurringFetchEndpoints(['courses', 'sections']); |
| 31 | + } |
| 32 | + |
| 33 | + setSectionsInRoom([]); |
| 34 | + |
| 35 | + return () => { |
| 36 | + apiState.setSingleFetchEndpoints([]); |
| 37 | + apiState.setRecurringFetchEndpoints([]); |
| 38 | + }; |
| 39 | + }, [apiState.selectedTerm, apiState]); |
| 40 | + |
| 41 | + let sectionsInBuilding: ISectionFromAPIWithSchedule[] = []; |
| 42 | + |
| 43 | + const buildings = apiState.buildings; |
| 44 | + let selectedBuilding: string; |
| 45 | + let selectedRoom: string; |
| 46 | + |
| 47 | + const handleBuildingSelect = useCallback(async (event: React.ChangeEvent<HTMLSelectElement>) => { |
| 48 | + selectedBuilding = event.target.value; |
| 49 | + sectionsInBuilding = apiState.sectionsWithParsedSchedules.filter(section => section.buildingName === selectedBuilding); |
| 50 | + |
| 51 | + const availableRooms: string[] = []; |
| 52 | + for (const section of sectionsInBuilding) { |
| 53 | + if (section.room !== null && !availableRooms.includes(section.room)) { |
| 54 | + availableRooms.push(section.room); |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + availableRooms.sort(); |
| 59 | + setRooms(availableRooms); |
| 60 | + setSectionsInRoom([]); |
| 61 | + }, []); |
| 62 | + |
| 63 | + const handleRoomSelect = useCallback(async (event: React.ChangeEvent<HTMLSelectElement>) => { |
| 64 | + selectedRoom = event.target.value; |
| 65 | + |
| 66 | + const sections = sectionsInBuilding.filter(section => section.room === selectedRoom) |
| 67 | + .map(section => ({...section, course: apiState.courseById.get(section.courseId)!})); |
| 68 | + setSectionsInRoom(sections); |
| 69 | + }, []); |
| 70 | + |
| 71 | + const firstDate = useMemo<Date | undefined>(() => { |
| 72 | + let start = new Date(); |
| 73 | + if (sectionsInRoom.length > 0 && sectionsInRoom[0].parsedTime?.firstDate?.date) { |
| 74 | + start = sectionsInRoom[0].parsedTime?.firstDate?.date; |
| 75 | + } |
| 76 | + |
| 77 | + for (const section of sectionsInRoom) { |
| 78 | + if (section.parsedTime?.firstDate?.date && section.parsedTime?.lastDate?.date) { |
| 79 | + if (section.parsedTime?.firstDate?.date <= new Date() && section.parsedTime?.lastDate?.date >= new Date()) { |
| 80 | + return new Date(); |
| 81 | + } |
| 82 | + |
| 83 | + start = section.parsedTime?.firstDate?.date; |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + return start; |
| 88 | + }, [sectionsInRoom]); |
| 89 | + |
| 90 | + useEffect(() => { |
| 91 | + if (firstDate) { |
| 92 | + calendar.navigation.setDate(firstDate); |
| 93 | + } |
| 94 | + }, [firstDate]); |
| 95 | + |
| 96 | + const bodyWithEvents = useMemo(() => ({ |
| 97 | + ...calendar.body, |
| 98 | + value: calendar.body.value.map(week => ({ |
| 99 | + ...week, |
| 100 | + value: week.value.map(day => { |
| 101 | + const events = []; |
| 102 | + |
| 103 | + const start = day.value; |
| 104 | + const end = add(day.value, {days: 1}); |
| 105 | + |
| 106 | + for (const section of sectionsInRoom ?? []) { |
| 107 | + if (section.parsedTime) { |
| 108 | + for (const occurrence of occurrenceGeneratorCache(JSON.stringify(section.time), start, end, section.parsedTime)) { |
| 109 | + if (events.filter(event => event.start.toISOString() === occurrence.date.toISOString()).length > 3) { |
| 110 | + break; |
| 111 | + } |
| 112 | + |
| 113 | + events.push({ |
| 114 | + section, |
| 115 | + start: occurrence.date as Date, |
| 116 | + end: occurrence.end as Date ?? new Date(), |
| 117 | + }); |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + return { |
| 123 | + ...day, |
| 124 | + events: events.sort((a, b) => a.start.getTime() - b.start.getTime()).map(event => ({ |
| 125 | + ...event, |
| 126 | + key: `${event.section.id}-${event.start.toISOString()}-${event.end.toISOString()}`, |
| 127 | + label: `${event.section.course.title} ${event.section.section} (${event.section.course.subject}${event.section.course.crse})`, |
| 128 | + })), |
| 129 | + }; |
| 130 | + }), |
| 131 | + })), |
| 132 | + }), [calendar.body, sectionsInRoom]); |
| 133 | + |
| 134 | + return ( |
| 135 | + <> |
| 136 | + <NextSeo |
| 137 | + title='MTU Courses | Classroom Schedules' |
| 138 | + description='A listing of when classrooms have classes scheduled in them' |
| 139 | + /> |
| 140 | + |
| 141 | + <Head> |
| 142 | + {isFirstRender && ( |
| 143 | + <> |
| 144 | + <link rel='preload' href={`${process.env.NEXT_PUBLIC_API_ENDPOINT!}/semesters`} as='fetch' crossOrigin='anonymous'/> |
| 145 | + <link rel='preload' href={`${process.env.NEXT_PUBLIC_API_ENDPOINT!}/buildings`} as='fetch' crossOrigin='anonymous'/> |
| 146 | + </> |
| 147 | + )} |
| 148 | + </Head> |
| 149 | + |
| 150 | + <Flex w='100%' flexDir={'column'} justifyContent='center' alignItems='center'> |
| 151 | + |
| 152 | + <Skeleton m='4' display='inline-block' isLoaded={apiState.hasDataForTrackedEndpoints}> |
| 153 | + <HStack> |
| 154 | + <Select |
| 155 | + w='auto' |
| 156 | + variant='filled' |
| 157 | + placeholder='Select building' |
| 158 | + aria-label='Select a building to view' |
| 159 | + onChange={handleBuildingSelect} |
| 160 | + > |
| 161 | + {buildings.map(building => ( |
| 162 | + <option key={building.name} value={building.name}>{building.name}</option> |
| 163 | + ))} |
| 164 | + </Select> |
| 165 | + |
| 166 | + <Select |
| 167 | + w='auto' |
| 168 | + variant='filled' |
| 169 | + placeholder='Select room' |
| 170 | + aria-label='Select a room to view' |
| 171 | + onChange={handleRoomSelect} |
| 172 | + > |
| 173 | + {rooms.map(room => ( |
| 174 | + <option key={room} value={room}>{room}</option> |
| 175 | + ))} |
| 176 | + </Select> |
| 177 | + |
| 178 | + </HStack> |
| 179 | + </Skeleton> |
| 180 | + |
| 181 | + <Skeleton display='inline-block' isLoaded={apiState.hasDataForTrackedEndpoints}> |
| 182 | + <CalendarToolbar |
| 183 | + navigation={calendar.navigation} |
| 184 | + view={calendar.view} |
| 185 | + label={format(calendar.cursorDate, 'MMMM yyyy')}/> |
| 186 | + |
| 187 | + <Table |
| 188 | + shadow='base' |
| 189 | + rounded='md' |
| 190 | + w='min-content' |
| 191 | + h='100%' |
| 192 | + className={styles.table} |
| 193 | + > |
| 194 | + { |
| 195 | + calendar.view.isMonthView && ( |
| 196 | + <MonthView |
| 197 | + body={bodyWithEvents} |
| 198 | + headers={calendar.headers} |
| 199 | + onEventClick={() => undefined} |
| 200 | + /> |
| 201 | + ) |
| 202 | + } |
| 203 | + |
| 204 | + { |
| 205 | + calendar.view.isWeekView && ( |
| 206 | + <WeekView |
| 207 | + body={bodyWithEvents} |
| 208 | + headers={calendar.headers} |
| 209 | + onEventClick={() => undefined} |
| 210 | + /> |
| 211 | + ) |
| 212 | + } |
| 213 | + </Table> |
| 214 | + </Skeleton> |
| 215 | + |
| 216 | + </Flex> |
| 217 | + </> |
| 218 | + ); |
| 219 | +}); |
| 220 | + |
| 221 | +export default ClassroomSchedules; |
0 commit comments