From 9e44215dd5ebc7aded65ea940804eed1eea9ee79 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Fri, 17 Jul 2026 16:57:43 +0400 Subject: [PATCH] feat(project-editor): footer validation rules (contact email, social link URLs, payment icons) Real gaps, not fabricated: isValidEmail existed in primitives.ts but was never called anywhere; social-link URL check only lived as a per-row template hint (never blocked publish or set the nav badge); payment icons with only src or only alt set were silently accepted. - invalid-contact-email: company.contacts.email must be a valid email (error) - invalid-social-link-url: footer.socialLinks entries need a valid http(s) URL (warning) - incomplete-payment-icon: a payment icon needs both src and alt, or neither (warning) Wired into footer-section via the existing fieldError() pattern. Header has no equivalent gap today (every header field is a bool/enum, always valid by construction) so nothing was added there. Co-Authored-By: Claude Sonnet 5 --- .../sections/footer-section.component.html | 8 +++++- .../sections/footer-section.component.ts | 6 +++++ .../services/project-validator.service.ts | 26 +++++++++++++++++++ src/app/i18n/en.ts | 3 +++ src/app/i18n/hy.ts | 3 +++ src/app/i18n/ru.ts | 3 +++ src/app/i18n/translations.ts | 3 +++ 7 files changed, 51 insertions(+), 1 deletion(-) 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 d6e8a29..658125e 100644 --- a/src/app/features/project-editor/sections/footer-section.component.html +++ b/src/app/features/project-editor/sections/footer-section.component.html @@ -4,7 +4,7 @@ - + @@ -12,6 +12,9 @@
+ @if (fieldError('footer.paymentIcons'); as msg) { +

{{ msg }}

+ } + @if (fieldError('footer.socialLinks'); as msg) { +

{{ msg }}

+ } { + const messageKey = this.facade.fieldError(key); + return messageKey ? this.translate.t(messageKey) : null; + }; readonly staticPagesValue = computed(() => (this.bootstrap()?.footer?.staticPageKeys ?? this.bootstrap()?.footer?.legalPageKeys ?? []).join(', ')); readonly copyrightValue = computed(() => { diff --git a/src/app/features/project-editor/services/project-validator.service.ts b/src/app/features/project-editor/services/project-validator.service.ts index b76b7c5..23f290d 100644 --- a/src/app/features/project-editor/services/project-validator.service.ts +++ b/src/app/features/project-editor/services/project-validator.service.ts @@ -3,6 +3,7 @@ import { BootstrapConfig } from '../../../shared/models/config'; import { ProjectEditorSectionId } from '../models/project-editor.model'; import { extractStyleBlocks, + isValidEmail, isValidHexColor, isValidHttpUrl, normalizeRoute, @@ -56,6 +57,9 @@ export class ProjectValidator { ...this.cssIssues(bootstrap), ...this.translationIssues(bootstrap), ...this.layoutIssues(bootstrap), + ...this.footerContactIssues(bootstrap), + ...this.footerSocialLinkIssues(bootstrap), + ...this.footerPaymentIconIssues(bootstrap), ]; } @@ -222,4 +226,26 @@ export class ProjectValidator { ? [error('invalid-layouts', 'builder.validationInvalidLayouts', 'theme', 'layout.type')] : []; } + + private footerContactIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const email = bootstrap.company?.contacts?.email; + return !email || isValidEmail(email) + ? [] + : [error('invalid-contact-email', 'builder.validationInvalidContactEmail', 'footer', 'company.contacts.email')]; + } + + private footerSocialLinkIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const hasInvalidUrl = (bootstrap.footer?.socialLinks ?? []).some(link => !!link.url && !isValidHttpUrl(link.url)); + return hasInvalidUrl + ? [warning('invalid-social-link-url', 'builder.validationInvalidSocialLinkUrl', 'footer', 'footer.socialLinks')] + : []; + } + + /** A payment icon row with only one of src/alt set is a broken image reference or missing accessibility text. */ + private footerPaymentIconIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const hasIncompleteRow = (bootstrap.footer?.paymentIcons ?? []).some(icon => !!icon.src !== !!icon.alt); + return hasIncompleteRow + ? [warning('incomplete-payment-icon', 'builder.validationIncompletePaymentIcon', 'footer', 'footer.paymentIcons')] + : []; + } } diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 0a2651c..971ca6a 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -550,6 +550,9 @@ export const en: Translations = { validationInvalidCss: 'A static page contains invalid CSS.', validationDuplicateRoutes: 'Two or more pages share the same route.', validationInvalidWidgetConfig: 'A widget is missing a required field (id, type, version, or props).', + validationInvalidContactEmail: 'The company contact email is not a valid email address.', + validationInvalidSocialLinkUrl: 'One or more footer social links have an invalid URL.', + validationIncompletePaymentIcon: 'A footer payment icon is missing its image or alt text.', undo: 'Undo', redo: 'Redo', statusDraft: 'Draft', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index cc81e5d..2a764f5 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -550,6 +550,9 @@ export const hy: Translations = { validationInvalidCss: 'Ստատիկ էջը պարունակում է անվավեր CSS։', validationDuplicateRoutes: 'Երկու կամ ավելի էջ ունեն նույն երթուղին։', validationInvalidWidgetConfig: 'Վիջեթին բացակայում է պարտադիր դաշտ (id, type, version կամ props)։', + validationInvalidContactEmail: 'Ընկերության կոնտակտային էլ. փոստը վավեր չէ։', + validationInvalidSocialLinkUrl: 'Ֆուտերի սոցիալական հղումներից մեկը կամ մի քանիսը վավեր URL չունեն։', + validationIncompletePaymentIcon: 'Ֆուտերի վճարային պատկերակին բացակայում է պատկերը կամ alt տեքստը։', undo: 'Հետարկել', redo: 'Կրկնել', statusDraft: 'Սևագիր', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index c844f89..cfadaee 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -550,6 +550,9 @@ export const ru: Translations = { validationInvalidCss: 'Статическая страница содержит недопустимый CSS.', validationDuplicateRoutes: 'Две или более страницы используют один и тот же маршрут.', validationInvalidWidgetConfig: 'У виджета отсутствует обязательное поле (id, type, version или props).', + validationInvalidContactEmail: 'Контактный email компании указан некорректно.', + validationInvalidSocialLinkUrl: 'У одной или нескольких социальных ссылок в футере некорректный URL.', + validationIncompletePaymentIcon: 'У иконки оплаты в футере отсутствует изображение или альтернативный текст.', undo: 'Отменить', redo: 'Повторить', statusDraft: 'Черновик', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 9590b6a..a2ef5c1 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -548,6 +548,9 @@ export interface Translations { validationInvalidCss: string; validationDuplicateRoutes: string; validationInvalidWidgetConfig: string; + validationInvalidContactEmail: string; + validationInvalidSocialLinkUrl: string; + validationIncompletePaymentIcon: string; undo: string; redo: string; statusDraft: string;