Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
}
],
// Temporary for TS v6 migration, fix asap: https://github.com/icssc/AntAlmanac/issues/2096
"react/set-state-in-effect": "off",
"react/refs": "off",
"react/purity": "off"
"react/set-state-in-effect": "off"
},
"globals": {},
"ignorePatterns": [
Expand Down
5 changes: 1 addition & 4 deletions apps/antalmanac-planner/site/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
"jsx_a11y/prefer-tag-over-role": "off",
"typescript/triple-slash-reference": "off",
// Temporary for TS v6 migration, fix asap: https://github.com/icssc/AntAlmanac/issues/2096
"react/set-state-in-effect": "off",
"react/purity": "off",
"react/preserve-manual-memoization": "off",
"react/refs": "off"
"react/set-state-in-effect": "off"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Enabling react/purity still leaves the planner Logo component constructing a Date during render, so the planner lint target will fail. Move this date into a lazy state or otherwise fix the render-time impure call before enabling the rule.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/antalmanac-planner/site/.oxlintrc.json, line 24:

<comment>Enabling `react/purity` still leaves the planner `Logo` component constructing a `Date` during render, so the planner lint target will fail. Move this date into a lazy state or otherwise fix the render-time impure call before enabling the rule.</comment>

<file context>
@@ -21,10 +21,7 @@
-        "react/purity": "off",
-        "react/preserve-manual-memoization": "off",
-        "react/refs": "off"
+        "react/set-state-in-effect": "off"
     },
     "settings": {
</file context>

},
"settings": {
"next": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const MajorCourseList: FC<MajorCourseListProps> = ({
selectedSpecId,
onCatalogYearChange,
}) => {
const dispatch = useAppDispatch();

const storeKeyPrefix = `major-${majorWithSpec.major.id}`;
const [specsLoading, setSpecsLoading] = useState(false);
const [resultsLoading, setResultsLoading] = useState(false);
Expand All @@ -82,7 +84,6 @@ const MajorCourseList: FC<MajorCourseListProps> = ({
specOptions.unshift({ value: noSpec, label: noSpec.name });
}

const dispatch = useAppDispatch();
const { defaultCatalogYear } = getCatalogYearDefaults();

const loadSpecs = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ const AddYearPopup: FC<AddYearProps> = ({ buttonSize }) => {
const currentPlan = useAppSelector(selectCurrentPlan);
const plannerYears = currentPlan.content.yearPlans;
const dispatch = useAppDispatch();
const [currentDate] = useState(() => new Date());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: currentDate is captured only when AddYearPopup mounts, so an empty roadmap opened after New Year can default to the previous year. Refresh the date when opening the add-year modal (or otherwise derive the current year at that user action) instead of retaining this mount-time snapshot.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/antalmanac-planner/site/src/app/roadmap/planner/AddYearPopup.tsx, line 29:

<comment>`currentDate` is captured only when `AddYearPopup` mounts, so an empty roadmap opened after New Year can default to the previous year. Refresh the date when opening the add-year modal (or otherwise derive the current year at that user action) instead of retaining this mount-time snapshot.</comment>

<file context>
@@ -26,10 +26,11 @@ const AddYearPopup: FC<AddYearProps> = ({ buttonSize }) => {
     const currentPlan = useAppSelector(selectCurrentPlan);
     const plannerYears = currentPlan.content.yearPlans;
     const dispatch = useAppDispatch();
+    const [currentDate] = useState(() => new Date());
 
     const placeholderName = 'Year ' + (plannerYears.length + 1);
</file context>


const placeholderName = 'Year ' + (plannerYears.length + 1);
const placeholderYear =
plannerYears.length === 0 ? new Date().getFullYear() : plannerYears[plannerYears.length - 1].startYear + 1;
plannerYears.length === 0 ? currentDate.getFullYear() : plannerYears[plannerYears.length - 1].startYear + 1;

const saveHandler = (yearData: PlannerYearData) => {
const { startYear, name, quarters } = yearData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const Quarter: FC<QuarterProps> = ({ yearIndex, quarterIndex, data }) => {
const startYear = currentPlan.content.yearPlans[yearIndex].startYear;
const courses = data.courses;

const [currentDate] = useState(() => new Date());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This freezes the planner's academic-year calculation for the lifetime of each mounted Quarter. If a planner remains open across the September boundary, course offering warnings continue using the previous academic year; refresh the date/year at the boundary or otherwise use a refreshable current-year source.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/antalmanac-planner/site/src/app/roadmap/planner/Quarter.tsx, line 61:

<comment>This freezes the planner's academic-year calculation for the lifetime of each mounted `Quarter`. If a planner remains open across the September boundary, course offering warnings continue using the previous academic year; refresh the date/year at the boundary or otherwise use a refreshable current-year source.</comment>

<file context>
@@ -58,6 +58,8 @@ const Quarter: FC<QuarterProps> = ({ yearIndex, quarterIndex, data }) => {
     const startYear = currentPlan.content.yearPlans[yearIndex].startYear;
     const courses = data.courses;
 
+    const [currentDate] = useState(() => new Date());
+
     // Calculate Quarter Stats
</file context>


// Calculate Quarter Stats
const unitCount = useMemo(() => {
const courses = data.courses;
Expand Down Expand Up @@ -212,7 +214,6 @@ const Quarter: FC<QuarterProps> = ({ yearIndex, quarterIndex, data }) => {
});

const searchPrevYears = 2;
const currentDate = new Date();
const currentStartYear =
currentDate.getMonth() >= 8 ? currentDate.getFullYear() : currentDate.getFullYear() - 1;
const useYear = Math.min(currentStartYear, startYear);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const MultiplanDropdown: FC<MultiplanDropdownProps> = ({
const [showDropdown, setShowDropdown] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const [popoverAnchorEl, setPopoverAnchorEl] = useState<HTMLDivElement | null>(null);

const allPlansCopy = allPlans.map((p) => ({ ...p }));

Expand All @@ -114,13 +115,18 @@ const MultiplanDropdown: FC<MultiplanDropdownProps> = ({
dispatch(setPlanIndex(allPlans.length));
};

const handleClick = () => {
setPopoverAnchorEl(containerRef.current);
setShowDropdown(!showDropdown);
};

return (
<div ref={containerRef}>
<Button
className="dropdown-button"
variant="outlined"
color="inherit"
onClick={() => setShowDropdown(!showDropdown)}
onClick={handleClick}
endIcon={<ArrowDropDownIcon />}
/** @todo potentially add this override to the theme as a variant */
sx={{ borderColor: theme.palette.text.secondary }}
Expand All @@ -131,7 +137,7 @@ const MultiplanDropdown: FC<MultiplanDropdownProps> = ({
className="multi-plan-selector"
open={showDropdown}
anchorReference="anchorEl"
anchorEl={containerRef.current}
anchorEl={popoverAnchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
onClose={() => {
if (!isDragging) setShowDropdown(false);
Expand Down
14 changes: 10 additions & 4 deletions apps/antalmanac-planner/site/src/component/AppHeader/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { CircularProgress } from '@mui/material';
import type { SelectChangeEvent } from '@mui/material/Select';
import { type QuarterName } from '@peterportal/types';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState, useCallback } from 'react';
import { useEffect, useState, useCallback, useMemo } from 'react';
import type { FormEvent } from 'react';

import { isCustomCourse, quarterDisplayNames } from '../../helpers/planner';
Expand Down Expand Up @@ -233,9 +233,15 @@ const ExportDialog = ({ showModal, setShowModal }: ExportDialogProps) => {

const currentWeek = parseCurrentWeek(schedule?.currentWeek ?? '');

const selectedRoadmap = allPlans.find((plan) => String(plan.id) === selectedRoadmapId);
const roadmapYears = selectedRoadmap?.content.yearPlans ?? [];
const selectedYear = roadmapYears.find((year) => String(year.startYear) === selectedYearStart);
const selectedRoadmap = useMemo(
() => allPlans.find((plan) => String(plan.id) === selectedRoadmapId),
[allPlans, selectedRoadmapId]
);
const roadmapYears = useMemo(() => selectedRoadmap?.content.yearPlans ?? [], [selectedRoadmap?.content.yearPlans]);
const selectedYear = useMemo(
() => roadmapYears.find((year) => String(year.startYear) === selectedYearStart),
[roadmapYears, selectedYearStart]
);
const yearQuarters = selectedYear?.quarters ?? [];

const handleClose = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import './RecentOfferingsTooltip.scss';
import { Tooltip } from '@mui/material';
import { FC } from 'react';
import { FC, useState } from 'react';

import RecentOfferingsTable from '../RecentOfferingsTable/RecentOfferingsTable';

Expand All @@ -9,9 +9,11 @@ interface RecentOfferingsTooltipProps {
}

const RecentOfferingsTooltip: FC<RecentOfferingsTooltipProps> = ({ terms }) => {
const [currentDate] = useState(() => new Date());
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not snapshot new Date() for the component lifetime; this leaves the previous-academic-year badges stale across October 1. Recalculate the date when rendering, or add an explicit time-based refresh if a stable render value is required.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/antalmanac-planner/site/src/component/RecentOfferingsTooltip/RecentOfferingsTooltip.tsx, line 14:

<comment>Do not snapshot `new Date()` for the component lifetime; this leaves the previous-academic-year badges stale across October 1. Recalculate the date when rendering, or add an explicit time-based refresh if a stable render value is required.</comment>

<file context>
@@ -11,7 +11,9 @@ interface RecentOfferingsTooltipProps {
     if (terms.length === 0) return null;
 
-    const prevYear = new Date().getMonth() > 9 ? new Date().getFullYear() : new Date().getFullYear() - 1;
+    const [currentDate] = useState(() => new Date());
+
+    const prevYear = currentDate.getMonth() > 9 ? currentDate.getFullYear() : currentDate.getFullYear() - 1;
</file context>


if (terms.length === 0) return null;

const prevYear = new Date().getMonth() > 9 ? new Date().getFullYear() : new Date().getFullYear() - 1;
const prevYear = currentDate.getMonth() > 9 ? currentDate.getFullYear() : currentDate.getFullYear() - 1;
const prevOfferings = [];

// if the course was offered in the previous academic year, add the corresponding emoji to prevOfferings
Expand Down
13 changes: 0 additions & 13 deletions apps/antalmanac-planner/site/src/hooks/firstRenderer.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function SectionColorSelector() {
const postHog = usePostHog();

const buttonRef = useRef<HTMLDivElement>(null);
const [menuAnchorElement, setMenuAnchorElement] = useState<HTMLDivElement | null>(null);
const [menuOpen, setMenuOpen] = useState(false);
// Synchronous mirror of `menuOpen` used to gate hover previews. MUI keeps menu items mounted
// (and firing mouse events) during the close animation, so a stray `onMouseEnter` from a
Expand Down Expand Up @@ -114,6 +115,7 @@ export function SectionColorSelector() {

const openMenu = useCallback(() => {
menuOpenRef.current = true;
setMenuAnchorElement(buttonRef.current);
setMenuOpen(true);
}, []);

Expand Down Expand Up @@ -198,12 +200,12 @@ export function SectionColorSelector() {
</Box>

<Menu
anchorEl={buttonRef.current}
anchorEl={menuAnchorElement}
open={menuOpen}
onClose={handleClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
slotProps={{ paper: { sx: { width: buttonRef.current?.offsetWidth, maxHeight: 320 } } }}
slotProps={{ paper: { sx: { width: menuAnchorElement?.offsetWidth, maxHeight: 320 } } }}
>
{options.map((option) => {
const isSelected = option.id === sectionColor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Alert, Snackbar, type SnackbarCloseReason } from '@mui/material';
import { useShallow } from 'zustand/react/shallow';

export const NotificationSnackbar = () => {
const { open, snackbarClosed, message, severity, durationSeconds, position, style } = useSnackbarStore(
const { open, snackbarClosed, message, severity, durationSeconds, position, style, snackbarKey } = useSnackbarStore(
useShallow((store) => ({
open: store.open,
snackbarClosed: store.snackbarClosed,
Expand All @@ -16,13 +16,12 @@ export const NotificationSnackbar = () => {
durationSeconds: store.durationSeconds,
position: store.position,
style: store.style,
snackbarKey: store.snackbarKey,
}))
);

const isMobile = useIsMobile();

const snackbarKey = open ? Date.now() : null;

const handleClose = (_event?: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => {
if (reason === 'clickaway') {
return;
Expand All @@ -31,9 +30,11 @@ export const NotificationSnackbar = () => {
snackbarClosed();
};

const key = open ? snackbarKey : null;

return (
<Snackbar
key={snackbarKey}
key={key}
open={open}
autoHideDuration={durationSeconds * 1000}
anchorOrigin={position}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ interface RecruitmentBannerProps {

export function RecruitmentBanner({ deptValue }: RecruitmentBannerProps) {
const [bannerVisibility, setBannerVisibility] = useState(true);
const [now, setNow] = useState(() => Date.now());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This mount-time now snapshot can prevent an expired dismissal from ever becoming eligible again while the course results pane stays mounted. Update the reference time when checking expiry (or schedule periodic refreshes) so the 11-week dismissal window is measured against the current time rather than the component mount time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/antalmanac-scheduler/site/src/components/RightPane/CoursePane/CourseRenderPane/RecruitmentBanner.tsx, line 15:

<comment>This mount-time `now` snapshot can prevent an expired dismissal from ever becoming eligible again while the course results pane stays mounted. Update the reference time when checking expiry (or schedule periodic refreshes) so the 11-week dismissal window is measured against the current time rather than the component mount time.</comment>

<file context>
@@ -12,17 +12,19 @@ interface RecruitmentBannerProps {
 
 export function RecruitmentBanner({ deptValue }: RecruitmentBannerProps) {
     const [bannerVisibility, setBannerVisibility] = useState(true);
+    const [now, setNow] = useState(() => Date.now());
+
     const theme = useTheme();
</file context>


const theme = useTheme();

const recruitmentDismissalTime = getLocalStorageRecruitmentDismissalTime();
const dismissedRecently =
recruitmentDismissalTime !== null &&
Date.now() - parseInt(recruitmentDismissalTime) < 11 * 7 * 24 * 3600 * 1000;
recruitmentDismissalTime !== null && now - parseInt(recruitmentDismissalTime) < 11 * 7 * 24 * 3600 * 1000;
const isRelevantDept = RELEVANT_DEPARTMENTS.includes(deptValue.toUpperCase());
const displayRecruitmentBanner = bannerVisibility && !dismissedRecently && isRelevantDept;

const handleClick = () => {
setLocalStorageRecruitmentDismissalTime(Date.now().toString());
setNow(Date.now());
setBannerVisibility(false);
};

Expand Down
12 changes: 11 additions & 1 deletion apps/antalmanac-scheduler/site/src/stores/SnackbarStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface SnackbarOptions {
durationSeconds: number;
position: ComponentProps<typeof Snackbar>['anchorOrigin'];
style: SxProps | null;
snackbarKey: number;
}

const DEFAULT_DURATION_SECONDS: SnackbarOptions['durationSeconds'] = 3;
Expand All @@ -22,6 +23,7 @@ const initialState: SnackbarOptions & { open: boolean } = {
durationSeconds: DEFAULT_DURATION_SECONDS,
position: DEFAULT_POSITION,
style: DEFAULT_STYLE,
snackbarKey: 0,
};

export const useSnackbarStore = create(
Expand All @@ -46,5 +48,13 @@ export function openSnackbar(
style = DEFAULT_STYLE,
}: Partial<Omit<SnackbarOptions, 'severity' | 'message'>> = {}
) {
useSnackbarStore.setState({ open: true, message, severity, durationSeconds, position, style });
useSnackbarStore.setState({
open: true,
message,
severity,
durationSeconds,
position,
style,
snackbarKey: useSnackbarStore.getState().snackbarKey + 1,
});
}
Loading