diff --git a/src/app/features/project-editor/services/project-validator.service.ts b/src/app/features/project-editor/services/project-validator.service.ts new file mode 100644 index 0000000..5f16b8c --- /dev/null +++ b/src/app/features/project-editor/services/project-validator.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { BootstrapConfig } from '../../../shared/models/config'; + +export interface ProjectValidationIssue { + code: string; + message: string; +} + +const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; +const HTTP_URL = /^https?:\/\/\S+$/; + +@Injectable({ providedIn: 'root' }) +export class ProjectValidator { + validate(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + return [ + ...this.brandingIssues(bootstrap), + ...this.languageIssues(bootstrap), + ...this.urlIssues(bootstrap), + ...this.duplicateSlugIssues(bootstrap), + ...this.homepageIssues(bootstrap), + ...this.navigationIssues(bootstrap), + ...this.colorIssues(bootstrap), + ]; + } + + private brandingIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + return bootstrap.branding.logoUrl ? [] : [{ code: 'missing-logo', message: 'builder.validationMissingLogo' }]; + } + + private languageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + return bootstrap.localization.supportedLocales.length > 0 ? [] : [{ code: 'no-languages', message: 'builder.validationNoLanguages' }]; + } + + private urlIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const url = bootstrap.tenant.websiteBaseUrl; + return !url || HTTP_URL.test(url) ? [] : [{ code: 'invalid-url', message: 'builder.validationInvalidUrl' }]; + } + + private duplicateSlugIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const staticPages = bootstrap.staticPages; + if (!staticPages || Array.isArray(staticPages)) { + return []; + } + const slugs = Object.values(staticPages).map(page => page.slug); + const hasDuplicates = slugs.some((slug, index) => slugs.indexOf(slug) !== index); + return hasDuplicates ? [{ code: 'duplicate-slugs', message: 'builder.validationDuplicateSlugs' }] : []; + } + + private homepageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const homePage = bootstrap.pages.find(page => page.key === 'home' || page.route.path === '/'); + if (!homePage || homePage.sections.length === 0) { + return [{ code: 'empty-homepage', message: 'builder.validationEmptyHomepage' }]; + } + const hasMissingWidgetType = homePage.sections.some(section => section.widgets.some(widget => !widget.type?.trim())); + return hasMissingWidgetType ? [{ code: 'missing-widget', message: 'builder.validationMissingWidget' }] : []; + } + + private navigationIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const keyOf = (item: { label?: string | Record; route?: string }): string => + `${typeof item.label === 'string' ? item.label : JSON.stringify(item.label ?? {})}|${item.route ?? ''}`; + const keys = bootstrap.navigation.header.map(keyOf); + const hasDuplicates = keys.some((key, index) => keys.indexOf(key) !== index); + return hasDuplicates ? [{ code: 'duplicate-nav-links', message: 'builder.validationDuplicateNavLinks' }] : []; + } + + private colorIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] { + const invalid = Object.values(bootstrap.theme.palette).some(value => !HEX_COLOR.test(value)); + return invalid ? [{ code: 'invalid-colors', message: 'builder.validationInvalidColors' }] : []; + } +} diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index ec621c0..008c031 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -504,6 +504,14 @@ export const en: Translations = { importSource: 'Import Source', importError: 'Import failed. Invalid bootstrap JSON.', featureFlags: 'Feature Flags', + validationMissingLogo: 'Branding is missing a logo.', + validationNoLanguages: 'No languages are configured.', + validationInvalidUrl: 'The marketplace URL is invalid.', + validationDuplicateSlugs: 'Two or more static pages share the same slug.', + validationEmptyHomepage: 'The homepage has no sections.', + validationMissingWidget: 'A homepage section has a widget with no type.', + validationDuplicateNavLinks: 'Two or more header navigation links are duplicates.', + validationInvalidColors: 'One or more theme colors are not valid hex colors.', }, staticPages: { notFound: 'Page not found', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 150bbba..483e80e 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -504,6 +504,14 @@ export const hy: Translations = { importSource: 'Ներմուծման աղբյուր', importError: 'Ներմուծումը ձախողվեց։ Սխալ bootstrap JSON։', featureFlags: 'Feature flags', + validationMissingLogo: 'Բրենդինգում բացակայում է լոգոն։', + validationNoLanguages: 'Կարգավորված լեզուներ չկան։', + validationInvalidUrl: 'Մարքեթփլեյսի URL-ը սխալ է։', + validationDuplicateSlugs: 'Երկու կամ ավելի ստատիկ էջեր ունեն նույն slug-ը։', + validationEmptyHomepage: 'Գլխավոր էջում սեկցիաներ չկան։', + validationMissingWidget: 'Գլխավոր էջի սեկցիաներից մեկն ունի վիջեթ առանց տիպի։', + validationDuplicateNavLinks: 'Վերնագրի նավիգացիայում կան կրկնվող հղումներ։', + validationInvalidColors: 'Թեմայի գույներից մեկը կամ մի քանիսը վավեր hex գույն չեն։', }, staticPages: { notFound: 'Էջը չի գտնվել', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 79c1731..7d46a2b 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -504,6 +504,14 @@ export const ru: Translations = { importSource: 'Источник импорта', importError: 'Импорт не удался. Некорректный JSON bootstrap.', featureFlags: 'Фичи-флаги', + validationMissingLogo: 'В брендинге отсутствует логотип.', + validationNoLanguages: 'Не настроены языки.', + validationInvalidUrl: 'Некорректный URL маркетплейса.', + validationDuplicateSlugs: 'Две или более статические страницы имеют одинаковый slug.', + validationEmptyHomepage: 'На главной странице нет секций.', + validationMissingWidget: 'В секции главной страницы есть виджет без типа.', + validationDuplicateNavLinks: 'Две или более ссылки в навигации шапки дублируются.', + validationInvalidColors: 'Один или несколько цветов темы указаны некорректно.', }, staticPages: { notFound: 'Страница не найдена', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 48bcc43..d1b15f8 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -502,6 +502,14 @@ export interface Translations { importSource: string; importError: string; featureFlags: string; + validationMissingLogo: string; + validationNoLanguages: string; + validationInvalidUrl: string; + validationDuplicateSlugs: string; + validationEmptyHomepage: string; + validationMissingWidget: string; + validationDuplicateNavLinks: string; + validationInvalidColors: string; }; staticPages: { notFound: string;