diff --git a/projects/demo/src/app/app.config.ts b/projects/demo/src/app/app.config.ts index 0dff9917..edce13b2 100644 --- a/projects/demo/src/app/app.config.ts +++ b/projects/demo/src/app/app.config.ts @@ -2,24 +2,32 @@ import { routes } from './app.routes'; import { loadingInterceptor } from './interceptors/loading.interceptor'; import { NavigationNode } from './model/navigation'; import { GraphqlService } from './services/graphql.service'; +import { CustomTitleStrategy } from './strategies/custom-title.strategy'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Title } from '@angular/platform-browser'; import { provideAnimations } from '@angular/platform-browser/animations'; -import { provideRouter, Router, Route } from '@angular/router'; +import { ActivatedRouteSnapshot, provideRouter, Router, Route, TitleStrategy } from '@angular/router'; import { ApolloClientOptions, InMemoryCache } from '@apollo/client/core'; import { IDS_ICON_DEFAULT_CONFIG, IdsIconDefaultConfig } from '@i-cell/ids-angular/icon'; -import { provideTranslateService } from '@ngx-translate/core'; +import { provideTranslateService, TranslateService } from '@ngx-translate/core'; import { provideTranslateHttpLoader } from '@ngx-translate/http-loader'; import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { filter, firstValueFrom } from 'rxjs'; -function extractGeneratedSlugs(nodes: NavigationNode[]): string[] { - const slugs: string[] = []; +interface GeneratedSlug { + slug: string; + title?: string; +} + +function extractGeneratedSlugs(nodes: NavigationNode[]): GeneratedSlug[] { + const slugs: GeneratedSlug[] = []; nodes.forEach((node) => { if (node.page?.slug && node.page?.generated === true) { - slugs.push(node.page.slug); + slugs.push({ slug: node.page.slug, title: node.page.title ?? node.title }); } if (node.children) { slugs.push(...extractGeneratedSlugs(node.children)); @@ -28,26 +36,25 @@ function extractGeneratedSlugs(nodes: NavigationNode[]): string[] { return slugs; } -export function initializeDynamicRoutes(graphqlService: GraphqlService, router: Router): () => Promise { - return async() => { - const result = await firstValueFrom(graphqlService - .getNavigation() - .pipe(filter((res): res is { loading?: boolean } => - !(res as { loading?: boolean }).loading))) as { data?: { navs?: NavigationNode[] } }; +export function initializeDynamicRoutes(graphqlService: GraphqlService, router: Router, titleService: Title): () => Promise { + const buildAndApplyRoutes = async(): Promise => { + const result = (await firstValueFrom( + graphqlService.getNavigation().pipe(filter((res): res is { loading?: boolean } => !(res as { loading?: boolean }).loading)), + )) as { data?: { navs?: NavigationNode[] } }; const navs = result.data?.navs || []; - const generatedSlugs: string[] = []; + const generatedSlugs: GeneratedSlug[] = []; - // iterate through the navigation tree and extract slugs from generated pages navs.forEach((nav: NavigationNode) => { if (nav.tree) { generatedSlugs.push(...extractGeneratedSlugs(nav.tree)); } }); - const dynamicRoutes: Route[] = generatedSlugs.flatMap((slug) => [ + const dynamicRoutes: Route[] = generatedSlugs.flatMap(({ slug, title }) => [ { path: slug, + title, loadComponent: () => import('./pages/list-page/list-page.component').then((module) => module.ListPageComponent), data: { collection: 'pages', @@ -56,6 +63,7 @@ export function initializeDynamicRoutes(graphqlService: GraphqlService, router: }, { path: `${slug}/:slug`, + title: (routeSnapshot: ActivatedRouteSnapshot): string => (routeSnapshot.data['contentTitle'] as string | undefined) ?? title ?? '', loadComponent: () => import('./pages/list-page/content-page/content-page.component').then((module) => module.ContentPageComponent), data: { collection: slug, @@ -67,14 +75,52 @@ export function initializeDynamicRoutes(graphqlService: GraphqlService, router: const langRoute = currentConfig.find((route) => route.path === ':lang'); if (langRoute?.children) { const langChildren = langRoute.children; - const fallbackChild = langChildren.pop(); + const staticPaths = new Set([ + 'components', + 'home', + 'support', + '', + ]); + const staticChildren = langChildren.filter((route) => staticPaths.has(route.path ?? '')); + const fallbackChild = staticChildren.find((route) => route.path === ''); + const nonFallbackStaticChildren = staticChildren.filter((route) => route.path !== ''); - langChildren.push(...dynamicRoutes); - if (fallbackChild) { - langChildren.push(fallbackChild); - } + langRoute.children = [ + ...nonFallbackStaticChildren, + ...dynamicRoutes, + ...(fallbackChild ? [fallbackChild] : []), + ]; } router.resetConfig(currentConfig); + + try { + const urlTree = router.parseUrl(router.url); + const primaryOutlet = urlTree.root.children['primary']; + + if (primaryOutlet) { + const segments = primaryOutlet.segments; + + if (segments.length >= 2) { + const currentSlug = segments[1].path; + const matchedSlug = generatedSlugs.find((slug) => slug.slug === currentSlug); + + if (matchedSlug && matchedSlug.title) { + titleService.setTitle(`${matchedSlug.title} | i-Cell Design System`); + } + } + } + } catch(error) { + console.warn('Nem sikerült frissíteni a dinamikus route címét nyelvváltáskor', error); + } + }; + + return async() => { + const translate = inject(TranslateService); + translate.onLangChange.pipe(takeUntilDestroyed()).subscribe(() => { + void buildAndApplyRoutes(); + }); + + await buildAndApplyRoutes(); }; } @@ -105,10 +151,12 @@ export const appConfig: ApplicationConfig = { }), provideAnimations(), { provide: IDS_ICON_DEFAULT_CONFIG, useValue: iconDefaultConfig }, + { provide: TitleStrategy, useClass: CustomTitleStrategy }, provideAppInitializer(() => { const graphqlService = inject(GraphqlService); const router = inject(Router); - return initializeDynamicRoutes(graphqlService, router)(); + const titleService = inject(Title); + return initializeDynamicRoutes(graphqlService, router, titleService)(); }), ], }; diff --git a/projects/demo/src/app/app.routes.ts b/projects/demo/src/app/app.routes.ts index 9765529f..7e22471c 100644 --- a/projects/demo/src/app/app.routes.ts +++ b/projects/demo/src/app/app.routes.ts @@ -85,7 +85,7 @@ const DEMO_IMPORTS: Record Promise>> = { export const CURRENT_DEMO_SERVICE = new InjectionToken('CurrentDemoService'); -function buildComponentRoute(slug: string, demoService: Provider | EnvironmentProviders): Route { +function buildComponentRoute(slug: string, translationKey: string, demoService: Provider | EnvironmentProviders): Route { const componentImport = DEMO_IMPORTS[slug]; if (!componentImport) { throw new Error(`No component import found for slug: ${slug}`); @@ -93,6 +93,7 @@ function buildComponentRoute(slug: string, demoService: Provider | EnvironmentPr return { path: slug, component: ComponentDetailsComponent, + title: translationKey, providers: [ demoService, { provide: CURRENT_DEMO_SERVICE, useExisting: demoService }, @@ -125,55 +126,58 @@ export const routes: Routes = [ children: [ { path: 'components', + title: 'TITLES.COMPONENTS', children: [ { path: '', loadComponent: () => import('./pages/components/components.component').then((module) => module.ComponentsComponent), }, - buildComponentRoute('accordion', AccordionDemoService), - buildComponentRoute('autocomplete', AutocompleteDemoService), - buildComponentRoute('avatar', AvatarDemoService), - buildComponentRoute('badge', BadgeDemoService), - buildComponentRoute('breadcrumb', BreadcrumbDemoService), - buildComponentRoute('button', ButtonDemoService), - buildComponentRoute('card', CardDemoService), - buildComponentRoute('checkbox', CheckboxDemoService), - buildComponentRoute('chip', ChipDemoService), - buildComponentRoute('date-picker', DatepickerDemoService), - buildComponentRoute('dialog', DialogDemoService), - buildComponentRoute('divider', DividerDemoService), - buildComponentRoute('fieldset', FieldsetDemoService), - buildComponentRoute('form-field', FormFieldDemoService), - buildComponentRoute('icon', IconDemoService), - buildComponentRoute('icon-button', IconButtonDemoService), - buildComponentRoute('menu-item', MenuItemDemoService), - buildComponentRoute('message', MessageDemoService), - buildComponentRoute('notification', NotificationDemoService), - buildComponentRoute('option', OptionDemoService), - buildComponentRoute('overlay-panel', OverlayPanelDemoService), - buildComponentRoute('paginator', PaginatorDemoService), - buildComponentRoute('radio', RadioDemoService), - buildComponentRoute('scrollbar', ScrollbarDemoService), - buildComponentRoute('segmented-control', SegmentedControlDemoService), - buildComponentRoute('segmented-control-toggle', SegmentedControlToggleDemoService), - buildComponentRoute('select', SelectDemoService), - buildComponentRoute('side-nav', SideNavDemoService), - buildComponentRoute('side-sheet', SideSheetDemoService), - buildComponentRoute('snackbar', SnackbarDemoService), - buildComponentRoute('spinner', SpinnerDemoService), - buildComponentRoute('switch', SwitchDemoService), - buildComponentRoute('tab', TabDemoService), - buildComponentRoute('table', TableDemoService), - buildComponentRoute('tag', TagDemoService), - buildComponentRoute('tooltip', TooltipDemoService), + buildComponentRoute('accordion', 'COMPONENTS.ACCORDION', AccordionDemoService), + buildComponentRoute('autocomplete', 'COMPONENTS.AUTOCOMPLETE', AutocompleteDemoService), + buildComponentRoute('avatar', 'COMPONENTS.AVATAR', AvatarDemoService), + buildComponentRoute('badge', 'COMPONENTS.BADGE', BadgeDemoService), + buildComponentRoute('breadcrumb', 'COMPONENTS.BREADCRUMB', BreadcrumbDemoService), + buildComponentRoute('button', 'COMPONENTS.BUTTON', ButtonDemoService), + buildComponentRoute('card', 'COMPONENTS.CARD', CardDemoService), + buildComponentRoute('checkbox', 'COMPONENTS.CHECKBOX', CheckboxDemoService), + buildComponentRoute('chip', 'COMPONENTS.CHIP', ChipDemoService), + buildComponentRoute('date-picker', 'COMPONENTS.DATE_PICKER', DatepickerDemoService), + buildComponentRoute('dialog', 'COMPONENTS.DIALOG', DialogDemoService), + buildComponentRoute('divider', 'COMPONENTS.DIVIDER', DividerDemoService), + buildComponentRoute('fieldset', 'COMPONENTS.FIELDSET', FieldsetDemoService), + buildComponentRoute('form-field', 'COMPONENTS.FORM_FIELD', FormFieldDemoService), + buildComponentRoute('icon', 'COMPONENTS.ICON', IconDemoService), + buildComponentRoute('icon-button', 'COMPONENTS.ICON_BUTTON', IconButtonDemoService), + buildComponentRoute('menu-item', 'COMPONENTS.MENU_ITEM', MenuItemDemoService), + buildComponentRoute('message', 'COMPONENTS.MESSAGE', MessageDemoService), + buildComponentRoute('notification', 'COMPONENTS.NOTIFICATION', NotificationDemoService), + buildComponentRoute('option', 'COMPONENTS.OPTION', OptionDemoService), + buildComponentRoute('overlay-panel', 'COMPONENTS.OVERLAY_PANEL', OverlayPanelDemoService), + buildComponentRoute('paginator', 'COMPONENTS.PAGINATOR', PaginatorDemoService), + buildComponentRoute('radio', 'COMPONENTS.RADIO', RadioDemoService), + buildComponentRoute('scrollbar', 'COMPONENTS.SCROLLBAR', ScrollbarDemoService), + buildComponentRoute('segmented-control', 'COMPONENTS.SEGMENTED_CONTROL', SegmentedControlDemoService), + buildComponentRoute('segmented-control-toggle', 'COMPONENTS.SEGMENTED_CONTROL_TOGGLE', SegmentedControlToggleDemoService), + buildComponentRoute('select', 'COMPONENTS.SELECT', SelectDemoService), + buildComponentRoute('side-nav', 'COMPONENTS.SIDE_NAV', SideNavDemoService), + buildComponentRoute('side-sheet', 'COMPONENTS.SIDE_SHEET', SideSheetDemoService), + buildComponentRoute('snackbar', 'COMPONENTS.SNACKBAR', SnackbarDemoService), + buildComponentRoute('spinner', 'COMPONENTS.SPINNER', SpinnerDemoService), + buildComponentRoute('switch', 'COMPONENTS.SWITCH', SwitchDemoService), + buildComponentRoute('tab', 'COMPONENTS.TAB', TabDemoService), + buildComponentRoute('table', 'COMPONENTS.TABLE', TableDemoService), + buildComponentRoute('tag', 'COMPONENTS.TAG', TagDemoService), + buildComponentRoute('tooltip', 'COMPONENTS.TOOLTIP', TooltipDemoService), ], }, { path: 'home', + title: 'TITLES.HOME', loadComponent: () => import('./pages/index/index.component').then((module) => module.IndexComponent), }, { path: 'support', + title: 'TITLES.SUPPORT', loadComponent: () => import('./pages/issue-report/issue-report.component').then((module) => module.IssueReportComponent), }, { diff --git a/projects/demo/src/app/strategies/custom-title.strategy.ts b/projects/demo/src/app/strategies/custom-title.strategy.ts new file mode 100644 index 00000000..bc9ed37f --- /dev/null +++ b/projects/demo/src/app/strategies/custom-title.strategy.ts @@ -0,0 +1,40 @@ +import { inject, Injectable } from '@angular/core'; +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + +const APP_TITLE_SUFFIX = 'i-Cell Design System'; + +@Injectable({ providedIn: 'root' }) +export class CustomTitleStrategy extends TitleStrategy { + private readonly _translate = inject(TranslateService); + private _lastRouterState?: RouterStateSnapshot; + + constructor() { + super(); + this._translate.onLangChange.subscribe(() => { + if (this._lastRouterState) { + this.updateTitle(this._lastRouterState); + } + }); + } + + public override updateTitle(routerState: RouterStateSnapshot): void { + this._lastRouterState = routerState; + + let finalTitle: string | undefined = this.buildTitle(routerState); + + if (finalTitle && this._isTranslationKey(finalTitle)) { + finalTitle = this._translate.instant(finalTitle); + } + + if (finalTitle) { + document.title = `${finalTitle} | ${APP_TITLE_SUFFIX}`; + } else { + document.title = APP_TITLE_SUFFIX; + } + } + + private _isTranslationKey(str: string): boolean { + return /^[A-Z_]+(\.[A-Z_]+)*$/.test(str); + } +} diff --git a/projects/demo/src/assets/i18n/en.json b/projects/demo/src/assets/i18n/en.json index c8f01d5e..1418d00a 100644 --- a/projects/demo/src/assets/i18n/en.json +++ b/projects/demo/src/assets/i18n/en.json @@ -9,6 +9,11 @@ "PATTERNS": "Patterns" }, "RESOURCES": "Resources", + "TITLES": { + "HOME": "Home", + "SUPPORT": "Support", + "COMPONENTS": "Components" + }, "COMPONENTS": { "ACCORDION": "Accordion", "AUTOCOMPLETE": "Auto complete", @@ -27,7 +32,9 @@ "ICON": "Icon", "ICON_BUTTON": "Icon button", "MENU_ITEM": "Menu item", + "MESSAGE": "Message", "NOTIFICATION": "Notification", + "OPTION": "Option", "OVERLAY_PANEL": "Overlay panel", "PAGINATOR": "Paginator", "RADIO": "Radio", diff --git a/projects/demo/src/assets/i18n/hu.json b/projects/demo/src/assets/i18n/hu.json index cdca4f44..f3f437e6 100644 --- a/projects/demo/src/assets/i18n/hu.json +++ b/projects/demo/src/assets/i18n/hu.json @@ -9,6 +9,11 @@ "PATTERNS": "Minták" }, "RESOURCES": "Forrásanyagok", + "TITLES": { + "HOME": "Kezdőlap", + "SUPPORT": "Támogatás", + "COMPONENTS": "Komponensek" + }, "COMPONENTS": { "ACCORDION": "Accordion", "AUTOCOMPLETE": "Auto complete", @@ -27,7 +32,9 @@ "ICON": "Icon", "ICON_BUTTON": "Icon button", "MENU_ITEM": "Menu item", + "MESSAGE": "Message", "NOTIFICATION": "Notification", + "OPTION": "Option", "OVERLAY_PANEL": "Overlay panel", "PAGINATOR": "Paginator", "RADIO": "Radio",