diff --git a/src/components/generator/Calendar/CalendarComponent.jsx b/src/components/generator/Calendar/CalendarComponent.jsx
index cd51021..beabdb4 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,
@@ -204,7 +213,7 @@ export default function CalendarComponent({
const calendarApi = calendarRef.current?.getApi();
if (!calendarApi) return;
- const [startUnix, endUnix, duration] = durationLabel.split("-");
+ const [startUnix] = durationLabel.split("-");
const startDate = new Date(parseInt(startUnix) * 1000);
const navigationDate = calculateNavigationDate(startDate);
@@ -214,19 +223,7 @@ export default function CalendarComponent({
calendarApi.gotoDate(navigationDate);
});
- if (previousDuration == null) {
- previousDuration = duration;
- } else {
- handleDurationChange(
- previousDuration,
- duration,
- aprioriDurationTimetable,
- setCurrentTimetableIndex,
- setTimetables,
- sortOption,
- );
- previousDuration = duration;
- }
+ setCurrentTimetableIndex(0);
setSelectedDuration(durationLabel);
@@ -237,6 +234,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);
@@ -362,7 +366,10 @@ export default function CalendarComponent({
};
const handleNext = () => {
- setCurrentTimetableIndex((currentTimetableIndex + 1) % timetables.length);
+ if (visibleTimetables.length === 0) return;
+ setCurrentTimetableIndex(
+ (currentTimetableIndex + 1) % visibleTimetables.length,
+ );
};
const handleRenameDialogClose = () => {
@@ -398,8 +405,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,
);
};
@@ -445,11 +454,11 @@ export default function CalendarComponent({
// Prepare courses for timeline
useEffect(() => {
const courses = prepareCoursesForTimeline(
- timetables,
+ visibleTimetables,
currentTimetableIndex,
);
setCoursesForTimeline(courses);
- }, [timetables, currentTimetableIndex]);
+ }, [visibleTimetables, currentTimetableIndex]);
return (
@@ -472,7 +481,7 @@ export default function CalendarComponent({
handleNext={handleNext}
handleLast={handleLast}
currentTimetableIndex={currentTimetableIndex}
- timetables={timetables}
+ timetables={visibleTimetables}
selectedDuration={selectedDuration}
setSelectedDuration={setSelectedDuration}
durations={durations}
@@ -485,6 +494,7 @@ export default function CalendarComponent({
calendarRef,
showWeekends,
events,
+ handleDatesSet,
handleEventClick,
handleSelect,
handleSelectAllow,
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/hooks/useTimetableManagement.jsx b/src/components/generator/Calendar/hooks/useTimetableManagement.jsx
index c554918..6b5f6ce 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,
@@ -73,61 +64,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);
@@ -156,12 +94,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/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 || ""
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 (
-
+ <>
+
+
+ >
);
}
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 efa22b3..11b29a8 100644
--- a/src/pages/GeneratorPage.jsx
+++ b/src/pages/GeneratorPage.jsx
@@ -105,6 +105,8 @@ function GeneratorPage() {
addedCourses={addedCourses}
setAddedCourses={setAddedCourses}
setTimetables={setTimetables}
+ timetables={timetables}
+ durations={durations}
sortOption={sortOption}
generateTimetables={generateTimetables}
getValidTimetables={getValidTimetables}
@@ -132,6 +134,8 @@ function GeneratorPage() {
addedCourses={addedCourses}
setAddedCourses={setAddedCourses}
setTimetables={setTimetables}
+ timetables={timetables}
+ durations={durations}
sortOption={sortOption}
generateTimetables={generateTimetables}
getValidTimetables={getValidTimetables}