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

456 lines
No EOL
11 KiB
Markdown

# 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**
```typescript
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`
```typescript
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
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
// 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`
```typescript
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`
```typescript
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`
```typescript
// 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`
```typescript
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`
```typescript
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`
```typescript
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`
```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`
```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!