Sprint 7: add section engine
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user