feat(cms): add static pages module

This commit is contained in:
sdarbinyan
2026-07-10 13:52:01 +04:00
parent e2c8747fcc
commit 7d6c09a346
25 changed files with 890 additions and 159 deletions

View File

@@ -16,6 +16,7 @@ export interface FooterResolvedItem {
id: string;
label: string;
route: string;
external?: boolean;
}
export interface FooterResolvedGroup {
@@ -62,33 +63,17 @@ export class FooterResolverService {
private resolveFooterGroups(bootstrap: BootstrapConfig): FooterResolvedGroup[] {
const footer = bootstrap.navigation?.footer ?? [];
const lang = this.languageService.currentLanguage();
const legalKeys = bootstrap.footer?.legalPageKeys ?? [];
const legalItems = legalKeys
.map(key => {
const page = this.staticPageResolver.resolveByKeyFromBootstrap(bootstrap, key, lang);
if (!page) {
return null;
}
return {
id: `legal-${key}`,
label: page.title,
route: page.route
} as FooterResolvedItem;
})
.filter((item): item is FooterResolvedItem => item != null);
const contentPages = this.resolveBootstrapStaticPages(bootstrap, lang);
if (this.isGroupedFooter(footer)) {
const groups = footer
.map((group, index) => this.resolveGroupFromConfig(group, bootstrap, lang, index))
.filter((group): group is FooterResolvedGroup => group != null && group.items.length > 0);
if (legalItems.length > 0) {
groups.push({
id: 'footer-group-legal',
title: 'Legal',
items: legalItems
});
for (const group of contentPages) {
if (!groups.some(existing => existing.id === group.id)) {
groups.push(group);
}
}
return groups;
@@ -99,9 +84,7 @@ export class FooterResolverService {
.filter((item): item is FooterResolvedItem => item != null);
if (!items.length) {
return legalItems.length
? [{ id: 'footer-group-legal', title: 'Legal', items: legalItems }]
: [];
return contentPages;
}
const groups: FooterResolvedGroup[] = [{
@@ -110,15 +93,44 @@ export class FooterResolverService {
items
}];
if (legalItems.length > 0) {
groups.push({
id: 'footer-group-legal',
title: 'Legal',
items: legalItems
groups.push(...contentPages);
return groups;
}
private resolveBootstrapStaticPages(bootstrap: BootstrapConfig, lang: string): FooterResolvedGroup[] {
const rawPages = Object.values(bootstrap.staticPages ?? {}) as any[];
const pages = rawPages
.filter(page => page.showInFooter !== false)
.map(page => ({ raw: page, resolved: this.staticPageResolver.resolveByKeyFromBootstrap(bootstrap, page.id ?? '', lang) }))
.filter(entry => entry.resolved !== null);
const groups = new Map<string, FooterResolvedGroup>();
for (const page of pages) {
const raw = page.raw;
const resolved = page.resolved!;
const groupTitle = raw?.footerGroup || 'Legal';
const groupId = `footer-group-${String(groupTitle).toLowerCase().replace(/\s+/g, '-')}`;
if (!groups.has(groupId)) {
groups.set(groupId, { id: groupId, title: groupTitle, items: [] });
}
groups.get(groupId)!.items.push({ id: resolved.id, label: resolved.title, route: resolved.route });
}
const socialLinks = bootstrap.footer?.socialLinks ?? [];
if (socialLinks.length > 0) {
groups.set('footer-group-social', {
id: 'footer-group-social',
title: 'Social',
items: socialLinks.map(link => ({ id: link.id, label: link.label, route: link.url, external: true }))
});
}
return groups;
return [...groups.values()].map(group => ({
...group,
items: group.items.sort((left, right) => left.label.localeCompare(right.label))
}));
}
private resolveGroupFromConfig(
@@ -213,14 +225,8 @@ export class FooterResolverService {
}
private resolvePaymentIcons(footerConfig: FooterConfig | undefined): FooterPaymentIcon[] {
const fallback: FooterPaymentIcon[] = [
{ src: '/assets/images/mir-logo.svg', alt: 'MIR', width: 40, height: 28 },
{ src: '/assets/images/visa-logo.svg', alt: 'Visa', width: 40, height: 28 },
{ src: '/assets/images/mastercard-logo.svg', alt: 'Mastercard', width: 40, height: 28 }
];
if (!footerConfig?.paymentIcons?.length) {
return fallback;
return [];
}
return footerConfig.paymentIcons

View File

@@ -2,14 +2,16 @@ 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 { BootstrapConfig, LocalizedTextContent, ResolvedStaticPage } from '../../shared/models/config';
import { LanguageService } from '../../services/language.service';
import { ContentPageService } from '../../features/content-management/services/content-page.service';
@Injectable({ providedIn: 'root' })
export class StaticPageResolverService {
constructor(
private readonly configService: ConfigService,
private readonly languageService: LanguageService
private readonly languageService: LanguageService,
private readonly contentPageService: ContentPageService
) {}
resolveByKey(key: string, lang?: string): Observable<ResolvedStaticPage | null> {
@@ -25,40 +27,11 @@ export class StaticPageResolverService {
}
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
};
return this.contentPageService.resolvePage(bootstrap, key, lang);
}
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;
return this.contentPageService.resolvePage(bootstrap, this.normalizeRoute(route).replace(/^\//, ''), lang);
}
resolveFooterTitle(groupTitle: string | LocalizedTextContent | undefined, lang: string): string {
@@ -73,65 +46,6 @@ export class StaticPageResolverService {
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;
@@ -140,14 +54,6 @@ export class StaticPageResolverService {
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 '/';