Skip to content

Commit

Permalink
Merge pull request #54 from MTUHIDE/classroom-schedule
Browse files Browse the repository at this point in the history
  • Loading branch information
codetheweb authored Nov 17, 2024
2 parents 1649658 + 046e87a commit e04f0d8
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 4 deletions.
3 changes: 2 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
yarn lint-staged
#!/bin/sh
yarn lint-staged
13 changes: 10 additions & 3 deletions src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const PAGES = [
label: 'Transfer Courses',
href: '/transfer-courses',
},
{
label: 'Classrooms',
href: '/classroom-schedules',
},
{
label: 'About',
href: '/about',
Expand All @@ -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();
Expand All @@ -62,7 +66,7 @@ const Navbar = observer(() => {

return (
<Flex align='center' justify='space-between' wrap='wrap' p={4} as='nav' mb={8}>
<Flex flex={{lg: 1}} wrap='wrap' width='100%'>
<Flex flex={{lg: 1}} wrap='wrap' width='100%' flexWrap={{md: 'nowrap', base: 'wrap'}}>
<Box width='40px' height='40px' borderRadius='full' overflow='hidden' mr={5}>
<Logo/>
</Box>
Expand All @@ -87,6 +91,7 @@ const Navbar = observer(() => {
mr={6}
mt={{base: 4, md: 0}}
color='inherit'
whiteSpace={'nowrap'}
>
{page.label}
</Link>
Expand All @@ -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'}
>
{
Expand All @@ -121,8 +126,10 @@ const Navbar = observer(() => {
<HStack
display={{base: isOpen ? 'flex' : 'none', md: 'flex'}}
flex={{lg: 1}}
minWidth={'200px'}
justifyContent='end'
mt={{base: 4, md: 0}}
ml={6}
>

{
Expand Down
221 changes: 221 additions & 0 deletions src/pages/classroom-schedules.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
const [sectionsInRoom, setSectionsInRoom] = useState<Array<ISectionFromAPIWithSchedule & {course: ICourseFromAPI}>>([]);

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<HTMLSelectElement>) => {
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<HTMLSelectElement>) => {
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<Date | undefined>(() => {
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 (
<>
<NextSeo
title='MTU Courses | Classroom Schedules'
description='A listing of when classrooms have classes scheduled in them'
/>

<Head>
{isFirstRender && (
<>
<link rel='preload' href={`${process.env.NEXT_PUBLIC_API_ENDPOINT!}/semesters`} as='fetch' crossOrigin='anonymous'/>
<link rel='preload' href={`${process.env.NEXT_PUBLIC_API_ENDPOINT!}/buildings`} as='fetch' crossOrigin='anonymous'/>
</>
)}
</Head>

<Flex w='100%' flexDir={'column'} justifyContent='center' alignItems='center'>

<Skeleton m='4' display='inline-block' isLoaded={apiState.hasDataForTrackedEndpoints}>
<HStack>
<Select
w='auto'
variant='filled'
placeholder='Select building'
aria-label='Select a building to view'
onChange={handleBuildingSelect}
>
{buildings.map(building => (
<option key={building.name} value={building.name}>{building.name}</option>
))}
</Select>

<Select
w='auto'
variant='filled'
placeholder='Select room'
aria-label='Select a room to view'
onChange={handleRoomSelect}
>
{rooms.map(room => (
<option key={room} value={room}>{room}</option>
))}
</Select>

</HStack>
</Skeleton>

<Skeleton display='inline-block' isLoaded={apiState.hasDataForTrackedEndpoints}>
<CalendarToolbar
navigation={calendar.navigation}
view={calendar.view}
label={format(calendar.cursorDate, 'MMMM yyyy')}/>

<Table
shadow='base'
rounded='md'
w='min-content'
h='100%'
className={styles.table}
>
{
calendar.view.isMonthView && (
<MonthView
body={bodyWithEvents}
headers={calendar.headers}
onEventClick={() => undefined}
/>
)
}

{
calendar.view.isWeekView && (
<WeekView
body={bodyWithEvents}
headers={calendar.headers}
onEventClick={() => undefined}
/>
)
}
</Table>
</Skeleton>

</Flex>
</>
);
});

export default ClassroomSchedules;

0 comments on commit e04f0d8

Please sign in to comment.