feat static pages system with dynamic footer and safe html rendering
This commit is contained in:
@@ -41,6 +41,14 @@ const coreRoutes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'cart',
|
path: 'cart',
|
||||||
loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent)
|
loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'page/:key',
|
||||||
|
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':staticPath',
|
||||||
|
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,20 @@
|
|||||||
<p class="novo-footer-desc">{{ 'footer.description' | translate }}</p>
|
<p class="novo-footer-desc">{{ 'footer.description' | translate }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO(CMS): Render backend-configured footer content links here. -->
|
@for (group of footerGroups(); track group.id) {
|
||||||
|
<div class="novo-footer-col">
|
||||||
|
@if (group.title) {
|
||||||
|
<h4>{{ group.title }}</h4>
|
||||||
|
}
|
||||||
|
<ul class="novo-footer-links">
|
||||||
|
@for (item of group.items; track item.id) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="item.route | langRoute">{{ item.label | translate }}</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="novo-footer-bottom">
|
<div class="novo-footer-bottom">
|
||||||
@@ -36,7 +49,20 @@
|
|||||||
<p class="lavero-footer-desc">{{ 'footer.description' | translate }}</p>
|
<p class="lavero-footer-desc">{{ 'footer.description' | translate }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO(CMS): Render backend-configured footer content links here. -->
|
@for (group of footerGroups(); track group.id) {
|
||||||
|
<div class="lavero-footer-col">
|
||||||
|
@if (group.title) {
|
||||||
|
<h4>{{ group.title }}</h4>
|
||||||
|
}
|
||||||
|
<ul class="lavero-footer-links">
|
||||||
|
@for (item of group.items; track item.id) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="item.route | langRoute">{{ item.label | translate }}</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lavero-footer-bottom">
|
<div class="lavero-footer-bottom">
|
||||||
@@ -62,7 +88,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dexar-footer-columns">
|
<div class="dexar-footer-columns">
|
||||||
<!-- TODO(CMS): Render backend-configured footer content links here. -->
|
@for (group of footerGroups(); track group.id) {
|
||||||
|
<div class="dexar-footer-col">
|
||||||
|
@if (group.title) {
|
||||||
|
<h4>{{ group.title }}</h4>
|
||||||
|
}
|
||||||
|
<ul>
|
||||||
|
@for (item of group.items; track item.id) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="item.route | langRoute">{{ item.label | translate }}</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="dexar-footer-col">
|
<div class="dexar-footer-col">
|
||||||
<h4>{{ 'footer.payment' | translate }}</h4>
|
<h4>{{ 'footer.payment' | translate }}</h4>
|
||||||
|
|||||||
@@ -1,17 +1,52 @@
|
|||||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
import { LogoComponent } from '../logo/logo.component';
|
import { LogoComponent } from '../logo/logo.component';
|
||||||
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
|
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
|
||||||
|
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||||
|
import { ConfigService } from '../../core/config/config.service';
|
||||||
|
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
|
||||||
|
import { BootstrapConfig, FooterNavigationGroupConfig, NavigationConfig, NavigationItemConfig, FooterNavigationItemConfig } from '../../shared/models/config';
|
||||||
|
import { LanguageService } from '../../services/language.service';
|
||||||
|
|
||||||
|
interface FooterResolvedItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
route: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FooterResolvedGroup {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
items: FooterResolvedItem[];
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-footer',
|
selector: 'app-footer',
|
||||||
imports: [TranslatePipe, LogoComponent],
|
imports: [TranslatePipe, LogoComponent, RouterLink, LangRoutePipe],
|
||||||
templateUrl: './footer.component.html',
|
templateUrl: './footer.component.html',
|
||||||
styleUrls: ['./footer.component.scss'],
|
styleUrls: ['./footer.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent {
|
||||||
constructor(private readonly uiRuntime: UiRuntimeFacade) {}
|
readonly footerGroups = signal<FooterResolvedGroup[]>([]);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly uiRuntime: UiRuntimeFacade,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly staticPageResolver: StaticPageResolverService,
|
||||||
|
private readonly languageService: LanguageService
|
||||||
|
) {
|
||||||
|
this.configService.loadBootstrap().pipe(take(1)).subscribe({
|
||||||
|
next: bootstrap => {
|
||||||
|
this.footerGroups.set(this.resolveFooterGroups(bootstrap));
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.footerGroups.set([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
currentYear = new Date().getFullYear();
|
currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
@@ -30,4 +65,120 @@ export class FooterComponent {
|
|||||||
get isMarketplaceLavero(): boolean {
|
get isMarketplaceLavero(): boolean {
|
||||||
return this.uiRuntime.isMarketplaceVariant('lavero');
|
return this.uiRuntime.isMarketplaceVariant('lavero');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveFooterGroups(bootstrap: BootstrapConfig): FooterResolvedGroup[] {
|
||||||
|
const footer = bootstrap.navigation?.footer ?? [];
|
||||||
|
const lang = this.languageService.currentLanguage();
|
||||||
|
|
||||||
|
if (this.isGroupedFooter(footer)) {
|
||||||
|
return footer
|
||||||
|
.map((group, index) => this.resolveGroupFromConfig(group, bootstrap, lang, index))
|
||||||
|
.filter((group): group is FooterResolvedGroup => group != null && group.items.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = (footer as NavigationItemConfig[])
|
||||||
|
.map(item => this.resolveLegacyItem(item, bootstrap, lang))
|
||||||
|
.filter((item): item is FooterResolvedItem => item != null);
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
id: 'footer-group-default',
|
||||||
|
title: '',
|
||||||
|
items
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveGroupFromConfig(
|
||||||
|
group: FooterNavigationGroupConfig,
|
||||||
|
bootstrap: BootstrapConfig,
|
||||||
|
lang: string,
|
||||||
|
index: number
|
||||||
|
): FooterResolvedGroup | null {
|
||||||
|
const items = (group.items ?? [])
|
||||||
|
.map(item => this.resolveGroupItem(item, bootstrap, lang))
|
||||||
|
.filter((item): item is FooterResolvedItem => item != null);
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `footer-group-${index}`,
|
||||||
|
title: this.staticPageResolver.resolveFooterTitle(group.groupTitle, lang),
|
||||||
|
items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveGroupItem(
|
||||||
|
item: FooterNavigationItemConfig,
|
||||||
|
bootstrap: BootstrapConfig,
|
||||||
|
lang: string
|
||||||
|
): FooterResolvedItem | null {
|
||||||
|
if (item.visible === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'staticPage' && item.key) {
|
||||||
|
const staticPage = this.staticPageResolver.resolveByKeyFromBootstrap(bootstrap, item.key, lang);
|
||||||
|
if (!staticPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `static-${item.key}`,
|
||||||
|
label: staticPage.title,
|
||||||
|
route: staticPage.route
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = typeof item.label === 'string'
|
||||||
|
? item.label
|
||||||
|
: this.staticPageResolver.resolveFooterTitle(item.label, lang);
|
||||||
|
|
||||||
|
if (!item.route || !label) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${item.type}-${label}`,
|
||||||
|
label,
|
||||||
|
route: item.route
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveLegacyItem(item: NavigationItemConfig, bootstrap: BootstrapConfig, lang: string): FooterResolvedItem | null {
|
||||||
|
if (item.visible === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'staticPage' && item.key) {
|
||||||
|
const staticPage = this.staticPageResolver.resolveByKeyFromBootstrap(bootstrap, item.key, lang);
|
||||||
|
if (!staticPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
label: staticPage.title,
|
||||||
|
route: staticPage.route
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.route) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
label: item.labelKey ?? item.id,
|
||||||
|
route: item.route
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private isGroupedFooter(footer: NavigationConfig['footer']): footer is FooterNavigationGroupConfig[] {
|
||||||
|
return Array.isArray(footer) && footer.length > 0 && 'items' in footer[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
src/app/core/config/static-page-resolver.service.ts
Normal file
158
src/app/core/config/static-page-resolver.service.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { BootstrapConfig, LegacyStaticPageConfig, LocalizedHtmlContent, LocalizedTextContent, ResolvedStaticPage, StaticPageConfig } from '../../shared/models/config';
|
||||||
|
import { LanguageService } from '../../services/language.service';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class StaticPageResolverService {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly languageService: LanguageService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
resolveByKey(key: string, lang?: string): Observable<ResolvedStaticPage | null> {
|
||||||
|
return this.configService.loadBootstrap().pipe(
|
||||||
|
map(bootstrap => this.resolveByKeyFromBootstrap(bootstrap, key, lang ?? this.languageService.currentLanguage()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveByRoute(route: string, lang?: string): Observable<ResolvedStaticPage | null> {
|
||||||
|
return this.configService.loadBootstrap().pipe(
|
||||||
|
map(bootstrap => this.resolveByRouteFromBootstrap(bootstrap, route, lang ?? this.languageService.currentLanguage()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveByKeyFromBootstrap(bootstrap: BootstrapConfig, key: string, lang: string): ResolvedStaticPage | null {
|
||||||
|
const page = this.getPageByKey(bootstrap, key);
|
||||||
|
if (!page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = this.normalizeRoute(page.route || `/page/${key}`);
|
||||||
|
const title = this.resolveLocalizedText(page.title, lang, key);
|
||||||
|
const html = this.resolveLocalizedHtml(page.content, lang);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
route,
|
||||||
|
title,
|
||||||
|
html
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveByRouteFromBootstrap(bootstrap: BootstrapConfig, route: string, lang: string): ResolvedStaticPage | null {
|
||||||
|
const normalized = this.normalizeRoute(route);
|
||||||
|
const registry = this.getRegistry(bootstrap);
|
||||||
|
|
||||||
|
for (const [key, page] of Object.entries(registry)) {
|
||||||
|
const routePath = this.normalizeRoute(page.route || `/page/${key}`);
|
||||||
|
if (routePath === normalized) {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
route: routePath,
|
||||||
|
title: this.resolveLocalizedText(page.title, lang, key),
|
||||||
|
html: this.resolveLocalizedHtml(page.content, lang)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveFooterTitle(groupTitle: string | LocalizedTextContent | undefined, lang: string): string {
|
||||||
|
if (!groupTitle) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof groupTitle === 'string') {
|
||||||
|
return groupTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resolveLocalizedText(groupTitle, lang, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRegistry(bootstrap: BootstrapConfig): Record<string, StaticPageConfig> {
|
||||||
|
const raw = bootstrap.staticPages;
|
||||||
|
if (!raw) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.reduce<Record<string, StaticPageConfig>>((acc, item) => {
|
||||||
|
const legacy = item as LegacyStaticPageConfig;
|
||||||
|
const key = legacy.key;
|
||||||
|
if (!key) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = typeof legacy.route === 'string' ? legacy.route : legacy.route?.path ?? `/page/${key}`;
|
||||||
|
|
||||||
|
let titleMap: LocalizedTextContent;
|
||||||
|
if (typeof legacy.title === 'string') {
|
||||||
|
titleMap = { en: legacy.title };
|
||||||
|
} else {
|
||||||
|
titleMap = legacy.title ?? { en: key };
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentMap: LocalizedHtmlContent;
|
||||||
|
if (typeof legacy.content === 'string') {
|
||||||
|
contentMap = { en: legacy.content };
|
||||||
|
} else if (legacy.content && typeof legacy.content === 'object' && 'value' in legacy.content) {
|
||||||
|
contentMap = { en: legacy.content.value ?? '' };
|
||||||
|
} else {
|
||||||
|
contentMap = (legacy.content as LocalizedHtmlContent) ?? { en: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[key] = {
|
||||||
|
route,
|
||||||
|
title: titleMap,
|
||||||
|
content: contentMap,
|
||||||
|
visible: legacy.visible
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPageByKey(bootstrap: BootstrapConfig, key: string): StaticPageConfig | null {
|
||||||
|
const registry = this.getRegistry(bootstrap);
|
||||||
|
const page = registry[key];
|
||||||
|
if (!page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.visible === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveLocalizedText(text: LocalizedTextContent | undefined, lang: string, fallback: string): string {
|
||||||
|
if (!text) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text[lang] ?? text['en'] ?? Object.values(text)[0] ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveLocalizedHtml(content: LocalizedHtmlContent | undefined, lang: string): string {
|
||||||
|
if (!content) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return content[lang] ?? content['en'] ?? Object.values(content)[0] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeRoute(route: string): string {
|
||||||
|
if (!route) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return route.startsWith('/') ? route : `/${route}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/app/pages/static-page/static-page.component.html
Normal file
18
src/app/pages/static-page/static-page.component.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<main class="static-page page-container">
|
||||||
|
@if (loading()) {
|
||||||
|
<section class="static-page__state">{{ 'app.connecting' | translate }}</section>
|
||||||
|
} @else if (notFound()) {
|
||||||
|
<section class="static-page__state">
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>Страница не найдена</p>
|
||||||
|
<a [routerLink]="'/' | langRoute">На главную</a>
|
||||||
|
</section>
|
||||||
|
} @else {
|
||||||
|
<article class="static-page__content">
|
||||||
|
@if (title()) {
|
||||||
|
<h1>{{ title() }}</h1>
|
||||||
|
}
|
||||||
|
<div class="static-page__html" [innerHTML]="safeHtml()"></div>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</main>
|
||||||
39
src/app/pages/static-page/static-page.component.scss
Normal file
39
src/app/pages/static-page/static-page.component.scss
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.static-page {
|
||||||
|
min-height: 45vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.static-page__state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
color: var(--text-secondary, #667a77);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-primary, #1e3c38);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color, #497671);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.static-page__content {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--text-primary, #1e3c38);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.static-page__html {
|
||||||
|
color: var(--text-primary, #1e3c38);
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/app/pages/static-page/static-page.component.ts
Normal file
82
src/app/pages/static-page/static-page.component.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, SecurityContext, inject, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
|
||||||
|
import { LanguageService } from '../../services/language.service';
|
||||||
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
|
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-static-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, RouterLink, TranslatePipe, LangRoutePipe],
|
||||||
|
templateUrl: './static-page.component.html',
|
||||||
|
styleUrls: ['./static-page.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class StaticPageComponent {
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly sanitizer = inject(DomSanitizer);
|
||||||
|
private readonly staticPageResolver = inject(StaticPageResolverService);
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
|
||||||
|
readonly loading = signal(true);
|
||||||
|
readonly notFound = signal(false);
|
||||||
|
readonly title = signal('');
|
||||||
|
readonly safeHtml = signal<SafeHtml>(this.sanitizer.bypassSecurityTrustHtml(''));
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.route.paramMap.subscribe(params => {
|
||||||
|
const keyFromParam = params.get('key');
|
||||||
|
const staticPath = params.get('staticPath');
|
||||||
|
|
||||||
|
if (keyFromParam) {
|
||||||
|
this.loadByKey(keyFromParam);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staticPath) {
|
||||||
|
this.loadByPath(`/${staticPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notFound.set(true);
|
||||||
|
this.loading.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadByKey(key: string): void {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.notFound.set(false);
|
||||||
|
|
||||||
|
this.staticPageResolver.resolveByKey(key, this.languageService.currentLanguage()).subscribe(page => {
|
||||||
|
this.applyPage(page?.title ?? '', page?.html ?? '', !page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadByPath(path: string): void {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.notFound.set(false);
|
||||||
|
|
||||||
|
this.staticPageResolver.resolveByRoute(path, this.languageService.currentLanguage()).subscribe(page => {
|
||||||
|
this.applyPage(page?.title ?? '', page?.html ?? '', !page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyPage(title: string, html: string, notFound: boolean): void {
|
||||||
|
if (notFound) {
|
||||||
|
this.title.set('');
|
||||||
|
this.safeHtml.set(this.sanitizer.bypassSecurityTrustHtml(''));
|
||||||
|
this.notFound.set(true);
|
||||||
|
this.loading.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, html) ?? '';
|
||||||
|
this.title.set(title);
|
||||||
|
this.safeHtml.set(this.sanitizer.bypassSecurityTrustHtml(sanitized));
|
||||||
|
this.notFound.set(false);
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { NavigationConfig } from './navigation.model';
|
|||||||
import { PageConfig } from './page.model';
|
import { PageConfig } from './page.model';
|
||||||
import { PermissionsConfig } from './permissions.model';
|
import { PermissionsConfig } from './permissions.model';
|
||||||
import { SeoConfig } from './seo.model';
|
import { SeoConfig } from './seo.model';
|
||||||
|
import { StaticPagesConfig } from './static-page.model';
|
||||||
import { TenantConfig } from './tenant.model';
|
import { TenantConfig } from './tenant.model';
|
||||||
import { ThemeConfig } from './theme.model';
|
import { ThemeConfig } from './theme.model';
|
||||||
|
|
||||||
@@ -24,4 +25,5 @@ export interface BootstrapConfig {
|
|||||||
permissions: PermissionsConfig;
|
permissions: PermissionsConfig;
|
||||||
navigation: NavigationConfig;
|
navigation: NavigationConfig;
|
||||||
pages: PageConfig[];
|
pages: PageConfig[];
|
||||||
|
staticPages?: StaticPagesConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export * from './page.model';
|
|||||||
export * from './permissions.model';
|
export * from './permissions.model';
|
||||||
export * from './section.model';
|
export * from './section.model';
|
||||||
export * from './seo.model';
|
export * from './seo.model';
|
||||||
|
export * from './static-page.model';
|
||||||
export * from './tenant.model';
|
export * from './tenant.model';
|
||||||
export * from './theme.model';
|
export * from './theme.model';
|
||||||
export * from './widget.model';
|
export * from './widget.model';
|
||||||
|
|||||||
@@ -1,15 +1,37 @@
|
|||||||
|
export interface NavigationLocalizedText {
|
||||||
|
[locale: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NavigationItemConfig {
|
export interface NavigationItemConfig {
|
||||||
id: string;
|
id: string;
|
||||||
labelKey: string;
|
labelKey?: string;
|
||||||
route: string;
|
label?: string | NavigationLocalizedText;
|
||||||
|
route?: string;
|
||||||
|
type?: string;
|
||||||
|
key?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
order: number;
|
order?: number;
|
||||||
|
visible?: boolean;
|
||||||
visibleWhenFlags?: string[];
|
visibleWhenFlags?: string[];
|
||||||
children?: NavigationItemConfig[];
|
children?: NavigationItemConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FooterNavigationItemConfig {
|
||||||
|
type: string;
|
||||||
|
key?: string;
|
||||||
|
label?: string | NavigationLocalizedText;
|
||||||
|
labelKey?: string;
|
||||||
|
route?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FooterNavigationGroupConfig {
|
||||||
|
groupTitle?: string | NavigationLocalizedText;
|
||||||
|
items: FooterNavigationItemConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface NavigationConfig {
|
export interface NavigationConfig {
|
||||||
header: NavigationItemConfig[];
|
header: NavigationItemConfig[];
|
||||||
footer: NavigationItemConfig[];
|
footer: NavigationItemConfig[] | FooterNavigationGroupConfig[];
|
||||||
sidebar?: NavigationItemConfig[];
|
sidebar?: NavigationItemConfig[];
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/app/shared/models/config/static-page.model.ts
Normal file
36
src/app/shared/models/config/static-page.model.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export interface LocalizedHtmlContent {
|
||||||
|
[locale: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalizedTextContent {
|
||||||
|
[locale: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StaticPageConfig {
|
||||||
|
route: string;
|
||||||
|
title: LocalizedTextContent;
|
||||||
|
content: LocalizedHtmlContent;
|
||||||
|
seo?: {
|
||||||
|
title?: LocalizedTextContent;
|
||||||
|
description?: LocalizedTextContent;
|
||||||
|
robots?: string;
|
||||||
|
};
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyStaticPageConfig {
|
||||||
|
key: string;
|
||||||
|
route: string | { path: string; exact?: boolean };
|
||||||
|
title?: string | LocalizedTextContent;
|
||||||
|
content?: string | LocalizedHtmlContent | { value?: string; contentType?: string; source?: string };
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StaticPagesConfig = Record<string, StaticPageConfig> | LegacyStaticPageConfig[];
|
||||||
|
|
||||||
|
export interface ResolvedStaticPage {
|
||||||
|
key: string;
|
||||||
|
route: string;
|
||||||
|
title: string;
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user