Skip to content
138 changes: 117 additions & 21 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,16 @@ 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 neutral dates for June 1st and December 31st to capture DST variations
// Using UTC to avoid local timezone interpretation issues
const juneDate = new Date(Date.UTC(year, 5, 1)); // June 1st in UTC
const decemberDate = new Date(Date.UTC(year, 11, 31)); // December 31st in UTC

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

Expand Down Expand Up @@ -1464,7 +1465,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 +1497,13 @@ 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
// Create neutral UTC dates and let Intl.DateTimeFormat handle timezone conversion
const summerDate = new Date(Date.UTC(year, 5, 1)); // June 1st in UTC
const winterDate = new Date(Date.UTC(year, 11, 31)); // December 31st in UTC

for (const date of [summerDate, winterDate]) {
const formatter = new Intl.DateTimeFormat('en', {
Expand Down Expand Up @@ -1574,7 +1576,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 +1811,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 +2228,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 +2292,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 +2385,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 +2702,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