Moving away from Azure Devops #1
17 changed files with 331 additions and 134 deletions
16
build.js
16
build.js
|
|
@ -61,6 +61,22 @@ async function build() {
|
||||||
|
|
||||||
console.log('V2 bundle created: wwwroot/js/calendar-v2.js');
|
console.log('V2 bundle created: wwwroot/js/calendar-v2.js');
|
||||||
|
|
||||||
|
// V2 demo bundle (with DI transformer for autowiring)
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['src/v2/demo/index.ts'],
|
||||||
|
bundle: true,
|
||||||
|
outfile: 'wwwroot/js/v2-demo.js',
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: 'inline',
|
||||||
|
target: 'es2020',
|
||||||
|
minify: false,
|
||||||
|
keepNames: true,
|
||||||
|
platform: 'browser',
|
||||||
|
plugins: [NovadiUnplugin.esbuild({ debug: false, enableAutowiring: true })]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('V2 demo bundle created: wwwroot/js/v2-demo.js');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Build failed:', error);
|
console.error('Build failed:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
53
src/v2/V2CompositionRoot.ts
Normal file
53
src/v2/V2CompositionRoot.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Container } from '@novadi/core';
|
||||||
|
import { IGroupingRenderer } from './core/IGroupingRenderer';
|
||||||
|
import { IGroupingStore } from './core/IGroupingStore';
|
||||||
|
import { DateRenderer } from './features/date/DateRenderer';
|
||||||
|
import { DateService } from './core/DateService';
|
||||||
|
import { ITimeFormatConfig } from './core/ITimeFormatConfig';
|
||||||
|
import { ResourceRenderer } from './features/resource/ResourceRenderer';
|
||||||
|
import { TeamRenderer } from './features/team/TeamRenderer';
|
||||||
|
import { RendererRegistry } from './core/RendererRegistry';
|
||||||
|
import { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
||||||
|
import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
|
||||||
|
import { MockTeamStore, MockResourceStore } from './demo/MockStores';
|
||||||
|
import { DemoApp } from './demo/DemoApp';
|
||||||
|
|
||||||
|
const defaultTimeFormatConfig: ITimeFormatConfig = {
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
use24HourFormat: true,
|
||||||
|
locale: 'da-DK',
|
||||||
|
dateFormat: 'locale',
|
||||||
|
showSeconds: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createV2Container(): Container {
|
||||||
|
const container = new Container();
|
||||||
|
const builder = container.builder();
|
||||||
|
|
||||||
|
// Config
|
||||||
|
builder.registerInstance(defaultTimeFormatConfig).as<ITimeFormatConfig>();
|
||||||
|
|
||||||
|
// Services
|
||||||
|
builder.registerType(DateService).as<DateService>();
|
||||||
|
|
||||||
|
// Renderers - registreres som IGroupingRenderer
|
||||||
|
builder.registerType(DateRenderer).as<IGroupingRenderer>();
|
||||||
|
builder.registerType(ResourceRenderer).as<IGroupingRenderer>();
|
||||||
|
builder.registerType(TeamRenderer).as<IGroupingRenderer>();
|
||||||
|
|
||||||
|
// RendererRegistry modtager IGroupingRenderer[] automatisk (array injection)
|
||||||
|
builder.registerType(RendererRegistry).as<RendererRegistry>();
|
||||||
|
|
||||||
|
// Stores - registreres som IGroupingStore
|
||||||
|
builder.registerType(MockTeamStore).as<IGroupingStore>();
|
||||||
|
builder.registerType(MockResourceStore).as<IGroupingStore>();
|
||||||
|
|
||||||
|
// CalendarOrchestrator modtager IGroupingStore[] automatisk (array injection)
|
||||||
|
builder.registerType(CalendarOrchestrator).as<CalendarOrchestrator>();
|
||||||
|
builder.registerType(TimeAxisRenderer).as<TimeAxisRenderer>();
|
||||||
|
|
||||||
|
// Demo app
|
||||||
|
builder.registerType(DemoApp).as<DemoApp>();
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ViewConfig, GroupingConfig } from './ViewConfig';
|
import { ViewConfig, GroupingConfig } from './ViewConfig';
|
||||||
import { RenderContext } from './RenderContext';
|
import { RenderContext } from './RenderContext';
|
||||||
import { RendererRegistry } from './RendererRegistry';
|
import { RendererRegistry } from './RendererRegistry';
|
||||||
import { IStoreRegistry } from './IGroupingStore';
|
import { IGroupingStore } from './IGroupingStore';
|
||||||
|
|
||||||
interface HierarchyNode {
|
interface HierarchyNode {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -19,9 +19,13 @@ interface GroupingData {
|
||||||
export class CalendarOrchestrator {
|
export class CalendarOrchestrator {
|
||||||
constructor(
|
constructor(
|
||||||
private rendererRegistry: RendererRegistry,
|
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> {
|
async render(viewConfig: ViewConfig, container: HTMLElement): Promise<void> {
|
||||||
const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
|
const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
|
||||||
const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
|
const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
|
||||||
|
|
@ -66,7 +70,9 @@ export class CalendarOrchestrator {
|
||||||
continue;
|
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 items = rawItems.map((item: any) => ({ id: item.id, data: item }));
|
||||||
const byParent = g.parentKey
|
const byParent = g.parentKey
|
||||||
? this.groupBy(items, item => (item.data as any)[g.parentKey!])
|
? this.groupBy(items, item => (item.data as any)[g.parentKey!])
|
||||||
|
|
|
||||||
21
src/v2/core/DateService.ts
Normal file
21
src/v2/core/DateService.ts
Normal 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')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
export interface IGroupingStore<T = unknown> {
|
export interface IGroupingStore<T = unknown> {
|
||||||
|
readonly type: string;
|
||||||
getByIds(ids: string[]): T[];
|
getByIds(ids: string[]): T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStoreRegistry {
|
|
||||||
get(type: string): IGroupingStore;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
7
src/v2/core/ITimeFormatConfig.ts
Normal file
7
src/v2/core/ITimeFormatConfig.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface ITimeFormatConfig {
|
||||||
|
timezone: string;
|
||||||
|
use24HourFormat: boolean;
|
||||||
|
locale: string;
|
||||||
|
dateFormat: 'locale' | 'technical';
|
||||||
|
showSeconds: boolean;
|
||||||
|
}
|
||||||
41
src/v2/core/NavigationAnimator.ts
Normal file
41
src/v2/core/NavigationAnimator.ts
Normal 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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
84
src/v2/demo/DemoApp.ts
Normal file
84
src/v2/demo/DemoApp.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { CalendarOrchestrator } from '../core/CalendarOrchestrator';
|
||||||
|
import { TimeAxisRenderer } from '../features/timeaxis/TimeAxisRenderer';
|
||||||
|
import { NavigationAnimator } from '../core/NavigationAnimator';
|
||||||
|
import { DateService } from '../core/DateService';
|
||||||
|
import { ViewConfig } from '../core/ViewConfig';
|
||||||
|
|
||||||
|
export class DemoApp {
|
||||||
|
private animator!: NavigationAnimator;
|
||||||
|
private container!: HTMLElement;
|
||||||
|
private weekOffset = 0;
|
||||||
|
private views!: Record<string, ViewConfig>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private orchestrator: CalendarOrchestrator,
|
||||||
|
private timeAxisRenderer: TimeAxisRenderer,
|
||||||
|
private dateService: DateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.container = document.querySelector('swp-calendar-container') as HTMLElement;
|
||||||
|
|
||||||
|
// NavigationAnimator har DOM-dependencies - tilladt med new
|
||||||
|
this.animator = new NavigationAnimator(
|
||||||
|
document.querySelector('swp-header-track') as HTMLElement,
|
||||||
|
document.querySelector('swp-content-track') as HTMLElement
|
||||||
|
);
|
||||||
|
|
||||||
|
// View configs
|
||||||
|
const dates = this.dateService.getWeekDates();
|
||||||
|
this.views = {
|
||||||
|
simple: { templateId: 'simple', groupings: [{ type: 'date', values: dates }] },
|
||||||
|
resource: {
|
||||||
|
templateId: 'resource',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'resource', values: ['alice', 'bob', 'carol'] },
|
||||||
|
{ type: 'date', values: dates.slice(0, 3) }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
templateId: 'team',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'team', values: ['alpha', 'beta'] },
|
||||||
|
{ type: 'resource', values: ['alice', 'bob', 'carol', 'dave'], parentKey: 'teamId' },
|
||||||
|
{ type: 'date', values: dates.slice(0, 3) }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render time axis
|
||||||
|
this.timeAxisRenderer.render(document.getElementById('time-axis') as HTMLElement);
|
||||||
|
|
||||||
|
// Setup event handlers
|
||||||
|
this.setupNavigation();
|
||||||
|
this.setupViewSwitchers();
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
this.orchestrator.render(this.views.simple, this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupNavigation(): void {
|
||||||
|
document.getElementById('btn-prev')!.onclick = () => {
|
||||||
|
this.weekOffset--;
|
||||||
|
this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset);
|
||||||
|
this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container));
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('btn-next')!.onclick = () => {
|
||||||
|
this.weekOffset++;
|
||||||
|
this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset);
|
||||||
|
this.animator.slide('left', () => this.orchestrator.render(this.views.simple, this.container));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupViewSwitchers(): void {
|
||||||
|
document.getElementById('btn-simple')!.onclick = () =>
|
||||||
|
this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container));
|
||||||
|
|
||||||
|
document.getElementById('btn-resource')!.onclick = () =>
|
||||||
|
this.animator.slide('left', () => this.orchestrator.render(this.views.resource, this.container));
|
||||||
|
|
||||||
|
document.getElementById('btn-team')!.onclick = () =>
|
||||||
|
this.animator.slide('left', () => this.orchestrator.render(this.views.team, this.container));
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/v2/demo/MockStores.ts
Normal file
40
src/v2/demo/MockStores.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { IGroupingStore } from '../core/IGroupingStore';
|
||||||
|
|
||||||
|
export interface Team {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
teamId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockTeamStore implements IGroupingStore<Team> {
|
||||||
|
readonly type = 'team';
|
||||||
|
|
||||||
|
private teams: Team[] = [
|
||||||
|
{ id: 'alpha', name: 'Team Alpha' },
|
||||||
|
{ id: 'beta', name: 'Team Beta' }
|
||||||
|
];
|
||||||
|
|
||||||
|
getByIds(ids: string[]): Team[] {
|
||||||
|
return this.teams.filter(t => ids.includes(t.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockResourceStore implements IGroupingStore<Resource> {
|
||||||
|
readonly type = 'resource';
|
||||||
|
|
||||||
|
private resources: Resource[] = [
|
||||||
|
{ id: 'alice', name: 'Alice', teamId: 'alpha' },
|
||||||
|
{ id: 'bob', name: 'Bob', teamId: 'alpha' },
|
||||||
|
{ id: 'carol', name: 'Carol', teamId: 'beta' },
|
||||||
|
{ id: 'dave', name: 'Dave', teamId: 'beta' }
|
||||||
|
];
|
||||||
|
|
||||||
|
getByIds(ids: string[]): Resource[] {
|
||||||
|
return this.resources.filter(r => ids.includes(r.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/v2/demo/index.ts
Normal file
5
src/v2/demo/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createV2Container } from '../V2CompositionRoot';
|
||||||
|
import { DemoApp } from './DemoApp';
|
||||||
|
|
||||||
|
const app = createV2Container();
|
||||||
|
app.resolveType<DemoApp>().init();
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
import { IGroupingRenderer } from '../../core/IGroupingRenderer';
|
import { IGroupingRenderer } from '../../core/IGroupingRenderer';
|
||||||
import { RenderContext } from '../../core/RenderContext';
|
import { RenderContext } from '../../core/RenderContext';
|
||||||
|
import { DateService } from '../../core/DateService';
|
||||||
export interface IDateService {
|
|
||||||
parseISO(dateStr: string): Date;
|
|
||||||
getDayName(date: Date, format: 'short' | 'long'): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultDateService: IDateService = {
|
|
||||||
parseISO: (str) => new Date(str),
|
|
||||||
getDayName: (date, format) => date.toLocaleDateString('da-DK', { weekday: format })
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DateRenderer implements IGroupingRenderer {
|
export class DateRenderer implements IGroupingRenderer {
|
||||||
readonly type = 'date';
|
readonly type = 'date';
|
||||||
|
|
||||||
constructor(private dateService: IDateService = defaultDateService) {}
|
constructor(private dateService: DateService) {}
|
||||||
|
|
||||||
render(context: RenderContext): void {
|
render(context: RenderContext): void {
|
||||||
for (const dateStr of context.values) {
|
for (const dateStr of context.values) {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { DateRenderer, IDateService, defaultDateService } from './DateRenderer';
|
export { DateRenderer } from './DateRenderer';
|
||||||
|
|
|
||||||
10
src/v2/features/timeaxis/TimeAxisRenderer.ts
Normal file
10
src/v2/features/timeaxis/TimeAxisRenderer.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export class TimeAxisRenderer {
|
||||||
|
render(container: HTMLElement, startHour = 6, endHour = 20): void {
|
||||||
|
container.innerHTML = '';
|
||||||
|
for (let hour = startHour; hour <= endHour; hour++) {
|
||||||
|
const marker = document.createElement('swp-hour-marker');
|
||||||
|
marker.textContent = `${hour.toString().padStart(2, '0')}:00`;
|
||||||
|
container.appendChild(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,17 @@
|
||||||
export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig';
|
export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig';
|
||||||
export { RenderContext } from './core/RenderContext';
|
export { RenderContext } from './core/RenderContext';
|
||||||
export { IGroupingRenderer } from './core/IGroupingRenderer';
|
export { IGroupingRenderer } from './core/IGroupingRenderer';
|
||||||
export { IGroupingStore, IStoreRegistry } from './core/IGroupingStore';
|
export { IGroupingStore } from './core/IGroupingStore';
|
||||||
export { RendererRegistry } from './core/RendererRegistry';
|
export { RendererRegistry } from './core/RendererRegistry';
|
||||||
export { StoreRegistry } from './core/StoreRegistry';
|
|
||||||
export { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
export { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
||||||
|
export { NavigationAnimator } from './core/NavigationAnimator';
|
||||||
|
|
||||||
// Feature exports
|
// Feature exports
|
||||||
export { DateRenderer, IDateService, defaultDateService } from './features/date';
|
export { DateRenderer } from './features/date';
|
||||||
|
export { DateService } from './core/DateService';
|
||||||
|
export { ITimeFormatConfig } from './core/ITimeFormatConfig';
|
||||||
export { EventRenderer, IEventData, IEventStore } from './features/event';
|
export { EventRenderer, IEventData, IEventStore } from './features/event';
|
||||||
export { ResourceRenderer } from './features/resource';
|
export { ResourceRenderer } from './features/resource';
|
||||||
export { TeamRenderer } from './features/team';
|
export { TeamRenderer } from './features/team';
|
||||||
|
export { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,22 @@ swp-grid-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Viewport/Track for slide animation */
|
||||||
|
swp-header-viewport,
|
||||||
|
swp-content-viewport {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-header-track,
|
||||||
|
swp-content-track {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-header-track > swp-calendar-header,
|
||||||
|
swp-content-track > swp-scrollable-content {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
swp-calendar-header {
|
swp-calendar-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
||||||
101
wwwroot/v2.html
101
wwwroot/v2.html
|
|
@ -17,6 +17,8 @@
|
||||||
<swp-week-number>V2</swp-week-number>
|
<swp-week-number>V2</swp-week-number>
|
||||||
<swp-date-range id="view-info"></swp-date-range>
|
<swp-date-range id="view-info"></swp-date-range>
|
||||||
</swp-week-info>
|
</swp-week-info>
|
||||||
|
<swp-nav-button id="btn-prev">←</swp-nav-button>
|
||||||
|
<swp-nav-button id="btn-next">→</swp-nav-button>
|
||||||
</swp-calendar-nav>
|
</swp-calendar-nav>
|
||||||
|
|
||||||
<swp-calendar-container>
|
<swp-calendar-container>
|
||||||
|
|
@ -25,107 +27,26 @@
|
||||||
<swp-time-axis-content id="time-axis"></swp-time-axis-content>
|
<swp-time-axis-content id="time-axis"></swp-time-axis-content>
|
||||||
</swp-time-axis>
|
</swp-time-axis>
|
||||||
<swp-grid-container>
|
<swp-grid-container>
|
||||||
|
<swp-header-viewport>
|
||||||
|
<swp-header-track>
|
||||||
<swp-calendar-header></swp-calendar-header>
|
<swp-calendar-header></swp-calendar-header>
|
||||||
|
</swp-header-track>
|
||||||
|
</swp-header-viewport>
|
||||||
|
<swp-content-viewport>
|
||||||
|
<swp-content-track>
|
||||||
<swp-scrollable-content>
|
<swp-scrollable-content>
|
||||||
<swp-time-grid>
|
<swp-time-grid>
|
||||||
<swp-grid-lines></swp-grid-lines>
|
<swp-grid-lines></swp-grid-lines>
|
||||||
<swp-day-columns></swp-day-columns>
|
<swp-day-columns></swp-day-columns>
|
||||||
</swp-time-grid>
|
</swp-time-grid>
|
||||||
</swp-scrollable-content>
|
</swp-scrollable-content>
|
||||||
|
</swp-content-track>
|
||||||
|
</swp-content-viewport>
|
||||||
</swp-grid-container>
|
</swp-grid-container>
|
||||||
</swp-calendar-container>
|
</swp-calendar-container>
|
||||||
</swp-calendar>
|
</swp-calendar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module" src="js/v2-demo.js"></script>
|
||||||
import {
|
|
||||||
CalendarOrchestrator,
|
|
||||||
RendererRegistry,
|
|
||||||
StoreRegistry,
|
|
||||||
DateRenderer,
|
|
||||||
ResourceRenderer,
|
|
||||||
TeamRenderer
|
|
||||||
} from './js/calendar-v2.js';
|
|
||||||
|
|
||||||
const rendererRegistry = new RendererRegistry([
|
|
||||||
new DateRenderer(),
|
|
||||||
new ResourceRenderer(),
|
|
||||||
new TeamRenderer()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const mockTeams = [
|
|
||||||
{ id: 'alpha', name: 'Team Alpha' },
|
|
||||||
{ id: 'beta', name: 'Team Beta' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockResources = [
|
|
||||||
{ id: 'alice', name: 'Alice', teamId: 'alpha' },
|
|
||||||
{ id: 'bob', name: 'Bob', teamId: 'alpha' },
|
|
||||||
{ id: 'carol', name: 'Carol', teamId: 'beta' },
|
|
||||||
{ id: 'dave', name: 'Dave', teamId: 'beta' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const storeRegistry = new StoreRegistry();
|
|
||||||
storeRegistry.register('team', { getByIds: ids => mockTeams.filter(t => ids.includes(t.id)) });
|
|
||||||
storeRegistry.register('resource', { getByIds: ids => mockResources.filter(r => ids.includes(r.id)) });
|
|
||||||
|
|
||||||
const orchestrator = new CalendarOrchestrator(rendererRegistry, storeRegistry);
|
|
||||||
const container = document.querySelector('swp-calendar-container');
|
|
||||||
const viewInfo = document.getElementById('view-info');
|
|
||||||
|
|
||||||
function getWeekDates() {
|
|
||||||
const today = new Date();
|
|
||||||
const mon = new Date(today);
|
|
||||||
mon.setDate(today.getDate() - today.getDay() + 1);
|
|
||||||
return Array.from({ length: 5 }, (_, i) => {
|
|
||||||
const d = new Date(mon);
|
|
||||||
d.setDate(mon.getDate() + i);
|
|
||||||
return d.toISOString().split('T')[0];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const dates = getWeekDates();
|
|
||||||
|
|
||||||
const views = {
|
|
||||||
simple: {
|
|
||||||
templateId: 'simple',
|
|
||||||
groupings: [{ type: 'date', values: dates }]
|
|
||||||
},
|
|
||||||
resource: {
|
|
||||||
templateId: 'resource',
|
|
||||||
groupings: [
|
|
||||||
{ type: 'resource', values: ['alice', 'bob', 'carol'] },
|
|
||||||
{ type: 'date', values: dates.slice(0, 3) }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
templateId: 'team',
|
|
||||||
groupings: [
|
|
||||||
{ type: 'team', values: ['alpha', 'beta'] },
|
|
||||||
{ type: 'resource', values: ['alice', 'bob', 'carol', 'dave'], parentKey: 'teamId' },
|
|
||||||
{ type: 'date', values: dates.slice(0, 3) }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function generateTimeAxis() {
|
|
||||||
const el = document.getElementById('time-axis');
|
|
||||||
el.innerHTML = Array.from({ length: 15 }, (_, i) =>
|
|
||||||
`<swp-hour-marker>${(6 + i).toString().padStart(2, '0')}:00</swp-hour-marker>`
|
|
||||||
).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function render(view, label) {
|
|
||||||
viewInfo.textContent = label;
|
|
||||||
await orchestrator.render(view, container);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('btn-simple').onclick = () => render(views.simple, '5 datoer');
|
|
||||||
document.getElementById('btn-resource').onclick = () => render(views.resource, '3 resources × 3 datoer');
|
|
||||||
document.getElementById('btn-team').onclick = () => render(views.team, '2 teams × 2 resources × 3 datoer');
|
|
||||||
|
|
||||||
generateTimeAxis();
|
|
||||||
render(views.simple, '5 datoer');
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue