From 6596c1329ddde22f4cae31e55a8ed663959e3373 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Sun, 14 Dec 2025 17:14:31 -0800 Subject: [PATCH 1/5] add end date --- src/components/upload-zone.tsx | 68 ++++++++++++++++++++++++-- src/server/api/routers/schedule.ts | 4 ++ src/server/services/google-calendar.ts | 7 ++- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/components/upload-zone.tsx b/src/components/upload-zone.tsx index 730ca8b..c0581d5 100644 --- a/src/components/upload-zone.tsx +++ b/src/components/upload-zone.tsx @@ -36,6 +36,7 @@ const STORAGE_KEYS = { fileName: "schedulesync_fileName", events: "schedulesync_events", startDate: "schedulesync_startDate", + endDate: "schedulesync_endDate", } as const; export function UploadZone() { @@ -45,6 +46,7 @@ export function UploadZone() { const [events, setEvents] = useState(null); const [calendarUrl, setCalendarUrl] = useState(null); const [startDate, setStartDate] = useState(undefined); + const [endDate, setEndDate] = useState(undefined); const { isSignedIn } = useAuth(); const { openSignIn } = useClerk(); @@ -90,6 +92,15 @@ export function UploadZone() { } sessionStorage.removeItem(STORAGE_KEYS.startDate); } + const savedEndDate = sessionStorage.getItem(STORAGE_KEYS.endDate); + if (savedEndDate) { + try { + setEndDate(new Date(savedEndDate)); + } catch { + // Ignore parse errors + } + sessionStorage.removeItem(STORAGE_KEYS.endDate); + } // Remove the redirect flag after restoring sessionStorage.removeItem("schedulesync_oauth_redirect"); } @@ -145,7 +156,14 @@ export function UploadZone() { console.error("Failed to save startDate to sessionStorage:", e); } } - }, [preview, fileName, events, startDate]); + if (endDate) { + try { + sessionStorage.setItem(STORAGE_KEYS.endDate, endDate.toISOString()); + } catch (e) { + console.error("Failed to save endDate to sessionStorage:", e); + } + } + }, [preview, fileName, events, startDate, endDate]); // Combined handler that saves state first, then opens sign-in modal const handleSignInClick = useCallback(() => { @@ -237,8 +255,8 @@ export function UploadZone() { const handleDownloadIcal = useCallback(() => { if (!events || !startDate) return; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - generateIcal.mutate({ events, timezone, startDate }); - }, [events, startDate, generateIcal]); + generateIcal.mutate({ events, timezone, startDate, endDate }); + }, [events, startDate, endDate, generateIcal]); const handleSyncToGoogle = useCallback(() => { if (!events || !startDate) return; @@ -248,8 +266,9 @@ export function UploadZone() { calendarName: "Class Schedule (Generated by ScheduleSync)", timezone, startDate, + endDate, }); - }, [events, startDate, syncToGoogleCalendar]); + }, [events, startDate, endDate, syncToGoogleCalendar]); return (
@@ -490,6 +509,47 @@ export function UploadZone() { )} + {/* End Date Picker */} + {events && events.length > 0 && ( + + +
+ + + + + + + + + + {!endDate && ( +

+ Optional: If provided, recurring events will end on this date + instead of the default 16 weeks +

+ )} +
+
+
+ )} + {/* Export Buttons */} {events && events.length > 0 && (
diff --git a/src/server/api/routers/schedule.ts b/src/server/api/routers/schedule.ts index 8e8d581..7119fb8 100644 --- a/src/server/api/routers/schedule.ts +++ b/src/server/api/routers/schedule.ts @@ -39,6 +39,7 @@ export const scheduleRouter = createTRPCRouter({ repeatWeeks: z.number().optional().default(16), timezone: z.string().optional(), startDate: z.date(), + endDate: z.date().optional(), }), ) .mutation(({ input }) => { @@ -48,6 +49,7 @@ export const scheduleRouter = createTRPCRouter({ repeatWeeks: input.repeatWeeks, timezone: input.timezone, startDate: input.startDate, + semesterEndDate: input.endDate, }); return { icalContent }; }), @@ -61,6 +63,7 @@ export const scheduleRouter = createTRPCRouter({ repeatWeeks: z.number().optional().default(16), timezone: z.string().optional(), startDate: z.date(), + endDate: z.date().optional(), }), ) .mutation(async ({ input, ctx }) => { @@ -96,6 +99,7 @@ export const scheduleRouter = createTRPCRouter({ repeatWeeks: input.repeatWeeks, timezone, startDate: input.startDate, + endDate: input.endDate, }); // Return the calendar URL diff --git a/src/server/services/google-calendar.ts b/src/server/services/google-calendar.ts index b13a519..b0e7fe8 100644 --- a/src/server/services/google-calendar.ts +++ b/src/server/services/google-calendar.ts @@ -55,6 +55,7 @@ export async function addEventsToCalendar( repeatWeeks?: number; timezone?: string; startDate?: Date; + endDate?: Date; } = {}, ): Promise { const calendar = google.calendar({ version: "v3" }); @@ -66,6 +67,7 @@ export async function addEventsToCalendar( repeatWeeks = 16, timezone = "UTC", startDate = new Date(), + endDate, } = options; // Process all events in parallel for better performance @@ -81,6 +83,7 @@ export async function addEventsToCalendar( timezone, startDate, repeatWeeks, + endDate, ), ), ); @@ -132,6 +135,7 @@ async function createRecurringEvent( timezone: string, startDate: Date, repeatWeeks: number, + endDate?: Date, ) { // Find the next occurrence of this day of week const dayMap: Record = { @@ -158,7 +162,8 @@ async function createRecurringEvent( const endDateTime = parse(event.endTime, "HH:mm", firstOccurrence); // Calculate the last occurrence (until date) - const lastOccurrence = addWeeks(firstOccurrence, repeatWeeks); + // Use endDate if provided, otherwise calculate from repeatWeeks + const lastOccurrence = endDate ?? addWeeks(firstOccurrence, repeatWeeks); // Create RRULE for weekly recurrence // Format lastOccurrence as UTC for RRULE UNTIL (RFC 5545 requires UTC if 'Z' is present) From 01bd23b9d9905bf0b06b88c4ad1a88853aee522f Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Sun, 14 Dec 2025 17:27:49 -0800 Subject: [PATCH 2/5] validate that end date is after start date --- src/components/upload-zone.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/upload-zone.tsx b/src/components/upload-zone.tsx index c0581d5..962c1f8 100644 --- a/src/components/upload-zone.tsx +++ b/src/components/upload-zone.tsx @@ -27,7 +27,7 @@ import { import { cn } from "@/lib/utils"; import { api } from "@/trpc/react"; import type { ScheduleEvent } from "@/server/services/schedule-analyzer"; -import { format, parse } from "date-fns"; +import { format, parse, isBefore, startOfDay } from "date-fns"; import { useAuth, useClerk } from "@clerk/nextjs"; // Storage keys for persisting state across OAuth redirect @@ -109,6 +109,17 @@ export function UploadZone() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSignedIn]); + // Clear endDate if it becomes invalid (before startDate) + useEffect(() => { + if ( + startDate && + endDate && + isBefore(startOfDay(endDate), startOfDay(startDate)) + ) { + setEndDate(undefined); + } + }, [startDate, endDate]); + // Save state to sessionStorage before OAuth redirect const saveStateForSignIn = useCallback(() => { // Set OAuth redirect flag @@ -536,6 +547,12 @@ export function UploadZone() { selected={endDate} onSelect={setEndDate} initialFocus + disabled={ + startDate + ? (date) => + isBefore(startOfDay(date), startOfDay(startDate)) + : undefined + } /> From 5cb5748aab79bbbd0ce4e580e223747ddc4d9fdd Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Sun, 14 Dec 2025 17:29:08 -0800 Subject: [PATCH 3/5] small message tweak --- src/components/upload-zone.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/upload-zone.tsx b/src/components/upload-zone.tsx index 962c1f8..c20e330 100644 --- a/src/components/upload-zone.tsx +++ b/src/components/upload-zone.tsx @@ -559,7 +559,7 @@ export function UploadZone() { {!endDate && (

Optional: If provided, recurring events will end on this date - instead of the default 16 weeks + instead of the default 16 weeks from the start date

)}
From 0a061929b609c4bd51415331dbdf22aeadf6e849 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Sun, 14 Dec 2025 17:37:28 -0800 Subject: [PATCH 4/5] Update src/server/api/routers/schedule.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/server/api/routers/schedule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routers/schedule.ts b/src/server/api/routers/schedule.ts index 7119fb8..d69f68b 100644 --- a/src/server/api/routers/schedule.ts +++ b/src/server/api/routers/schedule.ts @@ -49,7 +49,7 @@ export const scheduleRouter = createTRPCRouter({ repeatWeeks: input.repeatWeeks, timezone: input.timezone, startDate: input.startDate, - semesterEndDate: input.endDate, + endDate: input.endDate, }); return { icalContent }; }), From cf1c9c61a0b44a7f23c4b7f5a46174548320d1a5 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Sun, 14 Dec 2025 17:40:09 -0800 Subject: [PATCH 5/5] Revert "Update src/server/api/routers/schedule.ts" This reverts commit 0a061929b609c4bd51415331dbdf22aeadf6e849. --- src/server/api/routers/schedule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routers/schedule.ts b/src/server/api/routers/schedule.ts index d69f68b..7119fb8 100644 --- a/src/server/api/routers/schedule.ts +++ b/src/server/api/routers/schedule.ts @@ -49,7 +49,7 @@ export const scheduleRouter = createTRPCRouter({ repeatWeeks: input.repeatWeeks, timezone: input.timezone, startDate: input.startDate, - endDate: input.endDate, + semesterEndDate: input.endDate, }); return { icalContent }; }),