Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate larc #1068

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/antalmanac/src/components/Calendar/CalendarToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ function CalendarPaneToolbar(props: CalendarPaneToolbarProps) {
flexWrap: 'wrap',
gap: 1,
alignItems: 'center',
padding: 1,
paddingX: 1.5,
paddingY: 1,
borderRadius: '4px 4px 0 0',
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SearchForm from './SearchForm/SearchForm';
import { openSnackbar } from '$actions/AppStoreActions';
import analyticsEnum, { logAnalytics } from '$lib/analytics';
import { Grades } from '$lib/grades';
import { Larc } from '$lib/larc';
import { WebSOC } from '$lib/websoc';
import { useCoursePaneStore } from '$stores/CoursePaneStore';

Expand All @@ -35,6 +36,7 @@ export function CoursePaneRoot() {
});
WebSOC.clearCache();
Grades.clearCache();
Larc.clearCache();
forceUpdate();
}, [forceUpdate]);

Expand Down
213 changes: 51 additions & 162 deletions apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import { Close } from '@mui/icons-material';
import { Alert, Box, IconButton, useMediaQuery } from '@mui/material';
import { AACourse, AASection, WebsocDepartment, WebsocSchool, WebsocAPIResponse, GE } from '@packages/antalmanac-types';
import { Box } from '@mui/material';
import {
AACourse,
AASection,
WebsocDepartment,
WebsocSchool,
WebsocAPIResponse,
GE,
LarcAPIResponse,
} from '@packages/antalmanac-types';
import { useCallback, useEffect, useState } from 'react';
import LazyLoad from 'react-lazyload';

import RightPaneStore from '../RightPaneStore';
import GeDataFetchProvider from '../SectionTable/GEDataFetchProvider';
import SectionTableLazyWrapper from '../SectionTable/SectionTableLazyWrapper';

import SchoolDeptCard from './SchoolDeptCard';
import darkModeLoadingGif from './SearchForm/Gifs/dark-loading.gif';
import loadingGif from './SearchForm/Gifs/loading.gif';
import darkNoNothing from './static/dark-no_results.png';
import noNothing from './static/no_results.png';

import { openSnackbar } from '$actions/AppStoreActions';
import analyticsEnum from '$lib/analytics';
import { ErrorMessage } from '$components/RightPane/CoursePane/Messages/ErrorMessage';
import { LoadingMessage } from '$components/RightPane/CoursePane/Messages/LoadingMessage';
import { RecruitmentBanner } from '$components/RightPane/CoursePane/RecruitmentBanner';
import { SectionTableWrapped } from '$components/RightPane/SectionTable/SectionTableWrapped';
import { Grades } from '$lib/grades';
import { getLocalStorageRecruitmentDismissalTime, setLocalStorageRecruitmentDismissalTime } from '$lib/localStorage';
import { Larc } from '$lib/larc';
import { WebSOC } from '$lib/websoc';
import AppStore from '$stores/AppStore';
import { useHoveredStore } from '$stores/HoveredStore';
import { useThemeStore } from '$stores/SettingsStore';

function getColors() {
const currentCourses = AppStore.schedule.getCurrentCourses();
Expand Down Expand Up @@ -56,131 +57,12 @@ const flattenSOCObject = (SOCObject: WebsocAPIResponse): (WebsocSchool | WebsocD
return accumulator;
}, []);
};
const RecruitmentBanner = () => {
const [bannerVisibility, setBannerVisibility] = useState(true);

const isDark = useThemeStore((store) => store.isDark);

// Display recruitment banner if more than 11 weeks (in ms) has passed since last dismissal
const recruitmentDismissalTime = getLocalStorageRecruitmentDismissalTime();
const dismissedRecently =
recruitmentDismissalTime !== null &&
Date.now() - parseInt(recruitmentDismissalTime) < 11 * 7 * 24 * 3600 * 1000;
const isSearchCS = ['COMPSCI', 'IN4MATX', 'I&C SCI', 'STATS'].includes(
RightPaneStore.getFormData().deptValue.toUpperCase()
);
const displayRecruitmentBanner = bannerVisibility && !dismissedRecently && isSearchCS;

const isMobileScreen = useMediaQuery('(max-width: 750px)');

return (
<Box sx={{ position: 'fixed', bottom: 5, right: isMobileScreen ? 5 : 75, zIndex: 999 }}>
{displayRecruitmentBanner ? (
<Alert
icon={false}
severity="info"
style={{
color: isDark ? '#ece6e6' : '#2e2e2e',
backgroundColor: isDark ? '#2e2e2e' : '#ece6e6',
}}
action={
<IconButton
aria-label="close"
size="small"
color="inherit"
onClick={() => {
setLocalStorageRecruitmentDismissalTime(Date.now().toString());
setBannerVisibility(false);
}}
>
<Close fontSize="inherit" />
</IconButton>
}
>
Interested in web development?
<br />
<a href="https://forms.gle/v32Cx65vwhnmxGPv8" target="__blank" rel="noopener noreferrer">
Join ICSSC and work on AntAlmanac and other projects!
</a>
<br />
We have opportunities for experienced devs and those with zero experience!
</Alert>
) : null}
</Box>
);
};

/* TODO: all this typecasting in the conditionals is pretty messy, but type guards don't really work in this context
* for reasons that are currently beyond me (probably something in the transpiling process that JS doesn't like).
* If you can find a way to make this cleaner, do it.
*/
const SectionTableWrapped = (
index: number,
data: { scheduleNames: string[]; courseData: (WebsocSchool | WebsocDepartment | AACourse)[] }
) => {
const { courseData, scheduleNames } = data;
const formData = RightPaneStore.getFormData();

let component;

if ((courseData[index] as WebsocSchool).departments !== undefined) {
const school = courseData[index] as WebsocSchool;
component = <SchoolDeptCard comment={school.schoolComment} type={'school'} name={school.schoolName} />;
} else if ((courseData[index] as WebsocDepartment).courses !== undefined) {
const dept = courseData[index] as WebsocDepartment;
component = <SchoolDeptCard name={`Department of ${dept.deptName}`} comment={dept.deptComment} type={'dept'} />;
} else if (formData.ge !== 'ANY') {
const course = courseData[index] as AACourse;
component = (
<GeDataFetchProvider
term={formData.term}
courseDetails={course}
allowHighlight={true}
scheduleNames={scheduleNames}
analyticsCategory={analyticsEnum.classSearch.title}
/>
);
} else {
const course = courseData[index] as AACourse;
component = (
<SectionTableLazyWrapper
term={formData.term}
courseDetails={course}
allowHighlight={true}
scheduleNames={scheduleNames}
analyticsCategory={analyticsEnum.classSearch.title}
/>
);
}

return <div>{component}</div>;
};

const LoadingMessage = () => {
const isDark = useThemeStore((store) => store.isDark);
return (
<Box sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img src={isDark ? darkModeLoadingGif : loadingGif} alt="Loading courses" />
</Box>
);
};

const ErrorMessage = () => {
const isDark = useThemeStore((store) => store.isDark);
return (
<Box sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img
src={isDark ? darkNoNothing : noNothing}
alt="No Results Found"
style={{ objectFit: 'contain', width: '80%', height: '80%' }}
/>
</Box>
);
};

export default function CourseRenderPane(props: { id?: number }) {
const [websocResp, setWebsocResp] = useState<WebsocAPIResponse>();
const [courseData, setCourseData] = useState<(WebsocSchool | WebsocDepartment | AACourse)[]>([]);
const [larcResp, setLarcResp] = useState<LarcAPIResponse>();

const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [scheduleNames, setScheduleNames] = useState(AppStore.getScheduleNames());
Expand Down Expand Up @@ -215,19 +97,29 @@ export default function CourseRenderPane(props: { id?: number }) {
instructor: formData.instructor,
};

const larcQueryParams = {
department: formData.deptValue.toUpperCase(),
term: '2024 Fall',
courseNumber: formData.courseNumber,
};

try {
// Query websoc for course information and populate gradescache
// Query websoc for course information, populate gradescache, and query larc conditionally
const [websocJsonResp, _] = await Promise.all([
websocQueryParams.units.includes(',')
? WebSOC.queryMultiple(websocQueryParams, 'units')
: WebSOC.query(websocQueryParams),
// Catch the error here so that the course pane still loads even if the grades cache fails to populate
Grades.populateGradesCache(gradesQueryParams).catch((error) => {
console.error(error);
openSnackbar('error', 'Error loading grades information');
}),
]);

if (larcQueryParams.department && larcQueryParams.term && larcQueryParams.courseNumber) {
const larcJsonResp = await Larc.query(larcQueryParams);
setLarcResp(larcJsonResp);
}

setError(false);
setWebsocResp(websocJsonResp);
setCourseData(flattenSOCObject(websocJsonResp));
Expand Down Expand Up @@ -279,33 +171,30 @@ export default function CourseRenderPane(props: { id?: number }) {
};
}, [setHoveredEvents]);

if (loading) {
return <LoadingMessage />;
}

if (error || courseData.length === 0) {
return <ErrorMessage />;
}

return (
<>
{loading ? (
<LoadingMessage />
) : error || courseData.length === 0 ? (
<ErrorMessage />
) : (
<>
<RecruitmentBanner />
<Box>
<Box sx={{ height: '50px', marginBottom: '5px' }} />
{courseData.map((_: WebsocSchool | WebsocDepartment | AACourse, index: number) => {
let heightEstimate = 200;
if ((courseData[index] as AACourse).sections !== undefined)
heightEstimate = (courseData[index] as AACourse).sections.length * 60 + 20 + 40;
return (
<LazyLoad once key={index} overflow height={heightEstimate} offset={500}>
{SectionTableWrapped(index, {
courseData: courseData,
scheduleNames: scheduleNames,
})}
</LazyLoad>
);
})}
</Box>
</>
)}
<RecruitmentBanner />
<Box>
<Box sx={{ height: '50px', marginBottom: '5px' }} />
{courseData.map((_: WebsocSchool | WebsocDepartment | AACourse, index: number) => {
let heightEstimate = 200;
if ((courseData[index] as AACourse).sections !== undefined)
heightEstimate = (courseData[index] as AACourse).sections.length * 60 + 20 + 40;
return (
<LazyLoad once key={index} overflow height={heightEstimate} offset={500}>
{SectionTableWrapped({ index, courseData, scheduleNames, larcData: larcResp })}
</LazyLoad>
);
})}
</Box>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Box } from '@mui/material';

import darkNoNothing from '../static/dark-no_results.png';
import noNothing from '../static/no_results.png';

import { useThemeStore } from '$stores/SettingsStore';

export function ErrorMessage() {
const isDark = useThemeStore((store) => store.isDark);
return (
<Box sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img
src={isDark ? darkNoNothing : noNothing}
alt="No Results Found"
style={{ objectFit: 'contain', width: '80%', height: '80%' }}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Box } from '@mui/material';

import darkModeLoadingGif from '../SearchForm/Gifs/dark-loading.gif';
import loadingGif from '../SearchForm/Gifs/loading.gif';

import { useThemeStore } from '$stores/SettingsStore';

export function LoadingMessage() {
const isDark = useThemeStore((store) => store.isDark);
return (
<Box sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img src={isDark ? darkModeLoadingGif : loadingGif} alt="Loading courses" />
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Close } from '@mui/icons-material';
import { useMediaQuery, Box, Alert, IconButton } from '@mui/material';
import { useState } from 'react';

import RightPaneStore from '$components/RightPane/RightPaneStore';
import { getLocalStorageRecruitmentDismissalTime, setLocalStorageRecruitmentDismissalTime } from '$lib/localStorage';
import { useThemeStore } from '$stores/SettingsStore';

export function RecruitmentBanner() {
const [bannerVisibility, setBannerVisibility] = useState(true);

const isDark = useThemeStore((store) => store.isDark);

// Display recruitment banner if more than 11 weeks (in ms) has passed since last dismissal
const recruitmentDismissalTime = getLocalStorageRecruitmentDismissalTime();
const dismissedRecently =
recruitmentDismissalTime !== null &&
Date.now() - parseInt(recruitmentDismissalTime) < 11 * 7 * 24 * 3600 * 1000;
const isSearchCS = ['COMPSCI', 'IN4MATX', 'I&C SCI', 'STATS'].includes(
RightPaneStore.getFormData().deptValue.toUpperCase()
);
const displayRecruitmentBanner = bannerVisibility && !dismissedRecently && isSearchCS;

const isMobileScreen = useMediaQuery('(max-width: 750px)');

return (
<Box sx={{ position: 'fixed', bottom: 5, right: isMobileScreen ? 5 : 75, zIndex: 999 }}>
{displayRecruitmentBanner ? (
<Alert
icon={false}
severity="info"
style={{
color: isDark ? '#ece6e6' : '#2e2e2e',
backgroundColor: isDark ? '#2e2e2e' : '#ece6e6',
}}
action={
<IconButton
aria-label="close"
size="small"
color="inherit"
onClick={() => {
setLocalStorageRecruitmentDismissalTime(Date.now().toString());
setBannerVisibility(false);
}}
>
<Close fontSize="inherit" />
</IconButton>
}
>
Interested in web development?
<br />
<a href="https://forms.gle/v32Cx65vwhnmxGPv8" target="__blank" rel="noopener noreferrer">
Join ICSSC and work on AntAlmanac and other projects!
</a>
<br />
We have opportunities for experienced devs and those with zero experience!
</Alert>
) : null}
</Box>
);
}
Loading