diff --git a/src/constants/EventTypes.ts b/src/constants/EventTypes.ts index 50ec025..654a8f7 100644 --- a/src/constants/EventTypes.ts +++ b/src/constants/EventTypes.ts @@ -24,8 +24,9 @@ export const EventTypes = { // Navigation events WEEK_CHANGED: 'calendar:weekchanged', WEEK_INFO_UPDATED: 'calendar:weekinfoupdated', + WEEK_CONTENT_RENDERED: 'calendar:weekcontentrendered', NAV_PREV: 'calendar:navprev', - NAV_NEXT: 'calendar:navnext', + NAV_NEXT: 'calendar:navnext', NAV_TODAY: 'calendar:navtoday', NAVIGATE_TO_DATE: 'calendar:navigatetodate', NAVIGATE_TO_TODAY: 'calendar:navigatetotoday', diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 19c4c6a..4493f37 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -76,9 +76,9 @@ export class CalendarManager { this.setView(this.currentView); this.setCurrentDate(this.currentDate); - // Step 5: Render events (after view is set) - console.log('🎨 Rendering events...'); - const events = this.eventManager.getEvents(); + // Step 5: Render events (after view is set) - only render events for current period + console.log('🎨 Rendering events for current period...'); + const events = this.getEventsForCurrentPeriod(); await this.eventRenderer.renderEvents(events); this.isInitialized = true; @@ -109,6 +109,11 @@ export class CalendarManager { currentView: view, date: this.currentDate }); + + // Re-render events for new view if calendar is initialized + if (this.isInitialized) { + this.rerenderEventsForCurrentPeriod(); + } } /** @@ -126,6 +131,11 @@ export class CalendarManager { currentDate: this.currentDate, view: this.currentView }); + + // Re-render events for new period if calendar is initialized + if (this.isInitialized) { + this.rerenderEventsForCurrentPeriod(); + } } /** @@ -302,4 +312,99 @@ export class CalendarManager { return previousDate; } + + /** + * Get events filtered for the current period (week/month/day) + */ + private getEventsForCurrentPeriod(): CalendarEvent[] { + const allEvents = this.eventManager.getEvents(); + + // Calculate current period based on view + const period = this.calculateCurrentPeriod(); + + // Filter events to only include those in the current period + const filteredEvents = allEvents.filter(event => { + const eventStart = new Date(event.start); + const eventEnd = new Date(event.end); + const periodStart = new Date(period.start); + const periodEnd = new Date(period.end); + + // Include event if it overlaps with the period + return eventStart <= periodEnd && eventEnd >= periodStart; + }); + + // Also filter out all-day events (handled by GridManager) + const nonAllDayEvents = filteredEvents.filter(event => !event.allDay); + + console.log(`CalendarManager: Filtered ${allEvents.length} total events to ${nonAllDayEvents.length} non-all-day events for current period`); + + return nonAllDayEvents; + } + + /** + * Calculate the current period based on view and date + */ + private calculateCurrentPeriod(): { start: string; end: string } { + const current = new Date(this.currentDate); + + switch (this.currentView) { + case 'day': + const dayStart = new Date(current); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(current); + dayEnd.setHours(23, 59, 59, 999); + return { + start: dayStart.toISOString(), + end: dayEnd.toISOString() + }; + + case 'week': + // Find start of week (Monday) + const weekStart = new Date(current); + const dayOfWeek = weekStart.getDay(); + const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Sunday = 0, so 6 days back to Monday + weekStart.setDate(weekStart.getDate() - daysToMonday); + weekStart.setHours(0, 0, 0, 0); + + // Find end of week (Sunday) + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekEnd.getDate() + 6); + weekEnd.setHours(23, 59, 59, 999); + + return { + start: weekStart.toISOString(), + end: weekEnd.toISOString() + }; + + case 'month': + const monthStart = new Date(current.getFullYear(), current.getMonth(), 1); + const monthEnd = new Date(current.getFullYear(), current.getMonth() + 1, 0, 23, 59, 59, 999); + return { + start: monthStart.toISOString(), + end: monthEnd.toISOString() + }; + + default: + // Fallback to week view + const fallbackStart = new Date(current); + fallbackStart.setDate(fallbackStart.getDate() - 3); + fallbackStart.setHours(0, 0, 0, 0); + const fallbackEnd = new Date(current); + fallbackEnd.setDate(fallbackEnd.getDate() + 3); + fallbackEnd.setHours(23, 59, 59, 999); + return { + start: fallbackStart.toISOString(), + end: fallbackEnd.toISOString() + }; + } + } + + /** + * Re-render events for the current period + */ + private async rerenderEventsForCurrentPeriod(): Promise { + console.log('CalendarManager: Re-rendering events for current period'); + const events = this.getEventsForCurrentPeriod(); + await this.eventRenderer.renderEvents(events); + } } \ No newline at end of file diff --git a/src/managers/DataManager.ts b/src/managers/DataManager.ts index e457fc6..9d5ac1f 100644 --- a/src/managers/DataManager.ts +++ b/src/managers/DataManager.ts @@ -113,6 +113,44 @@ export class DataManager { } } + /** + * Filter events to only include those within the specified period + */ + public filterEventsForPeriod(events: CalendarEvent[], period: Period): CalendarEvent[] { + const startDate = new Date(period.start); + const endDate = new Date(period.end); + + return events.filter(event => { + const eventStart = new Date(event.start); + const eventEnd = new Date(event.end); + + // Include event if it overlaps with the period + return eventStart <= endDate && eventEnd >= startDate; + }); + } + + /** + * Get events filtered by period and optionally by all-day status + */ + public getFilteredEvents(period: Period, excludeAllDay: boolean = false): CalendarEvent[] { + const cacheKey = `${period.start}-${period.end}`; + const cachedData = this.cache.get(cacheKey); + + if (!cachedData) { + console.warn('DataManager: No cached data found for period', period); + return []; + } + + let filteredEvents = this.filterEventsForPeriod(cachedData.events, period); + + if (excludeAllDay) { + filteredEvents = filteredEvents.filter(event => !event.allDay); + console.log(`DataManager: Filtered out all-day events, ${filteredEvents.length} non-all-day events remaining`); + } + + return filteredEvents; + } + /** * Create a new event */ @@ -270,7 +308,7 @@ export class DataManager { } /** - * Generate mock data for testing + * Generate mock data for testing - only generates events within the specified period */ private getMockData(period: Period): EventData { const events: CalendarEvent[] = []; @@ -282,11 +320,13 @@ export class DataManager { milestone: ['Project Deadline', 'Release Day', 'Demo Day'] }; - // Parse dates + // Parse dates - only generate events within this exact period const startDate = new Date(period.start); const endDate = new Date(period.end); - // Generate some events for each day + console.log(`DataManager: Generating mock events for period ${period.start} to ${period.end}`); + + // Generate some events for each day within the period for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { // Skip weekends for most events const dayOfWeek = d.getDay(); diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index be30643..80633a6 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -20,8 +20,9 @@ export class EventManager { } private setupEventListeners(): void { - // Keep only UI-related event listeners here if needed - // Data loading is now handled via direct method calls + // NOTE: Removed POC event listener to prevent interference with production code + // POC sliding animation should not trigger separate event rendering + // this.eventBus.on(EventTypes.WEEK_CONTENT_RENDERED, ...); } /** @@ -170,6 +171,82 @@ export class EventManager { this.syncEvents(); } + /** + * Load events for a specific week into a container (POC-style) + */ + private loadEventsForWeek(weekStart: Date, weekEnd: Date, container: HTMLElement): void { + console.log(`EventManager: Loading events for week ${weekStart.toDateString()} - ${weekEnd.toDateString()}`); + + // Filter events for this week + const weekEvents = this.events.filter(event => { + const eventDate = new Date(event.start); + return eventDate >= weekStart && eventDate <= weekEnd; + }); + + console.log(`EventManager: Found ${weekEvents.length} events for this week`); + + // Render events in the container (POC approach) + this.renderEventsInContainer(weekEvents, container); + } + + /** + * Render events in a specific container (POC-style) + */ + private renderEventsInContainer(events: CalendarEvent[], container: HTMLElement): void { + const dayColumns = container.querySelectorAll('swp-day-column'); + + events.forEach(event => { + const eventDate = new Date(event.start); + const dayOfWeek = eventDate.getDay(); // 0 = Sunday + const column = dayColumns[dayOfWeek]; + + if (column) { + const eventsLayer = column.querySelector('swp-events-layer'); + if (eventsLayer) { + this.renderEventInColumn(event, eventsLayer as HTMLElement); + } + } + }); + } + + /** + * Render a single event in a column (POC-style) + */ + private renderEventInColumn(event: CalendarEvent, eventsLayer: HTMLElement): void { + const eventElement = document.createElement('swp-event'); + eventElement.dataset.type = event.type || 'meeting'; + + // Calculate position (simplified - assumes 7 AM start like POC) + const startTime = new Date(event.start); + const hours = startTime.getHours(); + const minutes = startTime.getMinutes(); + const startMinutes = (hours - 7) * 60 + minutes; // 7 is start hour like POC + + // Calculate duration + const endTime = new Date(event.end); + const durationMs = endTime.getTime() - startTime.getTime(); + const durationMinutes = Math.floor(durationMs / (1000 * 60)); + + eventElement.style.top = `${startMinutes}px`; + eventElement.style.height = `${durationMinutes}px`; + + eventElement.innerHTML = ` + ${this.formatTime(hours, minutes)} + ${event.title} + `; + + eventsLayer.appendChild(eventElement); + } + + /** + * Format time for display (POC-style) + */ + private formatTime(hours: number, minutes: number): string { + const period = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`; + } + public destroy(): void { this.events = []; } diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index b7b6b3f..8e7cf8b 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -112,28 +112,65 @@ export class NavigationManager { } } + /** + * POC-style animation transition - creates new grid container and slides it in + */ private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { - const calendarContainer = document.querySelector('swp-calendar-container'); + const container = document.querySelector('swp-calendar-container'); + const currentGrid = container?.querySelector('swp-grid-container'); - if (!calendarContainer) { - console.warn('NavigationManager: Calendar container not found'); + if (!container || !currentGrid) { + console.warn('NavigationManager: Required DOM elements not found'); return; } - - // Add transition class for visual feedback - calendarContainer.classList.add('week-transition'); - - // Brief fade effect - setTimeout(() => { - calendarContainer.classList.add('week-transition-out'); + + console.log(`NavigationManager: Starting ${direction} animation to ${targetWeek.toDateString()}`); + + // Create new grid container (POC approach) + const newGrid = document.createElement('swp-grid-container'); + newGrid.innerHTML = ` + + + + + + + + `; + + // Position new grid off-screen (POC positioning) + newGrid.style.position = 'absolute'; + newGrid.style.top = '0'; + newGrid.style.left = '0'; + newGrid.style.width = '100%'; + newGrid.style.height = '100%'; + newGrid.style.transform = direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)'; + + // Add to container + container.appendChild(newGrid); + + // Render new content for target week + this.renderWeekContent(newGrid, targetWeek); + + // Animate transition (POC animation) + requestAnimationFrame(() => { + // Slide out current grid + (currentGrid as HTMLElement).style.transform = direction === 'next' ? 'translateX(-100%)' : 'translateX(100%)'; + (currentGrid as HTMLElement).style.opacity = '0.5'; - // Update the week after fade starts + // Slide in new grid + newGrid.style.transform = 'translateX(0)'; + + // Clean up after animation (POC cleanup) setTimeout(() => { - // Update currentWeek + currentGrid.remove(); + newGrid.style.position = 'relative'; + + // Update currentWeek only after animation is complete (POC logic) this.currentWeek = new Date(targetWeek); this.animationQueue--; - // If this was the last queued animation, ensure we're in sync + // If this was the last queued animation, ensure we're in sync (POC sync) if (this.animationQueue === 0) { this.currentWeek = new Date(this.targetWeek); } @@ -145,13 +182,70 @@ export class NavigationManager { weekEnd: DateUtils.addDays(this.currentWeek, 6) }); - // Remove transition classes - setTimeout(() => { - calendarContainer.classList.remove('week-transition', 'week-transition-out'); - }, 150); - - }, 150); // Half of transition duration - }, 50); + console.log(`NavigationManager: Completed ${direction} animation`); + }, 400); // Match POC timing + }); + } + + /** + * Render week content in the new grid container + */ + private renderWeekContent(gridContainer: HTMLElement, weekStart: Date): void { + const header = gridContainer.querySelector('swp-calendar-header'); + const dayColumns = gridContainer.querySelector('swp-day-columns'); + + if (!header || !dayColumns) return; + + // Clear existing content + header.innerHTML = ''; + dayColumns.innerHTML = ''; + + // Render headers for target week + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + for (let i = 0; i < 7; i++) { + const date = new Date(weekStart); + date.setDate(date.getDate() + i); + + const headerElement = document.createElement('swp-day-header'); + if (this.isToday(date)) { + headerElement.dataset.today = 'true'; + } + + headerElement.innerHTML = ` + ${days[date.getDay()]} + ${date.getDate()} + `; + headerElement.dataset.date = this.formatDate(date); + + header.appendChild(headerElement); + } + + // Render day columns for target week + for (let i = 0; i < 7; i++) { + const column = document.createElement('swp-day-column'); + const date = new Date(weekStart); + date.setDate(date.getDate() + i); + column.dataset.date = this.formatDate(date); + + const eventsLayer = document.createElement('swp-events-layer'); + column.appendChild(eventsLayer); + + dayColumns.appendChild(column); + } + + // NOTE: Removed POC event emission to prevent interference with production code + // POC events should not trigger production event rendering + // this.eventBus.emit(EventTypes.WEEK_CONTENT_RENDERED, { ... }); + } + + // Utility functions (from POC) + private formatDate(date: Date): string { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + } + + private isToday(date: Date): boolean { + const today = new Date(); + return date.toDateString() === today.toDateString(); } private updateWeekInfo(): void { diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 474c7fb..ff9f836 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -25,12 +25,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // Clear existing events first this.clearEvents(); - // Filter out all-day events (handled by GridManager) - const nonAllDayEvents = events.filter(event => !event.allDay); - console.log('BaseEventRenderer: Rendering', nonAllDayEvents.length, 'non-all-day events'); + // Events should already be filtered by DataManager - no need to filter here + console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events'); // Render each event in the correct column - nonAllDayEvents.forEach(event => { + events.forEach(event => { const column = this.findColumn(event); if (column) { @@ -166,13 +165,14 @@ export class DateEventRenderer extends BaseEventRenderer { */ export class ResourceEventRenderer extends BaseEventRenderer { findColumn(event: CalendarEvent): HTMLElement | null { - if (!event.resourceName) { - console.warn('ResourceEventRenderer: Event has no resourceName', event); + const resourceName = event.resource?.name; + if (!resourceName) { + console.warn('ResourceEventRenderer: Event has no resource.name', event); return null; } - const resourceColumn = document.querySelector(`swp-resource-column[data-resource="${event.resourceName}"]`) as HTMLElement; - console.log('ResourceEventRenderer: Looking for resource column with name', event.resourceName, 'found:', !!resourceColumn); + const resourceColumn = document.querySelector(`swp-resource-column[data-resource="${resourceName}"]`) as HTMLElement; + console.log('ResourceEventRenderer: Looking for resource column with name', resourceName, 'found:', !!resourceColumn); return resourceColumn; } } \ No newline at end of file diff --git a/wwwroot/css/calendar-sliding-animation.css b/wwwroot/css/calendar-sliding-animation.css new file mode 100644 index 0000000..8965d9f --- /dev/null +++ b/wwwroot/css/calendar-sliding-animation.css @@ -0,0 +1,24 @@ +/* POC-style Calendar Sliding Animation CSS */ + +/* Grid container base styles */ +swp-grid-container { + position: relative; + width: 100%; + transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform; + backface-visibility: hidden; + transform: translateZ(0); /* GPU acceleration */ +} + +/* Calendar container for sliding */ +swp-calendar-container { + position: relative; + overflow: hidden; +} + +/* Accessibility: Respect reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + swp-grid-container { + transition: none; + } +} \ No newline at end of file