feat(back-end front-end): shop improvements
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -107,10 +107,6 @@
|
||||
background: #f2eee5;
|
||||
}
|
||||
|
||||
.hero-media--portrait .hero-image {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user