import { ChangeDetectionStrategy, Component, inject, signal } 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 { ButtonComponent } from '../../../shared/ui/button/button.component'; import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component'; import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component'; import { WidgetConfig } from '../../../shared/models/config'; import { IconComponent } from '../../../shared/ui/icon/icon.component'; import { AppIconName } from '../../../shared/ui/icon/icon-registry'; interface HeroSlideDraft { title: string; subtitle: string; } const WIDGET_ICONS: Record = { hero: 'image', categories: 'layoutGrid', 'product-collection': 'package', }; const WIDGET_LABEL_KEYS: Record = { hero: 'builder.widgetTypeHero', categories: 'builder.widgetTypeCategories', 'product-collection': 'builder.widgetTypeProducts', }; @Component({ selector: 'app-project-editor-widgets-section', standalone: true, imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, SectionCardComponent, ToggleComponent, IconComponent], templateUrl: './widgets-section.component.html', styleUrls: ['./section.shared.scss', './widgets-section.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ProjectEditorWidgetsSectionComponent { private readonly facade = inject(ProjectEditorFacade); private readonly translate = inject(TranslateService); readonly widgets = this.facade.homepageWidgets; readonly fieldError = (key: string): string | null => { const messageKey = this.facade.fieldError(key); return messageKey ? this.translate.t(messageKey) : null; }; widgetIcon(type: string): AppIconName { return WIDGET_ICONS[type] ?? 'stop'; } widgetLabel(widget: WidgetConfig): string { if (widget.title) { return widget.title; } const key = WIDGET_LABEL_KEYS[widget.type]; return key ? this.translate.t(key) : widget.type; } isVisible(widget: WidgetConfig): boolean { return widget.visible !== false; } toggleVisible(widgetId: string): void { this.updateWidgetConfig(widgetId, widget => ({ ...widget, visible: !this.isVisible(widget) })); } moveWidget(widgetId: string, direction: -1 | 1): void { this.facade.updateBootstrap(current => ({ ...current, pages: current.pages.map(page => ({ ...page, sections: page.sections.map(section => { const index = section.widgets.findIndex(w => w.id === widgetId); if (index === -1) { return section; } const targetIndex = index + direction; if (targetIndex < 0 || targetIndex >= section.widgets.length) { return section; } const widgets = [...section.widgets]; [widgets[index], widgets[targetIndex]] = [widgets[targetIndex], widgets[index]]; return { ...section, widgets }; }) })) })); } duplicateWidget(widgetId: string): void { this.facade.updateBootstrap(current => ({ ...current, pages: current.pages.map(page => ({ ...page, sections: page.sections.map(section => { const index = section.widgets.findIndex(w => w.id === widgetId); if (index === -1) { return section; } const original = section.widgets[index]; const copy: WidgetConfig = { ...original, id: `${original.id}-copy-${Date.now()}` }; const widgets = [...section.widgets]; widgets.splice(index + 1, 0, copy); return { ...section, widgets }; }) })) })); } removeWidget(widgetId: string): void { if (!confirm(this.translate.t('builder.widgetRemoveConfirm'))) { return; } this.facade.updateBootstrap(current => ({ ...current, pages: current.pages.map(page => ({ ...page, sections: page.sections.map(section => ({ ...section, widgets: section.widgets.filter(w => w.id !== widgetId) })) })) })); } private updateWidgetConfig(widgetId: string, updater: (widget: WidgetConfig) => WidgetConfig): void { this.facade.updateBootstrap(current => ({ ...current, pages: current.pages.map(page => ({ ...page, sections: page.sections.map(section => ({ ...section, widgets: section.widgets.map(widget => widget.id !== widgetId ? widget : updater(widget)) })) })) })); } updateWidget(widgetId: string, updater: (props: Record) => Record): void { this.updateWidgetConfig(widgetId, widget => ({ ...widget, props: updater(widget.props ?? {}) })); } updateProp(widgetId: string, key: string, value: unknown): void { this.updateWidget(widgetId, props => ({ ...props, [key]: value })); } /** Extra slides beyond the widget's primary title/subtitle (which stay editable as "slide 1" via the existing fields above this list). */ heroSlides(widget: WidgetConfig): HeroSlideDraft[] { const raw = widget.props?.['slides']; if (!Array.isArray(raw)) { return []; } return raw.map(slide => ({ title: typeof slide?.title === 'string' ? slide.title : '', subtitle: typeof slide?.subtitle === 'string' ? slide.subtitle : '', })); } addHeroSlide(widgetId: string): void { this.updateWidget(widgetId, props => ({ ...props, slides: [...this.slidesArray(props), { title: '', subtitle: '' }], })); } updateHeroSlide(widgetId: string, index: number, field: 'title' | 'subtitle', value: string): void { this.updateWidget(widgetId, props => ({ ...props, slides: this.slidesArray(props).map((slide, i) => i === index ? { ...slide, [field]: value } : slide), })); } removeHeroSlide(widgetId: string, index: number): void { this.updateWidget(widgetId, props => ({ ...props, slides: this.slidesArray(props).filter((_, i) => i !== index), })); } private slidesArray(props: Record): HeroSlideDraft[] { const raw = props['slides']; return Array.isArray(raw) ? raw : []; } private readonly jsonDrafts = signal>({}); private readonly jsonErrors = signal>({}); /** * While the textarea holds invalid JSON, keep showing the user's own draft * (not the last-committed props) so their in-progress edit isn't silently * overwritten on the next change-detection pass. */ widgetJsonValue(widgetId: string, props: Record): string { return this.jsonDrafts()[widgetId] ?? this.propsJson(props); } widgetJsonError(widgetId: string): string | null { return this.jsonErrors()[widgetId] ?? null; } updateJson(widgetId: string, raw: string): void { try { const parsed = JSON.parse(raw); this.updateWidget(widgetId, () => parsed); this.jsonDrafts.update(({ [widgetId]: _removed, ...rest }) => rest); this.jsonErrors.update(({ [widgetId]: _removed, ...rest }) => rest); } catch { this.jsonDrafts.update(drafts => ({ ...drafts, [widgetId]: raw })); this.jsonErrors.update(errors => ({ ...errors, [widgetId]: this.translate.t('builder.widgetJsonInvalid') })); } } propsJson(props: Record): string { return JSON.stringify(props ?? {}, null, 2); } }