From b375f74745c95f1c4f1e293abb684f133a785fa2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 01:59:29 +0000 Subject: [PATCH 1/5] Initial plan From 0f8067be955385654816064ba6cab6fb7445598c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 02:32:02 +0000 Subject: [PATCH 2/5] feat: implement off-cycle DST timezone selection feature - Add isOffCycle flag to TimeZone interface to track manually selected DST variants - Update setSelectedDate to preserve off-cycle timezones when dates change - Enhance addTimezone method to accept isOffCycle parameter - Modify TimezoneModal callback signature to support off-cycle marking - Update plus button functionality to mark alternate timezones as off-cycle - Enhance search functionality to include alternate timezone variants - Add off-cycle detection for search-selected alternate timezones - Prevent automatic DST transitions for off-cycle timezones Fixes #167 Co-authored-by: tsmarvin <57049894+tsmarvin@users.noreply.github.com> --- src/scripts/index.ts | 82 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/src/scripts/index.ts b/src/scripts/index.ts index d423945..269e43d 100644 --- a/src/scripts/index.ts +++ b/src/scripts/index.ts @@ -41,6 +41,7 @@ export interface TimeZone { abbreviation: string; // Timezone abbreviation (e.g., "EDT", "JST") daylight?: DaylightData; isCustom?: boolean; // Flag to indicate custom timezone + isOffCycle?: boolean; // Flag to indicate timezone was manually selected in off-cycle state (prevents auto DST switching) coordinates?: { latitude: number; longitude: number }; // Optional coordinates for custom timezones } @@ -924,7 +925,10 @@ export class TimelineManager { } // Initialize modal with callback and selected date - this.modal = new TimezoneModal((timezone: TimeZone) => this.addTimezone(timezone), this.selectedDate); + this.modal = new TimezoneModal( + (timezone: TimeZone, isOffCycle?: boolean) => this.addTimezone(timezone, isOffCycle), + this.selectedDate, + ); // Initialize datetime modal with callback this.dateTimeModal = new DateTimeModal((dateTime: Date) => this.setSelectedDate(dateTime)); @@ -958,11 +962,16 @@ export class TimelineManager { this.renderTimeline(); } - public addTimezone(timezone: TimeZone): void { + public addTimezone(timezone: TimeZone, isOffCycle?: boolean): void { // Check if timezone already exists const exists = this.selectedTimezones.find(tz => tz.iana === timezone.iana); if (!exists) { - this.selectedTimezones.push(timezone); + // Create a copy of the timezone and set the off-cycle flag if specified + const timezoneToAdd: TimeZone = { + ...timezone, + ...(isOffCycle !== undefined && { isOffCycle }), + }; + this.selectedTimezones.push(timezoneToAdd); this.renderTimeline(); } } @@ -983,11 +992,29 @@ export class TimelineManager { public setSelectedDate(date: Date): void { this.selectedDate = new Date(date); - // Recalculate timezones for the new date to handle DST transitions - const { numRows } = getTimelineDimensions(); - this.selectedTimezones = getTimezonesForTimeline(numRows, this.selectedDate); + // Handle DST transitions while preserving off-cycle timezones + const updatedTimezones: TimeZone[] = []; + + for (const timezone of this.selectedTimezones) { + if (timezone.isOffCycle || timezone.isCustom) { + // Preserve off-cycle and custom timezones without DST adjustment + updatedTimezones.push(timezone); + } else { + // For regular timezones, recalculate for new date to handle DST transitions + const allTimezones = getAllTimezonesOrdered(this.selectedDate); + const updatedTimezone = allTimezones.find(tz => tz.iana === timezone.iana); + if (updatedTimezone) { + updatedTimezones.push(updatedTimezone); + } else { + // Fallback: keep original if not found in updated list + updatedTimezones.push(timezone); + } + } + } - // Re-add any custom timezones + this.selectedTimezones = updatedTimezones; + + // Re-add any saved custom timezones that might not be in the current selection const customTimezones = CustomTimezoneManager.getCustomTimezones(); for (const customTz of customTimezones) { // Only add if not already included @@ -998,7 +1025,10 @@ export class TimelineManager { } // Update the modal with the new date and recalculated timezones - this.modal = new TimezoneModal((timezone: TimeZone) => this.addTimezone(timezone), this.selectedDate); + this.modal = new TimezoneModal( + (timezone: TimeZone, isOffCycle?: boolean) => this.addTimezone(timezone, isOffCycle), + this.selectedDate, + ); this.renderTimeline(); } @@ -1785,11 +1815,11 @@ export class TimezoneModal { private filteredGroups: GroupedTimezone[]; private selectedIndex = 0; private currentUserTimezone: string; - private onTimezoneSelectedCallback: ((timezone: TimeZone) => void) | undefined; + private onTimezoneSelectedCallback: ((timezone: TimeZone, isOffCycle?: boolean) => void) | undefined; private userSearchQuery = ''; // Store user's search query separately private selectedDate: Date; // Date to use for timezone calculations - constructor(onTimezoneSelected?: (timezone: TimeZone) => void, selectedDate?: Date) { + constructor(onTimezoneSelected?: (timezone: TimeZone, isOffCycle?: boolean) => void, selectedDate?: Date) { this.selectedDate = selectedDate || new Date(); this.modal = document.getElementById('timezone-modal') as HTMLElement; this.overlay = document.getElementById('timezone-modal-overlay') as HTMLElement; @@ -1885,7 +1915,17 @@ export class TimezoneModal { // Check if query is an offset pattern (GMT-7, UTC+5:30, PDT-7, etc.) const offsetMatch = this.parseOffsetQuery(query); - for (const timezone of this.timezones) { + // Also search in grouped timezones for alternate variants + const allSearchTargets: TimeZone[] = [...this.timezones]; + + // Add alternate timezones from grouped timezones to search targets + for (const group of this.groupedTimezones) { + if (group.alternate && !allSearchTargets.find(tz => tz.iana === group.alternate?.iana)) { + allSearchTargets.push(group.alternate); + } + } + + for (const timezone of allSearchTargets) { let score = 0; // Handle offset search @@ -2187,9 +2227,9 @@ export class TimezoneModal { if (plusBtn && group.alternate) { plusBtn.addEventListener('click', e => { e.stopPropagation(); - // Select the alternate timezone directly + // Select the alternate timezone directly and mark as off-cycle if (this.onTimezoneSelectedCallback && group.alternate) { - this.onTimezoneSelectedCallback(group.alternate); + this.onTimezoneSelectedCallback(group.alternate, true); // Mark alternate as off-cycle } this.close(); }); @@ -2288,8 +2328,22 @@ export class TimezoneModal { const selectedTimezone = this.filteredTimezones[this.selectedIndex]; if (selectedTimezone) { console.log('Selected timezone:', selectedTimezone); + + // Check if this is an off-cycle variant by comparing with grouped timezones + let isOffCycle = false; + const matchingGroup = this.groupedTimezones.find( + group => + group.current.iana === selectedTimezone.iana || + (group.alternate && group.alternate.iana === selectedTimezone.iana), + ); + + if (matchingGroup && matchingGroup.alternate && matchingGroup.alternate.iana === selectedTimezone.iana) { + // This is the alternate timezone (off-cycle variant) + isOffCycle = true; + } + if (this.onTimezoneSelectedCallback) { - this.onTimezoneSelectedCallback(selectedTimezone); + this.onTimezoneSelectedCallback(selectedTimezone, isOffCycle); } this.close(); } From 2477026f84d9683f073f588e950e76eb99910a23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 03:08:03 +0000 Subject: [PATCH 3/5] refactor: address code review feedback for performance and clarity Co-authored-by: tsmarvin <57049894+tsmarvin@users.noreply.github.com> --- src/scripts/index.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/scripts/index.ts b/src/scripts/index.ts index 269e43d..705455e 100644 --- a/src/scripts/index.ts +++ b/src/scripts/index.ts @@ -41,7 +41,7 @@ export interface TimeZone { abbreviation: string; // Timezone abbreviation (e.g., "EDT", "JST") daylight?: DaylightData; isCustom?: boolean; // Flag to indicate custom timezone - isOffCycle?: boolean; // Flag to indicate timezone was manually selected in off-cycle state (prevents auto DST switching) + isOffCycle?: boolean; // Flag to indicate timezone was manually selected in alternate DST/Standard state (e.g., PST when PDT is current, prevents auto DST switching) coordinates?: { latitude: number; longitude: number }; // Optional coordinates for custom timezones } @@ -994,6 +994,7 @@ export class TimelineManager { // Handle DST transitions while preserving off-cycle timezones const updatedTimezones: TimeZone[] = []; + const allTimezones = getAllTimezonesOrdered(this.selectedDate); for (const timezone of this.selectedTimezones) { if (timezone.isOffCycle || timezone.isCustom) { @@ -1001,7 +1002,6 @@ export class TimelineManager { updatedTimezones.push(timezone); } else { // For regular timezones, recalculate for new date to handle DST transitions - const allTimezones = getAllTimezonesOrdered(this.selectedDate); const updatedTimezone = allTimezones.find(tz => tz.iana === timezone.iana); if (updatedTimezone) { updatedTimezones.push(updatedTimezone); @@ -1014,12 +1014,13 @@ export class TimelineManager { this.selectedTimezones = updatedTimezones; - // Re-add any saved custom timezones that might not be in the current selection + // Re-add any saved custom timezones that were not preserved in the updated selection const customTimezones = CustomTimezoneManager.getCustomTimezones(); + const existingIanas = new Set(this.selectedTimezones.map(tz => tz.iana)); + for (const customTz of customTimezones) { // Only add if not already included - const exists = this.selectedTimezones.find(tz => tz.iana === customTz.iana); - if (!exists) { + if (!existingIanas.has(customTz.iana)) { this.selectedTimezones.push(customTz); } } @@ -1811,6 +1812,7 @@ export class TimezoneModal { private downButton: HTMLElement; private timezones: TimeZone[]; private groupedTimezones: GroupedTimezone[]; + private groupedTimezonesLookup: Map; // Lookup map for efficient timezone searches private filteredTimezones: TimeZone[]; private filteredGroups: GroupedTimezone[]; private selectedIndex = 0; @@ -1837,6 +1839,15 @@ export class TimezoneModal { // Get both grouped and flat timezone lists this.groupedTimezones = getGroupedTimezones(this.selectedDate); + // Create lookup map for efficient timezone searches + this.groupedTimezonesLookup = new Map(); + for (const group of this.groupedTimezones) { + this.groupedTimezonesLookup.set(group.current.iana, group); + if (group.alternate) { + this.groupedTimezonesLookup.set(group.alternate.iana, group); + } + } + // Combine standard and custom timezones using the selected date const standardTimezones = getAllTimezonesOrdered(this.selectedDate); const customTimezones = CustomTimezoneManager.getCustomTimezones(); @@ -2331,11 +2342,7 @@ export class TimezoneModal { // Check if this is an off-cycle variant by comparing with grouped timezones let isOffCycle = false; - const matchingGroup = this.groupedTimezones.find( - group => - group.current.iana === selectedTimezone.iana || - (group.alternate && group.alternate.iana === selectedTimezone.iana), - ); + const matchingGroup = this.groupedTimezonesLookup.get(selectedTimezone.iana); if (matchingGroup && matchingGroup.alternate && matchingGroup.alternate.iana === selectedTimezone.iana) { // This is the alternate timezone (off-cycle variant) From dc31bcf162048bead6d4199424eaf4950144b493 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 03:24:58 +0000 Subject: [PATCH 4/5] fix: address code review feedback - improve comment clarity and optimize performance Co-authored-by: tsmarvin <57049894+tsmarvin@users.noreply.github.com> --- src/scripts/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scripts/index.ts b/src/scripts/index.ts index 705455e..fdb0c42 100644 --- a/src/scripts/index.ts +++ b/src/scripts/index.ts @@ -41,7 +41,7 @@ export interface TimeZone { abbreviation: string; // Timezone abbreviation (e.g., "EDT", "JST") daylight?: DaylightData; isCustom?: boolean; // Flag to indicate custom timezone - isOffCycle?: boolean; // Flag to indicate timezone was manually selected in alternate DST/Standard state (e.g., PST when PDT is current, prevents auto DST switching) + isOffCycle?: boolean; // Flag to indicate timezone was manually selected in alternate DST/Standard state (e.g., PST when PDT is current, prevents automatic DST transitions on date changes) coordinates?: { latitude: number; longitude: number }; // Optional coordinates for custom timezones } @@ -1839,7 +1839,7 @@ export class TimezoneModal { // Get both grouped and flat timezone lists this.groupedTimezones = getGroupedTimezones(this.selectedDate); - // Create lookup map for efficient timezone searches + // Create lookup map for efficient timezone group searches by IANA identifier this.groupedTimezonesLookup = new Map(); for (const group of this.groupedTimezones) { this.groupedTimezonesLookup.set(group.current.iana, group); @@ -1930,8 +1930,9 @@ export class TimezoneModal { const allSearchTargets: TimeZone[] = [...this.timezones]; // Add alternate timezones from grouped timezones to search targets + const existingIanas = new Set(allSearchTargets.map(tz => tz.iana)); for (const group of this.groupedTimezones) { - if (group.alternate && !allSearchTargets.find(tz => tz.iana === group.alternate?.iana)) { + if (group.alternate && !existingIanas.has(group.alternate.iana)) { allSearchTargets.push(group.alternate); } } From 3d56851f49a7c4bec5c128daee56fa7922f36e81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 03:43:31 +0000 Subject: [PATCH 5/5] perf: optimize object spread in addTimezone - avoid unnecessary copy when isOffCycle is undefined Co-authored-by: tsmarvin <57049894+tsmarvin@users.noreply.github.com> --- src/scripts/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/scripts/index.ts b/src/scripts/index.ts index fdb0c42..1420e40 100644 --- a/src/scripts/index.ts +++ b/src/scripts/index.ts @@ -967,10 +967,7 @@ export class TimelineManager { const exists = this.selectedTimezones.find(tz => tz.iana === timezone.iana); if (!exists) { // Create a copy of the timezone and set the off-cycle flag if specified - const timezoneToAdd: TimeZone = { - ...timezone, - ...(isOffCycle !== undefined && { isOffCycle }), - }; + const timezoneToAdd = isOffCycle !== undefined ? { ...timezone, isOffCycle } : timezone; this.selectedTimezones.push(timezoneToAdd); this.renderTimeline(); }