diff --git a/src/app/features/project-editor/components/project-editor-nav.component.ts b/src/app/features/project-editor/components/project-editor-nav.component.ts index 0d5e361..dde2de3 100644 --- a/src/app/features/project-editor/components/project-editor-nav.component.ts +++ b/src/app/features/project-editor/components/project-editor-nav.component.ts @@ -24,6 +24,7 @@ export class ProjectEditorNavComponent { { id: 'static-pages', label: 'builder.staticPages' }, { id: 'features', label: 'builder.marketplaceFeatures' }, { id: 'languages', label: 'builder.languagesTab' }, + { id: 'navigation', label: 'builder.navigationTab' }, { id: 'preview', label: 'builder.preview' }, ]; } diff --git a/src/app/features/project-editor/facade/project-editor.facade.ts b/src/app/features/project-editor/facade/project-editor.facade.ts index da51d84..c0ae30e 100644 --- a/src/app/features/project-editor/facade/project-editor.facade.ts +++ b/src/app/features/project-editor/facade/project-editor.facade.ts @@ -1,6 +1,7 @@ import { Injectable, computed, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { BootstrapConfig, DEFAULT_CATALOG_CONFIG, DEFAULT_HEADER_CONFIG, DEFAULT_PRODUCT_PAGE_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../../shared/models/config'; +import { NavigationItemConfig } from '../../../shared/models/config'; import { ConfigService } from '../../../core/config/config.service'; import { ProjectEditorIoService } from '../services/project-editor-io.service'; import { ProjectEditorPreviewService } from '../services/project-editor-preview.service'; @@ -88,6 +89,79 @@ export class ProjectEditorFacade { })); } + addNavLink(target: 'header' | 'footer'): void { + this.updateBootstrap(current => { + const list = current.navigation[target]; + if (!this.isFlatNavList(list)) { + return current; + } + const items = list as NavigationItemConfig[]; + const newItem: NavigationItemConfig = { + id: `nav-${Date.now()}`, + label: 'New link', + route: '/', + order: items.length + 1, + visible: true, + }; + return { ...current, navigation: { ...current.navigation, [target]: [...items, newItem] } }; + }); + } + + removeNavLink(target: 'header' | 'footer', id: string): void { + this.updateBootstrap(current => { + const list = current.navigation[target]; + if (!this.isFlatNavList(list)) { + return current; + } + return { + ...current, + navigation: { ...current.navigation, [target]: (list as NavigationItemConfig[]).filter(item => item.id !== id) }, + }; + }); + } + + updateNavLink(target: 'header' | 'footer', id: string, patch: Partial): void { + this.updateBootstrap(current => { + const list = current.navigation[target]; + if (!this.isFlatNavList(list)) { + return current; + } + return { + ...current, + navigation: { + ...current.navigation, + [target]: (list as NavigationItemConfig[]).map(item => (item.id !== id ? item : { ...item, ...patch })), + }, + }; + }); + } + + reorderNavLink(target: 'header' | 'footer', id: string, direction: -1 | 1): void { + this.updateBootstrap(current => { + const list = current.navigation[target]; + if (!this.isFlatNavList(list)) { + return current; + } + const items = [...(list as NavigationItemConfig[])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + const index = items.findIndex(item => item.id === id); + const nextIndex = index + direction; + if (index < 0 || nextIndex < 0 || nextIndex >= items.length) { + return current; + } + const tmp = items[index]; + items[index] = items[nextIndex]; + items[nextIndex] = tmp; + return { + ...current, + navigation: { ...current.navigation, [target]: items.map((item, order) => ({ ...item, order: order + 1 })) }, + }; + }); + } + + private isFlatNavList(list: NavigationItemConfig[] | { items: unknown }[]): list is NavigationItemConfig[] { + return list.length === 0 || !('items' in list[0]); + } + private normalize(config: BootstrapConfig): BootstrapConfig { return { ...config, diff --git a/src/app/features/project-editor/models/project-editor.model.ts b/src/app/features/project-editor/models/project-editor.model.ts index 9b7956e..1635e13 100644 --- a/src/app/features/project-editor/models/project-editor.model.ts +++ b/src/app/features/project-editor/models/project-editor.model.ts @@ -11,6 +11,7 @@ export type ProjectEditorSectionId = | 'static-pages' | 'features' | 'languages' + | 'navigation' | 'preview'; export interface ProjectEditorState { diff --git a/src/app/features/project-editor/pages/project-editor-page.component.html b/src/app/features/project-editor/pages/project-editor-page.component.html index 59d5cd2..821be84 100644 --- a/src/app/features/project-editor/pages/project-editor-page.component.html +++ b/src/app/features/project-editor/pages/project-editor-page.component.html @@ -24,6 +24,7 @@ @case ('static-pages') { } @case ('features') { } @case ('languages') { } + @case ('navigation') { } @case ('preview') { } } diff --git a/src/app/features/project-editor/pages/project-editor-page.component.ts b/src/app/features/project-editor/pages/project-editor-page.component.ts index 3773013..bde7495 100644 --- a/src/app/features/project-editor/pages/project-editor-page.component.ts +++ b/src/app/features/project-editor/pages/project-editor-page.component.ts @@ -13,13 +13,14 @@ import { ProjectEditorHomepageSectionComponent } from '../sections/homepage-sect import { ProjectEditorWidgetsSectionComponent } from '../sections/widgets-section.component'; import { ProjectEditorFeaturesSectionComponent } from '../sections/features-section.component'; import { ProjectEditorLanguagesSectionComponent } from '../sections/languages-section.component'; +import { ProjectEditorNavigationSectionComponent } from '../sections/navigation-section.component'; import { ProjectEditorPreviewSectionComponent } from '../sections/preview-section.component'; import { TranslatePipe } from '../../../i18n/translate.pipe'; import { StaticPagesEditorComponent } from '../../content-management/components/static-pages-editor.component'; import { ProjectEditorSectionId } from '../models/project-editor.model'; const KNOWN_SECTIONS: ProjectEditorSectionId[] = [ - 'general', 'branding', 'theme', 'header', 'footer', 'homepage', 'widgets', 'static-pages', 'features', 'languages', 'preview' + 'general', 'branding', 'theme', 'header', 'footer', 'homepage', 'widgets', 'static-pages', 'features', 'languages', 'navigation', 'preview' ]; @Component({ @@ -38,6 +39,7 @@ const KNOWN_SECTIONS: ProjectEditorSectionId[] = [ StaticPagesEditorComponent, ProjectEditorFeaturesSectionComponent, ProjectEditorLanguagesSectionComponent, + ProjectEditorNavigationSectionComponent, ProjectEditorPreviewSectionComponent, ], templateUrl: './project-editor-page.component.html', diff --git a/src/app/features/project-editor/sections/navigation-section.component.html b/src/app/features/project-editor/sections/navigation-section.component.html new file mode 100644 index 0000000..c589e55 --- /dev/null +++ b/src/app/features/project-editor/sections/navigation-section.component.html @@ -0,0 +1,57 @@ +
+
+

{{ 'builder.navigationHeader' | translate }}

+ +
+ +
+ @for (item of headerLinks(); track item.id) { +
+
+

{{ labelOf(item) }}

+
+ + + +
+
+
+ + + +
+
+ } +
+ +
+

{{ 'builder.navigationFooter' | translate }}

+ @if (footerLinks(); as footer) { + + } +
+ + @if (footerLinks(); as footer) { +
+ @for (item of footer; track item.id) { +
+
+

{{ labelOf(item) }}

+
+ + + +
+
+
+ + + +
+
+ } +
+ } @else { +

{{ 'builder.navigationFooterGrouped' | translate }}

+ } +
diff --git a/src/app/features/project-editor/sections/navigation-section.component.ts b/src/app/features/project-editor/sections/navigation-section.component.ts new file mode 100644 index 0000000..54ad943 --- /dev/null +++ b/src/app/features/project-editor/sections/navigation-section.component.ts @@ -0,0 +1,63 @@ +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ProjectEditorFacade } from '../facade/project-editor.facade'; +import { TranslatePipe } from '../../../i18n/translate.pipe'; +import { NavigationItemConfig } from '../../../shared/models/config'; + +@Component({ + selector: 'app-project-editor-navigation-section', + standalone: true, + imports: [FormsModule, TranslatePipe], + templateUrl: './navigation-section.component.html', + styleUrls: ['./section.shared.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ProjectEditorNavigationSectionComponent { + private readonly facade = inject(ProjectEditorFacade); + readonly bootstrap = this.facade.bootstrap; + + readonly headerLinks = computed(() => this.sorted(this.bootstrap()?.navigation.header ?? [])); + readonly footerLinks = computed(() => { + const footer = this.bootstrap()?.navigation.footer ?? []; + if (footer.length === 0) { + return []; + } + return 'items' in footer[0] ? null : this.sorted(footer as NavigationItemConfig[]); + }); + + labelOf(item: NavigationItemConfig): string { + if (typeof item.label === 'string' || !item.label) { + return item.label ?? ''; + } + const defaultLocale = this.bootstrap()?.localization.defaultLocale ?? 'en'; + return item.label[defaultLocale] ?? Object.values(item.label)[0] ?? ''; + } + + addLink(target: 'header' | 'footer'): void { + this.facade.addNavLink(target); + } + + removeLink(target: 'header' | 'footer', id: string): void { + this.facade.removeNavLink(target, id); + } + + move(target: 'header' | 'footer', id: string, direction: -1 | 1): void { + this.facade.reorderNavLink(target, id, direction); + } + + updateLabel(target: 'header' | 'footer', id: string, value: string): void { + this.facade.updateNavLink(target, id, { label: value }); + } + + updateRoute(target: 'header' | 'footer', id: string, value: string): void { + this.facade.updateNavLink(target, id, { route: value }); + } + + updateVisible(target: 'header' | 'footer', id: string, value: boolean): void { + this.facade.updateNavLink(target, id, { visible: value }); + } + + private sorted(items: NavigationItemConfig[]): NavigationItemConfig[] { + return [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + } +} diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 175eed7..ec621c0 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -490,6 +490,14 @@ export const en: Translations = { removeLanguage: 'Remove', setDefaultLanguage: 'Set as Default', defaultLanguageLabel: 'Default', + navigationTab: 'Navigation', + navigationHeader: 'Header Navigation', + navigationFooter: 'Footer Navigation', + navigationFooterGrouped: 'Footer navigation uses grouped columns and is not editable here yet — edit via the Footer tab.', + addLink: 'Add Link', + removeLink: 'Remove', + linkLabel: 'Label', + linkUrl: 'URL', exportBootstrap: 'Export Bootstrap', importBootstrap: 'Import Bootstrap', exportedBootstrap: 'Exported Bootstrap', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 6e2dc32..150bbba 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -490,6 +490,14 @@ export const hy: Translations = { removeLanguage: 'Հեռացնել', setDefaultLanguage: 'Դարձնել լռելյայն', defaultLanguageLabel: 'Լռելյայն', + navigationTab: 'Նավիգացիա', + navigationHeader: 'Վերին նավիգացիա', + navigationFooter: 'Ստորին նավիգացիա', + navigationFooterGrouped: 'Ստորին նավիգացիան խմբավորված է սյուներով և դեռ խմբագրելի չէ այստեղ. օգտագործեք «Footer» ներդիրը։', + addLink: 'Ավելացնել հղում', + removeLink: 'Հեռացնել', + linkLabel: 'Պիտակ', + linkUrl: 'URL', exportBootstrap: 'Արտահանել bootstrap', importBootstrap: 'Ներմուծել bootstrap', exportedBootstrap: 'Արտահանված bootstrap', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index bea94ed..79c1731 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -490,6 +490,14 @@ export const ru: Translations = { removeLanguage: 'Удалить', setDefaultLanguage: 'Сделать языком по умолчанию', defaultLanguageLabel: 'По умолчанию', + navigationTab: 'Навигация', + navigationHeader: 'Навигация в шапке', + navigationFooter: 'Навигация в подвале', + navigationFooterGrouped: 'Навигация подвала сгруппирована по колонкам и пока недоступна для редактирования здесь — используйте вкладку "Подвал".', + addLink: 'Добавить ссылку', + removeLink: 'Удалить', + linkLabel: 'Название', + linkUrl: 'URL', exportBootstrap: 'Экспорт bootstrap', importBootstrap: 'Импорт bootstrap', exportedBootstrap: 'Экспортированный bootstrap', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 742dab3..48bcc43 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -488,6 +488,14 @@ export interface Translations { removeLanguage: string; setDefaultLanguage: string; defaultLanguageLabel: string; + navigationTab: string; + navigationHeader: string; + navigationFooter: string; + navigationFooterGrouped: string; + addLink: string; + removeLink: string; + linkLabel: string; + linkUrl: string; exportBootstrap: string; importBootstrap: string; exportedBootstrap: string;