From 3afcfab2efbc79c6eaed0f8b6bd60d8f10ebfdff Mon Sep 17 00:00:00 2001 From: condyl Date: Sat, 10 Jan 2026 22:27:32 -0500 Subject: [PATCH 1/2] feat: filter timetable nav and warn on export --- .../generator/Calendar/CalendarComponent.jsx | 84 +++++---- .../Calendar/hooks/useTimetableManagement.jsx | 73 +------- .../Calendar/utils/calendarConfigUtils.js | 2 + .../Calendar/utils/calendarViewUtils.js | 165 ++++++++++-------- .../generator/Export/ExportCalendarButton.jsx | 117 ++++++++++++- .../Forms/InputFormBottomComponent.jsx | 4 +- .../Forms/Settings/ExportOptions.jsx | 4 +- src/pages/GeneratorPage.jsx | 4 + 8 files changed, 267 insertions(+), 186 deletions(-) diff --git a/src/components/generator/Calendar/CalendarComponent.jsx b/src/components/generator/Calendar/CalendarComponent.jsx index 30f0c2a..74bae15 100644 --- a/src/components/generator/Calendar/CalendarComponent.jsx +++ b/src/components/generator/Calendar/CalendarComponent.jsx @@ -1,4 +1,10 @@ -import React, { useState, useEffect, useContext, useCallback } from "react"; +import React, { + useState, + useEffect, + useContext, + useCallback, + useMemo, +} from "react"; import FullCalendar from "@fullcalendar/react"; import { useSnackbar } from "notistack"; import CalendarNavBar from "./CalendarNavBar"; @@ -30,14 +36,12 @@ import { useEventBusHandlers } from "./hooks/useEventBusHandlers.js"; import { prepareCoursesForTimeline } from "./utils/courseTimelineUtils.js"; import { calculateNavigationDate, - handleDurationChange, getCalendarViewNotificationMessage, + getVisibleTimetables, } from "./utils/calendarViewUtils.js"; import { getFullCalendarConfig } from "./utils/calendarConfigUtils.js"; import MultiLineSnackbar from "@/components/sitewide/MultiLineSnackbar"; import { useIsMobile } from "@/lib/utils/screenSizeUtils"; -let previousDuration = null; -let aprioriDurationTimetable = null; export default function CalendarComponent({ timetables, setTimetables, @@ -53,6 +57,7 @@ export default function CalendarComponent({ const courseColorsRef = React.useRef({}); const [events, setEvents] = useState([]); const [currentTimetableIndex, setCurrentTimetableIndex] = useState(0); + const [viewRange, setViewRange] = useState(null); const { setCourseDetails } = useContext(CourseDetailsContext); const { courseColors, setCalendarUpdateHandler, getDefaultColorForCourse } = useContext(CourseColorsContext); @@ -67,6 +72,11 @@ export default function CalendarComponent({ const [renameAnchorPosition, setRenameAnchorPosition] = useState(null); const [coursesForTimeline, setCoursesForTimeline] = useState([]); + const visibleTimetables = useMemo( + () => getVisibleTimetables(timetables, viewRange), + [timetables, viewRange], + ); + // Screen size detection const isMobile = useIsMobile(); @@ -81,13 +91,20 @@ export default function CalendarComponent({ }); useEffect(() => { - timetablesRef.current = timetables; - }, [timetables]); + timetablesRef.current = visibleTimetables; + }, [visibleTimetables]); useEffect(() => { currentTimetableIndexRef.current = currentTimetableIndex; }, [currentTimetableIndex]); + useEffect(() => { + if (visibleTimetables.length === 0) return; + if (currentTimetableIndex >= visibleTimetables.length) { + setCurrentTimetableIndex(0); + } + }, [currentTimetableIndex, visibleTimetables.length]); + useEffect(() => { courseColorsRef.current = courseColors; updateCalendarEvents(); @@ -95,7 +112,7 @@ export default function CalendarComponent({ useEffect(() => { updateCalendarEvents(); - }, [currentTimetableIndex, timetables]); + }, [currentTimetableIndex, visibleTimetables]); useEffect(() => { if (selectedDuration) { @@ -108,8 +125,9 @@ export default function CalendarComponent({ }, []); const handleLast = useCallback(() => { - setCurrentTimetableIndex(timetables.length - 1); - }, [timetables.length]); + if (visibleTimetables.length === 0) return; + setCurrentTimetableIndex(visibleTimetables.length - 1); + }, [visibleTimetables.length]); const updateCalendarEvents = useCallback(() => { const currentTimetables = timetablesRef.current; @@ -146,13 +164,6 @@ export default function CalendarComponent({ const hasWeekendClasses = checkForWeekendClasses(timetable); setShowWeekends(hasWeekendClasses); - if ( - previousDuration === selectedDuration.split("-")[2] && - JSON.stringify(timetable) - ) { - aprioriDurationTimetable = JSON.parse(JSON.stringify(timetable)); - } - const newEvents = createCalendarEvents( timetable, getDaysOfWeek, @@ -177,8 +188,6 @@ export default function CalendarComponent({ setNoTimetablesGenerated(false); } else { setNoCourses(true); - previousDuration = null; - aprioriDurationTimetable = null; const newEvents = createCalendarEvents( null, getDaysOfWeek, @@ -202,25 +211,13 @@ export default function CalendarComponent({ const handleCalendarViewClick = (durationLabel) => { const calendarApi = calendarRef.current.getApi(); - const [startUnix, endUnix, duration] = durationLabel.split("-"); + const [startUnix] = durationLabel.split("-"); const startDate = new Date(parseInt(startUnix) * 1000); const navigationDate = calculateNavigationDate(startDate); calendarApi.gotoDate(navigationDate); - if (previousDuration == null) { - previousDuration = duration; - } else { - handleDurationChange( - previousDuration, - duration, - aprioriDurationTimetable, - setCurrentTimetableIndex, - setTimetables, - sortOption, - ); - previousDuration = duration; - } + setCurrentTimetableIndex(0); setSelectedDuration(durationLabel); @@ -231,6 +228,13 @@ export default function CalendarComponent({ }); }; + const handleDatesSet = useCallback((dateInfo) => { + setViewRange({ + start: dateInfo.start, + end: dateInfo.end, + }); + }, []); + const [shiftHeld, setShiftHeld] = useState(false); const [hoveredElement, setHoveredElement] = useState(null); @@ -356,7 +360,10 @@ export default function CalendarComponent({ }; const handleNext = () => { - setCurrentTimetableIndex((currentTimetableIndex + 1) % timetables.length); + if (visibleTimetables.length === 0) return; + setCurrentTimetableIndex( + (currentTimetableIndex + 1) % visibleTimetables.length, + ); }; const handleRenameDialogClose = () => { @@ -392,8 +399,10 @@ export default function CalendarComponent({ }; const handlePrevious = () => { + if (visibleTimetables.length === 0) return; setCurrentTimetableIndex( - (currentTimetableIndex - 1 + timetables.length) % timetables.length, + (currentTimetableIndex - 1 + visibleTimetables.length) % + visibleTimetables.length, ); }; @@ -436,11 +445,11 @@ export default function CalendarComponent({ // Prepare courses for timeline useEffect(() => { const courses = prepareCoursesForTimeline( - timetables, + visibleTimetables, currentTimetableIndex, ); setCoursesForTimeline(courses); - }, [timetables, currentTimetableIndex]); + }, [visibleTimetables, currentTimetableIndex]); return (
@@ -463,7 +472,7 @@ export default function CalendarComponent({ handleNext={handleNext} handleLast={handleLast} currentTimetableIndex={currentTimetableIndex} - timetables={timetables} + timetables={visibleTimetables} selectedDuration={selectedDuration} setSelectedDuration={setSelectedDuration} durations={durations} @@ -476,6 +485,7 @@ export default function CalendarComponent({ calendarRef, showWeekends, events, + handleDatesSet, handleEventClick, handleSelect, handleSelectAllow, diff --git a/src/components/generator/Calendar/hooks/useTimetableManagement.jsx b/src/components/generator/Calendar/hooks/useTimetableManagement.jsx index 4eed4d1..71c4186 100644 --- a/src/components/generator/Calendar/hooks/useTimetableManagement.jsx +++ b/src/components/generator/Calendar/hooks/useTimetableManagement.jsx @@ -1,17 +1,8 @@ import { useCallback, useRef } from "react"; import { useSnackbar } from "notistack"; import MultiLineSnackbar from "@/components/sitewide/MultiLineSnackbar"; -import { - addPinnedComponent, - getPinnedComponents, -} from "@/lib/generator/pinnedComponents"; -import { - generateTimetables, - getValidTimetables, -} from "@/lib/generator/timetableGeneration/timetableGeneration"; let previousDuration = null; -let aprioriDurationTimetable = null; export const useTimetableManagement = ({ timetables, @@ -70,61 +61,8 @@ export const useTimetableManagement = ({ if (previousDuration == null) { previousDuration = duration; } else if (previousDuration !== duration) { - if (aprioriDurationTimetable && aprioriDurationTimetable.courses) { - const pinnedComponents = getPinnedComponents(); - let didPinNewComponent = false; - - aprioriDurationTimetable.courses.forEach((course) => { - const courseCode = course.courseCode; - - if (course.mainComponents) { - course.mainComponents.forEach((component) => { - if ( - component.schedule && - component.schedule.duration == previousDuration - ) { - const pinString = `${courseCode} MAIN ${component.id}`; - if (!pinnedComponents.includes(pinString)) { - addPinnedComponent(pinString); - didPinNewComponent = true; - } - } - }); - } - - if (course.secondaryComponents) { - Object.entries(course.secondaryComponents).forEach( - ([type, component]) => { - if ( - component && - component.schedule && - component.schedule.duration == previousDuration - ) { - const formattedType = - type.toLowerCase() === "tutorial" - ? "TUT" - : type.toLowerCase() === "seminar" - ? "SEM" - : type.toUpperCase(); - - const pinString = `${courseCode} ${formattedType} ${component.id}`; - if (!pinnedComponents.includes(pinString)) { - addPinnedComponent(pinString); - didPinNewComponent = true; - } - } - }, - ); - } - }); - - previousDuration = duration; - setCurrentTimetableIndex(0); - if (didPinNewComponent) { - generateTimetables(sortOption); - setTimetables(getValidTimetables()); - } - } + previousDuration = duration; + setCurrentTimetableIndex(0); } setSelectedDuration(durationLabel); @@ -153,12 +91,7 @@ export const useTimetableManagement = ({ ], ); - // Function to set the apriori duration timetable - const setAprioriDurationTimetable = useCallback((timetable, duration) => { - if (previousDuration === duration && JSON.stringify(timetable)) { - aprioriDurationTimetable = JSON.parse(JSON.stringify(timetable)); - } - }, []); + const setAprioriDurationTimetable = useCallback(() => {}, []); return { handleNext, diff --git a/src/components/generator/Calendar/utils/calendarConfigUtils.js b/src/components/generator/Calendar/utils/calendarConfigUtils.js index 88daa79..3c595b5 100644 --- a/src/components/generator/Calendar/utils/calendarConfigUtils.js +++ b/src/components/generator/Calendar/utils/calendarConfigUtils.js @@ -6,6 +6,7 @@ export const getFullCalendarConfig = ({ calendarRef, showWeekends, events, + handleDatesSet, handleEventClick, handleSelect, handleSelectAllow, @@ -31,6 +32,7 @@ export const getFullCalendarConfig = ({ eventClick: handleEventClick, eventMouseEnter: handleEventMouseEnter, eventMouseLeave: handleEventMouseLeave, + datesSet: handleDatesSet, selectable: true, selectMinDistance: 25, select: handleSelect, diff --git a/src/components/generator/Calendar/utils/calendarViewUtils.js b/src/components/generator/Calendar/utils/calendarViewUtils.js index 3cac8f1..f959874 100644 --- a/src/components/generator/Calendar/utils/calendarViewUtils.js +++ b/src/components/generator/Calendar/utils/calendarViewUtils.js @@ -1,11 +1,4 @@ -import { - addPinnedComponent, - getPinnedComponents, -} from "@/lib/generator/pinnedComponents"; -import { - generateTimetables, - getValidTimetables, -} from "@/lib/generator/timetableGeneration/timetableGeneration"; +import { getBaseComponentId } from "@/lib/generator/timetableGeneration/utils/componentIDUtils"; // Calculate navigation date based on start date and day of week export const calculateNavigationDate = (startDate) => { @@ -27,73 +20,103 @@ export const calculateNavigationDate = (startDate) => { return navigationDate; }; -// Handle duration change logic with component pinning -export const handleDurationChange = ( - previousDuration, - newDuration, - aprioriDurationTimetable, - setCurrentTimetableIndex, - setTimetables, - sortOption, -) => { - if (previousDuration !== newDuration) { - if (aprioriDurationTimetable && aprioriDurationTimetable.courses) { - const pinnedComponents = getPinnedComponents(); - let didPinNewComponent = false; - - aprioriDurationTimetable.courses.forEach((course) => { - const courseCode = course.courseCode; - - // Handle main components - if (course.mainComponents) { - course.mainComponents.forEach((component) => { - if ( - component.schedule && - component.schedule.duration == previousDuration - ) { - const pinString = `${courseCode} MAIN ${component.id}`; - if (!pinnedComponents.includes(pinString)) { - addPinnedComponent(pinString); - didPinNewComponent = true; - } - } - }); - } - - // Handle secondary components - if (course.secondaryComponents) { - Object.entries(course.secondaryComponents).forEach( - ([type, component]) => { - if ( - component && - component.schedule && - component.schedule.duration == previousDuration - ) { - const formattedType = - type.toLowerCase() === "tutorial" - ? "TUT" - : type.toLowerCase() === "seminar" - ? "SEM" - : type.toUpperCase(); - - const pinString = `${courseCode} ${formattedType} ${component.id}`; - if (!pinnedComponents.includes(pinString)) { - addPinnedComponent(pinString); - didPinNewComponent = true; - } - } - }, - ); - } +const getViewRangeSeconds = (viewRange) => { + if (!viewRange?.start || !viewRange?.end) { + return null; + } + + return { + start: Math.floor(viewRange.start.getTime() / 1000), + end: Math.floor((viewRange.end.getTime() - 1) / 1000), + }; +}; + +const doesComponentOverlapRange = (component, range) => { + if (!range) return true; + + const { startDate, endDate } = component.schedule || {}; + if (startDate == null || endDate == null) { + return true; + } + + return startDate <= range.end && endDate >= range.start; +}; + +const buildComponentSignature = (component, type, range) => { + if (!component || !component.schedule) return null; + if (!doesComponentOverlapRange(component, range)) return null; + + const baseId = getBaseComponentId(component.id); + const duration = component.schedule.duration ?? ""; + return `${type}:${baseId}:${duration}`; +}; + +export const getVisibleTimetableSignature = (timetable, viewRange) => { + if (!timetable?.courses || !viewRange) { + return null; + } + + const range = getViewRangeSeconds(viewRange); + const courseSignatures = []; + + timetable.courses.forEach((course) => { + const componentSignatures = []; + + if (course.mainComponents) { + course.mainComponents.forEach((component) => { + const signature = buildComponentSignature(component, "MAIN", range); + if (signature) componentSignatures.push(signature); }); + } + + if (course.secondaryComponents) { + const { lab, tutorial, seminar } = course.secondaryComponents; + const labSignature = buildComponentSignature(lab, "LAB", range); + const tutSignature = buildComponentSignature(tutorial, "TUT", range); + const semSignature = buildComponentSignature(seminar, "SEM", range); + + if (labSignature) componentSignatures.push(labSignature); + if (tutSignature) componentSignatures.push(tutSignature); + if (semSignature) componentSignatures.push(semSignature); + } - setCurrentTimetableIndex(0); - if (didPinNewComponent) { - generateTimetables(sortOption); - setTimetables(getValidTimetables()); - } + if (componentSignatures.length > 0) { + componentSignatures.sort(); + courseSignatures.push( + `${course.courseCode}|${componentSignatures.join(",")}`, + ); } + }); + + if (courseSignatures.length === 0) { + return "no-visible-courses"; } + + courseSignatures.sort(); + return courseSignatures.join("||"); +}; + +export const getVisibleTimetables = (timetables, viewRange) => { + if (!Array.isArray(timetables)) return []; + if (!viewRange) return timetables; + + const uniqueSignatures = new Set(); + const filteredTimetables = []; + + timetables.forEach((timetable) => { + const signature = getVisibleTimetableSignature(timetable, viewRange); + if (signature == null) { + filteredTimetables.push(timetable); + return; + } + + if (!uniqueSignatures.has(signature)) { + uniqueSignatures.add(signature); + filteredTimetables.push(timetable); + } + }); + + return filteredTimetables; }; // Get calendar view notification message diff --git a/src/components/generator/Export/ExportCalendarButton.jsx b/src/components/generator/Export/ExportCalendarButton.jsx index 5ca6f84..098c78e 100644 --- a/src/components/generator/Export/ExportCalendarButton.jsx +++ b/src/components/generator/Export/ExportCalendarButton.jsx @@ -1,11 +1,118 @@ -import React from "react"; +import React, { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { exportCal } from "@/lib/generator/ExportCal.js"; +import { getVisibleTimetables } from "@/components/generator/Calendar/utils/calendarViewUtils.js"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +const formatDurationText = (duration) => { + const [startUnix, endUnix, dur] = duration.split("-"); + const startDate = new Date(parseInt(startUnix, 10) * 1000); + const endDate = new Date(parseInt(endUnix, 10) * 1000); + + const startMonth = startDate.toLocaleString("default", { month: "short" }); + const endMonth = endDate.toLocaleString("default", { month: "short" }); + + return `${startMonth} - ${endMonth} (D${dur})`; +}; + +const getDurationRange = (duration) => { + const [startUnix, endUnix] = duration.split("-"); + return { + start: new Date(parseInt(startUnix, 10) * 1000), + end: new Date(parseInt(endUnix, 10) * 1000), + }; +}; + +const getDurationsWithMultipleVariants = (timetables, durations) => { + if (!Array.isArray(timetables) || timetables.length === 0) { + return []; + } + + if (!Array.isArray(durations) || durations.length === 0) { + return []; + } + + return durations + .map((duration) => { + const range = getDurationRange(duration); + const visibleTimetables = getVisibleTimetables(timetables, range); + return { + duration, + count: visibleTimetables.length, + }; + }) + .filter((entry) => entry.count > 1); +}; + +export default function ExportCalendarButton({ timetables, durations }) { + const [confirmOpen, setConfirmOpen] = useState(false); + + const durationsWithVariants = useMemo( + () => getDurationsWithMultipleVariants(timetables, durations), + [timetables, durations], + ); + + const handleExport = () => { + if (durationsWithVariants.length > 0) { + setConfirmOpen(true); + return; + } + + exportCal(); + }; -export default function ExportCalendarButton() { return ( - + <> + + + + + Multiple timetable options + + You still have more than one timetable variant in these terms. + Exporting now might not reflect your final schedule. + + +
+
+ Terms with variants +
+
    + {durationsWithVariants.map(({ duration, count }) => ( +
  • + {formatDurationText(duration)} ({count} timetables) +
  • + ))} +
+
+ + + + +
+
+ ); } diff --git a/src/components/generator/Forms/InputFormBottomComponent.jsx b/src/components/generator/Forms/InputFormBottomComponent.jsx index 85ffca0..69155d1 100644 --- a/src/components/generator/Forms/InputFormBottomComponent.jsx +++ b/src/components/generator/Forms/InputFormBottomComponent.jsx @@ -12,6 +12,8 @@ export default function InputFormBottomComponent({ addedCourses, setAddedCourses, setTimetables, + timetables, + durations, sortOption, generateTimetables, getValidTimetables, @@ -47,7 +49,7 @@ export default function InputFormBottomComponent({ )} - + diff --git a/src/components/generator/Forms/Settings/ExportOptions.jsx b/src/components/generator/Forms/Settings/ExportOptions.jsx index 5d3adcc..d043505 100644 --- a/src/components/generator/Forms/Settings/ExportOptions.jsx +++ b/src/components/generator/Forms/Settings/ExportOptions.jsx @@ -9,7 +9,7 @@ import { } from "@/components/ui/collapsible"; import { Separator } from "@/components/ui/separator"; -export default function ExportOptions() { +export default function ExportOptions({ timetables, durations }) { const [showHelp, setShowHelp] = useState(false); return ( @@ -17,7 +17,7 @@ export default function ExportOptions() {
{/* Main export section */}
- +
{/* Help section */} diff --git a/src/pages/GeneratorPage.jsx b/src/pages/GeneratorPage.jsx index 6be9129..eda64ba 100644 --- a/src/pages/GeneratorPage.jsx +++ b/src/pages/GeneratorPage.jsx @@ -102,6 +102,8 @@ function GeneratorPage() { addedCourses={addedCourses} setAddedCourses={setAddedCourses} setTimetables={setTimetables} + timetables={timetables} + durations={durations} sortOption={sortOption} generateTimetables={generateTimetables} getValidTimetables={getValidTimetables} @@ -129,6 +131,8 @@ function GeneratorPage() { addedCourses={addedCourses} setAddedCourses={setAddedCourses} setTimetables={setTimetables} + timetables={timetables} + durations={durations} sortOption={sortOption} generateTimetables={generateTimetables} getValidTimetables={getValidTimetables} From 05ba490420025e1480d9438b8606aef5b061de3e Mon Sep 17 00:00:00 2001 From: condyl Date: Sun, 18 Jan 2026 12:38:38 -0500 Subject: [PATCH 2/2] fix: timeline going to correct duration --- .../Calendar/CourseTimelineComponent.jsx | 30 +++++--- .../Calendar/utils/courseTimelineUtils.js | 77 +++++++++++-------- 2 files changed, 62 insertions(+), 45 deletions(-) diff --git a/src/components/generator/Calendar/CourseTimelineComponent.jsx b/src/components/generator/Calendar/CourseTimelineComponent.jsx index 55a650b..4114447 100644 --- a/src/components/generator/Calendar/CourseTimelineComponent.jsx +++ b/src/components/generator/Calendar/CourseTimelineComponent.jsx @@ -239,10 +239,15 @@ export default function CourseTimelineComponent({ course.string || `${course.code} ${course.section || ""} (${course.duration || ""})`; - let startDate = course.startDate - ? parseStartDate(course.startDate) - : null; - let endDate = course.endDate ? parseEndDate(course.endDate) : null; + let startDate = course.startDate; + let endDate = course.endDate; + + if (startDate && !(startDate instanceof Date)) { + startDate = parseStartDate(startDate); + } + if (endDate && !(endDate instanceof Date)) { + endDate = parseEndDate(endDate); + } if ( !startDate || @@ -268,6 +273,7 @@ export default function CourseTimelineComponent({ } return { + id: `${courseName}-${course.duration || "nodur"}-${startDate.getTime()}`, code: courseName, fullName: courseString, startDate, @@ -525,7 +531,7 @@ export default function CourseTimelineComponent({ {/* Course bars with labels */} {coursesWithDates.map((course, index) => ( - + {/* Course label */} handleCourseClick(course, event)} - onMouseEnter={() => setHoveredCourse(course.code)} + onMouseEnter={() => setHoveredCourse(course.id)} onMouseLeave={() => setHoveredCourse(null)} sx={{ position: "absolute", @@ -566,7 +572,7 @@ export default function CourseTimelineComponent({ cursor: "pointer", transition: "all 0.15s cubic-bezier(0.4, 0, 0.2, 1)", boxShadow: - hoveredCourse === course.code + hoveredCourse === course.id ? (theme) => `0 2px 4px ${alpha( course.color, @@ -597,7 +603,7 @@ export default function CourseTimelineComponent({ TransitionProps={{ timeout: 300 }} > setHoveredCourse(course.code)} + onMouseEnter={() => setHoveredCourse(course.id)} onMouseLeave={() => setHoveredCourse(null)} onClick={(event) => handleCourseClick(course, event)} sx={{ @@ -607,14 +613,14 @@ export default function CourseTimelineComponent({ width: `${course.widthPercent}%`, height: "12px", backgroundColor: course.color, - opacity: hoveredCourse === course.code ? 1 : 0.85, + opacity: hoveredCourse === course.id ? 1 : 0.85, borderRadius: "6px", cursor: "pointer", transition: "all 0.15s cubic-bezier(0.4, 0, 0.2, 1)", transform: - hoveredCourse === course.code ? "translateY(-1px)" : "none", + hoveredCourse === course.id ? "translateY(-1px)" : "none", boxShadow: - hoveredCourse === course.code + hoveredCourse === course.id ? (theme) => `0 2px 4px ${alpha( course.color, @@ -629,7 +635,7 @@ export default function CourseTimelineComponent({ transform: "translateY(0px) scale(0.98)", transition: "all 0.1s cubic-bezier(0.4, 0, 0.2, 1)", }, - zIndex: hoveredCourse === course.code ? 3 : 2, + zIndex: hoveredCourse === course.id ? 3 : 2, }} /> diff --git a/src/components/generator/Calendar/utils/courseTimelineUtils.js b/src/components/generator/Calendar/utils/courseTimelineUtils.js index 1529dab..9a81e23 100644 --- a/src/components/generator/Calendar/utils/courseTimelineUtils.js +++ b/src/components/generator/Calendar/utils/courseTimelineUtils.js @@ -1,4 +1,5 @@ import { getCourseData } from "@/lib/generator/courseData"; +import { getPinnedComponents } from "@/lib/generator/pinnedComponents"; export const prepareCoursesForTimeline = ( timetables, @@ -14,50 +15,60 @@ export const prepareCoursesForTimeline = ( const coursesData = getCourseData(); const courses = []; + const pinnedComponents = getPinnedComponents(); + + const pinnedDurations = {}; + + pinnedComponents.forEach((pin) => { + if (pin.includes("DURATION")) { + const parts = pin.split(" "); + const courseCode = parts[0]; + const duration = parts[2]; + pinnedDurations[courseCode] = duration; + } + }); if (coursesData && Object.keys(coursesData).length > 0) { for (const key of Object.keys(coursesData)) { const course = coursesData[key]; - if (course.courseCode) { - let sectionInfo = ""; - let durationInfo = ""; - let startDate = ""; - let endDate = ""; - - if (course.sections && course.sections.length > 0) { - const section = course.sections[0]; - if (section.sectionNumber) { - sectionInfo = section.sectionNumber; - } + if (course.sections && course.sections.length > 0) { + const processedOfferings = new Set(); + course.sections.forEach((section) => { if (section.schedule) { - if (section.schedule.duration) { - durationInfo = section.schedule.duration; - } - - if (section.schedule.startDate) { - startDate = section.schedule.startDate; + const duration = section.schedule.duration || ""; + const startDate = section.schedule.startDate || ""; + const endDate = section.schedule.endDate || ""; + const sectionNum = section.sectionNumber || ""; + const code = course.courseCode; + + if (pinnedDurations[code] && pinnedDurations[code] !== duration) { + return; } - if (section.schedule.endDate) { - endDate = section.schedule.endDate; + const offeringKey = `${code}-${duration}-${startDate}-${endDate}`; + + if ( + !processedOfferings.has(offeringKey) && + (startDate || endDate) + ) { + const courseStr = `${code} ${sectionNum} (${duration})`; + + const courseObject = { + string: courseStr, + code: code, + section: sectionNum, + duration: duration, + startDate: startDate, + endDate: endDate, + }; + + courses.push(courseObject); + processedOfferings.add(offeringKey); } } - } - - const courseStr = `${course.courseCode} ${sectionInfo} (${durationInfo})`; - - const courseObject = { - string: courseStr, - code: course.courseCode, - section: sectionInfo, - duration: durationInfo, - startDate: startDate, - endDate: endDate, - }; - - courses.push(courseObject); + }); } else if (course.code) { const courseStr = `${course.code} ${course.section || ""} (${ course.duration || ""