Sprint 7: add section engine

This commit is contained in:
sdarbinyan
2026-07-05 01:55:41 +04:00
parent d4a5daeb4c
commit 91d9444875
10 changed files with 203 additions and 62 deletions

View File

@@ -1,25 +1,13 @@
import { Injectable } from '@angular/core';
import { PageConfig } from '../../shared/models/config';
import { PageRenderModel } from './page-renderer.model';
import { SectionRendererService } from '../section-renderer/section-renderer.service';
import { SectionEngineService } from '../section-engine/section-engine.service';
@Injectable({ providedIn: 'root' })
export class PageRendererService {
constructor(private readonly sectionRenderer: SectionRendererService) {}
constructor(private readonly sectionEngine: SectionEngineService) {}
toRenderModel(page: PageConfig): PageRenderModel {
const sections = (page.sections ?? [])
.filter(section => section.visible !== false)
.sort((a, b) => a.order - b.order)
.map(section => this.sectionRenderer.toRenderNode(section));
return {
id: page.id,
key: page.key,
title: page.title,
layout: page.layout,
sections,
source: page
};
return this.sectionEngine.toPageRenderModel(page);
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { PageConfig } from '../../shared/models/config';
import { PageRenderModel } from '../page-renderer/page-renderer.model';
import { SectionRendererService } from '../section-renderer/section-renderer.service';
@Injectable({ providedIn: 'root' })
export class SectionEngineService {
constructor(private readonly sectionRenderer: SectionRendererService) {}
toPageRenderModel(page: PageConfig): PageRenderModel {
const sections = (page.sections ?? [])
.filter((section) => section.visible !== false)
.sort((a, b) => a.order - b.order)
.map((section) => this.sectionRenderer.toRenderNode(section));
return {
id: page.id,
key: page.key,
title: page.title,
layout: page.layout,
sections,
source: page
};
}
}

View File

@@ -1,10 +1,13 @@
import { SectionConfig } from '../../shared/models/config';
import { SectionLayoutConfig, SectionVisibilityConfig } from '../../shared/models/config';
import { WidgetRenderNode } from '../widget-host/widget-host.model';
export interface SectionRenderNode {
id: string;
type: string;
order: number;
layout?: SectionLayoutConfig;
visibility?: SectionVisibilityConfig;
featureFlag?: string;
visible?: boolean;
widgets: WidgetRenderNode[];

View File

@@ -22,6 +22,8 @@ export class SectionRendererService {
id: section.id,
type: section.type,
order: section.order,
layout: section.layout,
visibility: section.visibility,
featureFlag: section.featureFlag,
visible: section.visible,
widgets,

View File

@@ -16,7 +16,17 @@ import { ConfigService } from '../../core/config/config.service';
@if (model) {
<main class="dynamic-page-layout" [attr.data-layout]="model.layout">
@for (section of model.sections; track section.id) {
<section class="dynamic-section" [attr.data-section-type]="section.type">
<section
class="dynamic-section"
[attr.data-section-type]="section.type"
[attr.data-section-layout]="section.layout?.strategy ?? section.type"
[style.--section-columns]="section.layout?.columns ?? null"
[style.--section-gap]="section.layout?.gap ?? null"
[style.--section-align]="section.layout?.align ?? null"
[class.dynamic-section--desktop-hidden]="section.visibility?.desktop === false"
[class.dynamic-section--tablet-hidden]="section.visibility?.tablet === false"
[class.dynamic-section--mobile-hidden]="section.visibility?.mobile === false"
>
@for (widget of section.widgets; track widget.id) {
<div class="dynamic-widget" [attr.data-widget-type]="widget.type">
@if (resolveWidget(widget, section.id); as resolved) {
@@ -31,8 +41,30 @@ import { ConfigService } from '../../core/config/config.service';
`,
styles: [
`
.dynamic-page-layout { display: grid; gap: 1rem; }
.dynamic-section { display: grid; gap: 1rem; }
.dynamic-page-layout { display: grid; gap: 1.5rem; }
.dynamic-section {
display: grid;
gap: var(--section-gap, 1rem);
align-items: var(--section-align, stretch);
}
.dynamic-section[data-section-layout='grid'] {
grid-template-columns: repeat(var(--section-columns, 1), minmax(0, 1fr));
}
.dynamic-section[data-section-layout='hero'] {
grid-template-columns: minmax(0, 1fr);
}
.dynamic-section--desktop-hidden { display: none; }
@media (max-width: 1023px) {
.dynamic-section--tablet-hidden { display: none; }
}
@media (max-width: 767px) {
.dynamic-section--mobile-hidden { display: none; }
}
`
],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,22 +1,9 @@
<!-- novo VERSION - Modern Grid Layout -->
@if (isMarketplaceNovo) {
<div class="novo-home">
<section class="novo-hero novo-hero-compact">
<div class="novo-hero-content">
<h1 class="novo-hero-title">{{ 'home.welcomeTo' | translate:{ brand: brandName } }}</h1>
<p class="novo-hero-subtitle">{{ 'home.subtitle' | translate }}</p>
<a [routerLink]="'/search' | langRoute" class="novo-hero-btn">
{{ 'home.startSearch' | translate }}
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</a>
</div>
</section>
<!-- Items Carousel -->
<app-items-carousel />
@if (pageModel()) {
<app-dynamic-page-layout [model]="pageModel()" />
}
@if (loading()) {
<section class="novo-categories">
@@ -89,31 +76,9 @@
} @else {
<!-- DEXAR VERSION - Redesigned 2026 -->
<div class="dexar-home">
<!-- Hero Section with Full Width Image -->
<section class="dexar-hero">
<div class="dexar-hero-overlay">
<div class="dexar-hero-content">
<h1 class="dexar-hero-title">{{ 'home.dexarHeroTitle' | translate }}</h1>
<p class="dexar-hero-subtitle">{{ 'home.dexarHeroSubtitle' | translate }}</p>
<p class="dexar-hero-tagline">{{ 'home.dexarHeroTagline' | translate }}</p>
<div class="dexar-hero-actions">
<a (click)="scrollToCatalog()" class="dexar-btn-primary">
{{ 'home.goToCatalog' | translate }}
</a>
<button (click)="navigateToSearch()" class="dexar-btn-secondary">
{{ 'home.findProduct' | translate }}
<svg width="11" height="16" viewBox="0 0 11 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L9 8L1 15" stroke="#1E3C38" stroke-width="2"/>
</svg>
</button>
</div>
</div>
</div>
</section>
<!-- Items Carousel -->
<app-items-carousel />
@if (pageModel()) {
<app-dynamic-page-layout [model]="pageModel()" />
}
@if (loading()) {
<section class="dexar-categories">

View File

@@ -1,7 +1,9 @@
import { Component, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { LanguageService } from '../../services';
import { ItemsCarouselComponent } from '../../components/items-carousel/items-carousel.component';
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';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
@@ -10,7 +12,8 @@ import { Category } from '../../core/categories/models/category-domain.model';
@Component({
selector: 'app-home',
imports: [RouterLink, ItemsCarouselComponent, LangRoutePipe, TranslatePipe],
standalone: true,
imports: [RouterLink, DynamicPageLayoutComponent, LangRoutePipe, TranslatePipe],
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -20,12 +23,14 @@ export class HomeComponent implements OnInit {
private router: Router,
private langService: LanguageService,
private readonly uiRuntime: UiRuntimeFacade,
private readonly categoryFacade: CategoryFacade
private readonly categoryFacade: CategoryFacade,
private readonly websiteRuntime: WebsiteRuntimeFacade
) {}
categories = signal<Category[]>([]);
loading = signal(true);
error = signal<string | null>(null);
readonly pageModel = signal<PageRenderModel | null>(null);
readonly skeletonSlots = Array.from({ length: 6 });
// Memoized computed values for performance
@@ -63,9 +68,21 @@ export class HomeComponent implements OnInit {
}
ngOnInit(): void {
this.loadHomepageSections();
this.loadCategories();
}
private loadHomepageSections(): void {
this.websiteRuntime.getPageRenderModelForUrl(this.router.url).subscribe({
next: (model) => {
this.pageModel.set(model);
},
error: () => {
this.pageModel.set(null);
}
});
}
loadCategories(): void {
this.loading.set(true);
this.error.set(null);

View File

@@ -1,6 +1,17 @@
import { WidgetConfig } from './widget.model';
export type SectionViewport = 'desktop' | 'tablet' | 'mobile';
export type SectionLayoutStrategy = 'stack' | 'grid' | 'hero' | 'carousel' | 'split';
export interface SectionVisibilityConfig {
desktop?: boolean;
tablet?: boolean;
mobile?: boolean;
}
export interface SectionLayoutConfig {
strategy?: SectionLayoutStrategy;
columns?: number;
gap?: string;
align?: 'start' | 'center' | 'end' | 'stretch';
@@ -11,6 +22,7 @@ export interface SectionConfig {
type: string;
order: number;
layout?: SectionLayoutConfig;
visibility?: SectionVisibilityConfig;
widgets: WidgetConfig[];
featureFlag?: string;
visible?: boolean;