Skip to content
144 changes: 122 additions & 22 deletions src/scripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function getUserTimezone(date?: Date): TimeZone {
// Get user's timezone ID using Temporal (polyfill ensures availability)
const userTimezone = Temporal.Now.timeZoneId();

const now = date || new Date();
const now = date || new Date(Temporal.Now.instant().epochMilliseconds);

// Use Intl for offset calculation (proven compatibility)
const formatter = new Intl.DateTimeFormat('en', {
Expand Down Expand Up @@ -421,7 +421,7 @@ function getTimezoneAbbreviation(displayName: string, iana: string, date?: Date)
});

// Format the provided date (or current date) and extract the timezone abbreviation
const parts = formatter.formatToParts(date || new Date());
const parts = formatter.formatToParts(date || new Date(Temporal.Now.instant().epochMilliseconds));
const timeZonePart = parts.find(part => part.type === 'timeZoneName');

if (timeZonePart && timeZonePart.value && timeZonePart.value.length <= 5) {
Expand Down Expand Up @@ -469,7 +469,7 @@ function createTimezoneDisplayName(iana: string, offset: number, date?: Date): s
});

// Get the timezone name from the provided date (or current date)
const parts = formatter.formatToParts(date || new Date());
const parts = formatter.formatToParts(date || new Date(Temporal.Now.instant().epochMilliseconds));
const timeZonePart = parts.find(part => part.type === 'timeZoneName');

if (timeZonePart && timeZonePart.value) {
Expand Down Expand Up @@ -513,7 +513,7 @@ function formatOffset(offset: number): string {
* @returns Array of timeline hours with daylight information and date transitions
*/
export function generateTimelineHours(numHours: number, timezone: TimeZone, baseDate?: Date): TimelineHour[] {
const now = baseDate || new Date();
const now = baseDate || new Date(Temporal.Now.instant().epochMilliseconds);
const userTz = getUserTimezone();

// Get current hour in user's timezone and round down
Expand Down Expand Up @@ -924,7 +924,7 @@ export class TimelineManager {
private modal: TimezoneModal;
private dateTimeModal: DateTimeModal;
private selectedTimezones: TimeZone[] = [];
private selectedDate: Date = new Date(); // Default to today
private selectedDate: Date = new Date(Temporal.Now.instant().epochMilliseconds); // Default to today

constructor() {
this.container = document.getElementById('timeline-container') as HTMLElement;
Expand Down Expand Up @@ -1323,15 +1323,17 @@ function saveTimezoneCache(): void {
* Initialize timezone data by processing all browser timezones for June and December
* This expensive operation should only be done once on page load
*/
function initializeTimezoneData(year: number = new Date().getFullYear()): ProcessedTimezoneData {
function initializeTimezoneData(year: number = Temporal.Now.plainDateISO().year): ProcessedTimezoneData {
const userTimezone = Temporal.Now.timeZoneId();

// Get all supported timezones (comprehensive list)
const allTimezones = Intl.supportedValuesOf('timeZone');

// Create dates for June 1st and December 31st to capture DST variations
const juneDate = new Date(year, 5, 1); // June 1st
const decemberDate = new Date(year, 11, 31); // December 31st
// Create dates for June 1st and December 31st to capture DST variations using Temporal
const junePlainDate = Temporal.PlainDate.from({ year, month: 6, day: 1 });
const decemberPlainDate = Temporal.PlainDate.from({ year, month: 12, day: 31 });
const juneDate = new Date(junePlainDate.toZonedDateTime('UTC').epochMilliseconds);
const decemberDate = new Date(decemberPlainDate.toZonedDateTime('UTC').epochMilliseconds);

console.log(`Processing ${allTimezones.length} timezones for June and December variants...`);

Expand Down Expand Up @@ -1464,7 +1466,7 @@ function getTimezoneSetForDate(date: Date, processedData: ProcessedTimezoneData)
* @returns Array of timezone objects ordered from user's timezone around the globe
*/
export function getAllTimezonesOrdered(date?: Date): TimeZone[] {
const now = date || new Date();
const now = date || new Date(Temporal.Now.instant().epochMilliseconds);
const currentYear = now.getFullYear();

// Load cache from localStorage on first call
Expand Down Expand Up @@ -1496,12 +1498,16 @@ export function getAllTimezonesOrdered(date?: Date): TimeZone[] {
* Get timezone variations for a given IANA identifier using fixed dates
* Returns both summer (June 1st) and winter (December 31st) variations
*/
function getTimezoneVariations(iana: string, year: number = new Date().getFullYear()): TimeZone[] {
function getTimezoneVariations(iana: string, year: number = Temporal.Now.plainDateISO().year): TimeZone[] {
const variations: TimeZone[] = [];

// Use June 1st for summer time and December 31st for winter time
const summerDate = new Date(year, 5, 1); // June 1st
const winterDate = new Date(year, 11, 31); // December 31st
// Use June 1st for summer time and December 31st for winter time using Temporal
const summerPlainDate = Temporal.PlainDate.from({ year, month: 6, day: 1 });
const winterPlainDate = Temporal.PlainDate.from({ year, month: 12, day: 31 });

// Convert to Date objects for Intl.DateTimeFormat compatibility using the target timezone
const summerDate = new Date(summerPlainDate.toZonedDateTime(iana).epochMilliseconds);
Copy link

Copilot AI Aug 10, 2025

Choose a reason for hiding this comment

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

This creates a Date object with milliseconds from the target timezone, but Date objects are always in the local system timezone. This could lead to incorrect offset calculations when the system timezone differs from the target timezone. Consider using the epoch milliseconds directly with Intl.DateTimeFormat or maintaining the UTC approach from initializeTimezoneData().

Copilot uses AI. Check for mistakes.
const winterDate = new Date(winterPlainDate.toZonedDateTime(iana).epochMilliseconds);

for (const date of [summerDate, winterDate]) {
Copy link

Copilot AI Aug 10, 2025

Choose a reason for hiding this comment

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

Same issue as the summerDate - creating a Date object with timezone-specific milliseconds may cause incorrect calculations when the system timezone differs from the target timezone.

Suggested change
// Convert to Date objects for Intl.DateTimeFormat compatibility using the target timezone
const summerDate = new Date(summerPlainDate.toZonedDateTime(iana).epochMilliseconds);
const winterDate = new Date(winterPlainDate.toZonedDateTime(iana).epochMilliseconds);
for (const date of [summerDate, winterDate]) {
// Use Temporal.ZonedDateTime objects directly for formatting and calculations
const summerZdt = summerPlainDate.toZonedDateTime(iana);
const winterZdt = winterPlainDate.toZonedDateTime(iana);
for (const zdt of [summerZdt, winterZdt]) {
// For Intl.DateTimeFormat, convert to Date using the UTC instant
const date = new Date(zdt.toInstant().epochMilliseconds);

Copilot uses AI. Check for mistakes.
const formatter = new Intl.DateTimeFormat('en', {
Expand Down Expand Up @@ -1574,7 +1580,7 @@ function extractRegion(iana: string): string {
* Get grouped timezones with DST/Standard variations, prioritized by selected date
*/
export function getGroupedTimezones(selectedDate?: Date): GroupedTimezone[] {
const date = selectedDate || new Date();
const date = selectedDate || new Date(Temporal.Now.instant().epochMilliseconds);
const allTimezones = Intl.supportedValuesOf('timeZone');
const locationGroups = new Map<string, GroupedTimezone>();

Expand Down Expand Up @@ -1809,7 +1815,7 @@ export class TimezoneModal {
private selectedDate: Date; // Date to use for timezone calculations

constructor(onTimezoneSelected?: (timezone: TimeZone, isOffCycle?: boolean) => void, selectedDate?: Date) {
this.selectedDate = selectedDate || new Date();
this.selectedDate = selectedDate || new Date(Temporal.Now.instant().epochMilliseconds);
this.modal = document.getElementById('timezone-modal') as HTMLElement;
this.overlay = document.getElementById('timezone-modal-overlay') as HTMLElement;
this.input = document.getElementById('timezone-input') as HTMLInputElement;
Expand Down Expand Up @@ -2226,11 +2232,8 @@ export class TimezoneModal {
if (plusBtn && group.alternate) {
plusBtn.addEventListener('click', e => {
e.stopPropagation();
// Select the alternate timezone directly and mark as off-cycle
if (this.onTimezoneSelectedCallback && group.alternate) {
this.onTimezoneSelectedCallback(group.alternate, true); // Mark alternate as off-cycle
}
this.close();
// Expand to show both timezone options instead of directly adding alternate
this.showTimezoneOptions(group);
});
}

Expand Down Expand Up @@ -2293,13 +2296,30 @@ export class TimezoneModal {
? `${timezone.cityName} (${timezone.abbreviation})`
: `${timezone.cityName} (${timezone.abbreviation} ${offsetStr})`;

// Check if this timezone has an alternate available (for DST support in search results)
const matchingGroup = this.groupedTimezonesLookup.get(timezone.iana);
const hasAlternate = matchingGroup && matchingGroup.alternate;

item.innerHTML = `
<div class="wheel-timezone-name">
${displayText}
${hasAlternate ? '<span class="timezone-plus-btn" title="Select alternate timezone">+</span>' : ''}
</div>
<div class="wheel-timezone-display">${timezone.displayName}</div>
`;

// Handle plus button clicks (for search results)
if (hasAlternate) {
const plusBtn = item.querySelector('.timezone-plus-btn');
if (plusBtn && matchingGroup && matchingGroup.alternate) {
plusBtn.addEventListener('click', e => {
e.stopPropagation();
// Expand to show both timezone options instead of directly adding alternate
this.showTimezoneOptions(matchingGroup);
});
}
}

// Click handler
item.addEventListener('click', () => {
if (!isCenter) {
Expand Down Expand Up @@ -2369,6 +2389,86 @@ export class TimezoneModal {
document.body.style.overflow = '';
}

/**
* Show expanded view with both timezone options for selection
*/
private showTimezoneOptions(group: GroupedTimezone): void {
// Clear wheel and show both timezone options
this.wheel.innerHTML = '';

const optionsContainer = document.createElement('div');
optionsContainer.className = 'timezone-options-container';
optionsContainer.innerHTML = `
<div class="timezone-options-header">
<h3>Select ${group.location} Timezone</h3>
<div class="timezone-options-subtitle">Choose your preferred time offset</div>
</div>
<div class="timezone-options-list">
</div>
<div class="timezone-options-actions">
<button type="button" class="timezone-option-back">← Back to Search</button>
</div>
`;

const optionsList = optionsContainer.querySelector('.timezone-options-list') as HTMLElement;

// Add current timezone option
const currentOption = document.createElement('div');
currentOption.className = 'timezone-option current-option';
currentOption.innerHTML = `
<div class="timezone-option-header">
<div class="timezone-option-name">${group.current.cityName} (${group.current.abbreviation})</div>
<div class="timezone-option-badge">Current</div>
</div>
<div class="timezone-option-details">
<div class="timezone-option-offset">${formatOffset(group.current.offset)}</div>
<div class="timezone-option-display">${group.current.displayName}</div>
</div>
`;

currentOption.addEventListener('click', () => {
if (this.onTimezoneSelectedCallback) {
this.onTimezoneSelectedCallback(group.current);
}
this.close();
});

optionsList.appendChild(currentOption);

// Add alternate timezone option if available
if (group.alternate) {
const alternateOption = document.createElement('div');
alternateOption.className = 'timezone-option alternate-option';
alternateOption.innerHTML = `
<div class="timezone-option-header">
<div class="timezone-option-name">${group.alternate.cityName} (${group.alternate.abbreviation})</div>
<div class="timezone-option-badge">Alternate</div>
</div>
<div class="timezone-option-details">
<div class="timezone-option-offset">${formatOffset(group.alternate.offset)}</div>
<div class="timezone-option-display">${group.alternate.displayName}</div>
</div>
`;

alternateOption.addEventListener('click', () => {
if (this.onTimezoneSelectedCallback && group.alternate) {
this.onTimezoneSelectedCallback(group.alternate, true); // Mark alternate as off-cycle
}
this.close();
});

optionsList.appendChild(alternateOption);
}

// Back button functionality
const backButton = optionsContainer.querySelector('.timezone-option-back') as HTMLElement;
backButton.addEventListener('click', () => {
this.renderWheel();
});

this.wheel.appendChild(optionsContainer);
}

/**
* Show custom timezone creation option when offset query has no matches
*/
Expand Down Expand Up @@ -2606,7 +2706,7 @@ export class DateTimeModal {

// Set current datetime as default if no value is set
if (!this.input.value) {
const now = new Date();
const now = new Date(Temporal.Now.instant().epochMilliseconds);
// Format to datetime-local format (YYYY-MM-DDTHH:MM)
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
Expand Down
Loading