feat(admin): sprint 19 admin dashboard, routing, i18n
- Add admin dashboard feature (models/gateway/facade/components/page) - Wire admin/products routes and backoffice coming-soon placeholders - Add lastPublishedAt to ProjectEditorFacade/state - Add dashboard i18n keys (en/ru/hy) and docs/ADMIN.md Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ export class ProjectEditorFacade {
|
||||
status: 'draft',
|
||||
lastSavedBootstrap: null,
|
||||
lastSavedAt: null,
|
||||
lastPublishedAt: null,
|
||||
draftRestored: false,
|
||||
});
|
||||
|
||||
@@ -40,6 +41,7 @@ export class ProjectEditorFacade {
|
||||
readonly homepageWidgets = computed(() => this.homepagePage()?.sections.flatMap(section => section.widgets.map(widget => ({ sectionId: section.id, sectionType: section.type, widget }))) ?? []);
|
||||
readonly status = computed(() => this.state().status);
|
||||
readonly lastSavedAt = computed(() => this.state().lastSavedAt);
|
||||
readonly lastPublishedAt = computed(() => this.state().lastPublishedAt);
|
||||
readonly draftRestored = computed(() => this.state().draftRestored);
|
||||
readonly validationIssues = computed(() => {
|
||||
const current = this.bootstrap();
|
||||
@@ -182,6 +184,7 @@ export class ProjectEditorFacade {
|
||||
lastSavedBootstrap: JSON.parse(JSON.stringify(current)),
|
||||
originalBootstrap: JSON.parse(JSON.stringify(current)),
|
||||
lastSavedAt: savedAt,
|
||||
lastPublishedAt: savedAt,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ProjectEditorState {
|
||||
status: 'draft' | 'published';
|
||||
lastSavedBootstrap: BootstrapConfig | null;
|
||||
lastSavedAt: number | null;
|
||||
lastPublishedAt: number | null;
|
||||
draftRestored: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface ProjectValidationIssue {
|
||||
|
||||
const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
||||
const HTTP_URL = /^https?:\/\/\S+$/;
|
||||
const KNOWN_PLATFORM_LAYOUT_TYPES = new Set(['default', 'sidebar-left', 'carousel-home', 'minimal']);
|
||||
const KNOWN_SECTION_LAYOUT_STRATEGIES = new Set(['stack', 'grid', 'hero', 'carousel', 'split']);
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProjectValidator {
|
||||
@@ -20,6 +22,8 @@ export class ProjectValidator {
|
||||
...this.homepageIssues(bootstrap),
|
||||
...this.navigationIssues(bootstrap),
|
||||
...this.colorIssues(bootstrap),
|
||||
...this.translationIssues(bootstrap),
|
||||
...this.layoutIssues(bootstrap),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -69,4 +73,35 @@ export class ProjectValidator {
|
||||
const invalid = Object.values(bootstrap.theme.palette).some(value => !HEX_COLOR.test(value));
|
||||
return invalid ? [{ code: 'invalid-colors', message: 'builder.validationInvalidColors' }] : [];
|
||||
}
|
||||
|
||||
private translationIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
|
||||
const otherLocales = bootstrap.localization.supportedLocales.filter(locale => locale !== bootstrap.localization.defaultLocale);
|
||||
if (otherLocales.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const headerMissing = bootstrap.navigation.header.some(item => {
|
||||
const label = item.label;
|
||||
if (!label || typeof label !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return otherLocales.some(locale => !label[locale]?.trim());
|
||||
});
|
||||
|
||||
const staticPages = bootstrap.staticPages;
|
||||
const staticPagesMissing = staticPages && !Array.isArray(staticPages)
|
||||
? Object.values(staticPages).some(page => page.translations && otherLocales.some(locale => !page.translations![locale]))
|
||||
: false;
|
||||
|
||||
return headerMissing || staticPagesMissing ? [{ code: 'missing-translations', message: 'builder.validationMissingTranslations' }] : [];
|
||||
}
|
||||
|
||||
private layoutIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
|
||||
const invalidPlatformLayout = !!bootstrap.layout?.type && !KNOWN_PLATFORM_LAYOUT_TYPES.has(bootstrap.layout.type);
|
||||
const invalidSectionLayout = bootstrap.pages.some(page =>
|
||||
page.sections.some(section => !!section.layout?.strategy && !KNOWN_SECTION_LAYOUT_STRATEGIES.has(section.layout.strategy)),
|
||||
);
|
||||
|
||||
return invalidPlatformLayout || invalidSectionLayout ? [{ code: 'invalid-layouts', message: 'builder.validationInvalidLayouts' }] : [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user