import { ICalendarEvent } from '../../types/CalendarTypes'; import { EventService } from '../../storage/events/EventService'; import { calculateEventPosition, getDateKey, formatTimeRange, GridConfig } from '../../utils/PositionUtils'; /** * EventRenderer - Renders calendar events to the DOM * * CLEAN approach: * - Only data-id attribute on event element * - innerHTML contains only visible content * - Event data retrieved via EventService when needed */ export class EventRenderer { private readonly gridConfig: GridConfig = { dayStartHour: 6, dayEndHour: 18, hourHeight: 64 }; constructor(private eventService: EventService) {} /** * Render events for visible dates into day columns */ async render(container: HTMLElement, visibleDates: string[]): Promise { // Get date range for query const startDate = new Date(visibleDates[0]); const endDate = new Date(visibleDates[visibleDates.length - 1]); endDate.setHours(23, 59, 59, 999); // Fetch events from IndexedDB const events = await this.eventService.getByDateRange(startDate, endDate); // Group events by date const eventsByDate = this.groupEventsByDate(events); // Find day columns const dayColumns = container.querySelector('swp-day-columns'); if (!dayColumns) return; const columns = dayColumns.querySelectorAll('swp-day-column'); // Render events into columns columns.forEach((column, index) => { const dateKey = visibleDates[index]; const dateEvents = eventsByDate.get(dateKey) || []; // Get or create events layer let eventsLayer = column.querySelector('swp-events-layer'); if (!eventsLayer) { eventsLayer = document.createElement('swp-events-layer'); column.appendChild(eventsLayer); } // Clear existing events eventsLayer.innerHTML = ''; // Render each event dateEvents.forEach(event => { if (!event.allDay) { const eventElement = this.createEventElement(event); eventsLayer!.appendChild(eventElement); } }); }); } /** * Group events by their date key */ private groupEventsByDate(events: ICalendarEvent[]): Map { const map = new Map(); events.forEach(event => { const dateKey = getDateKey(event.start); const existing = map.get(dateKey) || []; existing.push(event); map.set(dateKey, existing); }); return map; } /** * Create a single event element * * CLEAN approach: * - Only data-id for lookup * - Visible content in innerHTML only */ private createEventElement(event: ICalendarEvent): HTMLElement { const element = document.createElement('swp-event'); // Only essential data attribute element.dataset.id = event.id; // Calculate position const position = calculateEventPosition(event.start, event.end, this.gridConfig); element.style.top = `${position.top}px`; element.style.height = `${position.height}px`; // Color class based on event type const colorClass = this.getColorClass(event); if (colorClass) { element.classList.add(colorClass); } // Visible content only element.innerHTML = ` ${formatTimeRange(event.start, event.end)} ${this.escapeHtml(event.title)} ${event.description ? `${this.escapeHtml(event.description)}` : ''} `; return element; } /** * Get color class based on event type */ private getColorClass(event: ICalendarEvent): string { const typeColors: Record = { 'customer': 'is-blue', 'vacation': 'is-green', 'break': 'is-amber', 'meeting': 'is-purple', 'blocked': 'is-red' }; return typeColors[event.type] || 'is-blue'; } /** * Escape HTML to prevent XSS */ private escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }