2026-08-13 09:14:56 +04:00
|
|
|
import { Injectable, inject } from '@angular/core';
|
2026-07-05 01:55:41 +04:00
|
|
|
import { PageConfig } from '../../shared/models/config';
|
|
|
|
|
import { PageRenderModel } from '../page-renderer/page-renderer.model';
|
|
|
|
|
import { SectionRendererService } from '../section-renderer/section-renderer.service';
|
2026-07-05 04:17:31 +04:00
|
|
|
import { PlatformLayoutConfig, PlatformLayoutType } from '../../shared/models/config';
|
|
|
|
|
import { SectionConfig } from '../../shared/models/config';
|
2026-08-13 09:14:56 +04:00
|
|
|
import { ConfigService } from '../../core/config/config.service';
|
2026-07-05 01:55:41 +04:00
|
|
|
|
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
|
|
|
export class SectionEngineService {
|
2026-08-13 09:14:56 +04:00
|
|
|
private readonly configService = inject(ConfigService);
|
|
|
|
|
|
2026-07-05 01:55:41 +04:00
|
|
|
constructor(private readonly sectionRenderer: SectionRendererService) {}
|
|
|
|
|
|
|
|
|
|
toPageRenderModel(page: PageConfig): PageRenderModel {
|
2026-07-05 04:17:31 +04:00
|
|
|
const layoutType = this.resolveLayoutType(page.layout);
|
|
|
|
|
|
|
|
|
|
const sections = this.normalizeSectionsByLayout(page.sections ?? [], layoutType)
|
2026-07-05 01:55:41 +04:00
|
|
|
.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,
|
2026-07-05 04:17:31 +04:00
|
|
|
layout: layoutType,
|
2026-07-05 01:55:41 +04:00
|
|
|
sections,
|
|
|
|
|
source: page
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-07-05 04:17:31 +04:00
|
|
|
|
2026-08-13 09:14:56 +04:00
|
|
|
/**
|
|
|
|
|
* Falls back to the site-wide builder setting (bootstrap.layout.type,
|
|
|
|
|
* "Site Layout" in the theme editor) when a page has no layout of its
|
|
|
|
|
* own - previously that global setting was saved but never read by
|
|
|
|
|
* rendering at all, so it had no visible effect.
|
|
|
|
|
*/
|
2026-07-05 04:17:31 +04:00
|
|
|
private resolveLayoutType(layout: PageConfig['layout']): string {
|
|
|
|
|
if (typeof layout === 'string') {
|
|
|
|
|
return layout;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 09:14:56 +04:00
|
|
|
const pageLayoutType = (layout as PlatformLayoutConfig)?.type;
|
|
|
|
|
if (pageLayoutType) {
|
|
|
|
|
return pageLayoutType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.configService.getBootstrapSnapshot()?.layout?.type ?? 'default';
|
2026-07-05 04:17:31 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private normalizeSectionsByLayout(sections: PageConfig['sections'], layoutType: string): PageConfig['sections'] {
|
|
|
|
|
const normalized = [...sections];
|
|
|
|
|
const lowerLayout = (layoutType as PlatformLayoutType).toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (lowerLayout === 'minimal') {
|
|
|
|
|
return normalized.filter(section => {
|
|
|
|
|
const type = section.type.toLowerCase();
|
|
|
|
|
return type === 'product-collection' || type === 'products' || type === 'catalog';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lowerLayout === 'sidebar-left') {
|
|
|
|
|
return this.composeSidebarLayout(normalized);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lowerLayout === 'carousel-home') {
|
|
|
|
|
return normalized.sort((left, right) => this.getCarouselPriority(left.type) - this.getCarouselPriority(right.type));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lowerLayout === 'default') {
|
|
|
|
|
return normalized.sort((left, right) => this.getDefaultPriority(left.type) - this.getDefaultPriority(right.type));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getDefaultPriority(type: string): number {
|
|
|
|
|
const normalizedType = type.toLowerCase();
|
|
|
|
|
if (normalizedType === 'categories') return 10;
|
|
|
|
|
if (normalizedType === 'product-collection' || normalizedType === 'products') return 20;
|
|
|
|
|
if (normalizedType === 'hero') return 30;
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSidebarPriority(type: string): number {
|
|
|
|
|
const normalizedType = type.toLowerCase();
|
|
|
|
|
if (normalizedType === 'categories') return 10;
|
|
|
|
|
if (normalizedType === 'product-collection' || normalizedType === 'products') return 20;
|
|
|
|
|
if (normalizedType === 'hero') return 30;
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getCarouselPriority(type: string): number {
|
|
|
|
|
const normalizedType = type.toLowerCase();
|
|
|
|
|
if (normalizedType === 'hero' || normalizedType === 'banner') return 10;
|
|
|
|
|
if (normalizedType === 'product-collection' || normalizedType === 'product-carousel') return 20;
|
|
|
|
|
if (normalizedType === 'categories') return 30;
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private composeSidebarLayout(sections: PageConfig['sections']): PageConfig['sections'] {
|
|
|
|
|
const categoriesSection = sections.find(section => section.type.toLowerCase() === 'categories');
|
|
|
|
|
const productsSection = sections.find(section => {
|
|
|
|
|
const type = section.type.toLowerCase();
|
|
|
|
|
return type === 'product-collection' || type === 'products' || type === 'catalog';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!categoriesSection || !productsSection) {
|
|
|
|
|
return [...sections].sort((left, right) => this.getSidebarPriority(left.type) - this.getSidebarPriority(right.type));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const combined: SectionConfig = {
|
|
|
|
|
...categoriesSection,
|
|
|
|
|
id: `${categoriesSection.id}-sidebar-layout`,
|
|
|
|
|
type: 'sidebar-left',
|
|
|
|
|
order: Math.min(categoriesSection.order, productsSection.order),
|
|
|
|
|
layout: {
|
|
|
|
|
strategy: 'grid',
|
|
|
|
|
columns: 2,
|
|
|
|
|
gap: '1.5rem',
|
|
|
|
|
align: 'start'
|
|
|
|
|
},
|
|
|
|
|
widgets: [
|
|
|
|
|
...categoriesSection.widgets.map((widget, index) => ({
|
|
|
|
|
...widget,
|
|
|
|
|
order: index,
|
|
|
|
|
padding: widget.padding ?? '0',
|
|
|
|
|
visibility: widget.visibility ?? { desktop: true, tablet: true, mobile: true }
|
|
|
|
|
})),
|
|
|
|
|
...productsSection.widgets.map((widget, index) => ({
|
|
|
|
|
...widget,
|
|
|
|
|
order: 100 + index,
|
|
|
|
|
padding: widget.padding ?? '0',
|
|
|
|
|
visibility: widget.visibility ?? { desktop: true, tablet: true, mobile: true }
|
|
|
|
|
}))
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
combined,
|
|
|
|
|
...sections.filter(section => section.id !== categoriesSection.id && section.id !== productsSection.id)
|
|
|
|
|
].sort((left, right) => left.order - right.order);
|
|
|
|
|
}
|
2026-07-05 01:55:41 +04:00
|
|
|
}
|