{{ entry.widget.type }} · {{ entry.widget.id }}
+
+ @for (entry of widgets(); track entry.widget.id; let first = $first; let last = $last) {
+
+
+
+
+
@switch (entry.widget.type) {
@case ('hero') {
diff --git a/src/app/features/project-editor/sections/widgets-section.component.scss b/src/app/features/project-editor/sections/widgets-section.component.scss
new file mode 100644
index 0000000..5d1a80f
--- /dev/null
+++ b/src/app/features/project-editor/sections/widgets-section.component.scss
@@ -0,0 +1,96 @@
+.widget-card--hidden {
+ opacity: 0.6;
+}
+
+.widget-card__header {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--space-sm);
+ margin-bottom: var(--space-sm);
+}
+
+.widget-card__icon {
+ font-size: 1.25rem;
+ color: var(--primary-color);
+ flex-shrink: 0;
+}
+
+.widget-card__title {
+ margin: 0;
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ flex: 1 1 auto;
+ min-width: 120px;
+}
+
+.widget-card__hidden-badge {
+ font-size: 0.7rem;
+ font-weight: 600;
+ letter-spacing: 0.4px;
+ text-transform: uppercase;
+ padding: 2px 8px;
+ border-radius: var(--radius-sm);
+ background: var(--bg-tertiary);
+ color: var(--text-light);
+}
+
+.widget-card__actions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ flex-wrap: wrap;
+
+ button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 34px;
+ height: 34px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ cursor: pointer;
+
+ &:hover:not(:disabled) {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--primary-color);
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+ }
+}
+
+.widget-card__remove:hover:not(:disabled) {
+ border-color: var(--error-color) !important;
+ color: var(--error-color) !important;
+}
+
+.widget-card__visible-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.widget-card__advanced {
+ margin-top: var(--space-sm);
+
+ summary {
+ cursor: pointer;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--text-light);
+ }
+}
diff --git a/src/app/features/project-editor/sections/widgets-section.component.ts b/src/app/features/project-editor/sections/widgets-section.component.ts
index 4add87c..243e875 100644
--- a/src/app/features/project-editor/sections/widgets-section.component.ts
+++ b/src/app/features/project-editor/sections/widgets-section.component.ts
@@ -6,13 +6,26 @@ 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'],
+ styleUrls: ['./section.shared.scss', './widgets-section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectEditorWidgetsSectionComponent {
@@ -24,22 +37,101 @@ export class ProjectEditorWidgetsSectionComponent {
return messageKey ? this.translate.t(messageKey) : null;
};
- updateWidget(widgetId: string, updater: (props: Record) => Record): void {
+ 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.map(widget => widget.id !== widgetId ? widget : ({
- ...widget,
- props: updater(widget.props ?? {})
- }))
+ 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 }));
}
diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts
index 1106336..7558b79 100644
--- a/src/app/i18n/en.ts
+++ b/src/app/i18n/en.ts
@@ -780,6 +780,28 @@ export const en: Translations = {
brandSocialEmpty: "No social share image yet - shared links will show a blank card until you add one.",
brandAccentColor: 'Accent',
brandBorderColor: 'Borders',
+ homepageOverviewTitle: 'Homepage',
+ homepageOverviewSubtitle: "What your marketplace's homepage is made of.",
+ homepageOverviewComplete: 'Your homepage is fully set up.',
+ homepageActiveSections: 'Active sections',
+ homepageDisabledSections: 'Hidden sections',
+ homepageHealthHero: 'Hero banner added',
+ homepageHealthCategories: 'Featured categories added',
+ homepageHealthProducts: 'Featured products added',
+ homepageHealthPromotion: 'Promotion block added',
+ homepageHealthNewsletter: 'Newsletter signup added',
+ homepageHealthAnySections: 'The page has at least one section',
+ widgetTypeHero: 'Hero banner',
+ widgetTypeCategories: 'Featured categories',
+ widgetTypeProducts: 'Featured products',
+ widgetHidden: 'Hidden',
+ widgetVisible: 'Show',
+ widgetMoveUp: 'Move up',
+ widgetMoveDown: 'Move down',
+ widgetDuplicate: 'Duplicate',
+ widgetRemove: 'Remove',
+ widgetRemoveConfirm: 'Remove this section from the homepage?',
+ widgetAdvancedSettings: 'Advanced settings',
},
dashboard: {
title: 'Dashboard',
diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts
index 8f13804..b0b2752 100644
--- a/src/app/i18n/hy.ts
+++ b/src/app/i18n/hy.ts
@@ -780,6 +780,28 @@ export const hy: Translations = {
brandSocialEmpty: 'Սոցիալական պատկեր դեռ վերբեռնված չէ․ հղումը կիսելիս գնորդները կտեսնեն դատարկ քարտ։',
brandAccentColor: 'Ընդգծող',
brandBorderColor: 'Եզրագծեր',
+ homepageOverviewTitle: 'Գլխավոր էջ',
+ homepageOverviewSubtitle: 'Ինչից է կազմված ձեր մարկետփլեյսի գլխավոր էջը։',
+ homepageOverviewComplete: 'Ձեր գլխավոր էջն ամբողջությամբ կարգավորված է։',
+ homepageActiveSections: 'Ակտիվ բլոկներ',
+ homepageDisabledSections: 'Թաքցված բլոկներ',
+ homepageHealthHero: 'Ողջունող բաններ ավելացված է',
+ homepageHealthCategories: 'Կատեգորիաների բլոկ ավելացված է',
+ homepageHealthProducts: 'Ապրանքների բլոկ ավելացված է',
+ homepageHealthPromotion: 'Ակցիայի բլոկ ավելացված է',
+ homepageHealthNewsletter: 'Բաժանորդագրման ձև ավելացված է',
+ homepageHealthAnySections: 'Էջում կա գոնե մեկ բլոկ',
+ widgetTypeHero: 'Ողջունող բաններ',
+ widgetTypeCategories: 'Կատեգորիաների բլոկ',
+ widgetTypeProducts: 'Ապրանքների բլոկ',
+ widgetHidden: 'Թաքցված',
+ widgetVisible: 'Ցուցադրել',
+ widgetMoveUp: 'Տեղափոխել վերև',
+ widgetMoveDown: 'Տեղափոխել ներքև',
+ widgetDuplicate: 'Կրկնօրինակել',
+ widgetRemove: 'Հեռացնել',
+ widgetRemoveConfirm: 'Հեռացնե՞լ այս բլոկը գլխավոր էջից։',
+ widgetAdvancedSettings: 'Լրացուցիչ կարգավորումներ',
},
dashboard: {
title: 'Կառավարման վահանակ',
diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts
index 8e30c06..72cef7b 100644
--- a/src/app/i18n/ru.ts
+++ b/src/app/i18n/ru.ts
@@ -780,6 +780,28 @@ export const ru: Translations = {
brandSocialEmpty: 'Изображение для соцсетей ещё не загружено — при публикации ссылки покупатели увидят пустую карточку.',
brandAccentColor: 'Акцентный',
brandBorderColor: 'Границы',
+ homepageOverviewTitle: 'Главная страница',
+ homepageOverviewSubtitle: 'Из чего состоит главная страница вашего маркетплейса.',
+ homepageOverviewComplete: 'Главная страница полностью настроена.',
+ homepageActiveSections: 'Активные блоки',
+ homepageDisabledSections: 'Скрытые блоки',
+ homepageHealthHero: 'Приветственный баннер добавлен',
+ homepageHealthCategories: 'Витрина категорий добавлена',
+ homepageHealthProducts: 'Витрина товаров добавлена',
+ homepageHealthPromotion: 'Блок акции добавлен',
+ homepageHealthNewsletter: 'Форма подписки добавлена',
+ homepageHealthAnySections: 'На странице есть хотя бы один блок',
+ widgetTypeHero: 'Приветственный баннер',
+ widgetTypeCategories: 'Витрина категорий',
+ widgetTypeProducts: 'Витрина товаров',
+ widgetHidden: 'Скрыт',
+ widgetVisible: 'Показывать',
+ widgetMoveUp: 'Переместить выше',
+ widgetMoveDown: 'Переместить ниже',
+ widgetDuplicate: 'Дублировать',
+ widgetRemove: 'Удалить',
+ widgetRemoveConfirm: 'Удалить этот блок с главной страницы?',
+ widgetAdvancedSettings: 'Дополнительные настройки',
},
dashboard: {
title: 'Панель управления',
diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts
index 213ad5d..88735d3 100644
--- a/src/app/i18n/translations.ts
+++ b/src/app/i18n/translations.ts
@@ -778,6 +778,28 @@ export interface Translations {
brandSocialEmpty: string;
brandAccentColor: string;
brandBorderColor: string;
+ homepageOverviewTitle: string;
+ homepageOverviewSubtitle: string;
+ homepageOverviewComplete: string;
+ homepageActiveSections: string;
+ homepageDisabledSections: string;
+ homepageHealthHero: string;
+ homepageHealthCategories: string;
+ homepageHealthProducts: string;
+ homepageHealthPromotion: string;
+ homepageHealthNewsletter: string;
+ homepageHealthAnySections: string;
+ widgetTypeHero: string;
+ widgetTypeCategories: string;
+ widgetTypeProducts: string;
+ widgetHidden: string;
+ widgetVisible: string;
+ widgetMoveUp: string;
+ widgetMoveDown: string;
+ widgetDuplicate: string;
+ widgetRemove: string;
+ widgetRemoveConfirm: string;
+ widgetAdvancedSettings: string;
};
dashboard: {
title: string;
{{ widgetLabel(entry.widget) }}
+ @if (!isVisible(entry.widget)) { + {{ 'builder.widgetHidden' | translate }} + } +
+
+
+
+
+
+
+
@@ -33,14 +58,17 @@
}
@default {
-
+
+
}
}