Steps in the right direction for animated date change

This commit is contained in:
Janus Knudsen 2025-08-12 00:07:39 +02:00
parent 5e966ddea2
commit f50f5ad53b
7 changed files with 378 additions and 37 deletions

View file

@ -24,6 +24,7 @@ export const EventTypes = {
// Navigation events // Navigation events
WEEK_CHANGED: 'calendar:weekchanged', WEEK_CHANGED: 'calendar:weekchanged',
WEEK_INFO_UPDATED: 'calendar:weekinfoupdated', WEEK_INFO_UPDATED: 'calendar:weekinfoupdated',
WEEK_CONTENT_RENDERED: 'calendar:weekcontentrendered',
NAV_PREV: 'calendar:navprev', NAV_PREV: 'calendar:navprev',
NAV_NEXT: 'calendar:navnext', NAV_NEXT: 'calendar:navnext',
NAV_TODAY: 'calendar:navtoday', NAV_TODAY: 'calendar:navtoday',

View file

@ -76,9 +76,9 @@ export class CalendarManager {
this.setView(this.currentView); this.setView(this.currentView);
this.setCurrentDate(this.currentDate); this.setCurrentDate(this.currentDate);
// Step 5: Render events (after view is set) // Step 5: Render events (after view is set) - only render events for current period
console.log('🎨 Rendering events...'); console.log('🎨 Rendering events for current period...');
const events = this.eventManager.getEvents(); const events = this.getEventsForCurrentPeriod();
await this.eventRenderer.renderEvents(events); await this.eventRenderer.renderEvents(events);
this.isInitialized = true; this.isInitialized = true;
@ -109,6 +109,11 @@ export class CalendarManager {
currentView: view, currentView: view,
date: this.currentDate 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, currentDate: this.currentDate,
view: this.currentView 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; 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<void> {
console.log('CalendarManager: Re-rendering events for current period');
const events = this.getEventsForCurrentPeriod();
await this.eventRenderer.renderEvents(events);
}
} }

View file

@ -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 * 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 { private getMockData(period: Period): EventData {
const events: CalendarEvent[] = []; const events: CalendarEvent[] = [];
@ -282,11 +320,13 @@ export class DataManager {
milestone: ['Project Deadline', 'Release Day', 'Demo Day'] 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 startDate = new Date(period.start);
const endDate = new Date(period.end); 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)) { for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
// Skip weekends for most events // Skip weekends for most events
const dayOfWeek = d.getDay(); const dayOfWeek = d.getDay();

View file

@ -20,8 +20,9 @@ export class EventManager {
} }
private setupEventListeners(): void { private setupEventListeners(): void {
// Keep only UI-related event listeners here if needed // NOTE: Removed POC event listener to prevent interference with production code
// Data loading is now handled via direct method calls // 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(); 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 = `
<swp-event-time>${this.formatTime(hours, minutes)}</swp-event-time>
<swp-event-title>${event.title}</swp-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 { public destroy(): void {
this.events = []; this.events = [];
} }

View file

@ -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 { 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) { if (!container || !currentGrid) {
console.warn('NavigationManager: Calendar container not found'); console.warn('NavigationManager: Required DOM elements not found');
return; return;
} }
// Add transition class for visual feedback console.log(`NavigationManager: Starting ${direction} animation to ${targetWeek.toDateString()}`);
calendarContainer.classList.add('week-transition');
// Brief fade effect // Create new grid container (POC approach)
setTimeout(() => { const newGrid = document.createElement('swp-grid-container');
calendarContainer.classList.add('week-transition-out'); newGrid.innerHTML = `
<swp-calendar-header></swp-calendar-header>
<swp-scrollable-content>
<swp-time-grid>
<swp-grid-lines></swp-grid-lines>
<swp-day-columns></swp-day-columns>
</swp-time-grid>
</swp-scrollable-content>
`;
// Update the week after fade starts // 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';
// Slide in new grid
newGrid.style.transform = 'translateX(0)';
// Clean up after animation (POC cleanup)
setTimeout(() => { setTimeout(() => {
// Update currentWeek currentGrid.remove();
newGrid.style.position = 'relative';
// Update currentWeek only after animation is complete (POC logic)
this.currentWeek = new Date(targetWeek); this.currentWeek = new Date(targetWeek);
this.animationQueue--; 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) { if (this.animationQueue === 0) {
this.currentWeek = new Date(this.targetWeek); this.currentWeek = new Date(this.targetWeek);
} }
@ -145,13 +182,70 @@ export class NavigationManager {
weekEnd: DateUtils.addDays(this.currentWeek, 6) weekEnd: DateUtils.addDays(this.currentWeek, 6)
}); });
// Remove transition classes console.log(`NavigationManager: Completed ${direction} animation`);
setTimeout(() => { }, 400); // Match POC timing
calendarContainer.classList.remove('week-transition', 'week-transition-out'); });
}, 150); }
}, 150); // Half of transition duration /**
}, 50); * 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 = `
<swp-day-name>${days[date.getDay()]}</swp-day-name>
<swp-day-date>${date.getDate()}</swp-day-date>
`;
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 { private updateWeekInfo(): void {

View file

@ -25,12 +25,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Clear existing events first // Clear existing events first
this.clearEvents(); this.clearEvents();
// Filter out all-day events (handled by GridManager) // Events should already be filtered by DataManager - no need to filter here
const nonAllDayEvents = events.filter(event => !event.allDay); console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events');
console.log('BaseEventRenderer: Rendering', nonAllDayEvents.length, 'non-all-day events');
// Render each event in the correct column // Render each event in the correct column
nonAllDayEvents.forEach(event => { events.forEach(event => {
const column = this.findColumn(event); const column = this.findColumn(event);
if (column) { if (column) {
@ -166,13 +165,14 @@ export class DateEventRenderer extends BaseEventRenderer {
*/ */
export class ResourceEventRenderer extends BaseEventRenderer { export class ResourceEventRenderer extends BaseEventRenderer {
findColumn(event: CalendarEvent): HTMLElement | null { findColumn(event: CalendarEvent): HTMLElement | null {
if (!event.resourceName) { const resourceName = event.resource?.name;
console.warn('ResourceEventRenderer: Event has no resourceName', event); if (!resourceName) {
console.warn('ResourceEventRenderer: Event has no resource.name', event);
return null; return null;
} }
const resourceColumn = document.querySelector(`swp-resource-column[data-resource="${event.resourceName}"]`) as HTMLElement; const resourceColumn = document.querySelector(`swp-resource-column[data-resource="${resourceName}"]`) as HTMLElement;
console.log('ResourceEventRenderer: Looking for resource column with name', event.resourceName, 'found:', !!resourceColumn); console.log('ResourceEventRenderer: Looking for resource column with name', resourceName, 'found:', !!resourceColumn);
return resourceColumn; return resourceColumn;
} }
} }

View file

@ -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;
}
}