Refactor calendar V2 core with DI and new features

Introduces dependency injection container and composition root
Adds core services like DateService and NavigationAnimator
Simplifies CalendarOrchestrator with improved store handling
Implements mock stores and demo application for V2 calendar
This commit is contained in:
Janus C. H. Knudsen 2025-12-07 14:31:16 +01:00
parent 1ad7d10266
commit a0c0ef9e8d
17 changed files with 331 additions and 134 deletions

View file

@ -1,7 +1,7 @@
import { ViewConfig, GroupingConfig } from './ViewConfig';
import { RenderContext } from './RenderContext';
import { RendererRegistry } from './RendererRegistry';
import { IStoreRegistry } from './IGroupingStore';
import { IGroupingStore } from './IGroupingStore';
interface HierarchyNode {
type: string;
@ -19,9 +19,13 @@ interface GroupingData {
export class CalendarOrchestrator {
constructor(
private rendererRegistry: RendererRegistry,
private storeRegistry: IStoreRegistry
private stores: IGroupingStore[]
) {}
private getStore(type: string): IGroupingStore | undefined {
return this.stores.find(s => s.type === type);
}
async render(viewConfig: ViewConfig, container: HTMLElement): Promise<void> {
const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
@ -66,7 +70,9 @@ export class CalendarOrchestrator {
continue;
}
const rawItems = this.storeRegistry.get(g.type).getByIds(g.values);
const store = this.getStore(g.type);
if (!store) continue;
const rawItems = store.getByIds(g.values);
const items = rawItems.map((item: any) => ({ id: item.id, data: item }));
const byParent = g.parentKey
? this.groupBy(items, item => (item.data as any)[g.parentKey!])

View file

@ -0,0 +1,21 @@
import dayjs from 'dayjs';
import { ITimeFormatConfig } from './ITimeFormatConfig';
export class DateService {
constructor(private config: ITimeFormatConfig) {}
parseISO(isoString: string): Date {
return dayjs(isoString).toDate();
}
getDayName(date: Date, format: 'short' | 'long' = 'short'): string {
return new Intl.DateTimeFormat(this.config.locale, { weekday: format }).format(date);
}
getWeekDates(offset = 0): string[] {
const monday = dayjs().startOf('week').add(1, 'day').add(offset, 'week');
return Array.from({ length: 5 }, (_, i) =>
monday.add(i, 'day').format('YYYY-MM-DD')
);
}
}

View file

@ -1,7 +1,4 @@
export interface IGroupingStore<T = unknown> {
readonly type: string;
getByIds(ids: string[]): T[];
}
export interface IStoreRegistry {
get(type: string): IGroupingStore;
}

View file

@ -0,0 +1,7 @@
export interface ITimeFormatConfig {
timezone: string;
use24HourFormat: boolean;
locale: string;
dateFormat: 'locale' | 'technical';
showSeconds: boolean;
}

View file

@ -0,0 +1,41 @@
export class NavigationAnimator {
constructor(
private headerTrack: HTMLElement,
private contentTrack: HTMLElement
) {}
async slide(direction: 'left' | 'right', renderFn: () => Promise<void>): Promise<void> {
const out = direction === 'left' ? '-100%' : '100%';
const into = direction === 'left' ? '100%' : '-100%';
await this.animateOut(out);
await renderFn();
await this.animateIn(into);
}
private async animateOut(translate: string): Promise<void> {
await Promise.all([
this.headerTrack.animate(
[{ transform: 'translateX(0)' }, { transform: `translateX(${translate})` }],
{ duration: 200, easing: 'ease-in' }
).finished,
this.contentTrack.animate(
[{ transform: 'translateX(0)' }, { transform: `translateX(${translate})` }],
{ duration: 200, easing: 'ease-in' }
).finished
]);
}
private async animateIn(translate: string): Promise<void> {
await Promise.all([
this.headerTrack.animate(
[{ transform: `translateX(${translate})` }, { transform: 'translateX(0)' }],
{ duration: 200, easing: 'ease-out' }
).finished,
this.contentTrack.animate(
[{ transform: `translateX(${translate})` }, { transform: 'translateX(0)' }],
{ duration: 200, easing: 'ease-out' }
).finished
]);
}
}

View file

@ -1,15 +0,0 @@
import { IGroupingStore, IStoreRegistry } from './IGroupingStore';
export class StoreRegistry implements IStoreRegistry {
private stores = new Map<string, IGroupingStore>();
register(type: string, store: IGroupingStore): void {
this.stores.set(type, store);
}
get(type: string): IGroupingStore {
const store = this.stores.get(type);
if (!store) throw new Error(`No store for type: ${type}`);
return store;
}
}