From 69d2f31a9ee0f37e55017cb47fe53a8da09aa986 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Fri, 3 Jul 2026 01:38:26 +0400 Subject: [PATCH] phase-8: add config-driven public website runtime route and dynamic layout --- src/app/app.routes.ts | 4 ++ .../facades/website/website-runtime.facade.ts | 34 ++++++++++++++ .../dynamic-page-layout.component.ts | 45 +++++++++++++++++++ .../pages/public/platform-home.component.ts | 43 ++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 src/app/facades/website/website-runtime.facade.ts create mode 100644 src/app/layouts/containers/dynamic-page-layout.component.ts create mode 100644 src/app/pages/public/platform-home.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 76c8414..6b8bc26 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -27,6 +27,10 @@ const coreRoutes: Routes = [ { path: 'cart', loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent) + }, + { + path: 'platform', + loadComponent: () => import('./pages/public/platform-home.component').then(m => m.PlatformHomeComponent) } ]; diff --git a/src/app/facades/website/website-runtime.facade.ts b/src/app/facades/website/website-runtime.facade.ts new file mode 100644 index 0000000..fd49453 --- /dev/null +++ b/src/app/facades/website/website-runtime.facade.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Observable, map } from 'rxjs'; +import { ConfigService } from '../../core/config/config.service'; +import { BrandingEngineService } from '../../theme/runtime/branding-engine.service'; +import { ThemeEngineService } from '../../theme/runtime/theme-engine.service'; +import { PageRendererService } from '../../dynamic-renderer/page-renderer/page-renderer.service'; +import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model'; +import { WidgetRegistryBootstrapService } from '../../widgets/registry/widget-registry.bootstrap.service'; + +@Injectable({ providedIn: 'root' }) +export class WebsiteRuntimeFacade { + constructor( + private readonly configService: ConfigService, + private readonly pageRenderer: PageRendererService, + private readonly registryBootstrap: WidgetRegistryBootstrapService, + private readonly themeEngine: ThemeEngineService, + private readonly brandingEngine: BrandingEngineService + ) {} + + initializeRuntime(): void { + this.registryBootstrap.registerDefaults(); + this.themeEngine.initialize(); + this.brandingEngine.initialize(); + } + + getPageRenderModel(pageKey: string): Observable { + return this.configService.loadBootstrap().pipe( + map(bootstrap => { + const page = bootstrap.pages.find(p => p.key === pageKey && p.visible !== false); + return page ? this.pageRenderer.toRenderModel(page) : null; + }) + ); + } +} diff --git a/src/app/layouts/containers/dynamic-page-layout.component.ts b/src/app/layouts/containers/dynamic-page-layout.component.ts new file mode 100644 index 0000000..506530e --- /dev/null +++ b/src/app/layouts/containers/dynamic-page-layout.component.ts @@ -0,0 +1,45 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { NgComponentOutlet } from '@angular/common'; +import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model'; +import { WidgetRenderNode } from '../../dynamic-renderer/widget-host/widget-host.model'; +import { WidgetHostService } from '../../dynamic-renderer/widget-host/widget-host.service'; + +@Component({ + selector: 'app-dynamic-page-layout', + standalone: true, + imports: [CommonModule, NgComponentOutlet], + template: ` + @if (model) { +
+ @for (section of model.sections; track section.id) { +
+ @for (widget of section.widgets; track widget.id) { +
+ @if (resolveWidget(widget); as resolved) { + + } +
+ } +
+ } +
+ } + `, + styles: [ + ` + .dynamic-page-layout { display: grid; gap: 1rem; } + .dynamic-section { display: grid; gap: 1rem; } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DynamicPageLayoutComponent { + @Input() model: PageRenderModel | null = null; + + constructor(private readonly widgetHost: WidgetHostService) {} + + resolveWidget(widget: WidgetRenderNode) { + return this.widgetHost.resolveWidget(widget.source); + } +} diff --git a/src/app/pages/public/platform-home.component.ts b/src/app/pages/public/platform-home.component.ts new file mode 100644 index 0000000..fa63385 --- /dev/null +++ b/src/app/pages/public/platform-home.component.ts @@ -0,0 +1,43 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { DynamicPageLayoutComponent } from '../../layouts/containers/dynamic-page-layout.component'; +import { PageRenderModel } from '../../dynamic-renderer/page-renderer/page-renderer.model'; +import { WebsiteRuntimeFacade } from '../../facades/website/website-runtime.facade'; + +@Component({ + selector: 'app-platform-home', + standalone: true, + imports: [DynamicPageLayoutComponent], + template: ` + @if (loading()) { +
Loading platform page...
+ } @else if (model()) { + + } @else { +
No page configuration found.
+ } + `, + styles: [ + ` + .platform-loading, .platform-empty { padding: 2rem; color: var(--text-secondary, #667a77); } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PlatformHomeComponent { + readonly model = signal(null); + readonly loading = signal(true); + + constructor(private readonly websiteRuntime: WebsiteRuntimeFacade) { + this.websiteRuntime.initializeRuntime(); + this.websiteRuntime.getPageRenderModel('home').subscribe({ + next: (model) => { + this.model.set(model); + this.loading.set(false); + }, + error: () => { + this.model.set(null); + this.loading.set(false); + } + }); + } +}