refactor: finalize bootstrap-driven layout and widget runtime

This commit is contained in:
sdarbinyan
2026-07-05 04:17:31 +04:00
parent 6550250d13
commit c901ec1e49
19 changed files with 697 additions and 181 deletions

View File

@@ -0,0 +1,246 @@
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { ConfigService } from './config.service';
import {
BootstrapConfig,
FooterConfig,
FooterNavigationGroupConfig,
FooterNavigationItemConfig,
NavigationConfig,
NavigationItemConfig
} from '../../shared/models/config';
import { StaticPageResolverService } from './static-page-resolver.service';
import { LanguageService } from '../../services/language.service';
export interface FooterResolvedItem {
id: string;
label: string;
route: string;
}
export interface FooterResolvedGroup {
id: string;
title: string;
items: FooterResolvedItem[];
}
export interface FooterPaymentIcon {
src: string;
alt: string;
width: number;
height: number;
}
export interface FooterResolvedModel {
groups: FooterResolvedGroup[];
paymentIcons: FooterPaymentIcon[];
copyrightText: string;
}
@Injectable({ providedIn: 'root' })
export class FooterResolverService {
constructor(
private readonly configService: ConfigService,
private readonly staticPageResolver: StaticPageResolverService,
private readonly languageService: LanguageService
) {}
resolveFooterModel(): Observable<FooterResolvedModel> {
return this.configService.loadBootstrap().pipe(
map((bootstrap) => ({
groups: this.resolveFooterGroups(bootstrap),
paymentIcons: this.resolvePaymentIcons(bootstrap.footer),
copyrightText: this.resolveCopyrightText(bootstrap)
}))
);
}
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);
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
});
}
return groups;
}
const items = (footer as NavigationItemConfig[])
.map(item => this.resolveLegacyItem(item, bootstrap, lang))
.filter((item): item is FooterResolvedItem => item != null);
if (!items.length) {
return legalItems.length
? [{ id: 'footer-group-legal', title: 'Legal', items: legalItems }]
: [];
}
const groups: FooterResolvedGroup[] = [{
id: 'footer-group-default',
title: '',
items
}];
if (legalItems.length > 0) {
groups.push({
id: 'footer-group-legal',
title: 'Legal',
items: legalItems
});
}
return groups;
}
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];
}
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 footerConfig.paymentIcons
.filter(icon => !!icon?.src)
.map(icon => ({
src: icon.src,
alt: icon.alt || 'Payment icon',
width: icon.width ?? 40,
height: icon.height ?? 28
}));
}
private resolveCopyrightText(bootstrap: BootstrapConfig): string {
const configured = bootstrap.footer?.copyrightText;
const lang = this.languageService.currentLanguage();
if (typeof configured === 'string' && configured.trim()) {
return configured;
}
if (configured && typeof configured === 'object') {
return this.staticPageResolver.resolveFooterTitle(configured, lang);
}
return '';
}
}