Calendar/architecture/month-view-refactoring-plan.md

456 lines
11 KiB
Markdown
Raw Normal View History

# 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!