Repository: icssc/AntAlmanac
Pull request #891 (2024-02-07 22:30:39)

Auto-Save Feature (Solving Conflict)
# Summary
## This Pull request is the same as #865 but solving the conflict with main

Closes #865 




Patch:
diff --git a/apps/antalmanac/src/actions/ActionTypesStore.ts b/apps/antalmanac/src/actions/ActionTypesStore.ts
new file mode 100644
index 00000000..1515d30f
--- /dev/null
+++ b/apps/antalmanac/src/actions/ActionTypesStore.ts
@@ -0,0 +1,163 @@
+import { EventEmitter } from 'events';
+import { RepeatingCustomEvent, ScheduleCourse } from '@packages/antalmanac-types';
+import { autoSaveSchedule } from '$actions/AppStoreActions';
+import AppStore from '$stores/AppStore';
+
+const MAX_UNSAVED_ACTIONS = 1000;
+
+export interface UndoAction {
+    type: 'undoAction';
+}
+
+export interface AddCourseAction {
+    type: 'addCourse';
+    course: ScheduleCourse;
+    scheduleIndex: number;
+}
+
+export interface DeleteCourseAction {
+    type: 'deleteCourse';
+    sectionCode: string;
+    term: string;
+}
+
+export interface AddCustomEventAction {
+    type: 'addCustomEvent';
+    customEvent: RepeatingCustomEvent;
+    scheduleIndices: number[];
+}
+
+export interface DeleteCustomEventAction {
+    type: 'deleteCustomEvent';
+    customEventId: number;
+}
+
+export interface EditCustomEventAction {
+    type: 'editCustomEvent';
+    editedCustomEvent: RepeatingCustomEvent;
+    newScheduleIndices: number[];
+}
+
+export interface ChangeCustomEventColorAction {
+    type: 'changeCustomEventColor';
+    customEventId: number;
+    newColor: string;
+}
+
+export interface ClearScheduleAction {
+    type: 'clearSchedule';
+}
+
+export interface CopyScheduleAction {
+    type: 'copySchedule';
+    to: number;
+}
+
+export interface ChangeCourseColorAction {
+    type: 'changeCourseColor';
+    sectionCode: string;
+    term: string;
+    newColor: string;
+}
+
+export type ActionType =
+    | AddCourseAction
+    | DeleteCourseAction
+    | AddCustomEventAction
+    | DeleteCustomEventAction
+    | EditCustomEventAction
+    | ChangeCustomEventColorAction
+    | ClearScheduleAction
+    | CopyScheduleAction
+    | ChangeCourseColorAction
+    | UndoAction;
+
+function parseUnsavedActionsString(unsavedActionsString: string): ActionType[] {
+    try {
+        return JSON.parse(unsavedActionsString);
+    } catch {
+        return [];
+    }
+}
+
+class ActionTypesStore extends EventEmitter {
+    constructor() {
+        super();
+    }
+
+    async autoSaveSchedule(action: ActionType) {
+        const autoSave = typeof Storage !== 'undefined' && window.localStorage.getItem('autoSave') == 'true';
+        if (autoSave) {
+            const savedUserID = window.localStorage.getItem('userID');
+
+            if (savedUserID) {
+                this.emit('autoSaveStart');
+                await autoSaveSchedule(savedUserID);
+                AppStore.unsavedChanges = false;
+                this.emit('autoSaveEnd');
+            }
+        } else {
+            const unsavedActionsString = window.localStorage.getItem('unsavedActions');
+            if (unsavedActionsString == null) {
+                const unsavedActions = [action];
+                localStorage.setItem('unsavedActions', JSON.stringify(unsavedActions));
+            } else {
+                let unsavedActions = parseUnsavedActionsString(unsavedActionsString);
+                if (unsavedActions.length > MAX_UNSAVED_ACTIONS) {
+                    unsavedActions = unsavedActions.slice(100);
+                }
+                unsavedActions.push(action);
+                localStorage.setItem('unsavedActions', JSON.stringify(unsavedActions));
+            }
+        }
+    }
+
+    async loadScheduleFromLocalSave() {
+        const unsavedActionsString = window.localStorage.getItem('unsavedActions');
+
+        if (unsavedActionsString == null) return;
+        if (!confirm('You have unsaved changes. Would you like to load them?')) {
+            window.localStorage.removeItem('unsavedActions');
+            return;
+        }
+
+        const actions = parseUnsavedActionsString(unsavedActionsString);
+
+        actions.forEach((action) => {
+            switch (action.type) {
+                case 'addCourse':
+                    AppStore.schedule.addCourse(action.course, action.scheduleIndex);
+                    break;
+                case 'deleteCourse':
+                    AppStore.schedule.deleteCourse(action.sectionCode, action.term);
+                    break;
+                case 'addCustomEvent':
+                    AppStore.schedule.addCustomEvent(action.customEvent, action.scheduleIndices);
+                    break;
+                case 'deleteCustomEvent':
+                    AppStore.schedule.deleteCustomEvent(action.customEventId);
+                    break;
+                case 'editCustomEvent':
+                    AppStore.schedule.editCustomEvent(action.editedCustomEvent, action.newScheduleIndices);
+                    break;
+                case 'changeCustomEventColor':
+                    AppStore.schedule.changeCustomEventColor(action.customEventId, action.newColor);
+                    break;
+                case 'changeCourseColor':
+                    AppStore.schedule.changeCourseColor(action.sectionCode, action.term, action.newColor);
+                    break;
+                case 'clearSchedule':
+                    AppStore.schedule.clearCurrentSchedule();
+                    break;
+                case 'copySchedule':
+                    AppStore.schedule.copySchedule(action.to);
+                    break;
+                default:
+                    break;
+            }
+        });
+    }
+}
+
+const actionTypesStore = new ActionTypesStore();
+export default actionTypesStore;
diff --git a/apps/antalmanac/src/actions/AppStoreActions.ts b/apps/antalmanac/src/actions/AppStoreActions.ts
index 4bf8caea..24c3814b 100644
--- a/apps/antalmanac/src/actions/AppStoreActions.ts
+++ b/apps/antalmanac/src/actions/AppStoreActions.ts
@@ -102,6 +102,31 @@ export const saveSchedule = async (userID: string, rememberMe: boolean) => {
     }
 };
 
+export async function autoSaveSchedule(userID: string) {
+    logAnalytics({
+        category: analyticsEnum.nav.title,
+        action: analyticsEnum.nav.actions.SAVE_SCHEDULE,
+        label: userID,
+    });
+    if (userID == null) return;
+    userID = userID.replace(/\s+/g, '');
+
+    if (userID.length < 0) return;
+    const scheduleSaveState = AppStore.schedule.getScheduleAsSaveState();
+
+    try {
+        await trpc.users.saveUserData.mutate({ id: userID, userData: scheduleSaveState });
+
+        AppStore.saveSchedule();
+    } catch (e) {
+        if (e instanceof TRPCError) {
+            openSnackbar('error', `Schedule could not be auto-saved under username "${userID}`);
+        } else {
+            openSnackbar('error', 'Network error or server is down.');
+        }
+    }
+}
+
 export const loadSchedule = async (userId: string, rememberMe: boolean) => {
     logAnalytics({
         category: analyticsEnum.nav.title,
diff --git a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx
index d9525ea4..b1dccb66 100644
--- a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx
+++ b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx
@@ -7,8 +7,8 @@ import { SyntheticEvent, useEffect, useState } from 'react';
 import { Calendar, DateLocalizer, momentLocalizer, Views } from 'react-big-calendar';
 
 import CalendarToolbar from './CalendarToolbar';
-import CourseCalendarEvent, { CalendarEvent } from './CourseCalendarEvent';
-import { getDefaultFinalsStartDate } from '$lib/termData';
+import CourseCalendarEvent, { CalendarEvent, CourseEvent } from './CourseCalendarEvent';
+import { getDefaultFinalsStartDate, getFinalsStartDateForTerm } from '$lib/termData';
 import AppStore from '$stores/AppStore';
 import locationIds from '$lib/location_ids';
 import { useTimeFormatStore } from '$stores/SettingsStore';
@@ -82,7 +82,7 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) {
     const { isMilitaryTime } = useTimeFormatStore();
     const { hoveredCourseEvents } = useHoveredStore();
 
-    const getEventsForCalendar = () => {
+    const getEventsForCalendar = (): CalendarEvent[] => {
         return showFinalsSchedule
             ? finalsEventsInCalendar
             : hoveredCourseEvents
@@ -163,9 +163,13 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) {
     const calendarTimeFormat = isMilitaryTime ? 'HH:mm' : 'h:mm A';
     const calendarGutterTimeFormat = isMilitaryTime ? 'HH:mm' : 'h A';
 
-    const defaultFinals = getDefaultFinalsStartDate();
-    const finalsDateFormat = defaultFinals ? 'ddd MM/DD' : 'ddd';
-    const date = showFinalsSchedule && defaultFinals ? defaultFinals : new Date(2018, 0, 1);
+    const onlyCourseEvents = eventsInCalendar.filter((e) => !e.isCustomEvent) as CourseEvent[];
+
+    const finalsDate =
+        onlyCourseEvents.length > 0 ? getFinalsStartDateForTerm(onlyCourseEvents[0].term) : getDefaultFinalsStartDate();
+
+    const finalsDateFormat = finalsDate ? 'ddd MM/DD' : 'ddd';
+    const date = showFinalsSchedule && finalsDate ? finalsDate : new Date(2018, 0, 1);
 
     // If a final is on a Saturday or Sunday, let the calendar start on Saturday
     moment.updateLocale('es-us', {
@@ -274,6 +278,7 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) {
                     step={15}
                     timeslots={2}
                     date={date}
+                    onNavigate={() => undefined}
                     min={getStartTime()}
                     max={new Date(2018, 0, 1, 23)}
                     events={events}
diff --git a/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx b/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx
index 6cb7d5cc..15ea5eca 100644
--- a/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx
+++ b/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx
@@ -213,6 +213,7 @@ const CourseCalendarEvent = (props: CourseCalendarEventProps) => {
                         <IconButton
                             size="small"
                             onClick={() => {
+                                props.closePopover();
                                 deleteCourse(sectionCode, term);
                                 logAnalytics({
                                     category: analyticsEnum.calendar.title,
@@ -303,7 +304,7 @@ const CourseCalendarEvent = (props: CourseCalendarEventProps) => {
                             to={`/map?location=${building ?? 0}`}
                             onClick={focusMap}
                         >
-                            {buildingCatalogue[+building].name}
+                            {buildingCatalogue[+building]?.name ?? ''}
                         </Link>
                     </div>
                 )}
diff --git a/apps/antalmanac/src/components/Calendar/Toolbar/CustomEventDialog/CustomEventDialog.tsx b/apps/antalmanac/src/components/Calendar/Toolbar/CustomEventDialog/CustomEventDialog.tsx
index 4fd5978d..95efe376 100644
--- a/apps/antalmanac/src/components/Calendar/Toolbar/CustomEventDialog/CustomEventDialog.tsx
+++ b/apps/antalmanac/src/components/Calendar/Toolbar/CustomEventDialog/CustomEventDialog.tsx
@@ -226,8 +226,8 @@ function CustomEventDialogs(props: CustomEventDialogProps) {
                         {disabled
                             ? 'Schedule and day must be checked'
                             : props.customEvent
-                            ? 'Save Changes'
-                            : 'Add Event'}
+                              ? 'Save Changes'
+                              : 'Add Event'}
                     </Button>
                 </DialogActions>
             </Dialog>
diff --git a/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx b/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx
index 95970876..db56c151 100644
--- a/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx
+++ b/apps/antalmanac/src/components/Header/LoadSaveFunctionality.tsx
@@ -6,9 +6,10 @@ import {
     DialogContentText,
     DialogTitle,
     TextField,
+    CircularProgress,
+    Checkbox,
+    FormControlLabel,
 } from '@material-ui/core';
-import Checkbox from '@material-ui/core/Checkbox';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
 import { CloudDownload, Save } from '@material-ui/icons';
 import { ChangeEvent, PureComponent, useEffect, useState } from 'react';
 
@@ -16,6 +17,7 @@ import { LoadingButton } from '@mui/lab';
 import { loadSchedule, saveSchedule } from '$actions/AppStoreActions';
 import { useThemeStore } from '$stores/SettingsStore';
 import AppStore from '$stores/AppStore';
+import actionTypesStore from '$actions/ActionTypesStore';
 
 interface LoadSaveButtonBaseProps {
     action: typeof saveSchedule;
@@ -32,6 +34,21 @@ interface LoadSaveButtonBaseState {
     rememberMe: boolean;
 }
 
+interface SaveLoadIconProps {
+    loading: boolean;
+    actionName: 'Save' | 'Load';
+}
+
+function SaveLoadIcon(props: SaveLoadIconProps) {
+    return props.loading ? (
+        <CircularProgress size={20} color="inherit" />
+    ) : props.actionName === 'Save' ? (
+        <Save />
+    ) : (
+        <CloudDownload />
+    );
+}
+
 class LoadSaveButtonBase extends PureComponent<LoadSaveButtonBaseProps, LoadSaveButtonBaseState> {
     state: LoadSaveButtonBaseState = {
         isOpen: false,
@@ -92,9 +109,9 @@ class LoadSaveButtonBase extends PureComponent<LoadSaveButtonBaseProps, LoadSave
                     id={this.props.id}
                     onClick={this.handleOpen}
                     color="inherit"
-                    startIcon={this.props.actionName === 'Save' ? <Save /> : <CloudDownload />}
+                    startIcon={<SaveLoadIcon loading={this.props.loading} actionName={this.props.actionName} />}
                     disabled={this.props.disabled}
-                    loading={this.props.loading}
+                    loading={false}
                 >
                     {this.props.actionName}
                 </LoadingButton>
@@ -147,6 +164,7 @@ const LoadSaveScheduleFunctionality = () => {
     const isDark = useThemeStore((store) => store.isDark);
 
     const [loading, setLoading] = useState(false);
+    const [saving, setSaving] = useState(false);
     const [skeletonMode, setSkeletonMode] = useState(AppStore.getSkeletonMode());
 
     const loadScheduleAndSetLoading = async (userID: string, rememberMe: boolean) => {
@@ -155,6 +173,12 @@ const LoadSaveScheduleFunctionality = () => {
         setLoading(false);
     };
 
+    const saveScheduleAndSetLoading = async (userID: string, rememberMe: boolean) => {
+        setSaving(true);
+        await saveSchedule(userID, rememberMe);
+        setSaving(false);
+    };
+
     useEffect(() => {
         const handleSkeletonModeChange = () => {
             setSkeletonMode(AppStore.getSkeletonMode());
@@ -178,14 +202,27 @@ const LoadSaveScheduleFunctionality = () => {
         }
     }, []);
 
+    useEffect(() => {
+        const handleAutoSaveStart = () => setSaving(true);
+        const handleAutoSaveEnd = () => setSaving(false);
+
+        actionTypesStore.on('autoSaveStart', handleAutoSaveStart);
+        actionTypesStore.on('autoSaveEnd', handleAutoSaveEnd);
+
+        return () => {
+            actionTypesStore.off('autoSaveStart', handleAutoSaveStart);
+            actionTypesStore.off('autoSaveEnd', handleAutoSaveEnd);
+        };
+    }, []);
+
     return (
         <div id="load-save-container">
             <LoadSaveButtonBase
                 id="save-button"
                 actionName={'Save'}
-                action={saveSchedule}
+                action={saveScheduleAndSetLoading}
                 disabled={loading}
-                loading={false}
+                loading={saving}
                 colorType={isDark ? 'secondary' : 'primary'}
             />
             <LoadSaveButtonBase
diff --git a/apps/antalmanac/src/components/Header/SettingsMenu.tsx b/apps/antalmanac/src/components/Header/SettingsMenu.tsx
index 7b4f20bf..5b45197a 100644
--- a/apps/antalmanac/src/components/Header/SettingsMenu.tsx
+++ b/apps/antalmanac/src/components/Header/SettingsMenu.tsx
@@ -4,8 +4,11 @@ import { Divider, Stack, Tooltip } from '@mui/material';
 import { CSSProperties } from '@material-ui/core/styles/withStyles';
 import { Close, DarkMode, Help, LightMode, Settings, SettingsBrightness } from '@mui/icons-material';
 
-import { usePreviewStore, useThemeStore, useTimeFormatStore } from '$stores/SettingsStore';
+import { usePreviewStore, useThemeStore, useTimeFormatStore, useAutoSaveStore } from '$stores/SettingsStore';
 import useCoursePaneStore from '$stores/CoursePaneStore';
+import appStore from '$stores/AppStore';
+import actionTypesStore from '$actions/ActionTypesStore';
+import { autoSaveSchedule } from '$actions/AppStoreActions';
 
 const lightSelectedStyle: CSSProperties = {
     backgroundColor: '#F0F7FF',
@@ -141,15 +144,30 @@ function TimeMenu() {
 
 function ExperimentalMenu() {
     const [previewMode, setPreviewMode] = usePreviewStore((store) => [store.previewMode, store.setPreviewMode]);
+    const [autoSave, setAutoSave] = useAutoSaveStore((store) => [store.autoSave, store.setAutoSave]);
 
     const handlePreviewChange = (event: React.ChangeEvent<HTMLInputElement>) => {
         setPreviewMode(event.target.checked);
     };
 
+    const handleAutoSaveChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
+        setAutoSave(event.target.checked);
+
+        if (!event.target.checked) return;
+
+        const savedUserID = window.localStorage.getItem('userID');
+
+        if (!savedUserID) return;
+        actionTypesStore.emit('autoSaveStart');
+        await autoSaveSchedule(savedUserID);
+        appStore.unsavedChanges = false;
+        actionTypesStore.emit('autoSaveEnd');
+    };
+
     return (
-        <Stack sx={{ padding: '1rem 1rem 0 1rem', width: '100%', display: 'flex' }} alignItems="middle">
-            <Box display="flex" justifyContent="space-between" width={1}>
-                <Box display="flex" alignItems="center" style={{ gap: 4 }}>
+        <Stack sx={{ padding: '1rem 1rem 0 1rem', width: '100%', display: 'flex', alignItems: 'middle' }}>
+            <Box style={{ display: 'flex', justifyContent: 'space-between', width: '1' }}>
+                <Box style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                     <Typography variant="h6" style={{ display: 'flex', alignItems: 'center', alignContent: 'center' }}>
                         Hover to Preview
                     </Typography>
@@ -157,7 +175,19 @@ function ExperimentalMenu() {
                         <Help />
                     </Tooltip>
                 </Box>
-                <Switch color="primary" value={previewMode} checked={previewMode} onChange={handlePreviewChange} />
+                <Switch color={'primary'} value={previewMode} checked={previewMode} onChange={handlePreviewChange} />
+            </Box>
+
+            <Box style={{ display: 'flex', justifyContent: 'space-between', width: '1' }}>
+                <Box style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
+                    <Typography variant="h6" style={{ display: 'flex', alignItems: 'center', alignContent: 'center' }}>
+                        Auto Save
+                    </Typography>
+                    <Tooltip title={<Typography>Auto Save your schedule!</Typography>}>
+                        <Help />
+                    </Tooltip>
+                </Box>
+                <Switch color={'primary'} value={autoSave} checked={autoSave} onChange={handleAutoSaveChange} />
             </Box>
         </Stack>
     );
diff --git a/apps/antalmanac/src/components/Map/Map.tsx b/apps/antalmanac/src/components/Map/Map.tsx
index 58c93c8a..d222015c 100644
--- a/apps/antalmanac/src/components/Map/Map.tsx
+++ b/apps/antalmanac/src/components/Map/Map.tsx
@@ -78,7 +78,7 @@ export function getCoursesPerBuilding() {
 }
 
 export function getCustomEventPerBuilding() {
-    const customEvents = AppStore.getCustomEvents();
+    const customEvents = AppStore.getCustomEventsInCalendar();
 
     const customEventBuildings = customEvents.map((e) => e.building).filter(notNull);
 
@@ -98,9 +98,9 @@ export function getCustomEventPerBuilding() {
 
     interface localCustomEventType {
         title: string;
-        start: string;
-        end: string;
-        days: boolean[];
+        start: Date;
+        end: Date;
+        days: string[];
         customEventID: number;
         color?: string | undefined;
         building?: string | undefined;
@@ -251,20 +251,24 @@ export default function CourseMap() {
 
         const markersToday =
             today === 'All' ? markerValues : markerValues.filter((course) => course.start.toString().includes(today));
+
         return markersToday
             .sort((a, b) => a.start.getTime() - b.start.getTime())
             .filter((marker, i, arr) => arr.findIndex((other) => other.sectionCode === marker.sectionCode) === i);
     }, [markers, today]);
 
     const customEventMarkersToDisplay = useMemo(() => {
-        const markerValues = Object.keys(customEventMarkers).flatMap((markerKey) => customEventMarkers[markerKey]);
+        const markerValues = Object.keys(customEventMarkers)
+            .flatMap((markerKey) => customEventMarkers[markerKey])
+            .filter((marker, i, arr) => arr.findIndex((other) => other.key === marker.key) === i);
 
         const markersToday =
             today === 'All'
                 ? markerValues
                 : markerValues.filter((event) => {
-                      return event.days.some((day, index) => day && WORK_WEEK[index] === today);
+                      return event.days.some((day) => day && today.includes(day));
                   });
+
         return markersToday.sort((a, b) => {
             const startDateA = new Date(`1970-01-01T${a.start}`);
             const startDateB = new Date(`1970-01-01T${b.start}`);
@@ -277,13 +281,16 @@ export default function CourseMap() {
      */
     const startDestPairs = useMemo(() => {
         const allEvents = [...markersToDisplay, ...customEventMarkersToDisplay];
-        return allEvents.reduce((acc, cur, index) => {
-            acc.push([cur]);
-            if (index > 0) {
-                acc[index - 1].push(cur);
-            }
-            return acc;
-        }, [] as (typeof allEvents)[]);
+        return allEvents.reduce(
+            (acc, cur, index) => {
+                acc.push([cur]);
+                if (index > 0) {
+                    acc[index - 1].push(cur);
+                }
+                return acc;
+            },
+            [] as (typeof allEvents)[]
+        );
     }, [markersToDisplay, customEventMarkersToDisplay]);
 
     return (
diff --git a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx
index e56f8f65..83f8e777 100644
--- a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx
+++ b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx
@@ -303,11 +303,14 @@ function SkeletonSchedule() {
     }, []);
 
     const sectionsByTerm: [string, string[]][] = useMemo(() => {
-        const result = skeletonSchedule.courses.reduce((accumulated, course) => {
-            accumulated[course.term] ??= [];
-            accumulated[course.term].push(course.sectionCode);
-            return accumulated;
-        }, {} as Record<string, string[]>);
+        const result = skeletonSchedule.courses.reduce(
+            (accumulated, course) => {
+                accumulated[course.term] ??= [];
+                accumulated[course.term].push(course.sectionCode);
+                return accumulated;
+            },
+            {} as Record<string, string[]>
+        );
 
         return Object.entries(result);
     }, [skeletonSchedule.courses]);
@@ -403,6 +406,13 @@ function AddedSectionsGrid() {
         return scheduleNames[scheduleIndex];
     }, [scheduleNames, scheduleIndex]);
 
+    // "No Courses Added Yet!" notification
+    const NoCoursesBox = (
+        <Box style={{ paddingTop: '12px', paddingBottom: '12px' }}>
+            <Typography align="left">No Courses Added Yet!</Typography>
+        </Box>
+    );
+
     return (
         <Box display="flex" flexDirection="column" gap={1}>
             <Box display="flex" width={1} position="absolute" zIndex="2">
@@ -412,6 +422,7 @@ function AddedSectionsGrid() {
             </Box>
             <Box style={{ marginTop: 50 }}>
                 <Typography variant="h6">{`${scheduleName} (${scheduleUnits} Units)`}</Typography>
+                {courses.length < 1 ? NoCoursesBox : null}
                 <Box display="flex" flexDirection="column" gap={1}>
                     {courses.map((course) => {
                         return (
diff --git a/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx b/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx
index 9f7876ac..3c08df2c 100644
--- a/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx
+++ b/apps/antalmanac/src/components/RightPane/AddedCourses/CustomEventDetailView.tsx
@@ -74,7 +74,7 @@ const CustomEventDetailView = (props: CustomEventDetailViewProps) => {
             />
             <Box sx={{ margin: '0.75rem', color: '#bbbbbb', fontSize: '1rem' }}>
                 <Link to={`/map?location=${customEvent.building ?? 0}`} onClick={focusMap}>
-                    {customEvent.building ? buildingCatalogue[+customEvent.building].name : ''}
+                    {(customEvent.building && buildingCatalogue[+customEvent.building]?.name) || ''}
                 </Link>
             </Box>
 
diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx
index 33b49df8..70f99aec 100644
--- a/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx
+++ b/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx
@@ -1,7 +1,7 @@
 import { useCallback, useEffect, useState } from 'react';
 import LazyLoad from 'react-lazyload';
 
-import { Alert, Box, GlobalStyles, IconButton } from '@mui/material';
+import { Alert, Box, GlobalStyles, IconButton, useMediaQuery } from '@mui/material';
 import { Close } from '@mui/icons-material';
 import { AACourse, AASection } from '@packages/antalmanac-types';
 import { WebsocDepartment, WebsocSchool, WebsocAPIResponse, GE } from 'peterportal-api-next-types';
@@ -27,7 +27,7 @@ function getColors() {
             accumulator[section.sectionCode] = section.color;
             return accumulator;
         },
-        {} as { [key: string]: string }
+        {} as Record<string, string>
     );
 
     return courseColors;
@@ -63,11 +63,15 @@ const RecruitmentBanner = () => {
     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);
+    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: 5, zIndex: 999 }}>
+        <Box sx={{ position: 'fixed', bottom: 5, right: isMobileScreen ? 5 : 75, zIndex: 999 }}>
             {displayRecruitmentBanner ? (
                 <Alert
                     icon={false}
diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/HelpBox.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/HelpBox.tsx
index 1fab92e8..e291c1dc 100644
--- a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/HelpBox.tsx
+++ b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/HelpBox.tsx
@@ -1,4 +1,16 @@
-import { Paper, ImageList, ImageListItem, Typography, Link, List, ListItemText, ListItem } from '@mui/material';
+import { Close } from '@mui/icons-material';
+import {
+    Paper,
+    ImageList,
+    ImageListItem,
+    Typography,
+    Link,
+    List,
+    ListItemText,
+    ListItem,
+    Box,
+    IconButton,
+} from '@mui/material';
 
 const images = [
     {
@@ -15,12 +27,21 @@ const images = [
     },
 ];
 
-function HelpBox() {
+interface HelpBoxProps {
+    onDismiss: () => void;
+}
+
+function HelpBox({ onDismiss }: HelpBoxProps) {
     return (
         <Paper variant="outlined" sx={{ padding: 2, marginBottom: '10px', marginRight: '5px' }}>
-            <Typography variant="h5" fontWeight="bold">
-                Need help planning your schedule?
-            </Typography>
+            <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+                <Typography variant="h5" fontWeight="bold">
+                    Need help planning your schedule?
+                </Typography>
+                <IconButton aria-label="close" size="large" color="inherit" onClick={onDismiss}>
+                    <Close fontSize="inherit" />
+                </IconButton>
+            </Box>
 
             <List component="ol" sx={{ listStyle: 'decimal', pl: 2, pb: 0 }}>
                 <ListItem sx={{ display: 'list-item', p: 0 }}>
diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/SearchForm.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/SearchForm.tsx
index a3433549..2bdc6f56 100644
--- a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/SearchForm.tsx
+++ b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/SearchForm.tsx
@@ -50,6 +50,7 @@ const styles: Styles<Theme, object> = {
 const SearchForm = (props: { classes: ClassNameMap; toggleSearch: () => void }) => {
     const { classes, toggleSearch } = props;
     const { manualSearchEnabled, toggleManualSearch } = useCoursePaneStore();
+    const [helpBoxVisibility, setHelpBoxVisibility] = useState(true);
 
     const onFormSubmit = (event: FormEvent) => {
         event.preventDefault();
@@ -57,7 +58,21 @@ const SearchForm = (props: { classes: ClassNameMap; toggleSearch: () => void })
     };
 
     const currentMonthIndex = new Date().getMonth(); // 0=Jan
-    const activeMonthIndices = [false, false, false, false, false, false, false, false, true, true, false, false];
+    // Active months: February/March for Spring planning, May/June for Fall planning, July/August for Summer planning,
+    // and November/December for Winter planning
+    const activeMonthIndices = [false, true, true, false, true, true, true, true, false, false, true, true];
+
+    // Display the help box only if more than 30 days has passed since the last dismissal and
+    // the current month is an active month
+    const helpBoxDismissalTime = window.localStorage.getItem('helpBoxDismissalTime');
+    const dismissedRecently =
+        helpBoxDismissalTime !== null && Date.now() - parseInt(helpBoxDismissalTime) < 30 * 24 * 3600 * 1000;
+    const displayHelpBox = helpBoxVisibility && !dismissedRecently && activeMonthIndices[currentMonthIndex];
+
+    const onHelpBoxDismiss = () => {
+        window.localStorage.setItem('helpBoxDismissalTime', Date.now().toString());
+        setHelpBoxVisibility(false);
+    };
 
     return (
         <div className={classes.rightPane}>
@@ -95,7 +110,7 @@ const SearchForm = (props: { classes: ClassNameMap; toggleSearch: () => void })
                 </div>
             </form>
 
-            {activeMonthIndices[currentMonthIndex] && <HelpBox />}
+            {displayHelpBox && <HelpBox onDismiss={onHelpBoxDismiss} />}
             <PrivacyPolicyBanner />
         </div>
     );
diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/EnrollmentHistoryPopup.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/EnrollmentHistoryPopup.tsx
index deab64e6..f55b75cc 100644
--- a/apps/antalmanac/src/components/RightPane/SectionTable/EnrollmentHistoryPopup.tsx
+++ b/apps/antalmanac/src/components/RightPane/SectionTable/EnrollmentHistoryPopup.tsx
@@ -1,18 +1,82 @@
-import { useState, useEffect, useMemo } from 'react';
-import { LineChart, Line, CartesianGrid, ResponsiveContainer, XAxis, YAxis, Tooltip, Legend } from 'recharts';
-import { Box, Link, Typography, Skeleton, useMediaQuery } from '@mui/material';
+import { useState, useEffect, useMemo, useCallback } from 'react';
+import {
+    LineChart,
+    Line,
+    CartesianGrid,
+    ResponsiveContainer,
+    XAxis,
+    YAxis,
+    Tooltip as RechartsTooltip,
+    Legend,
+} from 'recharts';
+import { ArrowBack, ArrowForward } from '@mui/icons-material';
+import { Box, IconButton, Link, Typography, Skeleton, Tooltip, useMediaQuery } from '@mui/material';
 import { MOBILE_BREAKPOINT } from '../../../globals';
 import { DepartmentEnrollmentHistory, EnrollmentHistory } from '$lib/enrollmentHistory';
 import { useThemeStore } from '$stores/SettingsStore';
 
-export interface EnrollmentHistoryPopupProps {
+type PopupHeaderCallback = () => void;
+
+interface PopupHeaderProps {
+    graphWidth: number;
+    graphIndex: number;
+    handleForward: PopupHeaderCallback;
+    handleBack: PopupHeaderCallback;
+    popupTitle: string;
+    enrollmentHistory: EnrollmentHistory[];
+}
+
+function PopupHeader({
+    graphWidth,
+    graphIndex,
+    handleForward,
+    handleBack,
+    popupTitle,
+    enrollmentHistory,
+}: PopupHeaderProps) {
+    const isMobileScreen = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT})`);
+
+    return (
+        <Box
+            sx={{
+                display: 'flex',
+                justifyContent: 'space-between',
+                alignItems: 'center',
+                width: graphWidth,
+            }}
+        >
+            <Tooltip title="Older Graph">
+                {/* In order for a tooltip to work properly with disabled buttons, we need to wrap the button in a span */}
+                <span>
+                    <IconButton onClick={handleBack} disabled={graphIndex === 0}>
+                        <ArrowBack />
+                    </IconButton>
+                </span>
+            </Tooltip>
+            <Typography sx={{ fontWeight: 500, fontSize: isMobileScreen ? '0.8rem' : '1rem', textAlign: 'center' }}>
+                {popupTitle}
+            </Typography>
+            <Tooltip title="Newer Graph">
+                <span>
+                    <IconButton onClick={handleForward} disabled={graphIndex === enrollmentHistory.length - 1}>
+                        <ArrowForward />
+                    </IconButton>
+                </span>
+            </Tooltip>
+        </Box>
+    );
+}
+
+interface EnrollmentHistoryPopupProps {
     department: string;
     courseNumber: string;
 }
 
 export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentHistoryPopupProps) {
     const [loading, setLoading] = useState(true);
-    const [enrollmentHistory, setEnrollmentHistory] = useState<EnrollmentHistory>();
+    const [enrollmentHistory, setEnrollmentHistory] = useState<EnrollmentHistory[]>();
+    const [graphIndex, setGraphIndex] = useState(0);
+
     const isMobileScreen = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT})`);
 
     const deptEnrollmentHistory = useMemo(() => new DepartmentEnrollmentHistory(department), [department]);
@@ -24,15 +88,24 @@ export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentH
             return 'No past enrollment data found for this course';
         }
 
-        return `${department} ${courseNumber} | ${enrollmentHistory.year} ${
-            enrollmentHistory.quarter
-        } | ${enrollmentHistory.instructors.join(', ')}`;
-    }, [courseNumber, department, enrollmentHistory]);
+        const currEnrollmentHistory = enrollmentHistory[graphIndex];
+        return `${department} ${courseNumber} | ${currEnrollmentHistory.year} ${
+            currEnrollmentHistory.quarter
+        } | ${currEnrollmentHistory.instructors.join(', ')}`;
+    }, [courseNumber, department, enrollmentHistory, graphIndex]);
     const isDark = useThemeStore((state) => state.isDark);
     const encodedDept = useMemo(() => encodeURIComponent(department), [department]);
     const axisColor = isDark ? '#fff' : '#111';
     const tooltipDateColor = '#111';
 
+    const handleBack = useCallback(() => {
+        setGraphIndex((prev) => prev - 1);
+    }, []);
+
+    const handleForward = useCallback(() => {
+        setGraphIndex((prev) => prev + 1);
+    }, []);
+
     useEffect(() => {
         if (!loading) {
             return;
@@ -41,6 +114,9 @@ export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentH
         deptEnrollmentHistory.find(courseNumber).then((data) => {
             if (data) {
                 setEnrollmentHistory(data);
+                // The graph index is the last past enrollment graph since we want to show
+                // the most recent quarter's graph
+                setGraphIndex(data.length - 1);
             }
             setLoading(false);
         });
@@ -64,20 +140,18 @@ export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentH
         );
     }
 
+    const lineChartData = enrollmentHistory[graphIndex].days;
+
     return (
         <Box sx={{ padding: 0.5 }}>
-            <Typography
-                sx={{
-                    marginTop: '.5rem',
-                    textAlign: 'center',
-                    fontWeight: 500,
-                    marginRight: '2rem',
-                    marginLeft: '2rem',
-                    marginBottom: '.5rem',
-                }}
-            >
-                {popupTitle}
-            </Typography>
+            <PopupHeader
+                graphWidth={graphWidth}
+                graphIndex={graphIndex}
+                handleForward={handleForward}
+                handleBack={handleBack}
+                popupTitle={popupTitle}
+                enrollmentHistory={enrollmentHistory}
+            />
             <Link
                 href={`https://zot-tracker.herokuapp.com/?dept=${encodedDept}&number=${courseNumber}&courseType=all`}
                 target="_blank"
@@ -85,11 +159,11 @@ export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentH
                 sx={{ display: 'flex', height: graphHeight, width: graphWidth }}
             >
                 <ResponsiveContainer width="95%" height="95%">
-                    <LineChart data={enrollmentHistory.days} style={{ cursor: 'pointer' }}>
+                    <LineChart data={lineChartData} style={{ cursor: 'pointer' }}>
                         <CartesianGrid strokeDasharray="3 3" />
                         <XAxis dataKey="date" tick={{ fontSize: 12, fill: axisColor }} />
                         <YAxis tick={{ fontSize: 12, fill: axisColor }} width={40} />
-                        <Tooltip labelStyle={{ color: tooltipDateColor }} />
+                        <RechartsTooltip labelStyle={{ color: tooltipDateColor }} />
                         <Legend />
                         <Line type="monotone" dataKey="totalEnrolled" stroke="#8884d8" name="Enrolled" dot={{ r: 2 }} />
                         <Line type="monotone" dataKey="maxCapacity" stroke="#82ca9d" name="Max" dot={{ r: 2 }} />
diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx
index 70515eee..223feac4 100644
--- a/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx
+++ b/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx
@@ -1,9 +1,8 @@
 import { useState, useEffect, useMemo } from 'react';
-import { Bar, BarChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from 'recharts';
+import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
 import { Box, Link, Typography, Skeleton } from '@mui/material';
 import { useThemeStore } from '$stores/SettingsStore';
 import GradesHelper, { type Grades } from '$lib/grades';
-
 export interface GradeData {
     grades: {
         name: string;
@@ -26,6 +25,11 @@ async function getGradeData(
         return undefined;
     }
 
+    const totalGrades = Object.values(Object.entries(courseGrades).filter(([key]) => key !== 'averageGPA')).reduce(
+        (acc, [_, value]) => acc + value,
+        0
+    );
+
     /**
      * Format data for displaying in chart.
      *
@@ -36,7 +40,7 @@ async function getGradeData(
         .map(([key, value]) => {
             return {
                 name: key.replace('grade', '').replace('Count', ''),
-                all: value,
+                all: Number(((value / totalGrades) * 100).toFixed(2)),
             };
         });
 
@@ -134,7 +138,18 @@ function GradesPopup(props: GradesPopupProps) {
                     <BarChart data={gradeData.grades} style={{ cursor: 'pointer' }}>
                         <CartesianGrid strokeDasharray="3 3" />
                         <XAxis dataKey="name" tick={{ fontSize: 12, fill: axisColor }} />
-                        <YAxis tick={{ fontSize: 12, fill: axisColor }} width={40} />
+                        <YAxis tick={{ fontSize: 12, fill: axisColor }} width={40} unit="%" />
+                        <Tooltip
+                            content={({ active, payload, label }) => (
+                                <GradeTooltip
+                                    active={active ?? false}
+                                    payload={(payload as Array<Payload>) ?? null}
+                                    label={label ?? ''}
+                                />
+                            )}
+                            position={{ y: 100 }}
+                            offset={-5}
+                        />
                         <Bar dataKey="all" fill="#5182ed" />
                     </BarChart>
                 </ResponsiveContainer>
@@ -143,4 +158,40 @@ function GradesPopup(props: GradesPopupProps) {
     );
 }
 
+const GradeTooltip = (props: GradeTooltipProps) => {
+    const { active, payload, label } = props;
+    if (active && payload && payload.length) {
+        return (
+            <>
+                <Box
+                    sx={{
+                        backgroundColor: '#5182ed',
+                        padding: '5px',
+                        border: '1px solid #000',
+                        borderRadius: '5px',
+                        boxShadow: '0 0 5px 0 rgba(0, 0, 0, 0.5)',
+                    }}
+                >
+                    <Typography variant="body1" align="center" sx={{ color: '#fff', fontWeight: 500 }}>
+                        {`${label}: ${payload[0].value}%`}
+                    </Typography>
+                </Box>
+            </>
+        );
+    }
+
+    return null;
+};
+
+export interface GradeTooltipProps {
+    active: boolean;
+    payload: Array<Payload> | null;
+    label: string;
+}
+
+export interface Payload {
+    name: string;
+    value: number;
+}
+
 export default GradesPopup;
diff --git a/apps/antalmanac/src/components/buttons/Screenshot.tsx b/apps/antalmanac/src/components/buttons/Screenshot.tsx
index be08ccbf..af13b12f 100644
--- a/apps/antalmanac/src/components/buttons/Screenshot.tsx
+++ b/apps/antalmanac/src/components/buttons/Screenshot.tsx
@@ -3,18 +3,19 @@ import Button from '@material-ui/core/Button';
 import { Panorama } from '@material-ui/icons';
 import { saveAs } from 'file-saver';
 import html2canvas from 'html2canvas';
-import { PureComponent } from 'react';
 
 import analyticsEnum, { logAnalytics } from '$lib/analytics';
 import { useThemeStore } from '$stores/SettingsStore';
 
-class ScreenshotButton extends PureComponent {
-    handleClick = () => {
-        const isDark = useThemeStore((store) => store.isDark);
+const ScreenshotButton = () => {
+    const { isDark } = useThemeStore();
+
+    const handleClick = () => {
         logAnalytics({
             category: analyticsEnum.calendar.title,
             action: analyticsEnum.calendar.actions.SCREENSHOT,
         });
+
         void html2canvas(document.getElementById('screenshot') as HTMLElement, {
             scale: 2.5,
             backgroundColor: isDark ? '#303030' : '#fafafa',
@@ -24,20 +25,13 @@ class ScreenshotButton extends PureComponent {
         });
     };
 
-    render() {
-        return (
-            <Tooltip title="Get a screenshot of your schedule">
-                <Button
-                    onClick={this.handleClick}
-                    variant="outlined"
-                    size="small"
-                    startIcon={<Panorama fontSize="small" />}
-                >
-                    Screenshot
-                </Button>
-            </Tooltip>
-        );
-    }
-}
+    return (
+        <Tooltip title="Get a screenshot of your schedule">
+            <Button onClick={handleClick} variant="outlined" size="small" startIcon={<Panorama fontSize="small" />}>
+                Screenshot
+            </Button>
+        </Tooltip>
+    );
+};
 
 export default ScreenshotButton;
diff --git a/apps/antalmanac/src/lib/buildingCatalogue.ts b/apps/antalmanac/src/lib/buildingCatalogue.ts
index fc8a8411..e93fe32f 100644
--- a/apps/antalmanac/src/lib/buildingCatalogue.ts
+++ b/apps/antalmanac/src/lib/buildingCatalogue.ts
@@ -168,60 +168,24 @@ export default {
         lng: -117.849297,
         imageURLs: ['463/vista-field.jpg'],
     },
-    83057: {
-        name: 'Biological Sciences III (BS3)',
-        lat: 33.645363,
-        lng: -117.846222,
-        imageURLs: ['463/biological-sciences-iii.jpg'],
-    },
     83058: {
         name: 'Bison Modular (BIMO)',
         lat: 33.643814,
         lng: -117.84639,
         imageURLs: ['463/bison-modular.jpg'],
     },
-    83059: {
-        name: 'Bonney Research Laboratory (BRL)',
-        lat: 33.646461,
-        lng: -117.846169,
-        imageURLs: ['463/bonney-research-laboratory.jpg'],
-    },
-    83060: {
-        name: 'Center for the Neurobiology of Learning & Memory Annex (CNLM)',
-        lat: 33.646629,
-        lng: -117.846428,
-        imageURLs: ['463/center-for-the-neurobiology-of-learning-and-memory-annex.jpg'],
-    },
     83061: {
         name: 'Greenhouse',
         lat: 33.64732,
         lng: -117.845367,
         imageURLs: ['463/greenhouse.jpg'],
     },
-    83062: {
-        name: 'McGaugh Hall (MH)',
-        lat: 33.645271,
-        lng: -117.844887,
-        imageURLs: ['463/mcgaugh-hall.jpg'],
-    },
-    83063: {
-        name: 'Qureshey Research Laboratory (QRL)',
-        lat: 33.646648,
-        lng: -117.845764,
-        imageURLs: ['463/qureshey-research-laboratory.jpg'],
-    },
     83064: {
         name: 'Howard Schneiderman Lecture Hall (HSLH)',
         lat: 33.645618,
         lng: -117.844666,
         imageURLs: ['463/howard-schneiderman-lecture-hall.jpg'],
     },
-    83065: {
-        name: 'Steinhaus Hall (SH)',
-        lat: 33.646347,
-        lng: -117.844887,
-        imageURLs: ['463/steinhaus-hall.jpg'],
-    },
     83068: {
         name: 'Arroyo Vista',
         lat: 33.646317,
@@ -241,7 +205,7 @@ export default {
         imageURLs: [],
     },
     83072: {
-        name: 'Mesa Court ',
+        name: 'Mesa Court',
         lat: 33.651924,
         lng: -117.844528,
         imageURLs: [],
@@ -378,12 +342,6 @@ export default {
         lng: -117.840149,
         imageURLs: ['463/engineering-and-comuting-trailer.jpg'],
     },
-    83102: {
-        name: 'Engineering Gateway (EG)',
-        lat: 33.643215,
-        lng: -117.840157,
-        imageURLs: ['463/engineering-gateway.jpg'],
-    },
     83103: {
         name: 'Engineering Hall (EH)',
         lat: 33.643799,
@@ -618,12 +576,6 @@ export default {
         lng: -117.835938,
         imageURLs: ['463/law.jpg'],
     },
-    83167: {
-        name: 'Multipurpose Academic & Administrative Building (MPAA)',
-        lat: 33.647156,
-        lng: -117.837013,
-        imageURLs: ['463/multipurpose-academic-and-admin-building.jpg'],
-    },
     83168: {
         name: 'Science Library (SLIB)',
         lat: 33.645908,
@@ -648,12 +600,6 @@ export default {
         lng: -117.844681,
         imageURLs: ['463/croul-hall.jpg'],
     },
-    83372: {
-        name: 'Multipurpose Science & Technology Building (MSTB)',
-        lat: 33.64209,
-        lng: -117.844467,
-        imageURLs: ['463/multipurpose-science-and-technology.jpg'],
-    },
     83373: {
         name: 'Natural Sciences I (NS1)',
         lat: 33.644348,
@@ -726,12 +672,6 @@ export default {
         lng: -117.835938,
         imageURLs: ['463/continuing-education-6.jpg'],
     },
-    83544: {
-        name: 'Social Ecology I (SE)',
-        lat: 33.646194,
-        lng: -117.838844,
-        imageURLs: ['463/social-ecology-i.jpg'],
-    },
     83545: {
         name: 'Social Ecology II (SE2)',
         lat: 33.64666,
@@ -756,12 +696,6 @@ export default {
         lng: -117.847198,
         imageURLs: ['463/kuci-radio-station.jpg'],
     },
-    83559: {
-        name: 'Student Center (SC)',
-        lat: 33.649323,
-        lng: -117.842255,
-        imageURLs: ['463/student-center_1.jpg'],
-    },
     83560: {
         name: 'Student Health Center (SHC)',
         lat: 33.64555,
@@ -774,12 +708,6 @@ export default {
         lng: -117.842316,
         imageURLs: ['463/student-services-i.jpg'],
     },
-    83565: {
-        name: 'Social & Behavioral Sciences Gateway (SBSG)',
-        lat: 33.647488,
-        lng: -117.839165,
-        imageURLs: ['463/social-and-behavioral-sciences-gateway.jpg'],
-    },
     83566: {
         name: 'Social Science Hall (SSH)',
         lat: 33.646236,
@@ -810,12 +738,6 @@ export default {
         lng: -117.839096,
         imageURLs: ['463/social-science-plaza-b.jpg'],
     },
-    83571: {
-        name: 'Social Science Tower (SST)',
-        lat: 33.646591,
-        lng: -117.840294,
-        imageURLs: ['463/social-science-tower.jpg'],
-    },
     83572: {
         name: 'Social Science Trailer (SSTR)',
         lat: 33.647007,
@@ -888,12 +810,6 @@ export default {
         lng: -117.838409,
         imageURLs: ['463/university-tower-2.jpg'],
     },
-    99610: {
-        name: 'Interim Classroom Facility (ICF)',
-        lat: 33.644463,
-        lng: -117.840012,
-        imageURLs: ['463/interim-classroom-facility.jpg'],
-    },
     101047: {
         name: 'University Hills Community Center',
         lat: 33.636307,
@@ -924,18 +840,6 @@ export default {
         lng: -117.838112,
         imageURLs: ['463/business-ii.jpg'],
     },
-    114585: {
-        name: '100 Theory',
-        lat: 33.644245,
-        lng: -117.855675,
-        imageURLs: [],
-    },
-    114586: {
-        name: 'Anteater Instruction & Research Building (AIRB)',
-        lat: 33.643009,
-        lng: -117.838013,
-        imageURLs: ['463/anteater-instruction-and-research-building.jpg'],
-    },
     114588: {
         name: 'Beckman Center of the National Academies of Sciences & Engineering',
         lat: 33.649414,
@@ -972,12 +876,6 @@ export default {
         lng: -117.846428,
         imageURLs: ['463/center-for-the-neurobiology-of-learning-and-memory-annex.jpg'],
     },
-    114597: {
-        name: 'Greenhouse',
-        lat: 33.64732,
-        lng: -117.845367,
-        imageURLs: ['463/greenhouse.jpg'],
-    },
     114598: {
         name: 'Merage School of Business (SB1)',
         lat: 33.647072,
@@ -996,54 +894,24 @@ export default {
         lng: -117.840157,
         imageURLs: ['463/engineering-gateway.jpg'],
     },
-    114601: {
-        name: 'Engineering Laboratory Facility (ELF)',
-        lat: 33.643822,
-        lng: -117.839622,
-        imageURLs: ['463/engineering-laboratory-facility.jpg'],
-    },
     114602: {
         name: 'Little Theatre',
         lat: 33.647213,
         lng: -117.844063,
         imageURLs: ['463/little-theatre.jpg'],
     },
-    114603: {
-        name: 'Humanities Interim Classroom Facility (HICF) | Studio Art Trailer',
-        lat: 33.646919,
-        lng: -117.84687,
-        imageURLs: ['463/humanities-interim-classroom-facility.jpg'],
-    },
     114604: {
         name: 'Multipurpose Academic & Administrative Building (MPAA)',
         lat: 33.647156,
         lng: -117.837013,
         imageURLs: ['463/multipurpose-academic-and-admin-building.jpg'],
     },
-    114605: {
-        name: 'Beckman Laser Institute (BLI)',
-        lat: 33.644192,
-        lng: -117.849915,
-        imageURLs: ['463/beckman-laser-institute.jpg'],
-    },
-    114606: {
-        name: 'Gillespie Neuroscience Research Facility (GNRF)',
-        lat: 33.644009,
-        lng: -117.851265,
-        imageURLs: ['463/gillespie-neuroscience-research-facility.jpg'],
-    },
     114607: {
         name: 'Sue & Bill Gross Hall: A CIRM Institute',
         lat: 33.643555,
         lng: -117.851501,
         imageURLs: ['463/gross-hall.jpg'],
     },
-    114608: {
-        name: 'Hewitt Research Hall (HRH)',
-        lat: 33.643505,
-        lng: -117.851921,
-        imageURLs: ['463/hewitt-hall.jpg'],
-    },
     114609: {
         name: 'Plumwood House (PH)',
         lat: 33.644718,
@@ -1062,54 +930,24 @@ export default {
         lng: -117.844887,
         imageURLs: ['463/steinhaus-hall.jpg'],
     },
-    114612: {
-        name: 'Bison Modular (BIMO)',
-        lat: 33.643814,
-        lng: -117.84639,
-        imageURLs: ['463/bison-modular.jpg'],
-    },
     114613: {
         name: 'Multipurpose Science & Technology Building (MSTB)',
         lat: 33.64209,
         lng: -117.844467,
         imageURLs: ['463/multipurpose-science-and-technology.jpg'],
     },
-    114614: {
-        name: 'Physical Sciences High Bay',
-        lat: 33.64362,
-        lng: -117.845291,
-        imageURLs: ['463/physical-sciences-high-bay.jpg'],
-    },
-    114615: {
-        name: 'Rowland Hall (RH)',
-        lat: 33.644531,
-        lng: -117.844162,
-        imageURLs: ['463/rowland-hall.jpg'],
-    },
     114616: {
         name: 'Anteater Instruction & Research Building (AIRB)',
         lat: 33.643009,
         lng: -117.838013,
         imageURLs: ['463/anteater-instruction-and-research-building.jpg'],
     },
-    114618: {
-        name: 'Sprague Hall (SPH)',
-        lat: 33.644062,
-        lng: -117.852066,
-        imageURLs: ['463/sprague-hall.jpg'],
-    },
     114621: {
         name: 'Facilities Management Building',
         lat: 33.664715,
         lng: -117.854805,
         imageURLs: [],
     },
-    114622: {
-        name: 'Humanities Gateway (HG)',
-        lat: 33.648266,
-        lng: -117.844406,
-        imageURLs: ['463/humanities-gateway.jpg'],
-    },
     114623: {
         name: 'Social Ecology I (SE)',
         lat: 33.646194,
@@ -1170,12 +1008,6 @@ export default {
         lng: -117.840294,
         imageURLs: ['463/social-science-tower.jpg'],
     },
-    114879: {
-        name: 'Gavin Herbert Eye Institute',
-        lat: 33.641743,
-        lng: -117.852249,
-        imageURLs: ['463/gavin-herbert-eye-institute.jpg'],
-    },
     114880: {
         name: 'Child Development School (CD)',
         lat: 33.662647,
@@ -1620,12 +1452,6 @@ export default {
         lng: -117.850998,
         imageURLs: ['463/gottschalk-pod.jpg'],
     },
-    255880: {
-        name: 'Student Center',
-        lat: 33.648888,
-        lng: -117.8424,
-        imageURLs: ['463/student-center_2.jpg'],
-    },
     255884: {
         name: 'Anteater Learning Pavilion (ALP)',
         lat: 33.647213,
@@ -1698,24 +1524,12 @@ export default {
         lng: -117.849152,
         imageURLs: [],
     },
-    364043: {
-        name: 'Student Services West',
-        lat: 33.646072,
-        lng: -117.849152,
-        imageURLs: [],
-    },
     407361: {
         name: 'ANTrepreneur Center',
         lat: 33.64621,
         lng: -117.837051,
         imageURLs: ['463/banning-house.jpg'],
     },
-    407362: {
-        name: 'Gateway Study Center (GC)',
-        lat: 33.647388,
-        lng: -117.841873,
-        imageURLs: ['463/gateway-study-center.jpg'],
-    },
     444368: {
         name: 'Plaza Verde',
         lat: 33.648224,
@@ -1752,14 +1566,8 @@ export default {
         lng: -117.848709,
         imageURLs: ['463/grounds-maintenance-facility.jpg'],
     },
-    444506: {
-        name: 'Facilities Management Building',
-        lat: 33.66478,
-        lng: -117.854935,
-        imageURLs: [],
-    },
     444507: {
-        name: 'Passport Services ',
+        name: 'Passport Services',
         lat: 33.664795,
         lng: -117.854523,
         imageURLs: [],
@@ -1812,42 +1620,12 @@ export default {
         lng: -117.844414,
         imageURLs: [],
     },
-    454486: {
-        name: 'Medical Surge I (MS1)',
-        lat: 33.646484,
-        lng: -117.850174,
-        imageURLs: ['463/medical-surge-i.jpg'],
-    },
-    454487: {
-        name: 'Donald Bren Hall (DBH)',
-        lat: 33.643288,
-        lng: -117.841965,
-        imageURLs: ['463/donald-bren-hall.jpg'],
-    },
-    454488: {
-        name: 'Engineering Hall (EH)',
-        lat: 33.643799,
-        lng: -117.841202,
-        imageURLs: ['463/engineering-hall.jpg'],
-    },
-    454489: {
-        name: 'Air Pollution Health Effects Laboratory (APHEL)',
-        lat: 33.664837,
-        lng: -117.853844,
-        imageURLs: [],
-    },
     454490: {
         name: 'Student Center South',
         lat: 33.648724,
         lng: -117.842705,
         imageURLs: [],
     },
-    454491: {
-        name: 'Student Center South',
-        lat: 33.648724,
-        lng: -117.842705,
-        imageURLs: [],
-    },
     454492: {
         name: 'Jao Family Sculpture Garden',
         lat: 33.647762,
@@ -1962,4 +1740,52 @@ export default {
         lng: -117.843788,
         imageURLs: ['463/1R7A7978.jpg'],
     },
+    363937: {
+        name: 'Mesa Parking Structure (MPS)',
+        lat: 33.6506,
+        lng: -117.84629,
+        imageURLs: [],
+    },
+    114576: {
+        name: 'Air Pollution Labs (APL)',
+        lat: 33.6648,
+        lng: -117.85382,
+        imageURLs: [],
+    },
+    363936: {
+        name: 'Student Center Parking Structure (SCPS)',
+        lat: 33.64971,
+        lng: -117.84189,
+        imageURLs: [],
+    },
+    363935: {
+        name: 'East Campus Parking Structure (ECPS)',
+        lat: 33.6451,
+        lng: -117.8282,
+        imageURLs: [],
+    },
+    363934: {
+        name: 'Social Science Parking Structure (SSPS)',
+        lat: 33.64768,
+        lng: -117.83801,
+        imageURLs: [],
+    },
+    363932: {
+        name: 'Anteater Parking Structure (APS)',
+        lat: 33.64314,
+        lng: -117.83783,
+        imageURLs: [],
+    },
+    367615: {
+        name: "Dean's Office",
+        lat: 33.64857,
+        lng: -117.84265,
+        imageURLs: [],
+    },
+    83062: {
+        name: 'McGaugh Hall (MH)',
+        lat: 33.64517,
+        lng: -117.84476,
+        imageURLs: [],
+    },
 } as Record<number, Building>;
diff --git a/apps/antalmanac/src/lib/enrollmentHistory.ts b/apps/antalmanac/src/lib/enrollmentHistory.ts
index bea9c847..cb2efb5e 100644
--- a/apps/antalmanac/src/lib/enrollmentHistory.ts
+++ b/apps/antalmanac/src/lib/enrollmentHistory.ts
@@ -44,7 +44,7 @@ export interface EnrollmentHistoryDay {
 
 export class DepartmentEnrollmentHistory {
     // Each key in the cache will be the department and courseNumber concatenated
-    static enrollmentHistoryCache: Record<string, EnrollmentHistory | null> = {};
+    static enrollmentHistoryCache: Record<string, EnrollmentHistory[] | null> = {};
     static termShortNames: string[] = termData.map((term) => term.shortName);
     static QUERY_TEMPLATE = `{
         enrollmentHistory(department: "$$DEPARTMENT$$", courseNumber: "$$COURSE_NUMBER$$", sectionType: Lec) {
@@ -68,30 +68,25 @@ export class DepartmentEnrollmentHistory {
         this.partialQueryString = DepartmentEnrollmentHistory.QUERY_TEMPLATE.replace('$$DEPARTMENT$$', department);
     }
 
-    async find(courseNumber: string): Promise<EnrollmentHistory | null> {
+    async find(courseNumber: string): Promise<EnrollmentHistory[] | null> {
         const cacheKey = this.department + courseNumber;
-        return (DepartmentEnrollmentHistory.enrollmentHistoryCache[cacheKey] ??= await this.queryEnrollmentHistory(
-            courseNumber
-        ));
+        return (DepartmentEnrollmentHistory.enrollmentHistoryCache[cacheKey] ??=
+            await this.queryEnrollmentHistory(courseNumber));
     }
 
-    async queryEnrollmentHistory(courseNumber: string): Promise<EnrollmentHistory | null> {
+    async queryEnrollmentHistory(courseNumber: string): Promise<EnrollmentHistory[] | null> {
         // Query for the enrollment history of all lecture sections that were offered
         const queryString = this.partialQueryString.replace('$$COURSE_NUMBER$$', courseNumber);
 
         const res = (await queryGraphQL<EnrollmentHistoryGraphQLResponse>(queryString))?.data?.enrollmentHistory;
 
-        if (res?.length) {
-            const parsedEnrollmentHistory = DepartmentEnrollmentHistory.parseEnrollmentHistoryResponse(res);
-            DepartmentEnrollmentHistory.sortEnrollmentHistory(parsedEnrollmentHistory);
-
-            // For now, just return the enrollment history of the most recent quarter
-            // instead of the entire array of enrollment histories
-            const latestEnrollmentHistory = parsedEnrollmentHistory[0];
-            return latestEnrollmentHistory;
+        if (!res?.length) {
+            return null;
         }
 
-        return null;
+        const parsedEnrollmentHistory = DepartmentEnrollmentHistory.parseEnrollmentHistoryResponse(res);
+        DepartmentEnrollmentHistory.sortEnrollmentHistory(parsedEnrollmentHistory);
+        return parsedEnrollmentHistory;
     }
 
     /**
@@ -140,7 +135,7 @@ export class DepartmentEnrollmentHistory {
 
     /**
      * Sorts the given array of enrollment histories so that
-     * the most recent quarters are in the beginning of the array.
+     * the oldest quarters are in the beginning of the array
      *
      * @param enrollmentHistory Array where each element represents the enrollment
      * history of a course section during one quarter
@@ -150,10 +145,10 @@ export class DepartmentEnrollmentHistory {
             const aTerm = `${a.year} ${a.quarter}`;
             const bTerm = `${b.year} ${b.quarter}`;
             // If the term for a appears earlier than the term for b in the list of
-            // term short names, then a must be the enrollment history for a later quarter
+            // term short names, then a must be the enrollment history for a more recent quarter
             return (
-                DepartmentEnrollmentHistory.termShortNames.indexOf(aTerm) -
-                DepartmentEnrollmentHistory.termShortNames.indexOf(bTerm)
+                DepartmentEnrollmentHistory.termShortNames.indexOf(bTerm) -
+                DepartmentEnrollmentHistory.termShortNames.indexOf(aTerm)
             );
         });
     }
diff --git a/apps/antalmanac/src/lib/location_ids.ts b/apps/antalmanac/src/lib/location_ids.ts
index 56e452a9..2fce39f6 100644
--- a/apps/antalmanac/src/lib/location_ids.ts
+++ b/apps/antalmanac/src/lib/location_ids.ts
@@ -10,7 +10,7 @@ const locationIds: Record<string, number> = {
     ICS: 83096,
     BRL: 114594,
     BLI: 83134,
-    'Mesa Court': 367611,
+    'Mesa Court': 83072,
     DBH: 83095,
     MS2: 83151,
     SC: 114624,
@@ -34,12 +34,12 @@ const locationIds: Record<string, number> = {
     MS1: 83150,
     LAW: 83166,
     ELF: 83104,
-    HT: 367490,
+    // HT: 367490,
     AIRB: 114616,
     CRCC: 83551,
     ET: 83106,
     EDUC: 114599,
-    CP: 114580,
+    CP: 444502,
     SB1: 114598,
     MAB: 83033,
     CRH: 83371,
@@ -50,12 +50,11 @@ const locationIds: Record<string, number> = {
     AITR: 83024,
     CDDC: 115051,
     MH: 83062,
-    'Student Center': 367612,
     PSLH: 83378,
     SCPS: 363936,
     ELH: 83105,
     SS2: 255997,
-    'Hitachi, PH': 114609,
+    PH: 114609,
     PSCB: 83376,
     HG: 83160,
     HSLH: 83064,
@@ -64,9 +63,9 @@ const locationIds: Record<string, number> = {
     CTT: 83026,
     CAC: 83027,
     ACT: 83022,
-    'Middle Earth': 367613,
+    'Middle Earth': 83073,
     SHC: 83560,
-    CNLMA: 114596,
+    CNLM: 114596,
     IH: 83140,
     FRF: 114575,
     Calit2: 114579,
@@ -90,7 +89,7 @@ const locationIds: Record<string, number> = {
     SE: 114623,
     'MS-C': 83146,
     SH: 114611,
-    'UCI Health': 367616,
+    //'UCI Health': 367616,
     SBSG: 114858,
     GNRF: 83136,
     SSPB: 83570,
@@ -103,7 +102,7 @@ const locationIds: Record<string, number> = {
     ICF: 83099,
     ALP: 255884,
     MSTB: 114613,
-    AH: 83619,
+    //AH: 83619,
     CH: 83050,
     ALH: 83618,
     SSH: 83566,
@@ -111,6 +110,195 @@ const locationIds: Record<string, number> = {
     'MS-E': 83149,
     APS: 363932,
     HH: 83161,
+    'Choral Studio': 83025,
+    'Production Studio': 83036,
+    DS: 83037,
+    'Anteater Ballpark': 83043,
+    'Anteater Stadium': 83045,
+    'Bren Events Center': 83046,
+    'Crawford Clubhouse': 83048,
+    'Crawford Field': 83049,
+    'Crawford Pool': 83051,
+    'Anteater Tennis Stadium': 83054,
+    'Track & Field House': 83055,
+    'Vista Field': 83056,
+    BIMO: 83058,
+    Greenhouse: 83061,
+    'Arroyo Vista': 83068,
+    'Camino del Sol': 83069,
+    'Campus Village': 83070,
+    'Palo Verde': 83074,
+    'Puerta del Sol': 83075,
+    'University Hills': 83076,
+    'Verano Place': 83077,
+    'Vista del Campo': 83078,
+    'Vista del Campo Norte': 83079,
+    'CAC G021 Arts Instructional Lab Classroom': 83081,
+    'OIT@CCC Drop-In Lab': 83083,
+    'Gateway Study Center Drop-In Lab': 83084,
+    'OIT@SST Drop-In Lab': 83086,
+    'OIT@Student Center Drop-In Lab': 83087,
+    'Humanities Studio Labs': 83090,
+    'Langson Library Labs': 83091,
+    'MSTB 210 Instructional Lab Classroom': 83092,
+    'NS1 2144 Instructional Lab Classroom': 83093,
+    'Science Library Labs': 83094,
+    "BC's Cavern": 83120,
+    'POD Social Sciences': 83122,
+    'Cafe Espresso': 83123,
+    'The Green Room': 83125,
+    'Phoenix Food Court': 83128,
+    'Starbucks @ Biological Sciences': 83130,
+    'Gottschalk Medical Plaza': 83137,
+    'Medical Education Building': 83141,
+    'Medical Sciences A Administration': 83143,
+    'Medical Sciences A Annex': 83144,
+    'Medical Sciences Classroom Facility': 83147,
+    'MS-D': 83148,
+    'MRI 3T Trailer': 83152,
+    'MRI 4T Trailer': 83153,
+    'Tamkin Student Lecture Building': 83156,
+    'Physical Sciences High Bay': 83377,
+    'Continuing Education 4': 83384,
+    'Continuing Education 3': 83385,
+    'Continuing Education 5': 83386,
+    'Continuing Education 6': 83387,
+    'Disability Services Center': 83552,
+    'KUCI Radio Station': 83558,
+    '100 Theory': 83609,
+    '111 Theory': 83611,
+    '5251 California': 83612,
+    '101 Academy': 83615,
+    'Administrative Modular': 83617,
+    'Faculty and Staff Housing Office': 83622,
+    'Newkirk Alumni Center': 83624,
+    'University Hills Community Center': 101047,
+    'Beckman Center of the National Academies of Sciences & Engineering': 114588,
+    'Farm School/Red Barn': 114590,
+    'Little Theatre': 114602,
+    'Gavin Herbert Eye Institute': 114610,
+    'Facilities Management Building': 114621,
+    'Student Health Center II': 114626,
+    'Court Building': 114628,
+    'Field Building': 114629,
+    'Irvine Barclay Theatre': 114659,
+    'Early Childhood Education Center': 114881,
+    "Children's Center": 114882,
+    'Extended Day Center': 114883,
+    'Infant Toddler Center I': 114884,
+    'Infant Toddler Center II': 114885,
+    'Verano Preschool': 114887,
+    'University Montessori School': 114888,
+    'American Heart Association': 114890,
+    'Yurt Dance Studio': 114891,
+    '20/20 Cafe': 114958,
+    'Au Bon Pain': 114959,
+    'Espress Yourself': 114960,
+    'Starbucks @ Paul Merage School of Business': 114961,
+    'Java City Kiosk': 114962,
+    'Med Ed Cafe': 114963,
+    'Neuropsychiatric Center': 115040,
+    'Douglas Hospital': 115041,
+    'University Hospital Tower': 115042,
+    'Manchester Pavilion': 115043,
+    'Faculty / Administration Offices': 115044,
+    'Pavilion 3': 115045,
+    'Chao Family Comprehensive Cancer Center': 115046,
+    'Imaging Services': 115047,
+    'Burn & Wound Therapy': 115048,
+    'Pavilion 4': 115049,
+    'Grunigen Medical Library': 115052,
+    'Laboratory Medicine Building': 115053,
+    'Parking & Security': 115054,
+    'Pavilion 1': 115055,
+    'Nikken Center': 115056,
+    'Pavilion 2': 115057,
+    Orthopaedics: 115058,
+    'Visitor Parking': 115059,
+    'Edward Shanbrom, MD Hall': 115060,
+    'Central Plant': 115061,
+    'Facilities Services': 115062,
+    'Administrative / Academic Offices': 115063,
+    'Home Dialysis': 115064,
+    'Courtyard Study Lounge Drop-In Lab': 123779,
+    'Watson Bridge': 126143,
+    'Aldrich Flag Poles': 126144,
+    'Ring Mall': 126145,
+    'Aldrich Park': 126146,
+    'Arts Bridge': 126147,
+    'Physical Sciences Plaza': 126148,
+    'Biological Sciences Plaza': 126149,
+    'Engineering Plaza': 126150,
+    'Social Sciences Plaza': 126151,
+    'Student Center Plaza': 126152,
+    'Humanities Plaza': 126153,
+    'Bostick Bridge': 126154,
+    'Athletics Bridge': 126155,
+    'Alumni Grove': 126156,
+    'Anteater Statue': 126157,
+    'Laurel L. Wilkening Rose Garden': 126158,
+    'Cesario Memorial': 126159,
+    'Bren Center Flag Poles': 126999,
+    'Claire Trevor Oscar/Emmy Display': 127000,
+    'Mesa Parking Bridge': 127001,
+    'Student Center Courtyard Stage': 127052,
+    'Palo Verde Bridge': 127246,
+    '120 Theory': 171183,
+    'Student Center Bridge': 186008,
+    'Continuing Education Bridge': 186011,
+    'Bridge Cafe': 186012,
+    'The Anteatery': 186013,
+    'Summer Session A': 197840,
+    'Summer Session B': 197841,
+    '131 Innovation': 255739,
+    '141 Innovation': 255740,
+    'Gottschalk P.O.D. Express': 255878,
+    'Anteater Community Resource Center': 255992,
+    'Engineering Computing Labs': 257153,
+    'Grunigen Medical Library Lab': 257154,
+    'MSTB 226 Instructional Lab Classroom': 257155,
+    'NS1 3112 Instructional Lab Classroom': 257156,
+    Taza: 264222,
+    'ALP 2800 Computer Focused Classroom': 363319,
+    'ALP 3600 Computer Focused Classroom': 363320,
+    'ALP 3610 Computer Focused Classroom': 363321,
+    'Student Services West': 364042,
+    'ANTrepreneur Center': 407361,
+    'Plaza Verde': 444368,
+    'Building Services Building': 444501,
+    'Electrical Substation': 444503,
+    'Facilities Management Shops': 444504,
+    'Grounds Maintenance Facility': 444505,
+    'Passport Services': 444507,
+    Receiving: 444508,
+    'Recycling Center': 444509,
+    'Thermal Energy Storage Tank': 444510,
+    APHEL: 445384,
+    Brandywine: 446803,
+    'Faculty Research Facility': 447392,
+    '5270 California': 451260,
+    UAG: 454485,
+    'Student Center South': 454490,
+    'Jao Family Sculpture Garden': 454492,
+    'Science Library Mesquite Garden': 454493,
+    'Anteater Plaza': 454494,
+    'Founders Court': 454495,
+    'Campus Village Community Center': 464544,
+    'Campus Village Housing Office': 464545,
+    'Mesa Court Community Center': 464546,
+    'Mesa Court Housing Office': 464547,
+    'Arroyo Vista Community Center & Housing Office': 464548,
+    'Middle Earth Housing Office': 464549,
+    'Palo Verde Housing Office': 464550,
+    'Palo Verde Community Center': 464551,
+    'Verano Place Housing Office': 464552,
+    'Infinity Fountain': 471175,
+    'Stacey Nicholas Tech Pavilion': 471176,
+    'Willed Body Program Dedication Monument': 471177,
+    'Schonfeld Fountain': 471178,
+    'Maya Lin Water Table': 471180,
+    'Gloria Gellman Sculpture': 471202,
+    'Sue & Bill Gross Hall: A CIRM Institute': 114607,
 };
 
 export default locationIds;
diff --git a/apps/antalmanac/src/lib/termData.ts b/apps/antalmanac/src/lib/termData.ts
index 0ea1a2dc..8e9cd073 100644
--- a/apps/antalmanac/src/lib/termData.ts
+++ b/apps/antalmanac/src/lib/termData.ts
@@ -29,7 +29,7 @@ class Term {
 const termData = [
     new Term('2024 Spring', '2024 Spring Quarter', [2024, 3, 1], [2024, 5, 8]),
     new Term('2024 Winter', '2024 Winter Quarter', [2024, 0, 8], [2024, 2, 16]),
-    new Term('2023 Fall', '2023 Fall Quarter', [2023, 8, 28]),
+    new Term('2023 Fall', '2023 Fall Quarter', [2023, 8, 28], [2023, 11, 9]),
     new Term('2023 Summer2', '2023 Summer Session 2', [2023, 7, 7]),
     new Term('2023 Summer10wk', '2023 10-wk Summer', [2023, 5, 26]),
     new Term('2023 Summer1', '2023 Summer Session 1', [2023, 5, 26]),
@@ -97,6 +97,15 @@ function getDefaultFinalsStart() {
     return termData[defaultTerm + 1].finalsStartDate;
 }
 
+function getFinalsStartForTerm(term: string) {
+    const termThatMatches = termData.find((t) => t.shortName === term);
+    if (termThatMatches === undefined) {
+        console.warn(`No matching term for ${term}`);
+        return getDefaultFinalsStart();
+    }
+    return termThatMatches.finalsStartDate;
+}
+
 /**
  * Returns the default finals start as Date object
  * Days offset by 1 to accomodate toggling with Saturday finals
@@ -107,4 +116,18 @@ function getDefaultFinalsStartDate() {
     return year && month && day ? new Date(year, month, day + 1) : undefined;
 }
 
-export { defaultTerm, getDefaultTerm, termData, getDefaultFinalsStart, getDefaultFinalsStartDate };
+function getFinalsStartDateForTerm(term: string) {
+    const date = getFinalsStartForTerm(term);
+    const [year, month, day] = date || [];
+    return year && month && day ? new Date(year, month, day + 1) : undefined;
+}
+
+export {
+    defaultTerm,
+    getDefaultTerm,
+    termData,
+    getDefaultFinalsStart,
+    getDefaultFinalsStartDate,
+    getFinalsStartForTerm,
+    getFinalsStartDateForTerm,
+};
diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts
index e6bae484..d1da68f8 100644
--- a/apps/antalmanac/src/stores/AppStore.ts
+++ b/apps/antalmanac/src/stores/AppStore.ts
@@ -6,6 +6,19 @@ import { Schedules } from './Schedules';
 import { SnackbarPosition } from '$components/NotificationSnackbar';
 import { CalendarEvent, CourseEvent } from '$components/Calendar/CourseCalendarEvent';
 import { useTabStore } from '$stores/TabStore';
+import actionTypesStore from '$actions/ActionTypesStore';
+import type {
+    AddCourseAction,
+    DeleteCourseAction,
+    AddCustomEventAction,
+    DeleteCustomEventAction,
+    EditCustomEventAction,
+    ChangeCustomEventColorAction,
+    ClearScheduleAction,
+    CopyScheduleAction,
+    ChangeCourseColorAction,
+    UndoAction,
+} from '$actions/ActionTypesStore';
 
 class AppStore extends EventEmitter {
     schedule: Schedules;
@@ -74,6 +87,12 @@ class AppStore extends EventEmitter {
             addedCourse = this.schedule.addCourse(newCourse, scheduleIndex);
         }
         this.unsavedChanges = true;
+        const action: AddCourseAction = {
+            type: 'addCourse',
+            course: newCourse,
+            scheduleIndex: scheduleIndex,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('addedCoursesChange');
         return addedCourse;
     }
@@ -90,6 +109,10 @@ class AppStore extends EventEmitter {
         return this.schedule.getCalendarizedCourseEvents();
     }
 
+    getCustomEventsInCalendar() {
+        return this.schedule.getCalendarizedCustomEvents();
+    }
+
     getFinalEventsInCalendar() {
         return this.schedule.getCalendarizedFinals();
     }
@@ -151,6 +174,12 @@ class AppStore extends EventEmitter {
     deleteCourse(sectionCode: string, term: string) {
         this.schedule.deleteCourse(sectionCode, term);
         this.unsavedChanges = true;
+        const action: DeleteCourseAction = {
+            type: 'deleteCourse',
+            sectionCode: sectionCode,
+            term: term,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('addedCoursesChange');
     }
 
@@ -163,6 +192,10 @@ class AppStore extends EventEmitter {
     undoAction() {
         this.schedule.revertState();
         this.unsavedChanges = true;
+        const action: UndoAction = {
+            type: 'undoAction',
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('addedCoursesChange');
         this.emit('customEventsChange');
         this.emit('colorChange', false);
@@ -174,24 +207,47 @@ class AppStore extends EventEmitter {
     addCustomEvent(customEvent: RepeatingCustomEvent, scheduleIndices: number[]) {
         this.schedule.addCustomEvent(customEvent, scheduleIndices);
         this.unsavedChanges = true;
+        const action: AddCustomEventAction = {
+            type: 'addCustomEvent',
+            customEvent: customEvent,
+            scheduleIndices: scheduleIndices,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('customEventsChange');
     }
 
     editCustomEvent(editedCustomEvent: RepeatingCustomEvent, newScheduleIndices: number[]) {
         this.schedule.editCustomEvent(editedCustomEvent, newScheduleIndices);
         this.unsavedChanges = true;
+        const action: EditCustomEventAction = {
+            type: 'editCustomEvent',
+            editedCustomEvent: editedCustomEvent,
+            newScheduleIndices: newScheduleIndices,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('customEventsChange');
     }
 
     deleteCustomEvent(customEventId: number) {
         this.schedule.deleteCustomEvent(customEventId);
         this.unsavedChanges = true;
+        const action: DeleteCustomEventAction = {
+            type: 'deleteCustomEvent',
+            customEventId: customEventId,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('customEventsChange');
     }
 
     changeCustomEventColor(customEventId: number, newColor: string) {
         this.schedule.changeCustomEventColor(customEventId, newColor);
         this.unsavedChanges = true;
+        const action: ChangeCustomEventColorAction = {
+            type: 'changeCustomEventColor',
+            customEventId: customEventId,
+            newColor: newColor,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.colorPickers[customEventId].emit('colorChange', newColor);
         this.emit('colorChange', false);
     }
@@ -213,11 +269,17 @@ class AppStore extends EventEmitter {
 
     saveSchedule() {
         this.unsavedChanges = false;
+        window.localStorage.removeItem('unsavedActions');
     }
 
     copySchedule(to: number) {
         this.schedule.copySchedule(to);
         this.unsavedChanges = true;
+        const action: CopyScheduleAction = {
+            type: 'copySchedule',
+            to: to,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('addedCoursesChange');
         this.emit('customEventsChange');
     }
@@ -229,6 +291,9 @@ class AppStore extends EventEmitter {
             return false;
         }
         this.unsavedChanges = false;
+
+        await actionTypesStore.loadScheduleFromLocalSave();
+
         this.emit('addedCoursesChange');
         this.emit('customEventsChange');
         this.emit('scheduleNamesChange');
@@ -263,6 +328,10 @@ class AppStore extends EventEmitter {
     clearSchedule() {
         this.schedule.clearCurrentSchedule();
         this.unsavedChanges = true;
+        const action: ClearScheduleAction = {
+            type: 'clearSchedule',
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.emit('addedCoursesChange');
         this.emit('customEventsChange');
     }
@@ -279,6 +348,13 @@ class AppStore extends EventEmitter {
     changeCourseColor(sectionCode: string, term: string, newColor: string) {
         this.schedule.changeCourseColor(sectionCode, term, newColor);
         this.unsavedChanges = true;
+        const action: ChangeCourseColorAction = {
+            type: 'changeCourseColor',
+            sectionCode: sectionCode,
+            term: term,
+            newColor: newColor,
+        };
+        actionTypesStore.autoSaveSchedule(action);
         this.colorPickers[sectionCode].emit('colorChange', newColor);
         this.emit('colorChange', false);
     }
diff --git a/apps/antalmanac/src/stores/Schedules.ts b/apps/antalmanac/src/stores/Schedules.ts
index 1179bb16..93566ebc 100644
--- a/apps/antalmanac/src/stores/Schedules.ts
+++ b/apps/antalmanac/src/stores/Schedules.ts
@@ -402,6 +402,13 @@ export class Schedules {
         return calendarizeCourseEvents(this.getCurrentCourses());
     }
 
+    /**
+     * Convert just custom events into calendar compatible format.
+     */
+    getCalendarizedCustomEvents() {
+        return calendarizeCustomEvents(this.getCurrentCustomEvents());
+    }
+
     /**
      * Convert finals into calendar friendly format
      */
diff --git a/apps/antalmanac/src/stores/SettingsStore.ts b/apps/antalmanac/src/stores/SettingsStore.ts
index 3e691a18..464d5324 100644
--- a/apps/antalmanac/src/stores/SettingsStore.ts
+++ b/apps/antalmanac/src/stores/SettingsStore.ts
@@ -85,3 +85,22 @@ export const usePreviewStore = create<PreviewStore>((set) => {
         },
     };
 });
+
+export interface AutoSaveStore {
+    autoSave: boolean;
+    setAutoSave: (autoSave: boolean) => void;
+}
+
+export const useAutoSaveStore = create<AutoSaveStore>((set) => {
+    const autoSave = typeof Storage !== 'undefined' && window.localStorage.getItem('autoSave') == 'true';
+
+    return {
+        autoSave,
+        setAutoSave: (autoSave) => {
+            if (typeof Storage !== 'undefined') {
+                window.localStorage.setItem('autoSave', autoSave.toString());
+            }
+            set({ autoSave });
+        },
+    };
+});
diff --git a/apps/antalmanac/src/stores/calendarizeHelpers.ts b/apps/antalmanac/src/stores/calendarizeHelpers.ts
index fe09f3d3..d2248510 100644
--- a/apps/antalmanac/src/stores/calendarizeHelpers.ts
+++ b/apps/antalmanac/src/stores/calendarizeHelpers.ts
@@ -3,7 +3,7 @@ import { HourMinute } from 'peterportal-api-next-types';
 import { RepeatingCustomEvent } from '@packages/antalmanac-types';
 import { CourseEvent, CustomEvent, Location } from '$components/Calendar/CourseCalendarEvent';
 import { notNull, getReferencesOccurring } from '$lib/utils';
-import { getDefaultFinalsStart } from '$lib/termData';
+import { getFinalsStartForTerm } from '$lib/termData';
 
 export const COURSE_WEEK_DAYS = ['Su', 'M', 'Tu', 'W', 'Th', 'F', 'Sa'];
 
@@ -107,11 +107,9 @@ export function calendarizeFinals(currentCourses: ScheduleCourse[] = []): Course
 
             /**
              * Fallback to January 2018 if no finals start date is available.
-             * defaultFinalsDay is handled later by day since it varies by day.
+             * finalsDay is handled later by day since it varies by day.
              */
-            const [defaultFinalsYear, defaultFinalsMonth, defaultFinalsDay] = [
-                ...(getDefaultFinalsStart() ?? [2018, 0]),
-            ];
+            const [finalsYear, finalsMonth, finalsDay] = [...(getFinalsStartForTerm(course.term) ?? [2018, 0])];
 
             return dayIndicesOcurring.map((dayIndex) => ({
                 color: course.section.color,
@@ -129,16 +127,16 @@ export function calendarizeFinals(currentCourses: ScheduleCourse[] = []): Course
                 sectionCode: course.section.sectionCode,
                 sectionType: 'Fin',
                 start: new Date(
-                    defaultFinalsYear,
-                    defaultFinalsMonth,
-                    defaultFinalsDay ? defaultFinalsDay + dayIndex : dayIndex - 1,
+                    finalsYear,
+                    finalsMonth,
+                    finalsDay ? finalsDay + dayIndex : dayIndex - 1,
                     startHour,
                     startMin
                 ),
                 end: new Date(
-                    defaultFinalsYear,
-                    defaultFinalsMonth,
-                    defaultFinalsDay ? defaultFinalsDay + dayIndex : dayIndex - 1,
+                    finalsYear,
+                    finalsMonth,
+                    finalsDay ? finalsDay + dayIndex : dayIndex - 1,
                     endHour,
                     endMin
                 ),


Test patch:
diff --git a/apps/antalmanac/tests/calendarize-helpers.test.ts b/apps/antalmanac/tests/calendarize-helpers.test.ts
index 785b789b..f52ff42a 100644
--- a/apps/antalmanac/tests/calendarize-helpers.test.ts
+++ b/apps/antalmanac/tests/calendarize-helpers.test.ts
@@ -61,7 +61,7 @@ describe('calendarize-helpers', () => {
                 status: 'OPEN',
                 sectionComment: 'placeholderSectionComment',
             },
-            term: 'placeholderTerm',
+            term: '2024 Winter',
         },
     ];
 
@@ -70,7 +70,7 @@ describe('calendarize-helpers', () => {
         {
             locations: [],
             color: 'placeholderColor',
-            term: 'placeholderTerm',
+            term: '2024 Winter',
             title: 'placeholderDeptCode placeholderCourseNumber',
             courseTitle: 'placeholderCourseTitle',
             instructors: [],
@@ -99,7 +99,7 @@ describe('calendarize-helpers', () => {
         {
             locations: [],
             color: 'placeholderColor',
-            term: 'placeholderTerm',
+            term: '2024 Winter',
             title: 'placeholderDeptCode placeholderCourseNumber',
             courseTitle: 'placeholderCourseTitle',
             instructors: [],
@@ -128,7 +128,7 @@ describe('calendarize-helpers', () => {
         {
             locations: [],
             color: 'placeholderColor',
-            term: 'placeholderTerm',
+            term: '2024 Winter',
             title: 'placeholderDeptCode placeholderCourseNumber',
             courseTitle: 'placeholderCourseTitle',
             instructors: [],
@@ -160,13 +160,13 @@ describe('calendarize-helpers', () => {
         {
             locations: [],
             color: 'placeholderColor',
-            term: 'placeholderTerm',
+            term: '2024 Winter',
             title: 'placeholderDeptCode placeholderCourseNumber',
             courseTitle: 'placeholderCourseTitle',
             instructors: [],
             sectionCode: 'placeholderSectionCode',
             sectionType: 'Fin',
-            start: new Date(2024, 2, 17, 1, 2), // Predicated on default term being Spring 2024
+            start: new Date(2024, 2, 17, 1, 2), // Winter 2024 dates
             end: new Date(2024, 2, 17, 3, 4), // ...
             finalExam: {
                 examStatus: 'SCHEDULED_FINAL',
diff --git a/apps/antalmanac/tests/locations.test.ts b/apps/antalmanac/tests/locations.test.ts
new file mode 100644
index 00000000..7afb6885
--- /dev/null
+++ b/apps/antalmanac/tests/locations.test.ts
@@ -0,0 +1,45 @@
+import { describe, test, expect } from 'vitest';
+import locationIds from '$lib/location_ids';
+import buildingCatalogue from '$lib/buildingCatalogue';
+
+describe('locationIds', () => {
+    test('no duplicate names', () => {
+        const names = Object.keys(locationIds);
+        const uniqueNames = new Set(names);
+        expect(names.length).toBe(uniqueNames.size);
+    });
+});
+
+describe('buildingCatalogue', () => {
+    test('no duplicate names', () => {
+        const names = Object.values(buildingCatalogue).map((building) => building.name);
+        const uniqueNames = new Set(names);
+        expect(names.length).toBe(uniqueNames.size);
+    });
+});
+
+describe('locationIds and buildingCatalogue', () => {
+    test('all locationIds are in buildingCatalogue', () => {
+        const locationNames = Object.keys(locationIds);
+        const buildingNames = Object.values(buildingCatalogue).map((building) => {
+            const buildingName = building.name.includes('(')
+                ? building.name.substring(building.name.indexOf('(') + 1, building.name.indexOf(')'))
+                : building.name;
+            return buildingName;
+        });
+        for (const locationName of locationNames) {
+            expect(buildingNames).toContain(locationName);
+        }
+    });
+
+    test('all buildingCatalogue are in locationIds', () => {
+        const locationNames = Object.keys(locationIds);
+        const buildingNames = Object.values(buildingCatalogue).map((building) => building.name);
+        for (const buildingName of buildingNames) {
+            const name = buildingName.includes('(')
+                ? buildingName.substring(buildingName.indexOf('(') + 1, buildingName.indexOf(')'))
+                : buildingName;
+            expect(locationNames).toContain(name);
+        }
+    });
+});