Skip to content

Commit e04f0d8

Browse files
authored
Merge pull request #54 from MTUHIDE/classroom-schedule
2 parents 1649658 + 046e87a commit e04f0d8

File tree

3 files changed

+233
-4
lines changed

3 files changed

+233
-4
lines changed

.husky/pre-commit

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
yarn lint-staged
1+
#!/bin/sh
2+
yarn lint-staged

src/components/navbar.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const PAGES = [
2222
label: 'Transfer Courses',
2323
href: '/transfer-courses',
2424
},
25+
{
26+
label: 'Classrooms',
27+
href: '/classroom-schedules',
28+
},
2529
{
2630
label: 'About',
2731
href: '/about',
@@ -36,7 +40,7 @@ const getTermDisplayName = (term: IPotentialFutureTerm) => {
3640
return `${SEMESTER_DISPLAY_MAPPING[term.semester]} ${term.year}`;
3741
};
3842

39-
const PATHS_THAT_REQUIRE_TERM_SELECTOR = new Set(['/', '/help/registration-script']);
43+
const PATHS_THAT_REQUIRE_TERM_SELECTOR = new Set(['/', '/help/registration-script', '/classroom-schedules']);
4044

4145
const Navbar = observer(() => {
4246
const router = useRouter();
@@ -62,7 +66,7 @@ const Navbar = observer(() => {
6266

6367
return (
6468
<Flex align='center' justify='space-between' wrap='wrap' p={4} as='nav' mb={8}>
65-
<Flex flex={{lg: 1}} wrap='wrap' width='100%'>
69+
<Flex flex={{lg: 1}} wrap='wrap' width='100%' flexWrap={{md: 'nowrap', base: 'wrap'}}>
6670
<Box width='40px' height='40px' borderRadius='full' overflow='hidden' mr={5}>
6771
<Logo/>
6872
</Box>
@@ -87,6 +91,7 @@ const Navbar = observer(() => {
8791
mr={6}
8892
mt={{base: 4, md: 0}}
8993
color='inherit'
94+
whiteSpace={'nowrap'}
9095
>
9196
{page.label}
9297
</Link>
@@ -100,7 +105,7 @@ const Navbar = observer(() => {
100105
width={{base: 'full', md: 'auto'}}
101106
mt={{base: 4, md: 0}}
102107
alignItems='center'
103-
flex={{lg: 1}}
108+
flexGrow={1}
104109
visibility={(shouldShowCourseSearch || shouldShowTransferSearch) ? 'visible' : 'hidden'}
105110
>
106111
{
@@ -121,8 +126,10 @@ const Navbar = observer(() => {
121126
<HStack
122127
display={{base: isOpen ? 'flex' : 'none', md: 'flex'}}
123128
flex={{lg: 1}}
129+
minWidth={'200px'}
124130
justifyContent='end'
125131
mt={{base: 4, md: 0}}
132+
ml={6}
126133
>
127134

128135
{

src/pages/classroom-schedules.tsx

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

Comments
 (0)