feat(back-end front-end): shop improvements
Some checks failed
Build and Deploy / test-backend (push) Successful in 38s
Build and Deploy / test-frontend (push) Successful in 1m4s
Build and Deploy / build-and-push (push) Failing after 1m15s
Build and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-03-13 16:16:49 +01:00
parent fcdede2dd6
commit 00af9a9701
39 changed files with 1886 additions and 299 deletions

View File

@@ -101,6 +101,22 @@
placeholder="Nero, Bianco..."
/>
</label>
<label class="form-field">
<span>Label IT</span>
<input type="text" [(ngModel)]="newVariant.colorLabelIt" />
</label>
<label class="form-field">
<span>Label EN</span>
<input type="text" [(ngModel)]="newVariant.colorLabelEn" />
</label>
<label class="form-field">
<span>Label DE</span>
<input type="text" [(ngModel)]="newVariant.colorLabelDe" />
</label>
<label class="form-field">
<span>Label FR</span>
<input type="text" [(ngModel)]="newVariant.colorLabelFr" />
</label>
<label class="form-field">
<span>Hex colore</span>
<input
@@ -229,7 +245,7 @@
class="color-dot"
[style.background-color]="getVariantColorHex(variant)"
></span>
{{ variant.colorName || "N/D" }}
{{ variant.colorLabelIt || variant.colorName || "N/D" }}
</span>
<span
>Stock spools:
@@ -290,6 +306,22 @@
<span>Colore</span>
<input type="text" [(ngModel)]="variant.colorName" />
</label>
<label class="form-field">
<span>Label IT</span>
<input type="text" [(ngModel)]="variant.colorLabelIt" />
</label>
<label class="form-field">
<span>Label EN</span>
<input type="text" [(ngModel)]="variant.colorLabelEn" />
</label>
<label class="form-field">
<span>Label DE</span>
<input type="text" [(ngModel)]="variant.colorLabelDe" />
</label>
<label class="form-field">
<span>Label FR</span>
<input type="text" [(ngModel)]="variant.colorLabelFr" />
</label>
<label class="form-field">
<span>Hex colore</span>
<input type="text" [(ngModel)]="variant.colorHex" />

View File

@@ -47,6 +47,10 @@ export class AdminFilamentStockComponent implements OnInit {
materialTypeId: 0,
variantDisplayName: '',
colorName: '',
colorLabelIt: '',
colorLabelEn: '',
colorLabelDe: '',
colorLabelFr: '',
colorHex: '',
finishType: 'GLOSSY',
brand: '',
@@ -206,6 +210,10 @@ export class AdminFilamentStockComponent implements OnInit {
this.newVariant.materialTypeId || this.materials[0]?.id || 0,
variantDisplayName: '',
colorName: '',
colorLabelIt: '',
colorLabelEn: '',
colorLabelDe: '',
colorLabelFr: '',
colorHex: '',
finishType: 'GLOSSY',
brand: '',
@@ -359,6 +367,10 @@ export class AdminFilamentStockComponent implements OnInit {
materialTypeId: Number(source.materialTypeId),
variantDisplayName: (source.variantDisplayName || '').trim(),
colorName: (source.colorName || '').trim(),
colorLabelIt: (source.colorLabelIt || '').trim() || undefined,
colorLabelEn: (source.colorLabelEn || '').trim() || undefined,
colorLabelDe: (source.colorLabelDe || '').trim() || undefined,
colorLabelFr: (source.colorLabelFr || '').trim() || undefined,
colorHex: (source.colorHex || '').trim() || undefined,
finishType: (source.finishType || 'GLOSSY').trim().toUpperCase(),
brand: (source.brand || '').trim() || undefined,

View File

@@ -206,27 +206,16 @@
/>
</label>
<label class="ui-form-field">
<span class="ui-form-caption">Nome categoria</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.name"
name="categoryName"
placeholder="Desk accessories"
/>
</label>
<label class="ui-form-field">
<span class="ui-form-caption">Slug</span>
<div class="input-with-action">
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.slug"
name="categorySlug"
placeholder="desk-accessories"
/>
[(ngModel)]="categoryForm.slug"
name="categorySlug"
placeholder="desk-accessories"
/>
<button
type="button"
class="ui-button ui-button--ghost"
@@ -237,36 +226,6 @@
</div>
</label>
<label class="ui-form-field form-field--wide">
<span class="ui-form-caption">Descrizione</span>
<textarea
class="ui-form-control textarea-control"
[(ngModel)]="categoryForm.description"
name="categoryDescription"
rows="3"
></textarea>
</label>
<label class="ui-form-field">
<span class="ui-form-caption">SEO title</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.seoTitle"
name="categorySeoTitle"
/>
</label>
<label class="ui-form-field">
<span class="ui-form-caption">SEO description</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.seoDescription"
name="categorySeoDescription"
/>
</label>
<label class="ui-form-field">
<span class="ui-form-caption">OG title</span>
<input
@@ -288,6 +247,141 @@
</label>
</div>
<div class="ui-language-toolbar">
<div class="ui-language-toolbar__copy">
<span>Lingua contenuti categoria</span>
<p>IT / EN / DE / FR</p>
</div>
<div class="ui-language-toolbar__toggle">
<button
*ngFor="let language of shopLanguages"
type="button"
class="ui-language-toolbar__button image-language-button"
[class.active]="activeContentLanguage === language"
[class.complete]="isCategoryContentLanguageComplete(language)"
[class.incomplete]="
isCategoryContentLanguageIncomplete(language)
"
[class.empty]="!isCategoryContentLanguageStarted(language)"
(click)="setActiveContentLanguage(language)"
>
<span class="image-language-button__label">
{{ languageLabels[language] }}
</span>
<span
class="image-language-button__state"
*ngIf="isCategoryContentLanguageComplete(language)"
>
OK
</span>
<span
class="image-language-button__state"
*ngIf="isCategoryContentLanguageIncomplete(language)"
>
...
</span>
</button>
</div>
</div>
<div class="ui-form-grid ui-form-grid--two">
<label class="ui-form-field">
<span class="ui-form-caption">
Nome categoria {{ languageLabels[activeContentLanguage] }}
</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.names[activeContentLanguage]"
[name]="'category-name-' + activeContentLanguage"
placeholder="Desk accessories"
/>
</label>
<label class="ui-form-field form-field--wide">
<span class="ui-form-caption">
Descrizione {{ languageLabels[activeContentLanguage] }}
</span>
<textarea
class="ui-form-control textarea-control"
[(ngModel)]="categoryForm.descriptions[activeContentLanguage]"
[name]="'category-description-' + activeContentLanguage"
rows="3"
></textarea>
</label>
</div>
<div class="ui-language-toolbar">
<div class="ui-language-toolbar__copy">
<span>Lingua SEO categoria</span>
<p>Stessa lingua attiva dell'editor</p>
</div>
<div class="ui-language-toolbar__toggle">
<button
*ngFor="let language of shopLanguages"
type="button"
class="ui-language-toolbar__button image-language-button"
[class.active]="activeContentLanguage === language"
[class.complete]="isCategorySeoLanguageComplete(language)"
[class.incomplete]="isCategorySeoLanguageIncomplete(language)"
[class.empty]="!isCategorySeoLanguageStarted(language)"
(click)="setActiveContentLanguage(language)"
>
<span class="image-language-button__label">
{{ languageLabels[language] }}
</span>
<span
class="image-language-button__state"
*ngIf="isCategorySeoLanguageComplete(language)"
>
OK
</span>
<span
class="image-language-button__state"
*ngIf="isCategorySeoLanguageIncomplete(language)"
>
...
</span>
</button>
</div>
</div>
<div class="ui-form-grid ui-form-grid--two">
<label class="ui-form-field">
<span class="ui-form-caption">
SEO title {{ languageLabels[activeContentLanguage] }}
</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.seoTitles[activeContentLanguage]"
[name]="'category-seo-title-' + activeContentLanguage"
/>
</label>
<label class="ui-form-field form-field--wide">
<span class="ui-form-caption">
SEO description {{ languageLabels[activeContentLanguage] }}
</span>
<textarea
class="ui-form-control"
[(ngModel)]="
categoryForm.seoDescriptions[activeContentLanguage]
"
[name]="'category-seo-description-' + activeContentLanguage"
rows="3"
></textarea>
<span
class="seo-counter"
[class.seo-counter--danger]="
categorySeoDescriptionLength(activeContentLanguage) > 160
"
>
{{ categorySeoDescriptionLength(activeContentLanguage) }}/160
</span>
</label>
</div>
<div class="toggle-row">
<label class="ui-checkbox">
<input

View File

@@ -41,10 +41,10 @@ interface CategoryFormState {
id: string | null;
parentCategoryId: string | null;
slug: string;
name: string;
description: string;
seoTitle: string;
seoDescription: string;
names: Record<ShopLanguage, string>;
descriptions: Record<ShopLanguage, string>;
seoTitles: Record<ShopLanguage, string>;
seoDescriptions: Record<ShopLanguage, string>;
ogTitle: string;
ogDescription: string;
indexable: boolean;
@@ -554,7 +554,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
}
slugifyCategoryFromName(): void {
this.categoryForm.slug = this.slugify(this.categoryForm.name);
const source =
this.categoryForm.names[this.activeContentLanguage] ||
this.categoryForm.names['it'];
this.categoryForm.slug = this.slugify(source);
}
setActiveContentLanguage(language: ShopLanguage): void {
@@ -603,6 +606,45 @@ export class AdminShopComponent implements OnInit, OnDestroy {
);
}
isCategoryContentLanguageComplete(language: ShopLanguage): boolean {
return !!this.categoryForm.names[language].trim();
}
isCategoryContentLanguageStarted(language: ShopLanguage): boolean {
return (
!!this.categoryForm.names[language].trim() ||
!!this.categoryForm.descriptions[language].trim()
);
}
isCategoryContentLanguageIncomplete(language: ShopLanguage): boolean {
return (
this.isCategoryContentLanguageStarted(language) &&
!this.isCategoryContentLanguageComplete(language)
);
}
isCategorySeoLanguageComplete(language: ShopLanguage): boolean {
return (
!!this.categoryForm.seoTitles[language].trim() &&
!!this.categoryForm.seoDescriptions[language].trim()
);
}
isCategorySeoLanguageStarted(language: ShopLanguage): boolean {
return (
!!this.categoryForm.seoTitles[language].trim() ||
!!this.categoryForm.seoDescriptions[language].trim()
);
}
isCategorySeoLanguageIncomplete(language: ShopLanguage): boolean {
return (
this.isCategorySeoLanguageStarted(language) &&
!this.isCategorySeoLanguageComplete(language)
);
}
preventRichTextToolbarMouseDown(event: MouseEvent): void {
event.preventDefault();
}
@@ -1228,10 +1270,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
id: null,
parentCategoryId: null,
slug: '',
name: '',
description: '',
seoTitle: '',
seoDescription: '',
names: this.createEmptyLocalizedTextRecord(),
descriptions: this.createEmptyLocalizedTextRecord(),
seoTitles: this.createEmptyLocalizedTextRecord(),
seoDescriptions: this.createEmptyLocalizedTextRecord(),
ogTitle: '',
ogDescription: '',
indexable: true,
@@ -1241,6 +1283,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
}
private resetCategoryForm(): void {
this.activeContentLanguage = 'it';
Object.assign(this.categoryForm, this.createEmptyCategoryForm());
}
@@ -1249,10 +1292,30 @@ export class AdminShopComponent implements OnInit, OnDestroy {
id: category.id,
parentCategoryId: category.parentCategoryId,
slug: category.slug ?? '',
name: category.name ?? '',
description: category.description ?? '',
seoTitle: category.seoTitle ?? '',
seoDescription: category.seoDescription ?? '',
names: {
it: category.nameIt ?? category.name ?? '',
en: category.nameEn ?? category.name ?? '',
de: category.nameDe ?? category.name ?? '',
fr: category.nameFr ?? category.name ?? '',
},
descriptions: {
it: category.descriptionIt ?? category.description ?? '',
en: category.descriptionEn ?? category.description ?? '',
de: category.descriptionDe ?? category.description ?? '',
fr: category.descriptionFr ?? category.description ?? '',
},
seoTitles: {
it: category.seoTitleIt ?? category.seoTitle ?? '',
en: category.seoTitleEn ?? category.seoTitle ?? '',
de: category.seoTitleDe ?? category.seoTitle ?? '',
fr: category.seoTitleFr ?? category.seoTitle ?? '',
},
seoDescriptions: {
it: category.seoDescriptionIt ?? category.seoDescription ?? '',
en: category.seoDescriptionEn ?? category.seoDescription ?? '',
de: category.seoDescriptionDe ?? category.seoDescription ?? '',
fr: category.seoDescriptionFr ?? category.seoDescription ?? '',
},
ogTitle: category.ogTitle ?? '',
ogDescription: category.ogDescription ?? '',
indexable: category.indexable,
@@ -1265,10 +1328,34 @@ export class AdminShopComponent implements OnInit, OnDestroy {
return {
parentCategoryId: this.categoryForm.parentCategoryId || null,
slug: this.categoryForm.slug.trim(),
name: this.categoryForm.name.trim(),
description: this.categoryForm.description.trim(),
seoTitle: this.categoryForm.seoTitle.trim(),
seoDescription: this.categoryForm.seoDescription.trim(),
name: this.categoryForm.names['it'].trim(),
nameIt: this.categoryForm.names['it'].trim(),
nameEn: this.categoryForm.names['en'].trim(),
nameDe: this.categoryForm.names['de'].trim(),
nameFr: this.categoryForm.names['fr'].trim(),
description: this.optionalValue(this.categoryForm.descriptions['it']),
descriptionIt: this.optionalValue(this.categoryForm.descriptions['it']),
descriptionEn: this.optionalValue(this.categoryForm.descriptions['en']),
descriptionDe: this.optionalValue(this.categoryForm.descriptions['de']),
descriptionFr: this.optionalValue(this.categoryForm.descriptions['fr']),
seoTitle: this.optionalValue(this.categoryForm.seoTitles['it']),
seoTitleIt: this.optionalValue(this.categoryForm.seoTitles['it']),
seoTitleEn: this.optionalValue(this.categoryForm.seoTitles['en']),
seoTitleDe: this.optionalValue(this.categoryForm.seoTitles['de']),
seoTitleFr: this.optionalValue(this.categoryForm.seoTitles['fr']),
seoDescription: this.optionalValue(this.categoryForm.seoDescriptions['it']),
seoDescriptionIt: this.optionalValue(
this.categoryForm.seoDescriptions['it'],
),
seoDescriptionEn: this.optionalValue(
this.categoryForm.seoDescriptions['en'],
),
seoDescriptionDe: this.optionalValue(
this.categoryForm.seoDescriptions['de'],
),
seoDescriptionFr: this.optionalValue(
this.categoryForm.seoDescriptions['fr'],
),
ogTitle: this.categoryForm.ogTitle.trim(),
ogDescription: this.categoryForm.ogDescription.trim(),
indexable: this.categoryForm.indexable,
@@ -1278,12 +1365,19 @@ export class AdminShopComponent implements OnInit, OnDestroy {
}
private validateCategoryForm(): string | null {
if (!this.categoryForm.name.trim()) {
return 'Il nome categoria è obbligatorio.';
for (const language of this.shopLanguages) {
if (!this.categoryForm.names[language].trim()) {
return `Il nome categoria ${this.languageLabels[language]} è obbligatorio.`;
}
}
if (!this.categoryForm.slug.trim()) {
return 'Lo slug categoria è obbligatorio.';
}
for (const language of this.shopLanguages) {
if (this.categoryForm.seoDescriptions[language].trim().length > 160) {
return `La SEO description categoria ${this.languageLabels[language]} deve avere massimo 160 caratteri.`;
}
}
return null;
}
@@ -1616,6 +1710,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
sku: this.optionalValue(existingVariant?.sku ?? ''),
variantLabel: materialCode,
colorName: stockVariant.colorName.trim(),
colorLabelIt: this.optionalValue(stockVariant.colorLabelIt ?? ''),
colorLabelEn: this.optionalValue(stockVariant.colorLabelEn ?? ''),
colorLabelDe: this.optionalValue(stockVariant.colorLabelDe ?? ''),
colorLabelFr: this.optionalValue(stockVariant.colorLabelFr ?? ''),
colorHex: this.optionalValue(
stockVariant.colorHex ?? '',
)?.toUpperCase(),
@@ -1714,7 +1812,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
}
private stockVariantLabel(variant: AdminFilamentVariant): string {
const colorName = variant.colorName.trim();
const colorName = (variant.colorLabelIt || variant.colorName).trim();
const variantDisplayName = variant.variantDisplayName.trim();
if (
variantDisplayName &&
@@ -2193,6 +2291,19 @@ export class AdminShopComponent implements OnInit, OnDestroy {
return this.productForm.seoDescriptions[language].trim().length;
}
categorySeoDescriptionLength(language: ShopLanguage): number {
return this.categoryForm.seoDescriptions[language].trim().length;
}
private createEmptyLocalizedTextRecord(): Record<ShopLanguage, string> {
return {
it: '',
en: '',
de: '',
fr: '',
};
}
private slugify(source: string): string {
return source
.normalize('NFD')

View File

@@ -32,6 +32,10 @@ export interface AdminFilamentVariant {
materialTechnicalTypeLabel?: string;
variantDisplayName: string;
colorName: string;
colorLabelIt: string;
colorLabelEn: string;
colorLabelDe: string;
colorLabelFr: string;
colorHex?: string;
finishType?: string;
brand?: string;
@@ -57,6 +61,10 @@ export interface AdminUpsertFilamentVariantPayload {
materialTypeId: number;
variantDisplayName: string;
colorName: string;
colorLabelIt?: string;
colorLabelEn?: string;
colorLabelDe?: string;
colorLabelFr?: string;
colorHex?: string;
finishType?: string;
brand?: string;

View File

@@ -30,9 +30,25 @@ export interface AdminShopCategory {
parentCategoryName: string | null;
slug: string;
name: string;
nameIt: string;
nameEn: string;
nameDe: string;
nameFr: string;
description: string | null;
descriptionIt: string | null;
descriptionEn: string | null;
descriptionDe: string | null;
descriptionFr: string | null;
seoTitle: string | null;
seoTitleIt: string | null;
seoTitleEn: string | null;
seoTitleDe: string | null;
seoTitleFr: string | null;
seoDescription: string | null;
seoDescriptionIt: string | null;
seoDescriptionEn: string | null;
seoDescriptionDe: string | null;
seoDescriptionFr: string | null;
ogTitle: string | null;
ogDescription: string | null;
indexable: boolean;
@@ -54,9 +70,25 @@ export interface AdminUpsertShopCategoryPayload {
parentCategoryId?: string | null;
slug: string;
name: string;
nameIt: string;
nameEn: string;
nameDe: string;
nameFr: string;
description?: string;
descriptionIt?: string;
descriptionEn?: string;
descriptionDe?: string;
descriptionFr?: string;
seoTitle?: string;
seoTitleIt?: string;
seoTitleEn?: string;
seoTitleDe?: string;
seoTitleFr?: string;
seoDescription?: string;
seoDescriptionIt?: string;
seoDescriptionEn?: string;
seoDescriptionDe?: string;
seoDescriptionFr?: string;
ogTitle?: string;
ogDescription?: string;
indexable: boolean;
@@ -69,6 +101,10 @@ export interface AdminShopProductVariant {
sku: string | null;
variantLabel: string;
colorName: string;
colorLabelIt: string;
colorLabelEn: string;
colorLabelDe: string;
colorLabelFr: string;
colorHex: string | null;
internalMaterialCode: string;
priceChf: number;
@@ -170,6 +206,10 @@ export interface AdminUpsertShopProductVariantPayload {
sku?: string;
variantLabel?: string;
colorName: string;
colorLabelIt?: string;
colorLabelEn?: string;
colorLabelDe?: string;
colorLabelFr?: string;
colorHex?: string;
internalMaterialCode: string;
priceChf: number;

View File

@@ -75,6 +75,10 @@ export interface VariantOption {
id: number;
name: string;
colorName: string;
colorLabelIt?: string;
colorLabelEn?: string;
colorLabelDe?: string;
colorLabelFr?: string;
hexColor: string;
finishType: string;
stockSpools: number;

View File

@@ -25,8 +25,8 @@ import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewe
import {
findColorHex,
getColorHex,
getColorLabelToken,
normalizeColorValue,
resolveLocalizedColorLabel,
} from '../../core/constants/colors.const';
@Component({
@@ -257,7 +257,7 @@ export class CheckoutComponent implements OnInit {
if (variantLabel) {
return variantLabel;
}
return getColorLabelToken(item?.shopVariantColorName);
return this.localizedShopColorLabel(item);
}
showItemMaterial(item: any): boolean {
@@ -286,12 +286,7 @@ export class CheckoutComponent implements OnInit {
}
itemColorLabel(item: any): string {
const shopColor = String(item?.shopVariantColorName ?? '').trim();
if (shopColor) {
return getColorLabelToken(shopColor) ?? '-';
}
const raw = String(item?.colorCode ?? '').trim();
return getColorLabelToken(raw) ?? '-';
return this.localizedShopColorLabel(item) || String(item?.colorCode ?? '-');
}
itemColorSwatch(item: any): string {
@@ -335,6 +330,16 @@ export class CheckoutComponent implements OnInit {
return !!this.previewLoading()[id];
}
private localizedShopColorLabel(item: any): string | null {
return resolveLocalizedColorLabel(this.languageService.selectedLang(), {
fallback: item?.shopVariantColorName ?? item?.colorCode,
it: item?.shopVariantColorLabelIt,
en: item?.shopVariantColorLabelEn,
de: item?.shopVariantColorLabelDe,
fr: item?.shopVariantColorLabelFr,
});
}
hasPreviewError(item: any): boolean {
const id = String(item?.id ?? '');
if (!id) {

View File

@@ -8,7 +8,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { environment } from '../../../environments/environment';
import {
findColorHex,
getColorLabelToken,
resolveLocalizedColorLabel,
} from '../../core/constants/colors.const';
import {
PriceBreakdownComponent,
@@ -29,9 +29,17 @@ interface PublicOrderItem {
shopProductName?: string;
shopVariantLabel?: string;
shopVariantColorName?: string;
shopVariantColorLabelIt?: string;
shopVariantColorLabelEn?: string;
shopVariantColorLabelDe?: string;
shopVariantColorLabelFr?: string;
shopVariantColorHex?: string;
filamentVariantDisplayName?: string;
filamentColorName?: string;
filamentColorLabelIt?: string;
filamentColorLabelEn?: string;
filamentColorLabelDe?: string;
filamentColorLabelFr?: string;
filamentColorHex?: string;
quality?: string;
nozzleDiameterMm?: number;
@@ -282,26 +290,14 @@ export class OrderComponent implements OnInit {
return variantLabel;
}
return getColorLabelToken(item?.shopVariantColorName);
return this.localizedColorLabel(item, 'shop');
}
itemColorLabel(item: PublicOrderItem): string {
const shopColor = String(item?.shopVariantColorName ?? '').trim();
if (shopColor) {
return getColorLabelToken(shopColor) ?? this.translate.instant('ORDER.NOT_AVAILABLE');
}
const filamentColor = String(item?.filamentColorName ?? '').trim();
if (filamentColor) {
return (
getColorLabelToken(filamentColor) ??
this.translate.instant('ORDER.NOT_AVAILABLE')
);
}
const rawColor = String(item?.colorCode ?? '').trim();
return (
getColorLabelToken(rawColor) ??
this.localizedColorLabel(item, 'shop') ||
this.localizedColorLabel(item, 'filament') ||
String(item?.colorCode ?? '').trim() ||
this.translate.instant('ORDER.NOT_AVAILABLE')
);
}
@@ -333,6 +329,29 @@ export class OrderComponent implements OnInit {
return !this.isShopItem(item);
}
private localizedColorLabel(
item: PublicOrderItem,
source: 'shop' | 'filament',
): string | null {
if (source === 'shop') {
return resolveLocalizedColorLabel(this.translate.currentLang, {
fallback: item.shopVariantColorName,
it: item.shopVariantColorLabelIt,
en: item.shopVariantColorLabelEn,
de: item.shopVariantColorLabelDe,
fr: item.shopVariantColorLabelFr,
});
}
return resolveLocalizedColorLabel(this.translate.currentLang, {
fallback: item.filamentColorName ?? item.colorCode,
it: item.filamentColorLabelIt,
en: item.filamentColorLabelEn,
de: item.filamentColorLabelDe,
fr: item.filamentColorLabelFr,
});
}
orderKind(order: PublicOrder | null): 'SHOP' | 'CALCULATOR' | 'MIXED' {
const items = order?.items ?? [];
const hasShop = items.some((item) => this.isShopItem(item));

View File

@@ -28,10 +28,7 @@
<div class="detail-grid">
<section class="visual-column">
<div
class="hero-media"
[class.hero-media--portrait]="selectedImageIsPortrait()"
>
<div class="hero-media">
@if (galleryImages().length > 1) {
<button
type="button"
@@ -56,7 +53,6 @@
[src]="imageUrl"
[alt]="selectedImage().altText || p.name"
class="hero-image"
(load)="onHeroImageLoad($event)"
/>
} @else {
<div class="image-fallback">

View File

@@ -107,10 +107,6 @@
background: #f2eee5;
}
.hero-media--portrait .hero-image {
object-fit: contain;
}
.image-fallback {
width: 100%;
height: 100%;

View File

@@ -18,7 +18,6 @@ import { LanguageService } from '../../core/services/language.service';
import {
findColorHex,
getColorHex,
getColorLabelToken,
} 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';
@@ -78,9 +77,6 @@ export class ProductDetailComponent {
readonly product = signal<ShopProductDetail | null>(null);
readonly selectedVariantId = signal<string | null>(null);
readonly selectedImageAssetId = signal<string | null>(null);
readonly selectedImageOrientation = signal<
'portrait' | 'landscape' | 'square' | null
>(null);
readonly quantity = signal(1);
readonly isAddingToCart = signal(false);
readonly addSuccess = signal(false);
@@ -198,9 +194,6 @@ export class ProductDetailComponent {
readonly selectedVariantCartQuantity = computed(() =>
this.shopService.quantityForVariant(this.selectedVariant()?.id),
);
readonly selectedImageIsPortrait = computed(
() => this.selectedImageOrientation() === 'portrait',
);
constructor() {
if (!this.shopService.cartLoaded()) {
@@ -315,25 +308,6 @@ export class ProductDetailComponent {
this.setSelectedImageAssetId(images[nextIndex].mediaAssetId);
}
onHeroImageLoad(event: Event): void {
const target = event.target;
if (!(target instanceof HTMLImageElement)) {
return;
}
if (target.naturalHeight > target.naturalWidth) {
this.selectedImageOrientation.set('portrait');
return;
}
if (target.naturalWidth > target.naturalHeight) {
this.selectedImageOrientation.set('landscape');
return;
}
this.selectedImageOrientation.set('square');
}
selectVariant(variant: ShopProductVariantOption): void {
this.selectedVariantId.set(variant.id);
this.selectedMaterialKey.set(this.materialKeyForVariant(variant));
@@ -407,9 +381,7 @@ export class ProductDetailComponent {
}
colorLabel(variant: ShopProductVariantOption): string {
return (
getColorLabelToken(variant.colorName || variant.variantLabel) ?? '-'
);
return variant.colorLabel || variant.colorName || variant.variantLabel || '-';
}
colorHex(variant: ShopProductVariantOption | null | undefined): string {
@@ -512,7 +484,6 @@ export class ProductDetailComponent {
private setSelectedImageAssetId(mediaAssetId: string | null): void {
this.selectedImageAssetId.set(mediaAssetId);
this.selectedImageOrientation.set(null);
}
private normalizeHexColor(value: string | null | undefined): string | null {

View File

@@ -55,6 +55,7 @@ export interface ShopProductVariantOption {
sku: string | null;
variantLabel: string | null;
colorName: string | null;
colorLabel: string | null;
colorHex: string | null;
priceChf: number;
isDefault: boolean;
@@ -138,6 +139,10 @@ export interface ShopCartItem {
shopProductName: string | null;
shopVariantLabel: string | null;
shopVariantColorName: string | null;
shopVariantColorLabelIt?: string | null;
shopVariantColorLabelEn?: string | null;
shopVariantColorLabelDe?: string | null;
shopVariantColorLabelFr?: string | null;
shopVariantColorHex: string | null;
materialCode: string | null;
quality: string | null;

View File

@@ -24,7 +24,7 @@ import { SeoService } from '../../core/services/seo.service';
import { LanguageService } from '../../core/services/language.service';
import {
findColorHex,
getColorLabelToken,
resolveLocalizedColorLabel,
} 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';
@@ -161,15 +161,20 @@ export class ShopPageComponent {
}
cartItemVariant(item: ShopCartItem): string | null {
return (
item.shopVariantLabel || getColorLabelToken(item.shopVariantColorName)
);
return item.shopVariantLabel || this.cartItemColor(item);
}
cartItemColor(item: ShopCartItem): string | null {
return (
getColorLabelToken(item.shopVariantColorName) ??
getColorLabelToken(item.colorCode)
resolveLocalizedColorLabel(this.languageService.selectedLang(), {
fallback: item.shopVariantColorName ?? item.colorCode,
it: item.shopVariantColorLabelIt,
en: item.shopVariantColorLabelEn,
de: item.shopVariantColorLabelDe,
fr: item.shopVariantColorLabelFr,
}) ??
item.shopVariantColorName ??
item.colorCode
);
}