Calendar/architecture/month-view-refactoring-plan.md
Janus Knudsen 3ddc6352f2 Refactors calendar architecture for month view
Prepares the calendar component for month view implementation
by introducing a strategy pattern for view management,
splitting configuration settings, and consolidating events
into a core set. It also removes dead code and enforces type safety,
improving overall code quality and maintainability.

Addresses critical issues identified in the code review,
laying the groundwork for efficient feature addition.
2025-08-20 19:42:13 +02:00

11 KiB

Month View Refactoring Plan

Purpose: Enable month view with minimal refactoring
Timeline: 3 days (6 hours of focused work)
Priority: High - Blocks new feature development

Overview

This plan addresses only the critical architectural issues that prevent month view implementation. By focusing on the minimal necessary changes, we can add month view in ~500 lines instead of ~2000 lines.


Current Blockers for Month View

🚫 Why Month View Can't Be Added Now

  1. GridManager is hardcoded for time-based views

    • Assumes everything is hours and columns
    • Time axis doesn't make sense for months
    • Hour-based scrolling irrelevant
  2. No strategy pattern for different view types

    • Would need entirely new managers
    • Massive code duplication
    • Inconsistent behavior
  3. Config assumes time-based views

    hourHeight: 60,
    dayStartHour: 0,
    snapInterval: 15
    // These are meaningless for month view!
    
  4. Event rendering tied to time positions

    • Events positioned by minutes
    • No concept of day cells
    • Can't handle multi-day spans properly

Phase 1: View Strategy Pattern (2 hours)

1.1 Create ViewStrategy Interface

New file: src/strategies/ViewStrategy.ts

export interface ViewStrategy {
  // Core rendering methods
  renderGrid(container: HTMLElement, context: ViewContext): void;
  renderEvents(events: CalendarEvent[], container: HTMLElement): void;
  
  // Configuration
  getLayoutConfig(): ViewLayoutConfig;
  getRequiredConfig(): string[]; // Which config keys this view needs
  
  // Navigation
  getNextPeriod(currentDate: Date): Date;
  getPreviousPeriod(currentDate: Date): Date;
  getPeriodLabel(date: Date): string;
}

export interface ViewContext {
  currentDate: Date;
  config: CalendarConfig;
  events: CalendarEvent[];
  container: HTMLElement;
}

1.2 Extract WeekViewStrategy

New file: src/strategies/WeekViewStrategy.ts

  • Move existing logic from GridRenderer
  • Keep all time-based rendering
  • Minimal changes to existing code
export class WeekViewStrategy implements ViewStrategy {
  renderGrid(container: HTMLElement, context: ViewContext): void {
    // Move existing GridRenderer.renderGrid() here
    this.createTimeAxis(container);
    this.createDayColumns(container, context);
    this.createTimeSlots(container);
  }
  
  renderEvents(events: CalendarEvent[], container: HTMLElement): void {
    // Move existing EventRenderer logic
    // Position by time as before
  }
}

1.3 Create MonthViewStrategy

New file: src/strategies/MonthViewStrategy.ts

export class MonthViewStrategy implements ViewStrategy {
  renderGrid(container: HTMLElement, context: ViewContext): void {
    // Create 7x6 grid
    this.createMonthHeader(container); // Mon-Sun
    this.createWeekRows(container, context);
  }
  
  renderEvents(events: CalendarEvent[], container: HTMLElement): void {
    // Render as small blocks in day cells
    // Handle multi-day spanning
  }
}

1.4 Update GridManager

Modify: src/managers/GridManager.ts

export class GridManager {
  private strategy: ViewStrategy;
  
  setViewStrategy(strategy: ViewStrategy): void {
    this.strategy = strategy;
  }
  
  render(): void {
    // Delegate to strategy
    this.strategy.renderGrid(this.grid, {
      currentDate: this.currentWeek,
      config: this.config,
      events: this.events,
      container: this.grid
    });
  }
}

Phase 2: Configuration Split (1 hour)

2.1 View-Specific Configs

New file: src/core/ViewConfigs.ts

// Shared by all views
export interface BaseViewConfig {
  locale: string;
  firstDayOfWeek: number;
  dateFormat: string;
  eventColors: Record<string, string>;
}

// Week/Day views only
export interface TimeViewConfig extends BaseViewConfig {
  hourHeight: number;
  dayStartHour: number;
  dayEndHour: number;
  snapInterval: number;
  showCurrentTime: boolean;
}

// Month view only
export interface MonthViewConfig extends BaseViewConfig {
  weeksToShow: number; // Usually 6
  showWeekNumbers: boolean;
  compactMode: boolean;
  eventLimit: number; // Max events shown per day
  showMoreText: string; // "+2 more"
}

2.2 Update CalendarConfig

Modify: src/core/CalendarConfig.ts

export class CalendarConfig {
  private viewConfigs: Map<string, BaseViewConfig> = new Map();
  
  constructor() {
    // Set defaults for each view
    this.viewConfigs.set('week', defaultWeekConfig);
    this.viewConfigs.set('month', defaultMonthConfig);
  }
  
  getViewConfig<T extends BaseViewConfig>(view: string): T {
    return this.viewConfigs.get(view) as T;
  }
}

Phase 3: Event Consolidation (1 hour)

3.1 Core Events Only

New file: src/constants/CoreEvents.ts

export const CoreEvents = {
  // View lifecycle (5 events)
  VIEW_CHANGED: 'view:changed',
  VIEW_RENDERED: 'view:rendered',
  
  // Navigation (3 events)
  DATE_CHANGED: 'date:changed',
  PERIOD_CHANGED: 'period:changed',
  
  // Data (4 events)
  EVENTS_LOADING: 'events:loading',
  EVENTS_LOADED: 'events:loaded',
  EVENT_CLICKED: 'event:clicked',
  EVENT_UPDATED: 'event:updated',
  
  // UI State (3 events)
  LOADING_START: 'ui:loading:start',
  LOADING_END: 'ui:loading:end',
  ERROR: 'ui:error',
  
  // Grid (3 events)
  GRID_RENDERED: 'grid:rendered',
  GRID_CLICKED: 'grid:clicked',
  CELL_CLICKED: 'cell:clicked'
};
// Total: ~18 events instead of 102!

3.2 Migration Map

Modify: src/constants/EventTypes.ts

// Keep old events but map to new ones
export const EventTypes = {
  VIEW_CHANGED: CoreEvents.VIEW_CHANGED, // Direct mapping
  WEEK_CHANGED: CoreEvents.PERIOD_CHANGED, // Renamed
  // ... etc
} as const;

Phase 4: Month-Specific Renderers (2 hours)

4.1 MonthGridRenderer

New file: src/renderers/MonthGridRenderer.ts

export class MonthGridRenderer {
  render(container: HTMLElement, date: Date): void {
    const grid = this.createGrid();
    
    // Add day headers
    ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].forEach(day => {
      grid.appendChild(this.createDayHeader(day));
    });
    
    // Add 6 weeks of days
    const dates = this.getMonthDates(date);
    dates.forEach(weekDates => {
      weekDates.forEach(date => {
        grid.appendChild(this.createDayCell(date));
      });
    });
    
    container.appendChild(grid);
  }
  
  private createGrid(): HTMLElement {
    const grid = document.createElement('div');
    grid.className = 'month-grid';
    grid.style.display = 'grid';
    grid.style.gridTemplateColumns = 'repeat(7, 1fr)';
    return grid;
  }
}

4.2 MonthEventRenderer

New file: src/renderers/MonthEventRenderer.ts

export class MonthEventRenderer {
  render(events: CalendarEvent[], container: HTMLElement): void {
    const dayMap = this.groupEventsByDay(events);
    
    dayMap.forEach((dayEvents, dateStr) => {
      const dayCell = container.querySelector(`[data-date="${dateStr}"]`);
      if (!dayCell) return;
      
      const limited = dayEvents.slice(0, 3); // Show max 3
      limited.forEach(event => {
        dayCell.appendChild(this.createEventBlock(event));
      });
      
      if (dayEvents.length > 3) {
        dayCell.appendChild(this.createMoreIndicator(dayEvents.length - 3));
      }
    });
  }
}

Phase 5: Integration (1 hour)

5.1 Wire ViewManager

Modify: src/managers/ViewManager.ts

private changeView(newView: CalendarView): void {
  // Create appropriate strategy
  let strategy: ViewStrategy;
  
  switch(newView) {
    case 'week':
    case 'day':
      strategy = new WeekViewStrategy();
      break;
    case 'month':
      strategy = new MonthViewStrategy();
      break;
  }
  
  // Update GridManager
  this.gridManager.setViewStrategy(strategy);
  
  // Trigger re-render
  this.eventBus.emit(CoreEvents.VIEW_CHANGED, { view: newView });
}

5.2 Enable Month Button

Modify: wwwroot/index.html

<!-- Remove disabled attribute -->
<swp-view-button data-view="month">Month</swp-view-button>

5.3 Add Month Styles

New file: wwwroot/css/calendar-month-css.css

.month-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
  background: var(--color-border);
}

.month-day-cell {
  background: white;
  min-height: 100px;
  padding: 4px;
  position: relative;
}

.month-day-number {
  font-weight: bold;
  margin-bottom: 4px;
}

.month-event {
  font-size: 0.75rem;
  padding: 2px 4px;
  margin: 1px 0;
  border-radius: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.month-more-indicator {
  font-size: 0.7rem;
  color: var(--color-text-secondary);
  cursor: pointer;
}

Implementation Timeline

Day 1 (Monday)

Morning (2 hours)

  • Implement ViewStrategy interface
  • Extract WeekViewStrategy
  • Create MonthViewStrategy skeleton

Afternoon (1 hour)

  • Split configuration
  • Update CalendarConfig

Day 2 (Tuesday)

Morning (2 hours)

  • Consolidate events to CoreEvents
  • Create migration mappings
  • Update critical event listeners

Afternoon (2 hours)

  • Implement MonthGridRenderer
  • Implement MonthEventRenderer

Day 3 (Wednesday)

Morning (2 hours)

  • Wire everything in ViewManager
  • Update HTML and CSS
  • Test month view
  • Fix edge cases

Success Metrics

Definition of Done

  • Month view displays 6 weeks correctly
  • Events show in day cells (max 3 + "more")
  • Navigation works (prev/next month)
  • Switching between week/month works
  • No regression in week view
  • Under 750 lines of new code

📊 Expected Impact

  • New code: ~500-750 lines (vs 2000 without refactoring)
  • Reusability: 80% of components shared
  • Future views: Day view = 100 lines, Year view = 200 lines
  • Test coverage: Easy to test strategies independently
  • Performance: No impact on existing views

Risk Mitigation

Potential Issues & Solutions

  1. CSS conflicts between views

    • Solution: Namespace all month CSS with .month-view
  2. Event overlap in month cells

    • Solution: Implement "more" indicator after 3 events
  3. Performance with many events

    • Solution: Only render visible month
  4. Browser compatibility

    • Solution: Use CSS Grid with flexbox fallback

Next Steps After Month View

Once this refactoring is complete, adding new views becomes trivial:

  • Day View: ~100 lines (reuse WeekViewStrategy with 1 column)
  • Year View: ~200 lines (12 small month grids)
  • Agenda View: ~150 lines (list layout)
  • Timeline View: ~300 lines (horizontal time axis)

The strategy pattern makes the calendar truly extensible!