From 7d513600d88e5d8c8a765bdab55bb8b889dc3d24 Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Wed, 20 Aug 2025 00:39:31 +0200 Subject: [PATCH] Refactors date handling for ISO week compatibility Centralizes all date calculations into a new `DateCalculator` class for better maintainability and consistency. Ensures correct ISO week handling (Monday as the first day) throughout the calendar. Updates `CalendarConfig` to use ISO day numbering (1-7 for Mon-Sun) for work week definitions. Fixes issue where date calculations were inconsistent. Enhances event rendering and navigation. Updates navigation logic to use pre-rendered events. Removes the need for `CONTAINER_READY_FOR_EVENTS` event. --- src/core/CalendarConfig.ts | 23 +-- src/factories/CalendarTypeFactory.ts | 5 +- src/factories/ManagerFactory.ts | 2 +- src/managers/GridManager.ts | 10 +- src/managers/NavigationManager.ts | 37 ++-- src/managers/ViewManager.ts | 2 + src/renderers/EventRenderer.ts | 28 ++- src/renderers/EventRendererManager.ts | 21 +-- src/renderers/HeaderRenderer.ts | 5 +- src/renderers/NavigationRenderer.ts | 25 ++- src/utils/DateCalculator.ts | 184 ++++++++++++++----- src/utils/DateUtils.ts | 230 ------------------------ wwwroot/css/calendar-components-css.css | 1 + 13 files changed, 230 insertions(+), 343 deletions(-) delete mode 100644 src/utils/DateUtils.ts diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts index cef5e5c..1231c7a 100644 --- a/src/core/CalendarConfig.ts +++ b/src/core/CalendarConfig.ts @@ -40,10 +40,9 @@ interface DateViewSettings { */ interface WorkWeekSettings { id: string; - workDays: number[]; // [1,2,3,4,5] for mon-fri - dayNames: string[]; // ['Mon','Tue','Wed','Thu','Fri'] + workDays: number[]; // ISO 8601: [1,2,3,4,5] for mon-fri (1=Mon, 7=Sun) totalDays: number; // 5 - firstWorkDay: number; // 1 = Monday + firstWorkDay: number; // ISO: 1 = Monday, 7 = Sunday } /** @@ -442,38 +441,33 @@ export class CalendarConfig { return { 'standard': { id: 'standard', - workDays: [1,2,3,4,5], - dayNames: ['Mon','Tue','Wed','Thu','Fri'], + workDays: [1,2,3,4,5], // Monday-Friday (ISO) totalDays: 5, firstWorkDay: 1 }, 'compressed': { id: 'compressed', - workDays: [1,2,3,4], - dayNames: ['Mon','Tue','Wed','Thu'], + workDays: [1,2,3,4], // Monday-Thursday (ISO) totalDays: 4, firstWorkDay: 1 }, 'midweek': { id: 'midweek', - workDays: [3,4,5], - dayNames: ['Wed','Thu','Fri'], + workDays: [3,4,5], // Wednesday-Friday (ISO) totalDays: 3, firstWorkDay: 3 }, 'weekend': { id: 'weekend', - workDays: [6,0], - dayNames: ['Sat','Sun'], + workDays: [6,7], // Saturday-Sunday (ISO) totalDays: 2, firstWorkDay: 6 }, 'fullweek': { id: 'fullweek', - workDays: [0,1,2,3,4,5,6], - dayNames: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], + workDays: [1,2,3,4,5,6,7], // Monday-Sunday (ISO) totalDays: 7, - firstWorkDay: 0 + firstWorkDay: 1 } }; } @@ -511,6 +505,7 @@ export class CalendarConfig { getCurrentWorkWeek(): string { return this.currentWorkWeek; } + } // Create singleton instance diff --git a/src/factories/CalendarTypeFactory.ts b/src/factories/CalendarTypeFactory.ts index 844b63a..45a66cc 100644 --- a/src/factories/CalendarTypeFactory.ts +++ b/src/factories/CalendarTypeFactory.ts @@ -4,6 +4,7 @@ import { CalendarType } from '../types/CalendarTypes'; import { HeaderRenderer, DateHeaderRenderer, ResourceHeaderRenderer } from '../renderers/HeaderRenderer'; import { ColumnRenderer, DateColumnRenderer, ResourceColumnRenderer } from '../renderers/ColumnRenderer'; import { EventRendererStrategy, DateEventRenderer, ResourceEventRenderer } from '../renderers/EventRenderer'; +import { calendarConfig } from '../core/CalendarConfig'; /** * Renderer configuration for a calendar type @@ -34,13 +35,13 @@ export class CalendarTypeFactory { this.registerRenderers('date', { headerRenderer: new DateHeaderRenderer(), columnRenderer: new DateColumnRenderer(), - eventRenderer: new DateEventRenderer() + eventRenderer: new DateEventRenderer(calendarConfig) }); this.registerRenderers('resource', { headerRenderer: new ResourceHeaderRenderer(), columnRenderer: new ResourceColumnRenderer(), - eventRenderer: new ResourceEventRenderer() + eventRenderer: new ResourceEventRenderer(calendarConfig) }); this.isInitialized = true; diff --git a/src/factories/ManagerFactory.ts b/src/factories/ManagerFactory.ts index 055c2d4..db24bb7 100644 --- a/src/factories/ManagerFactory.ts +++ b/src/factories/ManagerFactory.ts @@ -42,7 +42,7 @@ export class ManagerFactory { const eventRenderer = new EventRenderingService(eventBus, eventManager); const gridManager = new GridManager(); const scrollManager = new ScrollManager(); - const navigationManager = new NavigationManager(eventBus); + const navigationManager = new NavigationManager(eventBus, eventRenderer); const viewManager = new ViewManager(eventBus); // CalendarManager depends on all other managers diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 358e09d..17150f6 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -4,7 +4,6 @@ import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { EventTypes } from '../constants/EventTypes'; import { StateEvents } from '../types/CalendarState'; -import { DateUtils } from '../utils/DateUtils'; import { DateCalculator } from '../utils/DateCalculator'; import { ResourceCalendarData } from '../types/CalendarTypes'; import { GridRenderer } from '../renderers/GridRenderer'; @@ -105,6 +104,7 @@ export class GridManager { this.render(); }); + // Handle events loaded eventBus.on(EventTypes.EVENTS_LOADED, (e: Event) => { const detail = (e as CustomEvent).detail; @@ -147,12 +147,14 @@ export class GridManager { } } - console.log('GridManager: Starting render with grid element:', this.grid); + console.group(`🏗️ GRID RENDER: ${this.currentWeek?.toDateString()}`); + console.log('Updating grid styles and rendering structure...'); + this.styleManager.updateGridStyles(this.resourceData); this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents); const columnCount = this.styleManager.getColumnCount(this.resourceData); - console.log(`GridManager: Render complete - created ${columnCount} columns`); + console.log(`Grid structure complete - ${columnCount} columns created`); // Emit GRID_RENDERED event to trigger event rendering const weekEnd = this.currentWeek ? new Date(this.currentWeek.getTime() + 6 * 24 * 60 * 60 * 1000) : null; @@ -163,6 +165,8 @@ export class GridManager { endDate: weekEnd, columnCount: columnCount }); + + console.groupEnd(); } // Column count calculation moved to GridStyleManager diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index 2208b42..4c2d789 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -1,5 +1,6 @@ import { IEventBus } from '../types/CalendarTypes.js'; -import { DateUtils } from '../utils/DateUtils.js'; +import { EventRenderingService } from '../renderers/EventRendererManager.js'; +import { DateCalculator } from '../utils/DateCalculator.js'; import { EventTypes } from '../constants/EventTypes.js'; import { NavigationRenderer } from '../renderers/NavigationRenderer.js'; import { calendarConfig } from '../core/CalendarConfig.js'; @@ -11,15 +12,17 @@ import { calendarConfig } from '../core/CalendarConfig.js'; export class NavigationManager { private eventBus: IEventBus; private navigationRenderer: NavigationRenderer; + private dateCalculator: DateCalculator; private currentWeek: Date; private targetWeek: Date; private animationQueue: number = 0; - constructor(eventBus: IEventBus) { + constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) { console.log('🧭 NavigationManager: Constructor called'); this.eventBus = eventBus; - this.navigationRenderer = new NavigationRenderer(eventBus, calendarConfig); - this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC + this.dateCalculator = new DateCalculator(calendarConfig); + this.navigationRenderer = new NavigationRenderer(eventBus, calendarConfig, eventRenderer); + this.currentWeek = this.dateCalculator.getISOWeekStart(new Date()); this.targetWeek = new Date(this.currentWeek); this.init(); } @@ -83,7 +86,7 @@ export class NavigationManager { private navigateToToday(): void { const today = new Date(); - const todayWeekStart = DateUtils.getWeekStart(today, 0); + const todayWeekStart = this.dateCalculator.getISOWeekStart(today); // Reset to today this.targetWeek = new Date(todayWeekStart); @@ -101,7 +104,7 @@ export class NavigationManager { } private navigateToDate(date: Date): void { - const weekStart = DateUtils.getWeekStart(date, 0); + const weekStart = this.dateCalculator.getISOWeekStart(date); this.targetWeek = new Date(weekStart); const currentTime = this.currentWeek.getTime(); @@ -128,14 +131,16 @@ export class NavigationManager { return; } - console.log(`NavigationManager: Starting ${direction} animation to ${targetWeek.toDateString()}`); - + console.group(`🎬 NAVIGATION ANIMATION: ${direction} to ${targetWeek.toDateString()}`); + console.log('1. Creating new container with events...'); + let newGrid: HTMLElement; // Always create a fresh container for consistent behavior - console.log('NavigationManager: Creating new container'); newGrid = this.navigationRenderer.renderContainer(container as HTMLElement, targetWeek); + console.log('2. Starting slide animation...'); + // Clear any existing transforms before animation newGrid.style.transform = ''; (currentGrid as HTMLElement).style.transform = ''; @@ -161,6 +166,8 @@ export class NavigationManager { // Handle animation completion slideInAnimation.addEventListener('finish', () => { + console.log('3. Animation finished, cleaning up...'); + // Cleanup: Remove all old grids except the new one const allGrids = container.querySelectorAll('swp-grid-container'); for (let i = 0; i < allGrids.length - 1; i++) { @@ -184,24 +191,24 @@ export class NavigationManager { this.updateWeekInfo(); this.eventBus.emit(EventTypes.WEEK_CHANGED, { weekStart: this.currentWeek, - weekEnd: DateUtils.addDays(this.currentWeek, 6) + weekEnd: this.dateCalculator.addDays(this.currentWeek, 6) }); - // Emit animation complete event for ScrollManager this.eventBus.emit(EventTypes.NAVIGATION_ANIMATION_COMPLETE, { direction, weekStart: this.currentWeek }); - console.log(`NavigationManager: Completed ${direction} animation`); + console.log('✅ Animation completed successfully'); + console.groupEnd(); }); } private updateWeekInfo(): void { - const weekNumber = DateUtils.getWeekNumber(this.currentWeek); - const weekEnd = DateUtils.addDays(this.currentWeek, 6); - const dateRange = DateUtils.formatDateRange(this.currentWeek, weekEnd); + const weekNumber = this.dateCalculator.getWeekNumber(this.currentWeek); + const weekEnd = this.dateCalculator.addDays(this.currentWeek, 6); + const dateRange = this.dateCalculator.formatDateRange(this.currentWeek, weekEnd); // Notify other managers about week info update - DOM manipulation should happen via events this.eventBus.emit(EventTypes.WEEK_INFO_UPDATED, { diff --git a/src/managers/ViewManager.ts b/src/managers/ViewManager.ts index 1f9c8f3..3b010b0 100644 --- a/src/managers/ViewManager.ts +++ b/src/managers/ViewManager.ts @@ -36,6 +36,7 @@ export class ViewManager { // Setup workweek preset button handlers this.setupWorkweekButtonHandlers(); + } private setupViewButtonHandlers(): void { @@ -64,6 +65,7 @@ export class ViewManager { }); } + private initializeView(): void { this.updateViewButtons(); this.updateWorkweekButtons(); diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 1c71a39..4847a45 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -2,7 +2,7 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { CalendarConfig } from '../core/CalendarConfig'; -import { DateUtils } from '../utils/DateUtils'; +import { DateCalculator } from '../utils/DateCalculator'; /** * Interface for event rendering strategies @@ -16,6 +16,11 @@ export interface EventRendererStrategy { * Base class for event renderers with common functionality */ export abstract class BaseEventRenderer implements EventRendererStrategy { + protected dateCalculator: DateCalculator; + + constructor(config: CalendarConfig) { + this.dateCalculator = new DateCalculator(config); + } renderEvents(events: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void { console.log('BaseEventRenderer: renderEvents called with', events.length, 'events'); @@ -70,8 +75,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { eventElement.style.backgroundColor = event.metadata?.color || '#3498db'; // Format time for display - const startTime = this.formatTime(event.start); - const endTime = this.formatTime(event.end); + const startTime = this.dateCalculator.formatTime(new Date(event.start)); + const endTime = this.dateCalculator.formatTime(new Date(event.end)); // Create event content eventElement.innerHTML = ` @@ -160,15 +165,24 @@ export class DateEventRenderer extends BaseEventRenderer { protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] { const columnDate = column.dataset.date; - if (!columnDate) return []; + if (!columnDate) { + console.log(`DateEventRenderer: Column has no dataset.date`); + return []; + } const columnEvents = events.filter(event => { const eventDate = new Date(event.start); - const eventDateStr = DateUtils.formatDate(eventDate); - return eventDateStr === columnDate; + const eventDateStr = this.dateCalculator.formatISODate(eventDate); + const matches = eventDateStr === columnDate; + + if (!matches) { + console.log(`DateEventRenderer: Event ${event.title} (${eventDateStr}) does not match column (${columnDate})`); + } + + return matches; }); - console.log(`DateEventRenderer: Column ${columnDate} has ${columnEvents.length} events`); + console.log(`DateEventRenderer: Column ${columnDate} has ${columnEvents.length} events from ${events.length} total`); return columnEvents; } } diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 6590a08..13d3753 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -31,11 +31,7 @@ export class EventRenderingService { * Render events in a specific container for a given period */ public renderEvents(context: RenderContext): void { - console.log('EventRenderer: Rendering events for period', { - startDate: context.startDate, - endDate: context.endDate, - container: context.container - }); + console.log(` 📅 Getting events for ${context.startDate.toDateString()} - ${context.endDate.toDateString()}`); // Get events from EventManager for the period const events = this.eventManager.getEventsForPeriod( @@ -43,17 +39,17 @@ export class EventRenderingService { context.endDate ); - console.log(`EventRenderer: Found ${events.length} events for period`); + console.log(` 📊 Found ${events.length} events for period`); if (events.length === 0) { - console.log('EventRenderer: No events to render for this period'); + console.log(' ⚠️ No events to render for this period'); return; } // Use cached strategy to render events in the specific container this.strategy.renderEvents(events, context.container, calendarConfig); - console.log(`EventRenderer: Successfully rendered ${events.length} events`); + console.log(` ✅ Rendered ${events.length} events successfully`); } private setupEventListeners(): void { @@ -63,10 +59,11 @@ export class EventRenderingService { this.handleGridRendered(event as CustomEvent); }); - this.eventBus.on(EventTypes.CONTAINER_READY_FOR_EVENTS, (event: Event) => { - console.log('EventRenderer: Received CONTAINER_READY_FOR_EVENTS event'); - this.handleContainerReady(event as CustomEvent); - }); + // CONTAINER_READY_FOR_EVENTS removed - events are now pre-rendered synchronously + // this.eventBus.on(EventTypes.CONTAINER_READY_FOR_EVENTS, (event: Event) => { + // console.log('EventRenderer: Received CONTAINER_READY_FOR_EVENTS event'); + // this.handleContainerReady(event as CustomEvent); + // }); this.eventBus.on(EventTypes.VIEW_CHANGED, (event: Event) => { console.log('EventRenderer: Received VIEW_CHANGED event'); diff --git a/src/renderers/HeaderRenderer.ts b/src/renderers/HeaderRenderer.ts index 95488a0..7650206 100644 --- a/src/renderers/HeaderRenderer.ts +++ b/src/renderers/HeaderRenderer.ts @@ -37,15 +37,16 @@ export class DateHeaderRenderer implements HeaderRenderer { const weekDays = config.get('weekDays'); const daysToShow = dates.slice(0, weekDays); - const workWeekSettings = config.getWorkWeekSettings(); daysToShow.forEach((date, index) => { const header = document.createElement('swp-day-header'); if (this.dateCalculator.isToday(date)) { (header as any).dataset.today = 'true'; } + const dayName = this.dateCalculator.getDayName(date, 'short'); + header.innerHTML = ` - ${workWeekSettings.dayNames[index]} + ${dayName} ${date.getDate()} `; (header as any).dataset.date = this.dateCalculator.formatISODate(date); diff --git a/src/renderers/NavigationRenderer.ts b/src/renderers/NavigationRenderer.ts index 0d3f614..88cf69d 100644 --- a/src/renderers/NavigationRenderer.ts +++ b/src/renderers/NavigationRenderer.ts @@ -1,8 +1,8 @@ import { IEventBus } from '../types/CalendarTypes'; import { EventTypes } from '../constants/EventTypes'; -import { DateUtils } from '../utils/DateUtils'; import { CalendarConfig } from '../core/CalendarConfig'; import { DateCalculator } from '../utils/DateCalculator'; +import { EventRenderingService } from './EventRendererManager'; /** * NavigationRenderer - Handles DOM rendering for navigation containers @@ -12,10 +12,12 @@ export class NavigationRenderer { private eventBus: IEventBus; private config: CalendarConfig; private dateCalculator: DateCalculator; + private eventRenderer: EventRenderingService; - constructor(eventBus: IEventBus, config: CalendarConfig) { + constructor(eventBus: IEventBus, config: CalendarConfig, eventRenderer: EventRenderingService) { this.eventBus = eventBus; this.config = config; + this.eventRenderer = eventRenderer; this.dateCalculator = new DateCalculator(config); this.setupEventListeners(); } @@ -51,7 +53,10 @@ export class NavigationRenderer { * Render a complete container with content and events */ public renderContainer(parentContainer: HTMLElement, weekStart: Date): HTMLElement { - console.log('NavigationRenderer: Rendering new container for week:', weekStart.toDateString()); + const weekEnd = this.dateCalculator.addDays(weekStart, 6); + + console.group(`🎨 RENDERING CONTAINER: ${weekStart.toDateString()} - ${weekEnd.toDateString()}`); + console.log('1. Creating grid structure...'); // Create new grid container const newGrid = document.createElement('swp-grid-container'); @@ -75,17 +80,18 @@ export class NavigationRenderer { // Add to parent container parentContainer.appendChild(newGrid); - // Render week content (headers and columns) + console.log('2. Rendering headers and columns...'); this.renderWeekContentInContainer(newGrid, weekStart); - // Emit event to trigger event rendering - const weekEnd = DateUtils.addDays(weekStart, 6); - this.eventBus.emit(EventTypes.CONTAINER_READY_FOR_EVENTS, { + console.log('3. Pre-rendering events synchronously...'); + this.eventRenderer.renderEvents({ container: newGrid, startDate: weekStart, endDate: weekEnd }); + console.log('✅ Container ready with pre-rendered events'); + console.groupEnd(); return newGrid; } @@ -104,7 +110,6 @@ export class NavigationRenderer { // Get dates using DateCalculator const dates = this.dateCalculator.getWorkWeekDates(weekStart); - const workWeekSettings = this.config.getWorkWeekSettings(); // Render headers for target week dates.forEach((date, i) => { @@ -113,8 +118,10 @@ export class NavigationRenderer { headerElement.dataset.today = 'true'; } + const dayName = this.dateCalculator.getDayName(date, 'short'); + headerElement.innerHTML = ` - ${workWeekSettings.dayNames[i]} + ${dayName} ${date.getDate()} `; headerElement.dataset.date = this.dateCalculator.formatISODate(date); diff --git a/src/utils/DateCalculator.ts b/src/utils/DateCalculator.ts index be1574d..a795c53 100644 --- a/src/utils/DateCalculator.ts +++ b/src/utils/DateCalculator.ts @@ -13,7 +13,7 @@ export class DateCalculator { } /** - * Get dates for work week based on ISO week (starts Monday) + * Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7) * @param weekStart - Any date in the week * @returns Array of dates for the configured work days */ @@ -21,21 +21,15 @@ export class DateCalculator { const dates: Date[] = []; const workWeekSettings = this.config.getWorkWeekSettings(); - // Get Monday of the ISO week - const monday = this.getISOWeekStart(weekStart); + // Always use ISO week start (Monday) + const mondayOfWeek = this.getISOWeekStart(weekStart); - // Calculate dates for each work day - workWeekSettings.workDays.forEach(dayOfWeek => { - const date = new Date(monday); - - if (dayOfWeek === 0) { - // Sunday is 6 days after Monday - date.setDate(monday.getDate() + 6); - } else { - // Monday=1 becomes 0 days after, Tuesday=2 becomes 1 day after, etc. - date.setDate(monday.getDate() + dayOfWeek - 1); - } - + // Calculate dates for each work day using ISO numbering + workWeekSettings.workDays.forEach(isoDay => { + const date = new Date(mondayOfWeek); + // ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days + const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1; + date.setDate(mondayOfWeek.getDate() + daysFromMonday); dates.push(date); }); @@ -56,24 +50,14 @@ export class DateCalculator { return monday; } - /** - * Get the start of the week for a given date (legacy method) - * @param date - Any date in the week - * @param firstDayOfWeek - 0 for Sunday, 1 for Monday - * @returns The start date of the week - */ - getWeekStart(date: Date, firstDayOfWeek: number = 1): Date { - return this.getISOWeekStart(date); - } /** - * Get the end of the week for a given date + * Get the end of the ISO week for a given date * @param date - Any date in the week - * @param firstDayOfWeek - 0 for Sunday, 1 for Monday - * @returns The end date of the week + * @returns The end date of the ISO week (Sunday) */ - getWeekEnd(date: Date, firstDayOfWeek: number = 1): Date { - const weekStart = this.getWeekStart(date, firstDayOfWeek); + getWeekEnd(date: Date): Date { + const weekStart = this.getISOWeekStart(date); const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 6); weekEnd.setHours(23, 59, 59, 999); @@ -94,28 +78,40 @@ export class DateCalculator { } /** - * Format a date range for display + * Format a date range with customizable options * @param start - Start date * @param end - End date + * @param options - Formatting options * @returns Formatted date range string */ - formatDateRange(start: Date, end: Date): string { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + formatDateRange( + start: Date, + end: Date, + options: { + locale?: string; + month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'; + day?: 'numeric' | '2-digit'; + year?: 'numeric' | '2-digit'; + } = {} + ): string { + const { locale = 'en-US', month = 'short', day = 'numeric' } = options; - const startMonth = months[start.getMonth()]; - const endMonth = months[end.getMonth()]; - const startDay = start.getDate(); - const endDay = end.getDate(); const startYear = start.getFullYear(); const endYear = end.getFullYear(); - if (startYear !== endYear) { - return `${startMonth} ${startDay}, ${startYear} - ${endMonth} ${endDay}, ${endYear}`; - } else if (startMonth === endMonth) { - return `${startMonth} ${startDay} - ${endDay}, ${startYear}`; - } else { - return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${startYear}`; + const formatter = new Intl.DateTimeFormat(locale, { + month, + day, + year: startYear !== endYear ? 'numeric' : undefined + }); + + // @ts-ignore + if (typeof formatter.formatRange === 'function') { + // @ts-ignore + return formatter.formatRange(start, end); } + + return `${formatter.format(start)} - ${formatter.format(end)}`; } /** @@ -173,18 +169,110 @@ export class DateCalculator { } /** - * Get the day name for a date + * Get the day name for a date using Intl.DateTimeFormat * @param date - Date to get day name for * @param format - 'short' or 'long' * @returns Day name */ getDayName(date: Date, format: 'short' | 'long' = 'short'): string { - const days = { - short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], - long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] - }; - return days[format][date.getDay()]; + const formatter = new Intl.DateTimeFormat('en-US', { + weekday: format + }); + return formatter.format(date); } + + /** + * Format time to HH:MM + * @param date - Date to format + * @returns Time string + */ + formatTime(date: Date): string { + return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; + } + + /** + * Format time to 12-hour format + * @param date - Date to format + * @returns 12-hour time string + */ + formatTime12(date: Date): string { + const hours = date.getHours(); + const minutes = date.getMinutes(); + const period = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + + return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`; + } + + /** + * Convert minutes since midnight to time string + * @param minutes - Minutes since midnight + * @returns Time string + */ + minutesToTime(minutes: number): string { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + const period = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + + return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`; + } + + /** + * Convert time string to minutes since midnight + * @param timeStr - Time string + * @returns Minutes since midnight + */ + timeToMinutes(timeStr: string): number { + const [time] = timeStr.split('T').pop()!.split('.'); + const [hours, minutes] = time.split(':').map(Number); + return hours * 60 + minutes; + } + + /** + * Get minutes since start of day + * @param date - Date or ISO string + * @returns Minutes since midnight + */ + getMinutesSinceMidnight(date: Date | string): number { + const d = typeof date === 'string' ? new Date(date) : date; + return d.getHours() * 60 + d.getMinutes(); + } + + /** + * Calculate duration in minutes between two dates + * @param start - Start date or ISO string + * @param end - End date or ISO string + * @returns Duration in minutes + */ + getDurationMinutes(start: Date | string, end: Date | string): number { + const startDate = typeof start === 'string' ? new Date(start) : start; + const endDate = typeof end === 'string' ? new Date(end) : end; + return Math.floor((endDate.getTime() - startDate.getTime()) / 60000); + } + + /** + * Check if two dates are on the same day + * @param date1 - First date + * @param date2 - Second date + * @returns True if same day + */ + isSameDay(date1: Date, date2: Date): boolean { + return date1.toDateString() === date2.toDateString(); + } + + /** + * Check if event spans multiple days + * @param start - Start date or ISO string + * @param end - End date or ISO string + * @returns True if spans multiple days + */ + isMultiDay(start: Date | string, end: Date | string): boolean { + const startDate = typeof start === 'string' ? new Date(start) : start; + const endDate = typeof end === 'string' ? new Date(end) : end; + return !this.isSameDay(startDate, endDate); + } + } // Create singleton instance with config diff --git a/src/utils/DateUtils.ts b/src/utils/DateUtils.ts deleted file mode 100644 index 93ffc8d..0000000 --- a/src/utils/DateUtils.ts +++ /dev/null @@ -1,230 +0,0 @@ -// Date and time utility functions - -/** - * Date and time utility functions - */ -export class DateUtils { - /** - * Get start of week for a given date - */ - static getWeekStart(date: Date, firstDayOfWeek: number = 1): Date { - const d = new Date(date); - const day = d.getDay(); - const diff = (day - firstDayOfWeek + 7) % 7; - d.setDate(d.getDate() - diff); - d.setHours(0, 0, 0, 0); - return d; - } - - /** - * Get end of week for a given date - */ - static getWeekEnd(date: Date, firstDayOfWeek: number = 1): Date { - const start = this.getWeekStart(date, firstDayOfWeek); - const end = new Date(start); - end.setDate(end.getDate() + 6); - end.setHours(23, 59, 59, 999); - return end; - } - - /** - * Format date to YYYY-MM-DD - */ - static formatDate(date: Date): string { - return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; - } - - /** - * Format time to HH:MM - */ - static formatTime(date: Date): string { - return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; - } - - /** - * Format time to 12-hour format - */ - static formatTime12(date: Date): string { - const hours = date.getHours(); - const minutes = date.getMinutes(); - const period = hours >= 12 ? 'PM' : 'AM'; - const displayHours = hours % 12 || 12; - - return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`; - } - - /** - * Convert minutes since midnight to time string - */ - static minutesToTime(minutes: number): string { - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; - const period = hours >= 12 ? 'PM' : 'AM'; - const displayHours = hours % 12 || 12; - - return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`; - } - - /** - * Convert time string to minutes since midnight - */ - static timeToMinutes(timeStr: string): number { - const [time] = timeStr.split('T').pop()!.split('.'); - const [hours, minutes] = time.split(':').map(Number); - return hours * 60 + minutes; - } - - /** - * Get minutes since start of day - */ - static getMinutesSinceMidnight(date: Date | string): number { - const d = typeof date === 'string' ? new Date(date) : date; - return d.getHours() * 60 + d.getMinutes(); - } - - /** - * Calculate duration in minutes between two dates - */ - static getDurationMinutes(start: Date | string, end: Date | string): number { - const startDate = typeof start === 'string' ? new Date(start) : start; - const endDate = typeof end === 'string' ? new Date(end) : end; - return Math.floor((endDate.getTime() - startDate.getTime()) / 60000); - } - - /** - * Check if date is today - */ - static isToday(date: Date): boolean { - const today = new Date(); - return date.toDateString() === today.toDateString(); - } - - /** - * Check if two dates are on the same day - */ - static isSameDay(date1: Date, date2: Date): boolean { - return date1.toDateString() === date2.toDateString(); - } - - /** - * Check if event spans multiple days - */ - static isMultiDay(start: Date | string, end: Date | string): boolean { - const startDate = typeof start === 'string' ? new Date(start) : start; - const endDate = typeof end === 'string' ? new Date(end) : end; - return !this.isSameDay(startDate, endDate); - } - - /** - * Get day name - */ - static getDayName(date: Date, format: 'short' | 'long' = 'short'): string { - const days = { - short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], - long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] - }; - return days[format][date.getDay()]; - } - - /** - * Add days to date - */ - static addDays(date: Date, days: number): Date { - const result = new Date(date); - result.setDate(result.getDate() + days); - return result; - } - - /** - * Add minutes to date - */ - static addMinutes(date: Date, minutes: number): Date { - const result = new Date(date); - result.setMinutes(result.getMinutes() + minutes); - return result; - } - - /** - * Snap time to nearest interval - */ - static snapToInterval(date: Date, intervalMinutes: number): Date { - const minutes = date.getMinutes(); - const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes; - const result = new Date(date); - result.setMinutes(snappedMinutes); - result.setSeconds(0); - result.setMilliseconds(0); - return result; - } - - /** - * Get current time in minutes since day start - */ - static getCurrentTimeMinutes(dayStartHour: number = 0): number { - const now = new Date(); - const minutesSinceMidnight = now.getHours() * 60 + now.getMinutes(); - return minutesSinceMidnight - (dayStartHour * 60); - } - - /** - * Format duration to human readable string - */ - static formatDuration(minutes: number): string { - if (minutes < 60) { - return `${minutes} min`; - } - - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; - - if (mins === 0) { - return `${hours} hour${hours > 1 ? 's' : ''}`; - } - - return `${hours} hour${hours > 1 ? 's' : ''} ${mins} min`; - } - - /** - * Get ISO week number for a given date - */ - static getWeekNumber(date: Date): number { - const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); - const dayNum = d.getUTCDay() || 7; - d.setUTCDate(d.getUTCDate() + 4 - dayNum); - const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); - return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); - } - - /** - * Get month names array - */ - static getMonthNames(format: 'short' | 'long' = 'short'): string[] { - const months = { - short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - long: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] - }; - return months[format]; - } - - /** - * Format date range for display (e.g., "Jan 15 - 21, 2024" or "Jan 15 - Feb 2, 2024") - */ - static formatDateRange(startDate: Date, endDate: Date): string { - const monthNames = this.getMonthNames('short'); - - const startMonth = monthNames[startDate.getMonth()]; - const endMonth = monthNames[endDate.getMonth()]; - const startDay = startDate.getDate(); - const endDay = endDate.getDate(); - const startYear = startDate.getFullYear(); - const endYear = endDate.getFullYear(); - - if (startMonth === endMonth && startYear === endYear) { - return `${startMonth} ${startDay} - ${endDay}, ${startYear}`; - } else if (startYear !== endYear) { - return `${startMonth} ${startDay}, ${startYear} - ${endMonth} ${endDay}, ${endYear}`; - } else { - return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${startYear}`; - } - } -} \ No newline at end of file diff --git a/wwwroot/css/calendar-components-css.css b/wwwroot/css/calendar-components-css.css index df8ff1c..950bfc4 100644 --- a/wwwroot/css/calendar-components-css.css +++ b/wwwroot/css/calendar-components-css.css @@ -125,6 +125,7 @@ swp-preset-button { } } + /* Search container */ swp-search-container { margin-left: auto;