diff --git a/src/app/features/project-editor/sections/footer-section.component.html b/src/app/features/project-editor/sections/footer-section.component.html index db72d84..8f4e65f 100644 --- a/src/app/features/project-editor/sections/footer-section.component.html +++ b/src/app/features/project-editor/sections/footer-section.component.html @@ -1,15 +1,65 @@ @if (bootstrap(); as bootstrap) { -
-

{{ 'builder.footer' | translate }}

+
- - + +
+ + {{ 'adminCategories.chooseImage' | translate }} +
+
+ +
+ + + +
+ + {{ 'adminCategories.chooseImage' | translate }} + +
+
+
+
+ +
+ + + +
+ + + + + + +
+
+
+
+
-
+ + } diff --git a/src/app/features/project-editor/sections/footer-section.component.ts b/src/app/features/project-editor/sections/footer-section.component.ts index 5088d3c..9fe9a1c 100644 --- a/src/app/features/project-editor/sections/footer-section.component.ts +++ b/src/app/features/project-editor/sections/footer-section.component.ts @@ -4,11 +4,31 @@ import { ProjectEditorFacade } from '../facade/project-editor.facade'; import { TranslatePipe } from '../../../i18n/translate.pipe'; import { InputComponent } from '../../../shared/ui/input/input.component'; import { FormFieldComponent } from '../../../shared/ui/form-field/form-field.component'; +import { ButtonComponent } from '../../../shared/ui/button/button.component'; +import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component'; +import { KeyValueEditorComponent } from '../../../shared/ui/key-value-editor/key-value-editor.component'; +import { MediaPickerComponent } from '../../../shared/media/media-picker/media-picker.component'; +import { MediaAsset } from '../../../core/media/models/media-asset.model'; +import { FooterPaymentIconConfig, FooterSocialLinkConfig } from '../../../shared/models/config'; + +/** Mirrors the HTTP_URL check in project-validator.service.ts (kept as a local duplicate: shared/ui must not import from features). */ +const HTTP_URL = /^https?:\/\/\S+$/; + +type FooterMediaTarget = 'logo' | { type: 'paymentIcon'; index: number }; @Component({ selector: 'app-project-editor-footer-section', standalone: true, - imports: [FormsModule, TranslatePipe, InputComponent, FormFieldComponent], + imports: [ + FormsModule, + TranslatePipe, + InputComponent, + FormFieldComponent, + ButtonComponent, + SectionCardComponent, + KeyValueEditorComponent, + MediaPickerComponent + ], templateUrl: './footer-section.component.html', styleUrls: ['./section.shared.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -16,35 +36,88 @@ import { FormFieldComponent } from '../../../shared/ui/form-field/form-field.com export class ProjectEditorFooterSectionComponent { private readonly facade = inject(ProjectEditorFacade); readonly bootstrap = this.facade.bootstrap; - readonly paymentIconsValue = computed(() => (this.bootstrap()?.footer?.paymentIcons ?? []).map(icon => `${icon.src}|${icon.alt}`).join('\n')); - readonly socialLinksValue = computed(() => (this.bootstrap()?.footer?.socialLinks ?? []).map(link => `${link.id}|${link.label}|${link.url}`).join('\n')); + readonly staticPagesValue = computed(() => (this.bootstrap()?.footer?.staticPageKeys ?? this.bootstrap()?.footer?.legalPageKeys ?? []).join(', ')); readonly copyrightValue = computed(() => { const value = this.bootstrap()?.footer?.copyrightText; return typeof value === 'string' ? value : ''; }); + readonly paymentIconRows = computed(() => this.bootstrap()?.footer?.paymentIcons ?? []); + readonly socialLinkRows = computed(() => this.bootstrap()?.footer?.socialLinks ?? []); + + protected mediaPickerOpen = false; + private mediaPickerTarget: FooterMediaTarget | null = null; + + readonly createPaymentIconRow = (): FooterPaymentIconConfig => ({ src: '', alt: '' }); + + readonly createSocialLinkRow = (): FooterSocialLinkConfig => { + const index = (this.bootstrap()?.footer?.socialLinks?.length ?? 0) + 1; + return { id: `social-${index}`, label: '', url: '' }; + }; updateCompanyName(value: string): void { this.facade.updateBootstrap(current => ({ ...current, company: { ...current.company, companyName: value } })); } updateAddress(value: string): void { this.facade.updateBootstrap(current => ({ ...current, company: { ...current.company, address: { ...current.company.address, street: value } } })); } updatePhone(value: string): void { this.facade.updateBootstrap(current => ({ ...current, company: { ...current.company, contacts: { ...current.company.contacts, phone: value } }, branding: { ...current.branding, supportPhone: value } })); } updateEmail(value: string): void { this.facade.updateBootstrap(current => ({ ...current, company: { ...current.company, contacts: { ...current.company.contacts, email: value } }, branding: { ...current.branding, supportEmail: value } })); } updateCopyright(value: string): void { this.facade.updateBootstrap(current => ({ ...current, footer: { ...current.footer, copyrightText: value } })); } - updatePaymentIcons(value: string): void { - const paymentIcons = value.split('\n').map(line => line.trim()).filter(Boolean).map((line, index) => { - const [src, alt] = line.split('|'); - return { src: src?.trim() ?? '', alt: alt?.trim() ?? `icon-${index + 1}` }; - }); - this.facade.updateBootstrap(current => ({ ...current, footer: { ...current.footer, paymentIcons } })); - } - updateSocialLinks(value: string): void { - const socialLinks = value.split('\n').map(line => line.trim()).filter(Boolean).map((line, index) => { - const [id, label, url] = line.split('|'); - return { id: id?.trim() || `social-${index + 1}`, label: label?.trim() || '', url: url?.trim() || '' }; - }); - this.facade.updateBootstrap(current => ({ ...current, footer: { ...current.footer, socialLinks } })); - } updateStaticPages(value: string): void { const staticPageKeys = value.split(',').map(item => item.trim()).filter(Boolean); this.facade.updateBootstrap(current => ({ ...current, footer: { ...current.footer, staticPageKeys, legalPageKeys: staticPageKeys } })); } + + updateLogo(value: string): void { + this.facade.updateBootstrap(current => ({ ...current, footer: { ...current.footer, logoUrl: value } })); + } + + openLogoPicker(): void { + this.mediaPickerTarget = 'logo'; + this.mediaPickerOpen = true; + } + + openPaymentIconPicker(index: number): void { + this.mediaPickerTarget = { type: 'paymentIcon', index }; + this.mediaPickerOpen = true; + } + + onImagePicked(asset: MediaAsset): void { + const target = this.mediaPickerTarget; + if (target === 'logo') { + this.updateLogo(asset.url); + } else if (target?.type === 'paymentIcon') { + this.updatePaymentIconSrc(target.index, asset.url); + } + this.mediaPickerOpen = false; + } + + onPaymentIconsChange(rows: FooterPaymentIconConfig[]): void { + this.facade.updateBootstrap(current => ({ ...current, footer: { ...current.footer, paymentIcons: rows } })); + } + + updatePaymentIconSrc(index: number, src: string): void { + const rows = this.paymentIconRows().map((row, i) => (i === index ? { ...row, src } : row)); + this.onPaymentIconsChange(rows); + } + + updatePaymentIconAlt(index: number, alt: string): void { + const rows = this.paymentIconRows().map((row, i) => (i === index ? { ...row, alt } : row)); + this.onPaymentIconsChange(rows); + } + + onSocialLinksChange(rows: FooterSocialLinkConfig[]): void { + this.facade.updateBootstrap(current => ({ ...current, footer: { ...current.footer, socialLinks: rows } })); + } + + updateSocialLinkLabel(index: number, label: string): void { + const rows = this.socialLinkRows().map((row, i) => (i === index ? { ...row, label } : row)); + this.onSocialLinksChange(rows); + } + + updateSocialLinkUrl(index: number, url: string): void { + const rows = this.socialLinkRows().map((row, i) => (i === index ? { ...row, url } : row)); + this.onSocialLinksChange(rows); + } + + isValidUrl(url: string): boolean { + return !url || HTTP_URL.test(url); + } } diff --git a/src/app/features/project-editor/sections/header-section.component.html b/src/app/features/project-editor/sections/header-section.component.html index fe2ad0e..7c4b1c7 100644 --- a/src/app/features/project-editor/sections/header-section.component.html +++ b/src/app/features/project-editor/sections/header-section.component.html @@ -1,14 +1,13 @@ @if (bootstrap(); as bootstrap) { -
-

{{ 'builder.header' | translate }}

+
@for (item of items; track item.key) { }
-
+ } diff --git a/src/app/features/project-editor/sections/header-section.component.ts b/src/app/features/project-editor/sections/header-section.component.ts index 043bfab..0cb7f7a 100644 --- a/src/app/features/project-editor/sections/header-section.component.ts +++ b/src/app/features/project-editor/sections/header-section.component.ts @@ -3,11 +3,13 @@ import { FormsModule } from '@angular/forms'; import { ProjectEditorFacade } from '../facade/project-editor.facade'; import { TranslatePipe } from '../../../i18n/translate.pipe'; import { HeaderConfig } from '../../../shared/models/config'; +import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component'; +import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component'; @Component({ selector: 'app-project-editor-header-section', standalone: true, - imports: [FormsModule, TranslatePipe], + imports: [FormsModule, TranslatePipe, SectionCardComponent, ToggleComponent], templateUrl: './header-section.component.html', styleUrls: ['./section.shared.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index dcae278..ea8b78a 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -452,6 +452,15 @@ export const en: Translations = { copyright: 'Copyright', paymentIcons: 'Payment Icons', socialLinks: 'Social Links', + footerLogo: 'Footer Logo', + addPaymentIcon: 'Add Payment Icon', + removePaymentIcon: 'Remove', + iconAltLabel: 'Icon Label', + addSocialLink: 'Add Social Link', + removeSocialLink: 'Remove', + socialLinkLabel: 'Platform', + socialLinkUrl: 'URL', + invalidUrl: 'Enter a valid URL starting with http:// or https://.', createPage: 'Create Page', deletePage: 'Delete Page', pageId: 'Page ID', @@ -590,6 +599,9 @@ export const en: Translations = { copyrightDesc: 'Copyright line shown at the bottom of every page.', paymentIconsDesc: 'One payment icon entry per line (image URL and label) shown in the footer.', socialLinksDesc: 'One social link entry per line (platform and URL) shown in the footer.', + footerLogoDesc: 'Logo shown in the footer, if different from the header logo.', + iconAltLabelDesc: 'Alt text describing the payment icon for accessibility.', + socialLinkUrlDesc: 'Full URL to the social media profile, must start with http:// or https://.', staticPagesFieldDesc: 'Static pages (about, terms, privacy, etc.) linked from the footer.', sectionVisibleDesc: 'Whether this homepage section is shown to shoppers.', layoutStrategyLabel: 'Layout strategy', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 58e62d9..c678129 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -452,6 +452,15 @@ export const hy: Translations = { copyright: 'Copyright', paymentIcons: 'Վճարման icon-ներ', socialLinks: 'Սոցիալական հղումներ', + footerLogo: 'Footer-ի լոգո', + addPaymentIcon: 'Ավելացնել վճարային icon', + removePaymentIcon: 'Հեռացնել', + iconAltLabel: 'Icon-ի պիտակ', + addSocialLink: 'Ավելացնել սոցիալական հղում', + removeSocialLink: 'Հեռացնել', + socialLinkLabel: 'Հարթակ', + socialLinkUrl: 'URL', + invalidUrl: 'Մուտքագրեք վավեր URL, որը սկսվում է http:// կամ https://։', createPage: 'Ստեղծել էջ', deletePage: 'Ջնջել էջը', pageId: 'Էջի ID', @@ -590,6 +599,9 @@ export const hy: Translations = { copyrightDesc: 'Copyright տողը՝ ցուցադրվում է յուրաքանչյուր էջի ներքևում։', paymentIconsDesc: 'Մեկ վճարային icon մեկ տողում (պատկերի URL և պիտակ)՝ ցուցադրվում է footer-ում։', socialLinksDesc: 'Մեկ սոցիալական հղում մեկ տողում (հարթակ և URL)՝ ցուցադրվում է footer-ում։', + footerLogoDesc: 'Footer-ում ցուցադրվող լոգո, եթե տարբերվում է header-ի լոգոյից։', + iconAltLabelDesc: 'Վճարային icon-ի այլընտրանքային տեքստ՝ մատչելիության համար։', + socialLinkUrlDesc: 'Սոցիալական պրոֆիլի ամբողջական URL, պետք է սկսվի http:// կամ https://-ով։', staticPagesFieldDesc: 'Ստատիկ էջեր (մեր մասին, պայմաններ, գաղտնիության քաղաքականություն և այլն), որոնց հղում է անում footer-ը։', sectionVisibleDesc: 'Արդյո՞ք գլխավոր էջի այս սեկցիան ցուցադրվում է գնորդներին։', layoutStrategyLabel: 'Դասավորության ռազմավարություն', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 198e60a..3f2bb84 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -452,6 +452,15 @@ export const ru: Translations = { copyright: 'Копирайт', paymentIcons: 'Иконки оплаты', socialLinks: 'Социальные ссылки', + footerLogo: 'Логотип в футере', + addPaymentIcon: 'Добавить иконку оплаты', + removePaymentIcon: 'Удалить', + iconAltLabel: 'Подпись иконки', + addSocialLink: 'Добавить соцсеть', + removeSocialLink: 'Удалить', + socialLinkLabel: 'Платформа', + socialLinkUrl: 'URL', + invalidUrl: 'Введите корректный URL, начинающийся с http:// или https://.', createPage: 'Создать страницу', deletePage: 'Удалить страницу', pageId: 'ID страницы', @@ -590,6 +599,9 @@ export const ru: Translations = { copyrightDesc: 'Строка копирайта внизу каждой страницы.', paymentIconsDesc: 'По одной иконке оплаты на строку (URL изображения и подпись), показываются в футере.', socialLinksDesc: 'По одной социальной ссылке на строку (платформа и URL), показываются в футере.', + footerLogoDesc: 'Логотип, отображаемый в футере, если отличается от логотипа в шапке.', + iconAltLabelDesc: 'Альтернативный текст для иконки оплаты (для доступности).', + socialLinkUrlDesc: 'Полный URL профиля в соцсети, должен начинаться с http:// или https://.', staticPagesFieldDesc: 'Статические страницы (о компании, условия, политика и т.д.), на которые ссылается футер.', sectionVisibleDesc: 'Показывать ли эту секцию главной страницы покупателям.', layoutStrategyLabel: 'Стратегия раскладки', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 92b1b38..644bdee 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -450,6 +450,15 @@ export interface Translations { copyright: string; paymentIcons: string; socialLinks: string; + footerLogo: string; + addPaymentIcon: string; + removePaymentIcon: string; + iconAltLabel: string; + addSocialLink: string; + removeSocialLink: string; + socialLinkLabel: string; + socialLinkUrl: string; + invalidUrl: string; createPage: string; deletePage: string; pageId: string; @@ -588,6 +597,9 @@ export interface Translations { copyrightDesc: string; paymentIconsDesc: string; socialLinksDesc: string; + footerLogoDesc: string; + iconAltLabelDesc: string; + socialLinkUrlDesc: string; staticPagesFieldDesc: string; sectionVisibleDesc: string; layoutStrategyLabel: string;