diff --git a/src/app/features/content-management/components/static-page-preview/static-page-preview.component.html b/src/app/features/content-management/components/static-page-preview/static-page-preview.component.html new file mode 100644 index 0000000..e9a13dd --- /dev/null +++ b/src/app/features/content-management/components/static-page-preview/static-page-preview.component.html @@ -0,0 +1,19 @@ +
+
+ @for (device of devices; track device.id) { + {{ device.labelKey | translate }} + } +
+
+
+ @if (title()) { +

{{ title() }}

+ } +
+
+
+
diff --git a/src/app/features/content-management/components/static-page-preview/static-page-preview.component.scss b/src/app/features/content-management/components/static-page-preview/static-page-preview.component.scss new file mode 100644 index 0000000..34e5fba --- /dev/null +++ b/src/app/features/content-management/components/static-page-preview/static-page-preview.component.scss @@ -0,0 +1,47 @@ +.static-page-preview { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.static-page-preview__toolbar { + display: flex; + gap: 0.5rem; +} + +.static-page-preview__viewport { + display: flex; + justify-content: center; + padding: 1rem; + background: var(--bg-secondary, #f5f5f5); + border-radius: var(--radius-md, 12px); + overflow-x: auto; +} + +.static-page-preview__frame { + max-width: 100%; + background: var(--bg-primary, #fff); + border: 1px solid var(--border-color, #d3dad9); + border-radius: var(--radius-md, 12px); + padding: 1.5rem; + box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.1)); +} + +.static-page-preview__title { + margin: 0 0 1rem; + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary, #1e3c38); +} + +.static-page-preview__content { + color: var(--text-primary, #1e3c38); + line-height: 1.6; + overflow-wrap: break-word; +} + +@media (prefers-reduced-motion: reduce) { + .static-page-preview__frame { + transition: none; + } +} diff --git a/src/app/features/content-management/components/static-page-preview/static-page-preview.component.ts b/src/app/features/content-management/components/static-page-preview/static-page-preview.component.ts new file mode 100644 index 0000000..0cc9968 --- /dev/null +++ b/src/app/features/content-management/components/static-page-preview/static-page-preview.component.ts @@ -0,0 +1,55 @@ +import { ChangeDetectionStrategy, Component, SecurityContext, computed, inject, input, signal } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; + +export type PreviewDevice = 'desktop' | 'tablet' | 'mobile'; + +interface DevicePreset { + readonly id: PreviewDevice; + readonly labelKey: string; + readonly width: string; +} + +const DEVICE_PRESETS: readonly DevicePreset[] = [ + { id: 'desktop', labelKey: 'staticPages.previewDesktop', width: '100%' }, + { id: 'tablet', labelKey: 'staticPages.previewTablet', width: '768px' }, + { id: 'mobile', labelKey: 'staticPages.previewMobile', width: '375px' }, +]; + +/** + * Client-side, sanitized preview of a static page's HTML at desktop/tablet/ + * mobile widths - no navigation, no publish, no dependency on + * ProjectEditorPreviewService's full-app reload. Uses the same + * DomSanitizer.sanitize(SecurityContext.HTML, ...) pattern as the real + * storefront renderer (StaticPageComponent) so what you preview here is + * exactly what will render live. + */ +@Component({ + selector: 'app-static-page-preview', + standalone: true, + imports: [TranslatePipe, ButtonComponent], + templateUrl: './static-page-preview.component.html', + styleUrl: './static-page-preview.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StaticPagePreviewComponent { + private readonly sanitizer = inject(DomSanitizer); + + readonly html = input.required(); + readonly title = input(''); + + readonly devices = DEVICE_PRESETS; + readonly activeDevice = signal('desktop'); + + readonly activeWidth = computed(() => this.devices.find(device => device.id === this.activeDevice())?.width ?? '100%'); + + readonly safeHtml = computed(() => { + const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, this.html()) ?? ''; + return this.sanitizer.bypassSecurityTrustHtml(sanitized); + }); + + setDevice(device: PreviewDevice): void { + this.activeDevice.set(device); + } +} diff --git a/src/app/features/content-management/components/static-pages-editor.component.html b/src/app/features/content-management/components/static-pages-editor.component.html index da414fa..7806588 100644 --- a/src/app/features/content-management/components/static-pages-editor.component.html +++ b/src/app/features/content-management/components/static-pages-editor.component.html @@ -78,6 +78,7 @@ {{ 'staticPages.duplicatePage' | translate }} + {{ 'staticPages.previewToggle' | translate }} @if (page.status === 'published') { {{ 'staticPages.unpublishPageAction' | translate }} } @else { @@ -87,6 +88,10 @@ + @if (previewOpenPageId() === page.id) { + + } +
diff --git a/src/app/features/content-management/components/static-pages-editor.component.ts b/src/app/features/content-management/components/static-pages-editor.component.ts index bafdfed..3e01d88 100644 --- a/src/app/features/content-management/components/static-pages-editor.component.ts +++ b/src/app/features/content-management/components/static-pages-editor.component.ts @@ -17,6 +17,7 @@ import { SelectComponent, SelectOption } from '../../../shared/ui/select/select. import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component'; import { MediaPickerComponent } from '../../../shared/media/media-picker/media-picker.component'; import { MediaAsset } from '../../../core/media/models/media-asset.model'; +import { StaticPagePreviewComponent } from './static-page-preview/static-page-preview.component'; type StatusFilter = 'all' | ContentPageStatus; const ALL_LOCALES_FILTER = 'all'; @@ -38,6 +39,7 @@ type MediaField = 'heroImage' | 'thumbnail'; SelectComponent, ToggleComponent, MediaPickerComponent, + StaticPagePreviewComponent, ], templateUrl: './static-pages-editor.component.html', styleUrls: ['../../project-editor/sections/section.shared.scss', './static-pages-editor.component.scss'], @@ -119,6 +121,22 @@ export class StaticPagesEditorComponent { return JSON.stringify(page) !== JSON.stringify(original); } + readonly previewOpenPageId = signal(null); + + togglePreview(pageId: string): void { + this.previewOpenPageId.set(this.previewOpenPageId() === pageId ? null : pageId); + } + + previewHtml(page: ContentPage): string { + const defaultLocale = this.bootstrap()?.localization.defaultLocale ?? 'en'; + return page.translations[defaultLocale]?.html ?? Object.values(page.translations)[0]?.html ?? ''; + } + + previewTitle(page: ContentPage): string { + const defaultLocale = this.bootstrap()?.localization.defaultLocale ?? 'en'; + return page.translations[defaultLocale]?.title || page.title; + } + protected mediaPickerOpen = false; private mediaPickerTarget: { pageId: string; field: MediaField } | null = null; diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index a17d55a..af2c689 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -748,6 +748,10 @@ export const en: Translations = { customTemplateLabel: 'Custom template', seoRobotsLabel: 'Robots', disabledBadge: 'Disabled', + previewDesktop: 'Desktop', + previewTablet: 'Tablet', + previewMobile: 'Mobile', + previewToggle: 'Preview', }, widgets: { unavailable: 'Widget unavailable', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 62c9eab..5484405 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -748,6 +748,10 @@ export const hy: Translations = { customTemplateLabel: 'Հատուկ ձևանմուշ', seoRobotsLabel: 'Robots', disabledBadge: 'Անջատված', + previewDesktop: 'Դեսքթոփ', + previewTablet: 'Պլանշետ', + previewMobile: 'Հեռախոս', + previewToggle: 'Նախադիտում', }, widgets: { unavailable: 'Վիջեթը հասանելի չէ', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 201f08f..c61c31f 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -748,6 +748,10 @@ export const ru: Translations = { customTemplateLabel: 'Пользовательский шаблон', seoRobotsLabel: 'Robots', disabledBadge: 'Отключена', + previewDesktop: 'Десктоп', + previewTablet: 'Планшет', + previewMobile: 'Телефон', + previewToggle: 'Превью', }, widgets: { unavailable: 'Виджет недоступен', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 33b933c..5b5d43f 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -746,6 +746,10 @@ export interface Translations { customTemplateLabel: string; seoRobotsLabel: string; disabledBadge: string; + previewDesktop: string; + previewTablet: string; + previewMobile: string; + previewToggle: string; }; widgets: { unavailable: string;