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 { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component'; import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component'; import { WidgetConfig } from '../../../shared/models/config'; const WIDGET_ICONS: Record = { hero: 'pi-image', categories: 'pi-th-large', 'product-collection': 'pi-box', }; 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, SectionCardComponent, ToggleComponent], 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): string { return WIDGET_ICONS[type] ?? 'pi-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 })); } 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); } }