dev #37
@@ -57,7 +57,8 @@ export class SeoService {
|
|||||||
this.asString(mergedData['seoDescription']) ?? this.defaultDescription;
|
this.asString(mergedData['seoDescription']) ?? this.defaultDescription;
|
||||||
const robots = this.asString(mergedData['seoRobots']) ?? 'index, follow';
|
const robots = this.asString(mergedData['seoRobots']) ?? 'index, follow';
|
||||||
const ogTitle = this.asString(mergedData['ogTitle']) ?? title;
|
const ogTitle = this.asString(mergedData['ogTitle']) ?? title;
|
||||||
const ogDescription = this.asString(mergedData['ogDescription']) ?? description;
|
const ogDescription =
|
||||||
|
this.asString(mergedData['ogDescription']) ?? description;
|
||||||
|
|
||||||
this.applySeoValues(title, description, robots, ogTitle, ogDescription);
|
this.applySeoValues(title, description, robots, ogTitle, ogDescription);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,10 @@
|
|||||||
[ngModel]="orderTypeFilter"
|
[ngModel]="orderTypeFilter"
|
||||||
(ngModelChange)="onOrderTypeFilterChange($event)"
|
(ngModelChange)="onOrderTypeFilterChange($event)"
|
||||||
>
|
>
|
||||||
<option *ngFor="let option of orderTypeFilterOptions" [ngValue]="option">
|
<option
|
||||||
|
*ngFor="let option of orderTypeFilterOptions"
|
||||||
|
[ngValue]="option"
|
||||||
|
>
|
||||||
{{ option }}
|
{{ option }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -133,7 +136,9 @@
|
|||||||
<span
|
<span
|
||||||
class="order-type-badge"
|
class="order-type-badge"
|
||||||
[class.order-type-badge--shop]="orderKind(selectedOrder) === 'SHOP'"
|
[class.order-type-badge--shop]="orderKind(selectedOrder) === 'SHOP'"
|
||||||
[class.order-type-badge--mixed]="orderKind(selectedOrder) === 'MIXED'"
|
[class.order-type-badge--mixed]="
|
||||||
|
orderKind(selectedOrder) === 'MIXED'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ orderKindLabel(selectedOrder) }}
|
{{ orderKindLabel(selectedOrder) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -162,7 +167,8 @@
|
|||||||
<strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span>
|
<strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui-meta-item">
|
<div class="ui-meta-item">
|
||||||
<strong>Tipo ordine</strong><span>{{ orderKindLabel(selectedOrder) }}</span>
|
<strong>Tipo ordine</strong
|
||||||
|
><span>{{ orderKindLabel(selectedOrder) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui-meta-item">
|
<div class="ui-meta-item">
|
||||||
<strong>Totale</strong
|
<strong>Totale</strong
|
||||||
@@ -279,7 +285,9 @@
|
|||||||
></span>
|
></span>
|
||||||
<span>
|
<span>
|
||||||
{{ getItemColorLabel(item) }}
|
{{ getItemColorLabel(item) }}
|
||||||
<ng-container *ngIf="getItemColorCodeSuffix(item) as colorCode">
|
<ng-container
|
||||||
|
*ngIf="getItemColorCodeSuffix(item) as colorCode"
|
||||||
|
>
|
||||||
({{ colorCode }})
|
({{ colorCode }})
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</span>
|
</span>
|
||||||
@@ -300,7 +308,12 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ui-button ui-button--ghost"
|
class="ui-button ui-button--ghost"
|
||||||
(click)="downloadItemFile(item.id, item.originalFilename || itemDisplayName(item))"
|
(click)="
|
||||||
|
downloadItemFile(
|
||||||
|
item.id,
|
||||||
|
item.originalFilename || itemDisplayName(item)
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ downloadItemLabel(item) }}
|
{{ downloadItemLabel(item) }}
|
||||||
</button>
|
</button>
|
||||||
@@ -373,7 +386,10 @@
|
|||||||
|
|
||||||
<h4>Parametri per file</h4>
|
<h4>Parametri per file</h4>
|
||||||
<div class="file-color-list">
|
<div class="file-color-list">
|
||||||
<div class="file-color-row" *ngFor="let item of printItems(selectedOrder)">
|
<div
|
||||||
|
class="file-color-row"
|
||||||
|
*ngFor="let item of printItems(selectedOrder)"
|
||||||
|
>
|
||||||
<span class="filename">{{ item.originalFilename }}</span>
|
<span class="filename">{{ item.originalFilename }}</span>
|
||||||
<span class="file-color">
|
<span class="file-color">
|
||||||
{{ getItemMaterialLabel(item) }} | Colore:
|
{{ getItemMaterialLabel(item) }} | Colore:
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ export class AdminDashboardComponent implements OnInit {
|
|||||||
this.selectedOrder = order;
|
this.selectedOrder = order;
|
||||||
this.selectedStatus = order.status;
|
this.selectedStatus = order.status;
|
||||||
this.selectedPaymentMethod = order.paymentMethod || 'OTHER';
|
this.selectedPaymentMethod = order.paymentMethod || 'OTHER';
|
||||||
this.showPrintDetails = this.showPrintDetails && this.hasPrintItems(order);
|
this.showPrintDetails =
|
||||||
|
this.showPrintDetails && this.hasPrintItems(order);
|
||||||
this.detailLoading = false;
|
this.detailLoading = false;
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -446,7 +447,8 @@ export class AdminDashboardComponent implements OnInit {
|
|||||||
this.selectedStatus = updatedOrder.status;
|
this.selectedStatus = updatedOrder.status;
|
||||||
this.selectedPaymentMethod =
|
this.selectedPaymentMethod =
|
||||||
updatedOrder.paymentMethod || this.selectedPaymentMethod;
|
updatedOrder.paymentMethod || this.selectedPaymentMethod;
|
||||||
this.showPrintDetails = this.showPrintDetails && this.hasPrintItems(updatedOrder);
|
this.showPrintDetails =
|
||||||
|
this.showPrintDetails && this.hasPrintItems(updatedOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyListFiltersAndSelection(): void {
|
private applyListFiltersAndSelection(): void {
|
||||||
|
|||||||
@@ -393,10 +393,7 @@ export class CheckoutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private loadStlPreviews(session: any): void {
|
private loadStlPreviews(session: any): void {
|
||||||
if (
|
if (!this.sessionId || !Array.isArray(session?.items)) {
|
||||||
!this.sessionId ||
|
|
||||||
!Array.isArray(session?.items)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,9 @@
|
|||||||
<app-button
|
<app-button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
(click)="completeOrder()"
|
(click)="completeOrder()"
|
||||||
[disabled]="!selectedPaymentMethod || o.paymentStatus === 'REPORTED'"
|
[disabled]="
|
||||||
|
!selectedPaymentMethod || o.paymentStatus === 'REPORTED'
|
||||||
|
"
|
||||||
[fullWidth]="true"
|
[fullWidth]="true"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
@@ -201,10 +203,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="order-item-meta">
|
<div class="order-item-meta">
|
||||||
<span>{{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }}</span>
|
<span
|
||||||
|
>{{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }}</span
|
||||||
|
>
|
||||||
<span *ngIf="showItemMaterial(item)">
|
<span *ngIf="showItemMaterial(item)">
|
||||||
{{ "CHECKOUT.MATERIAL" | translate }}:
|
{{ "CHECKOUT.MATERIAL" | translate }}:
|
||||||
{{ item.materialCode || ("ORDER.NOT_AVAILABLE" | translate) }}
|
{{
|
||||||
|
item.materialCode || ("ORDER.NOT_AVAILABLE" | translate)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="itemVariantLabel(item) as variantLabel">
|
<span *ngIf="itemVariantLabel(item) as variantLabel">
|
||||||
{{ "SHOP.VARIANT" | translate }}: {{ variantLabel }}
|
{{ "SHOP.VARIANT" | translate }}: {{ variantLabel }}
|
||||||
@@ -252,7 +258,9 @@
|
|||||||
<strong>{{ orderKindLabel(o) }}</strong>
|
<strong>{{ orderKindLabel(o) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="summary-label">{{ "ORDER.ITEM_COUNT" | translate }}</span>
|
<span class="summary-label">{{
|
||||||
|
"ORDER.ITEM_COUNT" | translate
|
||||||
|
}}</span>
|
||||||
<strong>{{ (o.items || []).length }}</strong>
|
<strong>{{ (o.items || []).length }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -263,7 +263,9 @@ export class OrderComponent implements OnInit {
|
|||||||
return shopName;
|
return shopName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(item?.originalFilename ?? this.translate.instant('ORDER.NOT_AVAILABLE'));
|
return String(
|
||||||
|
item?.originalFilename ?? this.translate.instant('ORDER.NOT_AVAILABLE'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
itemVariantLabel(item: PublicOrderItem): string | null {
|
itemVariantLabel(item: PublicOrderItem): string | null {
|
||||||
|
|||||||
@@ -46,9 +46,7 @@
|
|||||||
<div class="pricing">
|
<div class="pricing">
|
||||||
<span class="price">{{ priceLabel() | currency: "CHF" }}</span>
|
<span class="price">{{ priceLabel() | currency: "CHF" }}</span>
|
||||||
@if (hasPriceRange()) {
|
@if (hasPriceRange()) {
|
||||||
<small class="price-note">{{
|
<small class="price-note">{{ "SHOP.PRICE_FROM" | translate }}</small>
|
||||||
"SHOP.PRICE_FROM" | translate
|
|
||||||
}}</small>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
border: 1px solid rgba(16, 24, 32, 0.08);
|
border: 1px solid rgba(16, 24, 32, 0.08);
|
||||||
border-radius: 1.1rem;
|
border-radius: 1.1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background:
|
background: linear-gradient(
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 246, 241, 1));
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0.98),
|
||||||
|
rgba(248, 246, 241, 1)
|
||||||
|
);
|
||||||
box-shadow: 0 18px 40px rgba(16, 24, 32, 0.08);
|
box-shadow: 0 18px 40px rgba(16, 24, 32, 0.08);
|
||||||
transition:
|
transition:
|
||||||
transform 0.2s ease,
|
transform 0.2s ease,
|
||||||
@@ -24,7 +27,11 @@
|
|||||||
display: block;
|
display: block;
|
||||||
min-height: 244px;
|
min-height: 244px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top right, rgba(250, 207, 10, 0.28), transparent 42%),
|
radial-gradient(
|
||||||
|
circle at top right,
|
||||||
|
rgba(250, 207, 10, 0.28),
|
||||||
|
transparent 42%
|
||||||
|
),
|
||||||
linear-gradient(160deg, #f7f4ed 0%, #ece7db 100%);
|
linear-gradient(160deg, #f7f4ed 0%, #ece7db 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,9 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="thumb"
|
class="thumb"
|
||||||
[class.active]="selectedImage().mediaAssetId === image.mediaAssetId"
|
[class.active]="
|
||||||
|
selectedImage().mediaAssetId === image.mediaAssetId
|
||||||
|
"
|
||||||
(click)="selectImage(image.mediaAssetId)"
|
(click)="selectImage(image.mediaAssetId)"
|
||||||
>
|
>
|
||||||
@if (imageUrl(image); as imageUrl) {
|
@if (imageUrl(image); as imageUrl) {
|
||||||
@@ -68,13 +70,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="dimensions">
|
<div class="dimensions">
|
||||||
<span>
|
<span>
|
||||||
X {{ p.model3d.boundingBoxXMm || 0 | number: "1.0-1" }} mm
|
X
|
||||||
|
{{ p.model3d.boundingBoxXMm || 0 | number: "1.0-1" }} mm
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Y {{ p.model3d.boundingBoxYMm || 0 | number: "1.0-1" }} mm
|
Y
|
||||||
|
{{ p.model3d.boundingBoxYMm || 0 | number: "1.0-1" }} mm
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Z {{ p.model3d.boundingBoxZMm || 0 | number: "1.0-1" }} mm
|
Z
|
||||||
|
{{ p.model3d.boundingBoxZMm || 0 | number: "1.0-1" }} mm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +138,8 @@
|
|||||||
<span class="cart-pill">
|
<span class="cart-pill">
|
||||||
{{
|
{{
|
||||||
"SHOP.IN_CART_LONG"
|
"SHOP.IN_CART_LONG"
|
||||||
| translate: { count: selectedVariantCartQuantity() }
|
| translate
|
||||||
|
: { count: selectedVariantCartQuantity() }
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -157,7 +163,9 @@
|
|||||||
<small>{{ variant.variantLabel }}</small>
|
<small>{{ variant.variantLabel }}</small>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<strong>{{ variant.priceChf | currency: "CHF" }}</strong>
|
<strong>{{
|
||||||
|
variant.priceChf | currency: "CHF"
|
||||||
|
}}</strong>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -165,9 +173,13 @@
|
|||||||
<div class="quantity-row">
|
<div class="quantity-row">
|
||||||
<span>{{ "SHOP.QUANTITY" | translate }}</span>
|
<span>{{ "SHOP.QUANTITY" | translate }}</span>
|
||||||
<div class="qty-control">
|
<div class="qty-control">
|
||||||
<button type="button" (click)="decreaseQuantity()">-</button>
|
<button type="button" (click)="decreaseQuantity()">
|
||||||
|
-
|
||||||
|
</button>
|
||||||
<span>{{ quantity() }}</span>
|
<span>{{ quantity() }}</span>
|
||||||
<button type="button" (click)="increaseQuantity()">+</button>
|
<button type="button" (click)="increaseQuantity()">
|
||||||
|
+
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,9 +190,8 @@
|
|||||||
(click)="addToCart()"
|
(click)="addToCart()"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
(isAddingToCart()
|
(isAddingToCart() ? "SHOP.ADDING" : "SHOP.ADD_CART")
|
||||||
? "SHOP.ADDING"
|
| translate
|
||||||
: "SHOP.ADD_CART") | translate
|
|
||||||
}}
|
}}
|
||||||
</app-button>
|
</app-button>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
.product-page {
|
.product-page {
|
||||||
padding: var(--space-8) 0 var(--space-12);
|
padding: var(--space-8) 0 var(--space-12);
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(250, 207, 10, 0.18), transparent 20%),
|
radial-gradient(
|
||||||
|
circle at top left,
|
||||||
|
rgba(250, 207, 10, 0.18),
|
||||||
|
transparent 20%
|
||||||
|
),
|
||||||
linear-gradient(180deg, #faf7ee 0%, var(--color-bg) 25%);
|
linear-gradient(180deg, #faf7ee 0%, var(--color-bg) 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +44,11 @@
|
|||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
border: 1px solid rgba(16, 24, 32, 0.08);
|
border: 1px solid rgba(16, 24, 32, 0.08);
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top right, rgba(250, 207, 10, 0.3), transparent 30%),
|
radial-gradient(
|
||||||
|
circle at top right,
|
||||||
|
rgba(250, 207, 10, 0.3),
|
||||||
|
transparent 30%
|
||||||
|
),
|
||||||
linear-gradient(160deg, #f8f4ea 0%, #eee8db 100%);
|
linear-gradient(160deg, #f8f4ea 0%, #eee8db 100%);
|
||||||
box-shadow: 0 18px 42px rgba(16, 24, 32, 0.08);
|
box-shadow: 0 18px 42px rgba(16, 24, 32, 0.08);
|
||||||
}
|
}
|
||||||
@@ -325,13 +333,12 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-block {
|
.skeleton-block {
|
||||||
background:
|
background: linear-gradient(
|
||||||
linear-gradient(
|
110deg,
|
||||||
110deg,
|
rgba(255, 255, 255, 0.7) 8%,
|
||||||
rgba(255, 255, 255, 0.7) 8%,
|
rgba(238, 235, 226, 0.95) 18%,
|
||||||
rgba(238, 235, 226, 0.95) 18%,
|
rgba(255, 255, 255, 0.7) 33%
|
||||||
rgba(255, 255, 255, 0.7) 33%
|
);
|
||||||
);
|
|
||||||
background-size: 220% 100%;
|
background-size: 220% 100%;
|
||||||
animation: skeleton 1.35s linear infinite;
|
animation: skeleton 1.35s linear infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ export class ProductDetailComponent {
|
|||||||
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
toObservable(this.productSlug, { injector: this.injector }),
|
toObservable(this.productSlug, { injector: this.injector }),
|
||||||
toObservable(this.languageService.currentLang, { injector: this.injector }),
|
toObservable(this.languageService.currentLang, {
|
||||||
|
injector: this.injector,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(() => {
|
tap(() => {
|
||||||
@@ -160,13 +162,22 @@ export class ProductDetailComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.product.set(product);
|
this.product.set(product);
|
||||||
this.selectedVariantId.set(product.defaultVariant?.id ?? product.variants[0]?.id ?? null);
|
this.selectedVariantId.set(
|
||||||
this.selectedImageAssetId.set(product.primaryImage?.mediaAssetId ?? product.images[0]?.mediaAssetId ?? null);
|
product.defaultVariant?.id ?? product.variants[0]?.id ?? null,
|
||||||
|
);
|
||||||
|
this.selectedImageAssetId.set(
|
||||||
|
product.primaryImage?.mediaAssetId ??
|
||||||
|
product.images[0]?.mediaAssetId ??
|
||||||
|
null,
|
||||||
|
);
|
||||||
this.quantity.set(1);
|
this.quantity.set(1);
|
||||||
this.applySeo(product);
|
this.applySeo(product);
|
||||||
|
|
||||||
if (product.model3d?.url && product.model3d.originalFilename) {
|
if (product.model3d?.url && product.model3d.originalFilename) {
|
||||||
this.loadModelPreview(product.model3d.url, product.model3d.originalFilename);
|
this.loadModelPreview(
|
||||||
|
product.model3d.url,
|
||||||
|
product.model3d.originalFilename,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.modelFile.set(null);
|
this.modelFile.set(null);
|
||||||
this.modelLoading.set(false);
|
this.modelLoading.set(false);
|
||||||
@@ -239,7 +250,9 @@ export class ProductDetailComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
priceLabel(): number {
|
priceLabel(): number {
|
||||||
return this.selectedVariant()?.priceChf ?? this.product()?.priceFromChf ?? 0;
|
return (
|
||||||
|
this.selectedVariant()?.priceChf ?? this.product()?.priceFromChf ?? 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
colorLabel(variant: ShopProductVariantOption): string {
|
colorLabel(variant: ShopProductVariantOption): string {
|
||||||
@@ -282,7 +295,8 @@ export class ProductDetailComponent {
|
|||||||
product.seoDescription ||
|
product.seoDescription ||
|
||||||
product.excerpt ||
|
product.excerpt ||
|
||||||
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION');
|
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION');
|
||||||
const robots = product.indexable === false ? 'noindex, nofollow' : 'index, follow';
|
const robots =
|
||||||
|
product.indexable === false ? 'noindex, nofollow' : 'index, follow';
|
||||||
|
|
||||||
this.seoService.applyPageSeo({
|
this.seoService.applyPageSeo({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -129,7 +129,9 @@ describe('ShopService', () => {
|
|||||||
it('posts add-to-cart with credentials and replaces local cart state', () => {
|
it('posts add-to-cart with credentials and replaces local cart state', () => {
|
||||||
service.addToCart('variant-red', 2).subscribe();
|
service.addToCart('variant-red', 2).subscribe();
|
||||||
|
|
||||||
const request = httpMock.expectOne('http://localhost:8000/api/shop/cart/items');
|
const request = httpMock.expectOne(
|
||||||
|
'http://localhost:8000/api/shop/cart/items',
|
||||||
|
);
|
||||||
expect(request.request.method).toBe('POST');
|
expect(request.request.method).toBe('POST');
|
||||||
expect(request.request.withCredentials).toBeTrue();
|
expect(request.request.withCredentials).toBeTrue();
|
||||||
expect(request.request.body).toEqual({
|
expect(request.request.body).toEqual({
|
||||||
|
|||||||
@@ -251,9 +251,12 @@ export class ShopService {
|
|||||||
params = params.set('featured', String(featured));
|
params = params.set('featured', String(featured));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<ShopProductCatalogResponse>(`${this.apiUrl}/products`, {
|
return this.http.get<ShopProductCatalogResponse>(
|
||||||
params,
|
`${this.apiUrl}/products`,
|
||||||
});
|
{
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProduct(slug: string): Observable<ShopProductDetail> {
|
getProduct(slug: string): Observable<ShopProductDetail> {
|
||||||
@@ -337,10 +340,7 @@ export class ShopService {
|
|||||||
.pipe(tap((cart) => this.setCart(cart)));
|
.pipe(tap((cart) => this.setCart(cart)));
|
||||||
}
|
}
|
||||||
|
|
||||||
getProductModelFile(
|
getProductModelFile(urlOrPath: string, filename: string): Observable<File> {
|
||||||
urlOrPath: string,
|
|
||||||
filename: string,
|
|
||||||
): Observable<File> {
|
|
||||||
return this.http
|
return this.http
|
||||||
.get(this.resolveApiUrl(urlOrPath), {
|
.get(this.resolveApiUrl(urlOrPath), {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
|||||||
@@ -4,20 +4,16 @@
|
|||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<p class="ui-eyebrow">{{ "SHOP.HERO_EYEBROW" | translate }}</p>
|
<p class="ui-eyebrow">{{ "SHOP.HERO_EYEBROW" | translate }}</p>
|
||||||
<h1 class="ui-hero-display">
|
<h1 class="ui-hero-display">
|
||||||
{{
|
{{ selectedCategory()?.name || ("SHOP.TITLE" | translate) }}
|
||||||
selectedCategory()?.name || ("SHOP.TITLE" | translate)
|
|
||||||
}}
|
|
||||||
</h1>
|
</h1>
|
||||||
<p class="ui-copy-lead">
|
<p class="ui-copy-lead">
|
||||||
{{
|
{{ selectedCategory()?.description || ("SHOP.SUBTITLE" | translate) }}
|
||||||
selectedCategory()?.description ||
|
|
||||||
("SHOP.SUBTITLE" | translate)
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
<p class="ui-copy-subtitle">
|
<p class="ui-copy-subtitle">
|
||||||
{{
|
{{
|
||||||
selectedCategory()
|
selectedCategory()
|
||||||
? ("SHOP.CATEGORY_META" | translate: { count: selectedCategory()?.productCount || 0 })
|
? ("SHOP.CATEGORY_META"
|
||||||
|
| translate: { count: selectedCategory()?.productCount || 0 })
|
||||||
: ("SHOP.CATALOG_META_DESCRIPTION" | translate)
|
: ("SHOP.CATALOG_META_DESCRIPTION" | translate)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -75,7 +71,9 @@
|
|||||||
<app-card>
|
<app-card>
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<p class="panel-kicker">{{ "SHOP.CATEGORY_PANEL_KICKER" | translate }}</p>
|
<p class="panel-kicker">
|
||||||
|
{{ "SHOP.CATEGORY_PANEL_KICKER" | translate }}
|
||||||
|
</p>
|
||||||
<h2 class="panel-title">
|
<h2 class="panel-title">
|
||||||
{{ "SHOP.CATEGORY_PANEL_TITLE" | translate }}
|
{{ "SHOP.CATEGORY_PANEL_TITLE" | translate }}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -155,7 +153,9 @@
|
|||||||
<div class="qty-control">
|
<div class="qty-control">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
[disabled]="cartMutating() && busyLineItemId() === item.id"
|
[disabled]="
|
||||||
|
cartMutating() && busyLineItemId() === item.id
|
||||||
|
"
|
||||||
(click)="decreaseQuantity(item)"
|
(click)="decreaseQuantity(item)"
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
@@ -163,7 +163,9 @@
|
|||||||
<span>{{ item.quantity }}</span>
|
<span>{{ item.quantity }}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
[disabled]="cartMutating() && busyLineItemId() === item.id"
|
[disabled]="
|
||||||
|
cartMutating() && busyLineItemId() === item.id
|
||||||
|
"
|
||||||
(click)="increaseQuantity(item)"
|
(click)="increaseQuantity(item)"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
@@ -189,7 +191,9 @@
|
|||||||
<div class="cart-totals">
|
<div class="cart-totals">
|
||||||
<div class="cart-total-row">
|
<div class="cart-total-row">
|
||||||
<span>{{ "SHOP.CART_SUBTOTAL" | translate }}</span>
|
<span>{{ "SHOP.CART_SUBTOTAL" | translate }}</span>
|
||||||
<strong>{{ cart()?.itemsTotalChf || 0 | currency: "CHF" }}</strong>
|
<strong>{{
|
||||||
|
cart()?.itemsTotalChf || 0 | currency: "CHF"
|
||||||
|
}}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="cart-total-row">
|
<div class="cart-total-row">
|
||||||
<span>{{ "SHOP.CART_SHIPPING" | translate }}</span>
|
<span>{{ "SHOP.CART_SHIPPING" | translate }}</span>
|
||||||
@@ -284,7 +288,10 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="product-grid">
|
<div class="product-grid">
|
||||||
@for (product of products(); track trackByProduct($index, product)) {
|
@for (
|
||||||
|
product of products();
|
||||||
|
track trackByProduct($index, product)
|
||||||
|
) {
|
||||||
<app-product-card
|
<app-product-card
|
||||||
[product]="product"
|
[product]="product"
|
||||||
[cartQuantity]="productCartQuantity(product.id)"
|
[cartQuantity]="productCartQuantity(product.id)"
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
.shop-page {
|
.shop-page {
|
||||||
--shop-hero-bg: #f8f3e5;
|
--shop-hero-bg: #f8f3e5;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top right, rgba(250, 207, 10, 0.24), transparent 22%),
|
radial-gradient(
|
||||||
|
circle at top right,
|
||||||
|
rgba(250, 207, 10, 0.24),
|
||||||
|
transparent 22%
|
||||||
|
),
|
||||||
linear-gradient(180deg, #faf7ef 0%, #f6f2e8 24%, var(--color-bg) 24%);
|
linear-gradient(180deg, #faf7ef 0%, #f6f2e8 24%, var(--color-bg) 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,13 +300,12 @@
|
|||||||
.skeleton-card {
|
.skeleton-card {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
border-radius: 1.1rem;
|
border-radius: 1.1rem;
|
||||||
background:
|
background: linear-gradient(
|
||||||
linear-gradient(
|
110deg,
|
||||||
110deg,
|
rgba(255, 255, 255, 0.7) 8%,
|
||||||
rgba(255, 255, 255, 0.7) 8%,
|
rgba(238, 235, 226, 0.95) 18%,
|
||||||
rgba(238, 235, 226, 0.95) 18%,
|
rgba(255, 255, 255, 0.7) 33%
|
||||||
rgba(255, 255, 255, 0.7) 33%
|
);
|
||||||
);
|
|
||||||
background-size: 220% 100%;
|
background-size: 220% 100%;
|
||||||
animation: skeleton 1.35s linear infinite;
|
animation: skeleton 1.35s linear infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ import {
|
|||||||
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import { catchError, combineLatest, finalize, forkJoin, map, of, switchMap, tap } from 'rxjs';
|
import {
|
||||||
|
catchError,
|
||||||
|
combineLatest,
|
||||||
|
finalize,
|
||||||
|
forkJoin,
|
||||||
|
map,
|
||||||
|
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 { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
||||||
@@ -69,7 +78,9 @@ export class ShopPageComponent {
|
|||||||
() => this.selectedCategory()?.slug ?? this.categorySlug() ?? null,
|
() => this.selectedCategory()?.slug ?? this.categorySlug() ?? null,
|
||||||
);
|
);
|
||||||
readonly cartItems = computed(() =>
|
readonly cartItems = computed(() =>
|
||||||
(this.cart()?.items ?? []).filter((item) => item.lineItemType === 'SHOP_PRODUCT'),
|
(this.cart()?.items ?? []).filter(
|
||||||
|
(item) => item.lineItemType === 'SHOP_PRODUCT',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
readonly cartHasItems = computed(() => this.cartItems().length > 0);
|
readonly cartHasItems = computed(() => this.cartItems().length > 0);
|
||||||
|
|
||||||
@@ -87,7 +98,9 @@ export class ShopPageComponent {
|
|||||||
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
toObservable(this.categorySlug, { injector: this.injector }),
|
toObservable(this.categorySlug, { injector: this.injector }),
|
||||||
toObservable(this.languageService.currentLang, { injector: this.injector }),
|
toObservable(this.languageService.currentLang, {
|
||||||
|
injector: this.injector,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(() => {
|
tap(() => {
|
||||||
@@ -145,7 +158,9 @@ export class ShopPageComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cartItemName(item: ShopCartItem): string {
|
cartItemName(item: ShopCartItem): string {
|
||||||
return item.displayName || item.shopProductName || item.originalFilename || '-';
|
return (
|
||||||
|
item.displayName || item.shopProductName || item.originalFilename || '-'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cartItemVariant(item: ShopCartItem): string | null {
|
cartItemVariant(item: ShopCartItem): string | null {
|
||||||
@@ -261,12 +276,14 @@ export class ShopPageComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const title =
|
const title =
|
||||||
category.seoTitle || `${category.name} | ${this.translate.instant('SHOP.TITLE')} | 3D fab`;
|
category.seoTitle ||
|
||||||
|
`${category.name} | ${this.translate.instant('SHOP.TITLE')} | 3D fab`;
|
||||||
const description =
|
const description =
|
||||||
category.seoDescription ||
|
category.seoDescription ||
|
||||||
category.description ||
|
category.description ||
|
||||||
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION');
|
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION');
|
||||||
const robots = category.indexable === false ? 'noindex, nofollow' : 'index, follow';
|
const robots =
|
||||||
|
category.indexable === false ? 'noindex, nofollow' : 'index, follow';
|
||||||
|
|
||||||
this.seoService.applyPageSeo({
|
this.seoService.applyPageSeo({
|
||||||
title,
|
title,
|
||||||
|
|||||||
Reference in New Issue
Block a user