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.
This commit is contained in:
parent
7d513600d8
commit
3ddc6352f2
17 changed files with 1347 additions and 428 deletions
456
architecture/month-view-refactoring-plan.md
Normal file
456
architecture/month-view-refactoring-plan.md
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
# 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!
|
||||
Loading…
Add table
Add a link
Reference in a new issue