feat(back-end): rich text improvements
All checks were successful
Build and Deploy / test-backend (push) Successful in 28s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 27s
Build and Deploy / deploy (push) Successful in 8s

This commit is contained in:
2026-03-10 19:01:17 +01:00
parent d150c19f9f
commit edae13541f
5 changed files with 75 additions and 20 deletions

View File

@@ -711,7 +711,6 @@
[attr.aria-label]=" [attr.aria-label]="
'Descrizione ' + languageLabels[activeContentLanguage] 'Descrizione ' + languageLabels[activeContentLanguage]
" "
[innerHTML]="productForm.descriptions[activeContentLanguage]"
(input)="onDescriptionEditorInput($event)" (input)="onDescriptionEditorInput($event)"
(blur)="onDescriptionEditorBlur($event)" (blur)="onDescriptionEditorBlur($event)"
></div> ></div>

View File

@@ -263,20 +263,19 @@
gap: 0.4rem; gap: 0.4rem;
} }
.rich-text-toolbar__button { .admin-shop .rich-text-toolbar__button {
border: 1px solid var(--color-border); border: 1px solid var(--color-border) !important;
border-radius: var(--radius-sm); border-radius: var(--radius-sm) !important;
background: #fff; background: #fff !important;
color: var(--color-text); color: var(--color-text) !important;
min-height: 2rem; min-height: 2rem;
padding: 0.28rem 0.56rem; padding: 0.28rem 0.56rem !important;
font-size: 0.82rem; font-size: 0.82rem;
cursor: pointer;
} }
.rich-text-toolbar__button:hover { .admin-shop .rich-text-toolbar__button:hover:not(:disabled) {
border-color: #cbb88a; border-color: #cbb88a !important;
background: #fffdf4; background: #fffdf4 !important;
} }
.rich-text-editor { .rich-text-editor {

View File

@@ -140,10 +140,14 @@ const RICH_TEXT_ALLOWED_TAGS = new Set([
export class AdminShopComponent implements OnInit, OnDestroy { export class AdminShopComponent implements OnInit, OnDestroy {
private readonly adminShopService = inject(AdminShopService); private readonly adminShopService = inject(AdminShopService);
private readonly adminOperationsService = inject(AdminOperationsService); private readonly adminOperationsService = inject(AdminOperationsService);
private descriptionEditorElement: HTMLDivElement | null = null;
@ViewChild('workspaceRef') @ViewChild('workspaceRef')
private readonly workspaceRef?: ElementRef<HTMLDivElement>; private readonly workspaceRef?: ElementRef<HTMLDivElement>;
@ViewChild('descriptionEditorRef') @ViewChild('descriptionEditorRef')
private readonly descriptionEditorRef?: ElementRef<HTMLDivElement>; set descriptionEditorRef(value: ElementRef<HTMLDivElement> | undefined) {
this.descriptionEditorElement = value?.nativeElement ?? null;
this.renderActiveDescriptionInEditor();
}
readonly shopLanguages = SHOP_LANGUAGES; readonly shopLanguages = SHOP_LANGUAGES;
readonly mediaLanguages = MEDIA_LANGUAGES; readonly mediaLanguages = MEDIA_LANGUAGES;
@@ -318,6 +322,8 @@ export class AdminShopComponent implements OnInit, OnDestroy {
return; return;
} }
this.syncDescriptionFromEditor(this.descriptionEditorElement, true);
const validationError = this.validateProductForm(); const validationError = this.validateProductForm();
if (validationError) { if (validationError) {
this.errorMessage = validationError; this.errorMessage = validationError;
@@ -541,11 +547,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
setActiveContentLanguage(language: ShopLanguage): void { setActiveContentLanguage(language: ShopLanguage): void {
this.syncDescriptionFromEditor( this.syncDescriptionFromEditor(this.descriptionEditorElement, true);
this.descriptionEditorRef?.nativeElement ?? null,
true,
);
this.activeContentLanguage = language; this.activeContentLanguage = language;
this.renderActiveDescriptionInEditor();
} }
isContentLanguageComplete(language: ShopLanguage): boolean { isContentLanguageComplete(language: ShopLanguage): boolean {
@@ -1268,6 +1272,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
private resetProductForm(): void { private resetProductForm(): void {
Object.assign(this.productForm, this.createEmptyProductForm()); Object.assign(this.productForm, this.createEmptyProductForm());
this.renderActiveDescriptionInEditor();
} }
private createEmptyMaterialForm( private createEmptyMaterialForm(
@@ -1324,6 +1329,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
sortOrder: product.sortOrder ?? 0, sortOrder: product.sortOrder ?? 0,
materials: this.toMaterialForms(product.variants), materials: this.toMaterialForms(product.variants),
}); });
this.renderActiveDescriptionInEditor();
} }
private toMaterialForms( private toMaterialForms(
@@ -1841,8 +1847,19 @@ export class AdminShopComponent implements OnInit, OnDestroy {
this.productForm.descriptions[currentLanguage] = editor.innerHTML; 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 { private applyDescriptionExecCommand(command: string): void {
const editor = this.descriptionEditorRef?.nativeElement ?? null; const editor = this.descriptionEditorElement;
if (!editor) { if (!editor) {
return; return;
} }

View File

@@ -353,7 +353,7 @@
<app-stl-viewer <app-stl-viewer
[file]="modelPreviewFile" [file]="modelPreviewFile"
[height]="420" [height]="420"
[color]="selectedVariant()?.colorHex || '#facf0a'" [color]="colorHex(selectedVariant())"
></app-stl-viewer> ></app-stl-viewer>
} }
} }

View File

@@ -14,6 +14,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { catchError, combineLatest, finalize, of, switchMap, tap } from 'rxjs'; import { catchError, combineLatest, finalize, of, switchMap, tap } from 'rxjs';
import { SeoService } from '../../core/services/seo.service'; import { SeoService } from '../../core/services/seo.service';
import { LanguageService } from '../../core/services/language.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 { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component'; import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
@@ -52,6 +53,7 @@ interface ShopMaterialProperty {
styleUrl: './product-detail.component.scss', styleUrl: './product-detail.component.scss',
}) })
export class ProductDetailComponent { 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 destroyRef = inject(DestroyRef);
private readonly injector = inject(Injector); private readonly injector = inject(Injector);
private readonly router = inject(Router); private readonly router = inject(Router);
@@ -376,8 +378,18 @@ export class ProductDetailComponent {
return variant.colorName || variant.variantLabel || '-'; return variant.colorName || variant.variantLabel || '-';
} }
colorHex(variant: ShopProductVariantOption): string { colorHex(variant: ShopProductVariantOption | null | undefined): string {
return variant.colorHex || '#d5d8de'; 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 { 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 { private applySeo(product: ShopProductDetail): void {
const title = product.seoTitle || `${product.name} | 3D fab`; const title = product.seoTitle || `${product.name} | 3D fab`;
const description = const description =