dev #37
@@ -257,9 +257,7 @@
|
|||||||
<p class="file-name">
|
<p class="file-name">
|
||||||
<strong>{{ itemDisplayName(item) }}</strong>
|
<strong>{{ itemDisplayName(item) }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span class="item-kind-badge">
|
||||||
class="item-kind-badge"
|
|
||||||
>
|
|
||||||
{{ isShopItem(item) ? "Shop" : "Calcolatore" }}
|
{{ isShopItem(item) ? "Shop" : "Calcolatore" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -714,9 +714,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<textarea
|
<textarea
|
||||||
class="ui-form-control"
|
class="ui-form-control"
|
||||||
[(ngModel)]="
|
[(ngModel)]="productForm.seoDescriptions[activeContentLanguage]"
|
||||||
productForm.seoDescriptions[activeContentLanguage]
|
|
||||||
"
|
|
||||||
[name]="'product-seo-description-' + activeContentLanguage"
|
[name]="'product-seo-description-' + activeContentLanguage"
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|||||||
@@ -605,7 +605,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
availableMaterialChoices(currentMaterialCode: string): string[] {
|
availableMaterialChoices(currentMaterialCode: string): string[] {
|
||||||
const normalizedCurrentMaterialCode = currentMaterialCode.trim().toUpperCase();
|
const normalizedCurrentMaterialCode = currentMaterialCode
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
const selectedCodes = new Set(
|
const selectedCodes = new Set(
|
||||||
this.productForm.materials
|
this.productForm.materials
|
||||||
.map((material) => material.materialCode.trim().toUpperCase())
|
.map((material) => material.materialCode.trim().toUpperCase())
|
||||||
@@ -804,10 +806,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
isImageLanguageStarted(language: AdminMediaLanguage): boolean {
|
isImageLanguageStarted(language: AdminMediaLanguage): boolean {
|
||||||
const translation = this.imageUploadState.translations[language];
|
const translation = this.imageUploadState.translations[language];
|
||||||
return (
|
return !!translation.title.trim() || !!translation.altText.trim();
|
||||||
!!translation.title.trim() ||
|
|
||||||
!!translation.altText.trim()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isImageLanguageIncomplete(language: AdminMediaLanguage): boolean {
|
isImageLanguageIncomplete(language: AdminMediaLanguage): boolean {
|
||||||
@@ -1291,7 +1290,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const groups = new Map<string, AdminShopProductVariant[]>();
|
const groups = new Map<string, AdminShopProductVariant[]>();
|
||||||
for (const variant of variants) {
|
for (const variant of variants) {
|
||||||
const materialCode = (variant.internalMaterialCode ?? '').trim().toUpperCase();
|
const materialCode = (variant.internalMaterialCode ?? '')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
if (!materialCode) {
|
if (!materialCode) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1403,7 +1404,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
seoTitleEn: this.optionalValue(this.productForm.seoTitles['en']),
|
seoTitleEn: this.optionalValue(this.productForm.seoTitles['en']),
|
||||||
seoTitleDe: this.optionalValue(this.productForm.seoTitles['de']),
|
seoTitleDe: this.optionalValue(this.productForm.seoTitles['de']),
|
||||||
seoTitleFr: this.optionalValue(this.productForm.seoTitles['fr']),
|
seoTitleFr: this.optionalValue(this.productForm.seoTitles['fr']),
|
||||||
seoDescription: this.optionalValue(this.productForm.seoDescriptions['it']),
|
seoDescription: this.optionalValue(
|
||||||
|
this.productForm.seoDescriptions['it'],
|
||||||
|
),
|
||||||
seoDescriptionIt: this.optionalValue(
|
seoDescriptionIt: this.optionalValue(
|
||||||
this.productForm.seoDescriptions['it'],
|
this.productForm.seoDescriptions['it'],
|
||||||
),
|
),
|
||||||
@@ -1461,11 +1464,16 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
let defaultVariantKeyForMaterial: string | null = null;
|
let defaultVariantKeyForMaterial: string | null = null;
|
||||||
|
|
||||||
if (material.isDefault && persistedDefaultKey) {
|
if (material.isDefault && persistedDefaultKey) {
|
||||||
defaultVariantKeyForMaterial = stockVariants
|
defaultVariantKeyForMaterial =
|
||||||
.map((variant) =>
|
stockVariants
|
||||||
this.variantKey(materialCode, variant.colorName, variant.colorHex),
|
.map((variant) =>
|
||||||
)
|
this.variantKey(
|
||||||
.find((variantKey) => variantKey === persistedDefaultKey) ?? null;
|
materialCode,
|
||||||
|
variant.colorName,
|
||||||
|
variant.colorHex,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.find((variantKey) => variantKey === persistedDefaultKey) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
stockVariants.forEach((stockVariant, colorIndex) => {
|
stockVariants.forEach((stockVariant, colorIndex) => {
|
||||||
@@ -1487,7 +1495,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
sku: this.optionalValue(existingVariant?.sku ?? ''),
|
sku: this.optionalValue(existingVariant?.sku ?? ''),
|
||||||
variantLabel: materialCode,
|
variantLabel: materialCode,
|
||||||
colorName: stockVariant.colorName.trim(),
|
colorName: stockVariant.colorName.trim(),
|
||||||
colorHex: this.optionalValue(stockVariant.colorHex ?? '')?.toUpperCase(),
|
colorHex: this.optionalValue(
|
||||||
|
stockVariant.colorHex ?? '',
|
||||||
|
)?.toUpperCase(),
|
||||||
internalMaterialCode: materialCode,
|
internalMaterialCode: materialCode,
|
||||||
priceChf: Number(material.priceChf),
|
priceChf: Number(material.priceChf),
|
||||||
isDefault,
|
isDefault,
|
||||||
@@ -1518,7 +1528,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
).sort((left, right) => left.localeCompare(right));
|
).sort((left, right) => left.localeCompare(right));
|
||||||
}
|
}
|
||||||
|
|
||||||
private stockVariantsForMaterial(materialCode: string): AdminFilamentVariant[] {
|
private stockVariantsForMaterial(
|
||||||
|
materialCode: string,
|
||||||
|
): AdminFilamentVariant[] {
|
||||||
const targetMaterialCode = materialCode.trim().toUpperCase();
|
const targetMaterialCode = materialCode.trim().toUpperCase();
|
||||||
const seenKeys = new Set<string>();
|
const seenKeys = new Set<string>();
|
||||||
|
|
||||||
@@ -1529,7 +1541,8 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const leftName = `${left.colorName} ${left.variantDisplayName}`.trim();
|
const leftName = `${left.colorName} ${left.variantDisplayName}`.trim();
|
||||||
const rightName = `${right.colorName} ${right.variantDisplayName}`.trim();
|
const rightName =
|
||||||
|
`${right.colorName} ${right.variantDisplayName}`.trim();
|
||||||
return leftName.localeCompare(rightName);
|
return leftName.localeCompare(rightName);
|
||||||
})
|
})
|
||||||
.filter((variant) => {
|
.filter((variant) => {
|
||||||
@@ -1554,8 +1567,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.stockMaterialCodes().find((materialCode) => !selectedCodes.has(materialCode)) ??
|
this.stockMaterialCodes().find(
|
||||||
null
|
(materialCode) => !selectedCodes.has(materialCode),
|
||||||
|
) ?? null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,18 +56,15 @@
|
|||||||
(click)="addToCart()"
|
(click)="addToCart()"
|
||||||
[disabled]="!defaultVariantId() || addingToCart()"
|
[disabled]="!defaultVariantId() || addingToCart()"
|
||||||
>
|
>
|
||||||
{{
|
{{ (addingToCart() ? "SHOP.ADDING" : "SHOP.ADD_CART") | translate }}
|
||||||
(addingToCart() ? "SHOP.ADDING" : "SHOP.ADD_CART") | translate
|
|
||||||
}}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
[routerLink]="productLink()"
|
[routerLink]="productLink()"
|
||||||
[state]="navigationState()"
|
[state]="navigationState()"
|
||||||
class="view-btn"
|
class="view-btn"
|
||||||
>{{
|
>{{ "SHOP.DETAILS" | translate }}</a
|
||||||
"SHOP.DETAILS" | translate
|
>
|
||||||
}}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
@if (error()) {
|
@if (error()) {
|
||||||
<div class="state-card">{{ error() | translate }}</div>
|
<div class="state-card">{{ error() | translate }}</div>
|
||||||
} @else {
|
} @else {
|
||||||
@if (product(); as p) {
|
@if (product(); as p) {
|
||||||
<nav class="breadcrumbs">
|
<nav class="breadcrumbs">
|
||||||
<a [routerLink]="shopRootLink()">{{
|
<a [routerLink]="shopRootLink()">{{
|
||||||
"SHOP.BREADCRUMB_ROOT" | translate
|
"SHOP.BREADCRUMB_ROOT" | translate
|
||||||
@@ -83,7 +83,9 @@
|
|||||||
@if (p.model3d) {
|
@if (p.model3d) {
|
||||||
<div class="model-launch-row">
|
<div class="model-launch-row">
|
||||||
<div>
|
<div>
|
||||||
<p class="viewer-kicker">{{ "SHOP.MODEL_3D" | translate }}</p>
|
<p class="viewer-kicker">
|
||||||
|
{{ "SHOP.MODEL_3D" | translate }}
|
||||||
|
</p>
|
||||||
<div class="dimensions dimensions-inline">
|
<div class="dimensions dimensions-inline">
|
||||||
<span>
|
<span>
|
||||||
X
|
X
|
||||||
@@ -151,7 +153,9 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="material-option"
|
class="material-option"
|
||||||
[class.active]="selectedMaterial()?.key === material.key"
|
[class.active]="
|
||||||
|
selectedMaterial()?.key === material.key
|
||||||
|
"
|
||||||
(click)="selectMaterial(material.key)"
|
(click)="selectMaterial(material.key)"
|
||||||
>
|
>
|
||||||
<span class="material-copy">
|
<span class="material-copy">
|
||||||
@@ -228,7 +232,7 @@
|
|||||||
></button>
|
></button>
|
||||||
<div class="color-popup">
|
<div class="color-popup">
|
||||||
<div class="color-popup__category">
|
<div class="color-popup__category">
|
||||||
{{ (selectedMaterial()?.label || "") | uppercase }}
|
{{ selectedMaterial()?.label || "" | uppercase }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="color-popup__grid">
|
<div class="color-popup__grid">
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ export class ProductDetailComponent {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
readonly colorOptions = computed<ShopProductVariantOption[]>(() =>
|
readonly colorOptions = computed<ShopProductVariantOption[]>(
|
||||||
this.selectedMaterial()?.variants ?? [],
|
() => this.selectedMaterial()?.variants ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
readonly selectedMaterialProperties = computed<ShopMaterialProperty[]>(() =>
|
readonly selectedMaterialProperties = computed<ShopMaterialProperty[]>(() =>
|
||||||
@@ -494,11 +494,15 @@ export class ProductDetailComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private materialLabelForVariant(variant: ShopProductVariantOption | null): string {
|
private materialLabelForVariant(
|
||||||
|
variant: ShopProductVariantOption | null,
|
||||||
|
): string {
|
||||||
return String(variant?.variantLabel || '').trim() || 'Standard';
|
return String(variant?.variantLabel || '').trim() || 'Standard';
|
||||||
}
|
}
|
||||||
|
|
||||||
private materialKeyForVariant(variant: ShopProductVariantOption | null): string | null {
|
private materialKeyForVariant(
|
||||||
|
variant: ShopProductVariantOption | null,
|
||||||
|
): string | null {
|
||||||
if (!variant) {
|
if (!variant) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -508,7 +512,9 @@ export class ProductDetailComponent {
|
|||||||
private materialPropertiesFor(
|
private materialPropertiesFor(
|
||||||
materialLabel: string | null | undefined,
|
materialLabel: string | null | undefined,
|
||||||
): ShopMaterialProperty[] {
|
): ShopMaterialProperty[] {
|
||||||
const normalized = String(materialLabel ?? '').trim().toUpperCase();
|
const normalized = String(materialLabel ?? '')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
if (normalized.includes('ASA')) {
|
if (normalized.includes('ASA')) {
|
||||||
return [
|
return [
|
||||||
@@ -600,7 +606,13 @@ export class ProductDetailComponent {
|
|||||||
|
|
||||||
const currentTree = this.router.parseUrl(this.router.url);
|
const currentTree = this.router.parseUrl(this.router.url);
|
||||||
const targetTree = this.router.createUrlTree(
|
const targetTree = this.router.createUrlTree(
|
||||||
['/', this.languageService.selectedLang(), 'shop', 'p', targetProductSlug],
|
[
|
||||||
|
'/',
|
||||||
|
this.languageService.selectedLang(),
|
||||||
|
'shop',
|
||||||
|
'p',
|
||||||
|
targetProductSlug,
|
||||||
|
],
|
||||||
{
|
{
|
||||||
queryParams: currentTree.queryParams,
|
queryParams: currentTree.queryParams,
|
||||||
fragment: currentTree.fragment ?? undefined,
|
fragment: currentTree.fragment ?? undefined,
|
||||||
|
|||||||
@@ -38,8 +38,12 @@ export class ShopRouteService {
|
|||||||
return idPrefix ? `${idPrefix}-${tail}` : tail;
|
return idPrefix ? `${idPrefix}-${tail}` : tail;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveProductLookup(productPathSegment: string | null | undefined): ShopProductLookup {
|
resolveProductLookup(
|
||||||
const normalized = String(productPathSegment ?? '').trim().toLowerCase();
|
productPathSegment: string | null | undefined,
|
||||||
|
): ShopProductLookup {
|
||||||
|
const normalized = String(productPathSegment ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return {
|
return {
|
||||||
idPrefix: null,
|
idPrefix: null,
|
||||||
@@ -89,7 +93,9 @@ export class ShopRouteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private productIdPrefix(productId: string | null | undefined): string {
|
private productIdPrefix(productId: string | null | undefined): string {
|
||||||
const normalized = String(productId ?? '').trim().toLowerCase();
|
const normalized = String(productId ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
const canonicalUuidMatch = normalized.match(/^([0-9a-f]{8})-/);
|
const canonicalUuidMatch = normalized.match(/^([0-9a-f]{8})-/);
|
||||||
if (canonicalUuidMatch) {
|
if (canonicalUuidMatch) {
|
||||||
return canonicalUuidMatch[1];
|
return canonicalUuidMatch[1];
|
||||||
|
|||||||
@@ -273,15 +273,16 @@ export class ShopService {
|
|||||||
getProductByPublicPath(
|
getProductByPublicPath(
|
||||||
productPathSegment: string,
|
productPathSegment: string,
|
||||||
): Observable<ShopProductDetail> {
|
): Observable<ShopProductDetail> {
|
||||||
const lookup = this.shopRouteService.resolveProductLookup(productPathSegment);
|
const lookup =
|
||||||
|
this.shopRouteService.resolveProductLookup(productPathSegment);
|
||||||
if (!lookup.idPrefix && lookup.slugHint) {
|
if (!lookup.idPrefix && lookup.slugHint) {
|
||||||
return this.getProduct(lookup.slugHint);
|
return this.getProduct(lookup.slugHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getProductCatalog().pipe(
|
return this.getProductCatalog().pipe(
|
||||||
map((catalog) =>
|
map((catalog) =>
|
||||||
catalog.products.find(
|
catalog.products.find((product) =>
|
||||||
(product) => product.id.toLowerCase().startsWith(lookup.idPrefix ?? ''),
|
product.id.toLowerCase().startsWith(lookup.idPrefix ?? ''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
switchMap((product) => {
|
switchMap((product) => {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
[class.active]="!currentCategorySlug()"
|
[class.active]="!currentCategorySlug()"
|
||||||
(click)="navigateToCategory()"
|
(click)="navigateToCategory()"
|
||||||
>
|
>
|
||||||
<span class="category-name">{{ "SHOP.ALL_CATEGORIES" | translate }}</span>
|
<span class="category-name">{{
|
||||||
|
"SHOP.ALL_CATEGORIES" | translate
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="category-list">
|
<div class="category-list">
|
||||||
@@ -227,7 +229,9 @@
|
|||||||
<app-card class="shop-custom-cta-card">
|
<app-card class="shop-custom-cta-card">
|
||||||
<div class="shop-custom-cta-inner">
|
<div class="shop-custom-cta-inner">
|
||||||
<div class="shop-custom-cta-copy">
|
<div class="shop-custom-cta-copy">
|
||||||
<p class="panel-kicker">{{ "SHOP.CUSTOM_PART_FOOTER_TITLE" | translate }}</p>
|
<p class="panel-kicker">
|
||||||
|
{{ "SHOP.CUSTOM_PART_FOOTER_TITLE" | translate }}
|
||||||
|
</p>
|
||||||
<h2 class="shop-custom-cta-title">
|
<h2 class="shop-custom-cta-title">
|
||||||
{{ "SHOP.CUSTOM_PART_FOOTER_TEXT" | translate }}
|
{{ "SHOP.CUSTOM_PART_FOOTER_TEXT" | translate }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user