import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop'; 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 { TranslateService } from '../../../i18n/translate.service'; import { InputComponent } from '../../../shared/ui/input/input.component'; import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component'; import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component'; import { HomepageOverviewComponent } from './homepage/homepage-overview.component'; import { SectionConfig } from '../../../shared/models/config'; import { IconComponent } from '../../../shared/ui/icon/icon.component'; import { AppIconName } from '../../../shared/ui/icon/icon-registry'; export interface LayoutStrategyOption { value: 'stack' | 'grid' | 'hero' | 'carousel' | 'split'; labelKey: string; descKey: string; } /** Merchant-facing catalog of the block types an admin can add to the homepage. Not the widget manifest (that carries technical settingsSchema) - this is presentation-only, matching the pattern already used in widgets-section.component.ts. */ interface BlockCatalogEntry { type: string; icon: AppIconName; labelKey: string; descKey: string; defaultLayout: LayoutStrategyOption['value']; } const BLOCK_CATALOG: BlockCatalogEntry[] = [ { type: 'hero', icon: 'image', labelKey: 'builder.blockHero', descKey: 'builder.blockHeroDesc', defaultLayout: 'hero' }, { type: 'categories', icon: 'layoutGrid', labelKey: 'builder.blockCategories', descKey: 'builder.blockCategoriesDesc', defaultLayout: 'grid' }, { type: 'product-collection', icon: 'package', labelKey: 'builder.blockFeaturedProducts', descKey: 'builder.blockFeaturedProductsDesc', defaultLayout: 'grid' }, { type: 'product-carousel', icon: 'images', labelKey: 'builder.blockProductCarousel', descKey: 'builder.blockProductCarouselDesc', defaultLayout: 'carousel' }, { type: 'recently-viewed', icon: 'history', labelKey: 'builder.blockRecentlyViewed', descKey: 'builder.blockRecentlyViewedDesc', defaultLayout: 'grid' }, { type: 'banner', icon: 'megaphone', labelKey: 'builder.blockBanner', descKey: 'builder.blockBannerDesc', defaultLayout: 'split' }, { type: 'partners', icon: 'verified', labelKey: 'builder.blockPartners', descKey: 'builder.blockPartnersDesc', defaultLayout: 'stack' }, { type: 'html', icon: 'code', labelKey: 'builder.blockCustomHtml', descKey: 'builder.blockCustomHtmlDesc', defaultLayout: 'stack' }, ]; const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry])); @Component({ selector: 'app-project-editor-homepage-section', standalone: true, imports: [DragDropModule, FormsModule, TranslatePipe, InputComponent, SectionCardComponent, ToggleComponent, HomepageOverviewComponent, IconComponent], templateUrl: './homepage-section.component.html', styleUrls: ['./section.shared.scss', './homepage-section.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ProjectEditorHomepageSectionComponent { private readonly facade = inject(ProjectEditorFacade); private readonly translate = inject(TranslateService); readonly homePage = this.facade.homepagePage; readonly fieldError = (key: string): string | null => { const messageKey = this.facade.fieldError(key); return messageKey ? this.translate.t(messageKey) : null; }; readonly sections = computed(() => [...(this.homePage()?.sections ?? [])].sort((a, b) => a.order - b.order)); readonly blockCatalog = BLOCK_CATALOG; readonly layoutStrategyOptions: LayoutStrategyOption[] = [ { value: 'stack', labelKey: 'builder.layoutStrategyStack', descKey: 'builder.layoutStrategyStackDesc' }, { value: 'grid', labelKey: 'builder.layoutStrategyGrid', descKey: 'builder.layoutStrategyGridDesc' }, { value: 'hero', labelKey: 'builder.layoutStrategyHero', descKey: 'builder.layoutStrategyHeroDesc' }, { value: 'carousel', labelKey: 'builder.layoutStrategyCarousel', descKey: 'builder.layoutStrategyCarouselDesc' }, { value: 'split', labelKey: 'builder.layoutStrategySplit', descKey: 'builder.layoutStrategySplitDesc' }, ]; layoutStrategyDescKey(strategy: string | undefined): string { return this.layoutStrategyOptions.find(option => option.value === strategy)?.descKey ?? ''; } blockLabel(type: string): string { const entry = BLOCK_BY_TYPE.get(type); return entry ? this.translate.t(entry.labelKey) : type; } blockDescription(type: string): string { const entry = BLOCK_BY_TYPE.get(type); return entry ? this.translate.t(entry.descKey) : ''; } blockIcon(type: string): AppIconName { return BLOCK_BY_TYPE.get(type)?.icon ?? 'stop'; } drop(event: CdkDragDrop): void { const sections = [...this.sections()]; moveItemInArray(sections, event.previousIndex, event.currentIndex); this.replaceSections(sections.map((section, index) => ({ ...section, order: index + 1 }))); } updateSection(sectionId: string, field: 'visible' | 'type', value: unknown): void { this.replaceSections(this.sections().map(section => section.id !== sectionId ? section : ({ ...section, ...(field === 'type' ? { type: String(value) } : { visible: Boolean(value) }) }))); } updateLayout(sectionId: string, key: 'strategy' | 'columns', value: string): void { this.replaceSections(this.sections().map(section => section.id !== sectionId ? section : ({ ...section, layout: { ...section.layout, [key]: key === 'columns' ? Number(value) || 1 : value, } }))); } addBlock(type: string): void { const entry = BLOCK_BY_TYPE.get(type); if (!entry) { return; } const id = `section-${type}-${Date.now()}`; const widgetId = `widget-${type}-${Date.now()}`; const newSection: SectionConfig = { id, type, order: this.sections().length + 1, layout: { strategy: entry.defaultLayout, columns: 1, gap: '1.5rem', align: 'stretch' }, visibility: { desktop: true, tablet: true, mobile: true }, visible: true, widgets: [{ id: widgetId, type, version: '1.0.0', order: 1, visibility: { desktop: true, tablet: true, mobile: true }, visible: true, props: {}, }], }; this.replaceSections([...this.sections(), newSection]); } duplicateBlock(sectionId: string): void { const source = this.sections().find(section => section.id === sectionId); if (!source) { return; } const suffix = Date.now(); const copy: SectionConfig = { ...source, id: `${source.id}-copy-${suffix}`, order: this.sections().length + 1, widgets: source.widgets.map(widget => ({ ...widget, id: `${widget.id}-copy-${suffix}` })), }; this.replaceSections([...this.sections(), copy]); } removeBlock(sectionId: string): void { if (!confirm(this.translate.t('builder.blockRemoveConfirm'))) { return; } this.replaceSections(this.sections().filter(section => section.id !== sectionId)); } private replaceSections(sections: SectionConfig[]): void { this.facade.updateBootstrap(current => ({ ...current, pages: current.pages.map(page => page.id !== this.homePage()?.id ? page : ({ ...page, sections })) })); } }