From 65c1afd67b4b6b463c918d253c5cd8e9e2c26c5c Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 26 Oct 2023 12:19:52 -0700 Subject: [PATCH 1/4] Update Marker Popup Styling (#757) --- apps/antalmanac/src/components/Map/Map.css | 4 +- apps/antalmanac/src/components/Map/Map.tsx | 13 ++- apps/antalmanac/src/components/Map/Marker.tsx | 91 +++++++++++-------- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/apps/antalmanac/src/components/Map/Map.css b/apps/antalmanac/src/components/Map/Map.css index fed1eb0c8..ad46bf701 100644 --- a/apps/antalmanac/src/components/Map/Map.css +++ b/apps/antalmanac/src/components/Map/Map.css @@ -19,7 +19,9 @@ } /** + * Handle styling for the leaflet popup */ .leaflet-popup-content-wrapper { - border-radius: unset; + padding: 0; + width: 250px; } diff --git a/apps/antalmanac/src/components/Map/Map.tsx b/apps/antalmanac/src/components/Map/Map.tsx index c59c91724..efc81428e 100644 --- a/apps/antalmanac/src/components/Map/Map.tsx +++ b/apps/antalmanac/src/components/Map/Map.tsx @@ -272,12 +272,15 @@ export default function CourseMap() { stackIndex={coursesSameBuildingPrior.length} > - - Class: {marker.title} {marker.sectionType} + + Class: {marker.title}{' '} + {marker.sectionType} - - Room{allRoomsInBuilding.length > 1 && 's'}: {marker.locations[0].building}{' '} - {allRoomsInBuilding.join('/')} + + + Room{allRoomsInBuilding.length > 1 && 's'}: + {' '} + {marker.locations[0].building} {allRoomsInBuilding.join('/')} diff --git a/apps/antalmanac/src/components/Map/Marker.tsx b/apps/antalmanac/src/components/Map/Marker.tsx index edd8fa8f7..34a0110d1 100644 --- a/apps/antalmanac/src/components/Map/Marker.tsx +++ b/apps/antalmanac/src/components/Map/Marker.tsx @@ -1,8 +1,8 @@ import { forwardRef, type Ref } from 'react'; import Leaflet from 'leaflet'; import { Marker, Popup } from 'react-leaflet'; -import { Box, Button, Link, Typography } from '@mui/material'; -import { DirectionsWalk as DirectionsWalkIcon } from '@mui/icons-material'; +import { Box, Button, IconButton, Typography } from '@mui/material'; +import { DirectionsWalk as DirectionsWalkIcon, Info } from '@mui/icons-material'; const GOOGLE_MAPS_URL = 'https://www.google.com/maps/dir/?api=1&travelmode=walking&destination='; const IMAGE_CMS_URL = 'https://cms.concept3d.com/map/lib/image-cache/i.php?mapId=463&image='; @@ -82,53 +82,64 @@ const LocationMarker = forwardRef( - - {location ? ( - - {location} - - ) : ( - {location} - )} - - {image && ( - - - + )} - {children} + + + + + {location} + + {location && ( + + + + )} + - + {children} + + + + From b655b972560d3c51b7cb2ab2e9c95d95267c12e8 Mon Sep 17 00:00:00 2001 From: Douglas Hong <78244965+Douglas-Hong@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:24:15 -0700 Subject: [PATCH 2/4] Zotistics graph pops up on hover (#727) --- apps/antalmanac/src/components/PatchNotes.tsx | 9 ++- .../RightPane/CoursePane/CourseRenderPane.tsx | 1 - .../SectionTable/CourseInfoButton.tsx | 68 ++++++++++++------- .../RightPane/SectionTable/GradesPopup.tsx | 20 +++--- apps/antalmanac/src/stores/ColumnStore.ts | 2 +- 5 files changed, 61 insertions(+), 39 deletions(-) diff --git a/apps/antalmanac/src/components/PatchNotes.tsx b/apps/antalmanac/src/components/PatchNotes.tsx index eab0a78bc..601003c8e 100644 --- a/apps/antalmanac/src/components/PatchNotes.tsx +++ b/apps/antalmanac/src/components/PatchNotes.tsx @@ -51,15 +51,18 @@ function PatchNotes() { data-testid={dialogTestId} slots={{ backdrop: PatchNotesBackdrop }} > - {"What's New - August 2023"} + {"What's New - October 2023"} Features (gif of the new feature) { const [bannerVisibility, setBannerVisibility] = React.useState(true); diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoButton.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoButton.tsx index 0f54c60fd..4899f1271 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoButton.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoButton.tsx @@ -1,7 +1,7 @@ -import { Button, Popover, useMediaQuery } from '@material-ui/core'; +import { Button, Paper, Popper, useMediaQuery } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap } from '@material-ui/core/styles/withStyles'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { MOBILE_BREAKPOINT } from '../../../globals'; import { logAnalytics } from '$lib/analytics'; @@ -34,25 +34,55 @@ function CourseInfoButton({ }: CourseInfoButtonProps) { const [popupAnchor, setPopupAnchor] = useState(null); const isMobileScreen = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT})`); + const [isClicked, setIsClicked] = useState(false); + + useEffect(() => { + // When the user clicks on the button, it triggers both onMouseEnter + // and onClick. In order to log the analytics only once, we should + // have this hook when the popupAnchor changes + if (popupAnchor) { + logAnalytics({ + category: analyticsCategory, + action: analyticsAction, + }); + } + }, [popupAnchor, analyticsCategory, analyticsAction]); + + const handleMouseEnter = (event: React.MouseEvent) => { + // If there is popup content, allow the content to be shown when the button is hovered + // Note that on mobile devices, hovering is not possible, so the popup still needs to be able + // to appear when the button is clicked + if (popupContent) { + setPopupAnchor(event.currentTarget); + } + }; + + const handleMouseLeave = () => { + if (popupContent) { + setIsClicked(false); + setPopupAnchor(null); + } + }; + return ( - <> +
{popupContent && ( - setPopupAnchor(null)} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'center', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'center', - }} - > - {popupContent} - + + {popupContent} + )} - +
); } diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx index 16616d16c..01045f8d0 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx @@ -57,20 +57,22 @@ function GradesPopup(props: GradesPopupProps) { const [gradeData, setGradeData] = useState(); - const width = useMemo(() => (isMobileScreen ? 300 : 500), [isMobileScreen]); + const width = useMemo(() => (isMobileScreen ? 250 : 400), [isMobileScreen]); - const height = useMemo(() => (isMobileScreen ? 200 : 300), [isMobileScreen]); + const height = useMemo(() => (isMobileScreen ? 150 : 200), [isMobileScreen]); const graphTitle = useMemo(() => { return gradeData - ? `${deptCode} ${courseNumber}${instructor ? ` — ${instructor}` : "" } | Average GPA: ${gradeData.courseGrades.averageGPA.toFixed(2)}` + ? `${deptCode} ${courseNumber}${ + instructor ? ` — ${instructor}` : '' + } | Average GPA: ${gradeData.courseGrades.averageGPA.toFixed(2)}` : 'Grades are not available for this class.'; }, [deptCode, instructor, gradeData]); const gpaString = useMemo( - () => (gradeData ? `Average GPA: ${gradeData.courseGrades.averageGPA.toFixed(2)}` : ""), + () => (gradeData ? `Average GPA: ${gradeData.courseGrades.averageGPA.toFixed(2)}` : ''), [gradeData] - ) + ); useEffect(() => { if (loading === false) { @@ -107,15 +109,15 @@ function GradesPopup(props: GradesPopupProps) { const axisColor = isDarkMode() ? '#fff' : '#111'; return ( - + {graphTitle} diff --git a/apps/antalmanac/src/stores/ColumnStore.ts b/apps/antalmanac/src/stores/ColumnStore.ts index 936fe8434..7e346eaa6 100644 --- a/apps/antalmanac/src/stores/ColumnStore.ts +++ b/apps/antalmanac/src/stores/ColumnStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -import analyticsEnum, { logAnalytics } from '$lib/analytics'; import useTabStore from './TabStore'; +import analyticsEnum, { logAnalytics } from '$lib/analytics'; /** * Search results are displayed in a tabular format. From 3ac6df75304a46a811d71b088bdd825e3b3b5ff0 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 26 Oct 2023 19:11:56 -0700 Subject: [PATCH 3/4] fix: Color Changing Bug w/ Duplicate Courses (#719) --- .../src/components/Calendar/CalendarRoot.tsx | 1 - .../RightPane/CoursePane/CourseRenderPane.tsx | 190 ++++++++---------- .../SectionTable/SectionTableButtons.tsx | 1 + apps/antalmanac/src/stores/Schedules.ts | 19 +- 4 files changed, 101 insertions(+), 110 deletions(-) diff --git a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx index bd3b20ae3..23c1180ef 100644 --- a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx +++ b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx @@ -254,7 +254,6 @@ class ScheduleCalendar extends PureComponent { +function getColors() { + const courseColors = AppStore.schedule.getCurrentCourses().reduce((accumulator, { section }) => { accumulator[section.sectionCode] = section.color; return accumulator; }, {} as { [key: string]: string }); + + return courseColors; +} + +const flattenSOCObject = (SOCObject: WebsocAPIResponse): (WebsocSchool | WebsocDepartment | AACourse)[] => { + const courseColors = getColors(); + return SOCObject.schools.reduce((accumulator: (WebsocSchool | WebsocDepartment | AACourse)[], school) => { accumulator.push(school); @@ -43,7 +49,7 @@ function flattenSOCObject(SOCObject: WebsocAPIResponse): (WebsocSchool | WebsocD }, []); } const RecruitmentBanner = () => { - const [bannerVisibility, setBannerVisibility] = React.useState(true); + const [bannerVisibility, setBannerVisibility] = useState(true); // Display recruitment banner if more than 11 weeks (in ms) has passed since last dismissal const recruitmentDismissalTime = window.localStorage.getItem('recruitmentDismissalTime'); @@ -54,7 +60,7 @@ const RecruitmentBanner = () => { const displayRecruitmentBanner = bannerVisibility && !dismissedRecently && isSearchCS; return ( -
+ {displayRecruitmentBanner ? ( { setBannerVisibility(false); }} > - + } > @@ -85,8 +91,8 @@ const RecruitmentBanner = () => {
We have opportunities for experienced devs and those with zero experience!
- ) : null}{' '} -
+ ) : null} +
); }; @@ -136,33 +142,38 @@ const SectionTableWrapped = ( return
{component}
; }; -export function CourseRenderPane() { +const LoadingMessage = () => { + return ( + + Loading courses + + ); +}; + +const ErrorMessage = () => { + return ( + + No Results Found + + ); +}; + +export default function CourseRenderPane() { + const [websocResp, setWebsocResp] = useState(); + const [courseData, setCourseData] = useState<(WebsocSchool | WebsocDepartment | AACourse)[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [scheduleNames, setScheduleNames] = useState(AppStore.getScheduleNames()); - const [courseData, setCourseData] = useState<(WebsocSchool | WebsocDepartment | AACourse)[]>([]); const loadCourses = useCallback(async () => { setLoading(true); const formData = RightPaneStore.getFormData(); - const params = { - department: formData.deptValue, - term: formData.term, - ge: formData.ge, - courseNumber: formData.courseNumber, - sectionCodes: formData.sectionCode, - instructorName: formData.instructor, - units: formData.units, - endTime: formData.endTime, - startTime: formData.startTime, - fullCourses: formData.coursesFull, - building: formData.building, - room: formData.room, - division: formData.division, - }; - const websocQueryParams = { department: formData.deptValue, term: formData.term, @@ -194,6 +205,7 @@ export function CourseRenderPane() { ]); setError(false); + setWebsocResp(websocJsonResp); setCourseData(flattenSOCObject(websocJsonResp)); } catch (error) { console.error(error); @@ -204,95 +216,61 @@ export function CourseRenderPane() { } }, []); - useEffect(() => { - loadCourses(); - }, []); + const updateScheduleNames = () => { + setScheduleNames(AppStore.getScheduleNames()); + }; useEffect(() => { - const updateScheduleNames = () => { - setScheduleNames(AppStore.getScheduleNames()); + const changeColors = () => { + if (websocResp == null) { + return; + } + setCourseData(flattenSOCObject(websocResp)); }; - AppStore.on('scheduleNamesChange', updateScheduleNames); + AppStore.on('currentScheduleIndexChange', changeColors); return () => { - AppStore.off('scheduleNamesChange', updateScheduleNames); + AppStore.off('currentScheduleIndexChange', changeColors); }; - }, []); + }, [websocResp]); - if (loading) { - return ( -
- Loading courses -
- ); - } + useEffect(() => { + loadCourses(); + AppStore.on('scheduleNamesChange', updateScheduleNames); - if (error) { - return ( -
-
- No Results Found -
-
- ); - } + return () => { + AppStore.off('scheduleNamesChange', updateScheduleNames); + }; + }, [loadCourses]); return ( <> - -
-
- {courseData.length === 0 ? ( -
- No Results Found -
- ) : ( - courseData.map((_, index: number) => { - let heightEstimate = 200; - if ((courseData[index] as AACourse).sections !== undefined) - heightEstimate = (courseData[index] as AACourse).sections.length * 60 + 20 + 40; - - return ( - - {SectionTableWrapped(index, { courseData, scheduleNames })} - - ); - }) - )} -
+ {loading ? ( + + ) : error || courseData.length === 0 ? ( + + ) : ( + <> + + + + {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 ( + + {SectionTableWrapped(index, { + courseData: courseData, + scheduleNames: scheduleNames, + })} + + ); + })} + + + )} ); } - -export default CourseRenderPane; diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableButtons.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableButtons.tsx index 951ae796a..60144ba43 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableButtons.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableButtons.tsx @@ -52,6 +52,7 @@ export const ColorAndDelete = withStyles(styles)((props: ColorAndDeleteProps) => Date: Thu, 26 Oct 2023 20:26:36 -0700 Subject: [PATCH 4/4] fix: refresh button refetches courses (#750) --- .../RightPane/CoursePane/CoursePaneRoot.tsx | 2 +- .../RightPane/CoursePane/CourseRenderPane.tsx | 7 ++++--- apps/antalmanac/src/lib/course-helpers.ts | 4 ++-- apps/antalmanac/src/lib/helpers.ts | 21 ++++--------------- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/CoursePaneRoot.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/CoursePaneRoot.tsx index ddd4adc6d..5b4d06544 100644 --- a/apps/antalmanac/src/components/RightPane/CoursePane/CoursePaneRoot.tsx +++ b/apps/antalmanac/src/components/RightPane/CoursePane/CoursePaneRoot.tsx @@ -66,7 +66,7 @@ function RightPane() { {RightPaneStore.getDoDisplaySearch() ? ( ) : ( - + )}
); diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx index 74cd0c925..a543067e5 100644 --- a/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx +++ b/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx @@ -17,6 +17,7 @@ import AppStore from '$stores/AppStore'; import { isDarkMode, queryWebsoc, queryWebsocMultiple } from '$lib/helpers'; import Grades from '$lib/grades'; import analyticsEnum from '$lib/analytics'; +import { websocCache } from '$lib/course-helpers'; import { openSnackbar } from '$actions/AppStoreActions'; function getColors() { @@ -47,7 +48,7 @@ const flattenSOCObject = (SOCObject: WebsocAPIResponse): (WebsocSchool | WebsocD return accumulator; }, []); -} +}; const RecruitmentBanner = () => { const [bannerVisibility, setBannerVisibility] = useState(true); @@ -162,7 +163,7 @@ const ErrorMessage = () => { ); }; -export default function CourseRenderPane() { +export default function CourseRenderPane(props: { id?: number }) { const [websocResp, setWebsocResp] = useState(); const [courseData, setCourseData] = useState<(WebsocSchool | WebsocDepartment | AACourse)[]>([]); const [loading, setLoading] = useState(true); @@ -242,7 +243,7 @@ export default function CourseRenderPane() { return () => { AppStore.off('scheduleNamesChange', updateScheduleNames); }; - }, [loadCourses]); + }, [loadCourses, props.id]); return ( <> diff --git a/apps/antalmanac/src/lib/course-helpers.ts b/apps/antalmanac/src/lib/course-helpers.ts index c66c38d10..fbbc3327f 100644 --- a/apps/antalmanac/src/lib/course-helpers.ts +++ b/apps/antalmanac/src/lib/course-helpers.ts @@ -6,10 +6,10 @@ interface CacheEntry extends WebsocAPIResponse { timestamp: number; } -const websocCache: { [key: string]: CacheEntry } = {}; +export let websocCache: { [key: string]: CacheEntry } = {}; export function clearCache() { - Object.keys(websocCache).forEach((key) => delete websocCache[key]); //https://stackoverflow.com/a/19316873/14587004 + websocCache = {}; } export function getCourseInfo(SOCObject: WebsocAPIResponse) { diff --git a/apps/antalmanac/src/lib/helpers.ts b/apps/antalmanac/src/lib/helpers.ts index 279027c01..ce0c04ff1 100644 --- a/apps/antalmanac/src/lib/helpers.ts +++ b/apps/antalmanac/src/lib/helpers.ts @@ -1,8 +1,6 @@ -import React from 'react'; - import { WebsocSectionMeeting, WebsocSection, WebsocAPIResponse } from 'peterportal-api-next-types'; import { PETERPORTAL_GRAPHQL_ENDPOINT, PETERPORTAL_WEBSOC_ENDPOINT } from './api/endpoints'; -import Grades from './grades'; +import { websocCache } from './course-helpers'; import { addCourse, openSnackbar } from '$actions/AppStoreActions'; import AppStore from '$stores/AppStore'; import { RepeatingCustomEvent } from '$components/Calendar/Toolbar/CustomEventDialog/CustomEventDialog'; @@ -71,17 +69,6 @@ export async function queryZotCourse(schedule_name: string) { }; } -interface CacheEntry extends WebsocAPIResponse { - timestamp: number; -} - -const websocCache: { [key: string]: CacheEntry } = {}; - -export function clearCache() { - Object.keys(websocCache).forEach((key) => delete websocCache[key]); //https://stackoverflow.com/a/19316873/14587004 - Grades.clearCache(); -} - function cleanParams(record: Record) { if ('term' in record) { const termValue = record['term']; @@ -120,14 +107,14 @@ function cleanParams(record: Record) { // Construct a request to PeterPortal with the params as a query string export async function queryWebsoc(params: Record) { - // Construct a request to PeterPortal with the params as a query string const url = new URL(PETERPORTAL_WEBSOC_ENDPOINT); + const searchString = new URLSearchParams(cleanParams(params)).toString(); + if (websocCache[searchString]?.timestamp > Date.now() - 30 * 60 * 1000) { - //NOTE: Check out how caching works - //if cache hit and less than 30 minutes old return websocCache[searchString]; } + url.search = searchString; //The data from the API will duplicate a section if it has multiple locations.