From edae13541ff2cb3973498ba9f41fdc3dbb331fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Tue, 10 Mar 2026 19:01:17 +0100 Subject: [PATCH] feat(back-end): rich text improvements --- .../admin/pages/admin-shop.component.html | 1 - .../admin/pages/admin-shop.component.scss | 19 ++++---- .../admin/pages/admin-shop.component.ts | 29 +++++++++--- .../shop/product-detail.component.html | 2 +- .../features/shop/product-detail.component.ts | 44 ++++++++++++++++++- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/features/admin/pages/admin-shop.component.html b/frontend/src/app/features/admin/pages/admin-shop.component.html index 2992043..039c9d7 100644 --- a/frontend/src/app/features/admin/pages/admin-shop.component.html +++ b/frontend/src/app/features/admin/pages/admin-shop.component.html @@ -711,7 +711,6 @@ [attr.aria-label]=" 'Descrizione ' + languageLabels[activeContentLanguage] " - [innerHTML]="productForm.descriptions[activeContentLanguage]" (input)="onDescriptionEditorInput($event)" (blur)="onDescriptionEditorBlur($event)" > diff --git a/frontend/src/app/features/admin/pages/admin-shop.component.scss b/frontend/src/app/features/admin/pages/admin-shop.component.scss index 86749fb..7851e8a 100644 --- a/frontend/src/app/features/admin/pages/admin-shop.component.scss +++ b/frontend/src/app/features/admin/pages/admin-shop.component.scss @@ -263,20 +263,19 @@ gap: 0.4rem; } -.rich-text-toolbar__button { - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: #fff; - color: var(--color-text); +.admin-shop .rich-text-toolbar__button { + border: 1px solid var(--color-border) !important; + border-radius: var(--radius-sm) !important; + background: #fff !important; + color: var(--color-text) !important; min-height: 2rem; - padding: 0.28rem 0.56rem; + padding: 0.28rem 0.56rem !important; font-size: 0.82rem; - cursor: pointer; } -.rich-text-toolbar__button:hover { - border-color: #cbb88a; - background: #fffdf4; +.admin-shop .rich-text-toolbar__button:hover:not(:disabled) { + border-color: #cbb88a !important; + background: #fffdf4 !important; } .rich-text-editor { diff --git a/frontend/src/app/features/admin/pages/admin-shop.component.ts b/frontend/src/app/features/admin/pages/admin-shop.component.ts index cce7b12..d65f9ad 100644 --- a/frontend/src/app/features/admin/pages/admin-shop.component.ts +++ b/frontend/src/app/features/admin/pages/admin-shop.component.ts @@ -140,10 +140,14 @@ const RICH_TEXT_ALLOWED_TAGS = new Set([ export class AdminShopComponent implements OnInit, OnDestroy { private readonly adminShopService = inject(AdminShopService); private readonly adminOperationsService = inject(AdminOperationsService); + private descriptionEditorElement: HTMLDivElement | null = null; @ViewChild('workspaceRef') private readonly workspaceRef?: ElementRef; @ViewChild('descriptionEditorRef') - private readonly descriptionEditorRef?: ElementRef; + set descriptionEditorRef(value: ElementRef | undefined) { + this.descriptionEditorElement = value?.nativeElement ?? null; + this.renderActiveDescriptionInEditor(); + } readonly shopLanguages = SHOP_LANGUAGES; readonly mediaLanguages = MEDIA_LANGUAGES; @@ -318,6 +322,8 @@ export class AdminShopComponent implements OnInit, OnDestroy { return; } + this.syncDescriptionFromEditor(this.descriptionEditorElement, true); + const validationError = this.validateProductForm(); if (validationError) { this.errorMessage = validationError; @@ -541,11 +547,9 @@ export class AdminShopComponent implements OnInit, OnDestroy { } setActiveContentLanguage(language: ShopLanguage): void { - this.syncDescriptionFromEditor( - this.descriptionEditorRef?.nativeElement ?? null, - true, - ); + this.syncDescriptionFromEditor(this.descriptionEditorElement, true); this.activeContentLanguage = language; + this.renderActiveDescriptionInEditor(); } isContentLanguageComplete(language: ShopLanguage): boolean { @@ -1268,6 +1272,7 @@ export class AdminShopComponent implements OnInit, OnDestroy { private resetProductForm(): void { Object.assign(this.productForm, this.createEmptyProductForm()); + this.renderActiveDescriptionInEditor(); } private createEmptyMaterialForm( @@ -1324,6 +1329,7 @@ export class AdminShopComponent implements OnInit, OnDestroy { sortOrder: product.sortOrder ?? 0, materials: this.toMaterialForms(product.variants), }); + this.renderActiveDescriptionInEditor(); } private toMaterialForms( @@ -1841,8 +1847,19 @@ export class AdminShopComponent implements OnInit, OnDestroy { this.productForm.descriptions[currentLanguage] = editor.innerHTML; } + private renderActiveDescriptionInEditor(): void { + const editor = this.descriptionEditorElement; + if (!editor) { + return; + } + const html = this.productForm.descriptions[this.activeContentLanguage] ?? ''; + if (editor.innerHTML !== html) { + editor.innerHTML = html; + } + } + private applyDescriptionExecCommand(command: string): void { - const editor = this.descriptionEditorRef?.nativeElement ?? null; + const editor = this.descriptionEditorElement; if (!editor) { return; } diff --git a/frontend/src/app/features/shop/product-detail.component.html b/frontend/src/app/features/shop/product-detail.component.html index 4bc6f6d..e4fbc8d 100644 --- a/frontend/src/app/features/shop/product-detail.component.html +++ b/frontend/src/app/features/shop/product-detail.component.html @@ -353,7 +353,7 @@ } } diff --git a/frontend/src/app/features/shop/product-detail.component.ts b/frontend/src/app/features/shop/product-detail.component.ts index 60f6a53..e7a96bd 100644 --- a/frontend/src/app/features/shop/product-detail.component.ts +++ b/frontend/src/app/features/shop/product-detail.component.ts @@ -14,6 +14,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { catchError, combineLatest, finalize, of, switchMap, tap } from 'rxjs'; import { SeoService } from '../../core/services/seo.service'; import { LanguageService } from '../../core/services/language.service'; +import { getColorHex } from '../../core/constants/colors.const'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component'; @@ -52,6 +53,7 @@ interface ShopMaterialProperty { styleUrl: './product-detail.component.scss', }) export class ProductDetailComponent { + private static readonly HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; private readonly destroyRef = inject(DestroyRef); private readonly injector = inject(Injector); private readonly router = inject(Router); @@ -376,8 +378,18 @@ export class ProductDetailComponent { return variant.colorName || variant.variantLabel || '-'; } - colorHex(variant: ShopProductVariantOption): string { - return variant.colorHex || '#d5d8de'; + colorHex(variant: ShopProductVariantOption | null | undefined): string { + const normalizedHex = this.normalizeHexColor(variant?.colorHex); + if (normalizedHex) { + return normalizedHex; + } + + const fallbackByName = this.colorHexFromName(variant?.colorName); + if (fallbackByName) { + return fallbackByName; + } + + return '#d5d8de'; } materialPriceLabel(material: ShopMaterialOption): number { @@ -464,6 +476,34 @@ export class ProductDetailComponent { }); } + private normalizeHexColor(value: string | null | undefined): string | null { + const raw = String(value ?? '').trim(); + if (!raw) { + return null; + } + + const withHash = raw.startsWith('#') ? raw : `#${raw}`; + if (!ProductDetailComponent.HEX_COLOR_PATTERN.test(withHash)) { + return null; + } + + return withHash.toUpperCase(); + } + + private colorHexFromName(value: string | null | undefined): string | null { + const colorName = String(value ?? '').trim(); + if (!colorName) { + return null; + } + + const fallback = getColorHex(colorName); + if (!fallback || fallback === '#facf0a') { + return null; + } + + return fallback; + } + private applySeo(product: ShopProductDetail): void { const title = product.seoTitle || `${product.name} | 3D fab`; const description =