style: apply prettier formatting

This commit is contained in:
printcalc-ci
2026-03-10 09:54:16 +00:00
parent 7cd9ef53b5
commit b9e6916dfe
14 changed files with 293 additions and 110 deletions

View File

@@ -62,7 +62,9 @@
> >
<circle cx="8" cy="21" r="1"></circle> <circle cx="8" cy="21" r="1"></circle>
<circle cx="19" cy="21" r="1"></circle> <circle cx="19" cy="21" r="1"></circle>
<path d="M2.05 2h2l2.4 12.45a2 2 0 0 0 2 1.55h9.7a2 2 0 0 0 1.95-1.57L22 7H6"></path> <path
d="M2.05 2h2l2.4 12.45a2 2 0 0 0 2 1.55h9.7a2 2 0 0 0 1.95-1.57L22 7H6"
></path>
</svg> </svg>
@if (cartItemCount() > 0) { @if (cartItemCount() > 0) {
<span class="cart-badge">{{ cartItemCount() }}</span> <span class="cart-badge">{{ cartItemCount() }}</span>
@@ -145,7 +147,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)"
> >
- -
@@ -153,7 +157,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)"
> >
+ +
@@ -180,7 +186,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>

View File

@@ -1,11 +1,19 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, DestroyRef, computed, inject, signal } from '@angular/core'; import { Component, DestroyRef, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationStart, Router, RouterLink, RouterLinkActive } from '@angular/router'; import {
NavigationStart,
Router,
RouterLink,
RouterLinkActive,
} from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { LanguageService } from '../services/language.service'; import { LanguageService } from '../services/language.service';
import { routes } from '../../app.routes'; import { routes } from '../../app.routes';
import { ShopCartItem, ShopService } from '../../features/shop/services/shop.service'; import {
ShopCartItem,
ShopService,
} from '../../features/shop/services/shop.service';
import { finalize } from 'rxjs'; import { finalize } from 'rxjs';
@Component({ @Component({
@@ -28,7 +36,9 @@ export class NavbarComponent {
readonly cart = this.shopService.cart; readonly cart = this.shopService.cart;
readonly cartLoading = this.shopService.cartLoading; readonly cartLoading = this.shopService.cartLoading;
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);
readonly cartItemCount = this.shopService.cartItemCount; readonly cartItemCount = this.shopService.cartItemCount;
@@ -127,7 +137,9 @@ export class NavbarComponent {
} }
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 {

View File

@@ -2,9 +2,7 @@
<header class="section-header"> <header class="section-header">
<div class="header-copy"> <div class="header-copy">
<h2>Richieste di contatto</h2> <h2>Richieste di contatto</h2>
<p> <p>Richieste preventivo personalizzato ricevute dal sito.</p>
Richieste preventivo personalizzato ricevute dal sito.
</p>
<span class="total-pill ui-pill">{{ requests.length }} richieste</span> <span class="total-pill ui-pill">{{ requests.length }} richieste</span>
</div> </div>
<button <button

View File

@@ -2,9 +2,7 @@
<header class="dashboard-header section-header"> <header class="dashboard-header section-header">
<div> <div>
<h1>Ordini</h1> <h1>Ordini</h1>
<p> <p>Seleziona un ordine a sinistra e gestiscilo nel dettaglio a destra.</p>
Seleziona un ordine a sinistra e gestiscilo nel dettaglio a destra.
</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button <button

View File

@@ -2,9 +2,7 @@
<header class="section-header"> <header class="section-header">
<div> <div>
<h2>Sessioni quote</h2> <h2>Sessioni quote</h2>
<p> <p>Sessioni create dal configuratore con stato e conversione ordine.</p>
Sessioni create dal configuratore con stato e conversione ordine.
</p>
</div> </div>
<button <button
type="button" type="button"

View File

@@ -17,7 +17,11 @@
<strong>{{ categories.length }}</strong> <strong>{{ categories.length }}</strong>
<span>categorie</span> <span>categorie</span>
</article> </article>
<button type="button" class="ui-button ui-button--ghost" (click)="loadWorkspace()"> <button
type="button"
class="ui-button ui-button--ghost"
(click)="loadWorkspace()"
>
Aggiorna Aggiorna
</button> </button>
<button type="button" class="ui-button" (click)="startCreateProduct()"> <button type="button" class="ui-button" (click)="startCreateProduct()">
@@ -26,7 +30,9 @@
</div> </div>
</header> </header>
<p class="ui-banner ui-banner--error" *ngIf="errorMessage">{{ errorMessage }}</p> <p class="ui-banner ui-banner--error" *ngIf="errorMessage">
{{ errorMessage }}
</p>
<p class="ui-banner ui-banner--success" *ngIf="successMessage"> <p class="ui-banner ui-banner--success" *ngIf="successMessage">
{{ successMessage }} {{ successMessage }}
</p> </p>
@@ -49,7 +55,9 @@
class="ui-button ui-button--ghost" class="ui-button ui-button--ghost"
(click)="toggleCategoryManager()" (click)="toggleCategoryManager()"
> >
{{ showCategoryManager ? "Chiudi categorie" : "Gestisci categorie" }} {{
showCategoryManager ? "Chiudi categorie" : "Gestisci categorie"
}}
</button> </button>
</div> </div>
@@ -89,7 +97,10 @@
[ngModel]="productStatusFilter" [ngModel]="productStatusFilter"
(ngModelChange)="onProductStatusFilterChange($event)" (ngModelChange)="onProductStatusFilterChange($event)"
> >
<option *ngFor="let filter of productStatusFilters" [ngValue]="filter"> <option
*ngFor="let filter of productStatusFilters"
[ngValue]="filter"
>
{{ filter }} {{ filter }}
</option> </option>
</select> </select>
@@ -97,7 +108,10 @@
</div> </div>
</div> </div>
<section class="category-manager ui-subpanel ui-subpanel--elevated" *ngIf="showCategoryManager"> <section
class="category-manager ui-subpanel ui-subpanel--elevated"
*ngIf="showCategoryManager"
>
<header class="category-manager__header"> <header class="category-manager__header">
<div> <div>
<h3>Categorie shop</h3> <h3>Categorie shop</h3>
@@ -146,10 +160,16 @@
<div class="panel-heading"> <div class="panel-heading">
<div> <div>
<h4> <h4>
{{ categoryForm.id ? "Modifica categoria" : "Nuova categoria" }} {{
categoryForm.id ? "Modifica categoria" : "Nuova categoria"
}}
</h4> </h4>
<p *ngIf="categoryForm.id">Aggiorna struttura, SEO e visibilità.</p> <p *ngIf="categoryForm.id">
<p *ngIf="!categoryForm.id">Crea una nuova categoria del catalogo.</p> Aggiorna struttura, SEO e visibilità.
</p>
<p *ngIf="!categoryForm.id">
Crea una nuova categoria del catalogo.
</p>
</div> </div>
</div> </div>
@@ -340,9 +360,13 @@
</div> </div>
</td> </td>
<td>{{ product.categoryName }}</td> <td>{{ product.categoryName }}</td>
<td>{{ product.activeVariantCount }} / {{ product.variantCount }}</td>
<td> <td>
{{ product.priceFromChf | currency: "CHF" : "symbol" : "1.2-2" }} {{ product.activeVariantCount }} / {{ product.variantCount }}
</td>
<td>
{{
product.priceFromChf | currency: "CHF" : "symbol" : "1.2-2"
}}
<span *ngIf="product.priceToChf !== product.priceFromChf"> <span *ngIf="product.priceToChf !== product.priceFromChf">
- -
{{ {{
@@ -366,7 +390,9 @@
</td> </td>
</tr> </tr>
<tr *ngIf="filteredProducts.length === 0"> <tr *ngIf="filteredProducts.length === 0">
<td colspan="5">Nessun prodotto trovato con i filtri correnti.</td> <td colspan="5">
Nessun prodotto trovato con i filtri correnti.
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -388,7 +414,11 @@
<div class="detail-header"> <div class="detail-header">
<div> <div>
<h2> <h2>
{{ productMode === "create" ? "Nuovo prodotto" : selectedProduct?.name }} {{
productMode === "create"
? "Nuovo prodotto"
: selectedProduct?.name
}}
</h2> </h2>
<p *ngIf="productMode === 'create'"> <p *ngIf="productMode === 'create'">
Compila i campi e salva per creare un nuovo prodotto shop. Compila i campi e salva per creare un nuovo prodotto shop.
@@ -412,20 +442,29 @@
</span> </span>
</div> </div>
<p class="detail-loading" *ngIf="detailLoading">Caricamento dettaglio...</p> <p class="detail-loading" *ngIf="detailLoading">
Caricamento dettaglio...
</p>
<div class="ui-meta-grid" *ngIf="productMode === 'edit' && selectedProduct"> <div
class="ui-meta-grid"
*ngIf="productMode === 'edit' && selectedProduct"
>
<div class="ui-meta-item"> <div class="ui-meta-item">
<strong>Categoria</strong> <strong>Categoria</strong>
<span>{{ selectedProduct.categoryName }}</span> <span>{{ selectedProduct.categoryName }}</span>
</div> </div>
<div class="ui-meta-item"> <div class="ui-meta-item">
<strong>Creato il</strong> <strong>Creato il</strong>
<span>{{ selectedProduct.createdAt | date: "dd.MM.yyyy HH:mm" }}</span> <span>{{
selectedProduct.createdAt | date: "dd.MM.yyyy HH:mm"
}}</span>
</div> </div>
<div class="ui-meta-item"> <div class="ui-meta-item">
<strong>Aggiornato il</strong> <strong>Aggiornato il</strong>
<span>{{ selectedProduct.updatedAt | date: "dd.MM.yyyy HH:mm" }}</span> <span>{{
selectedProduct.updatedAt | date: "dd.MM.yyyy HH:mm"
}}</span>
</div> </div>
<div class="ui-meta-item"> <div class="ui-meta-item">
<strong>Media</strong> <strong>Media</strong>
@@ -530,7 +569,9 @@
<div class="panel-heading"> <div class="panel-heading">
<div> <div>
<h3>Contenuti localizzati</h3> <h3>Contenuti localizzati</h3>
<p>Nome obbligatorio in tutte le lingue. Descrizioni opzionali.</p> <p>
Nome obbligatorio in tutte le lingue. Descrizioni opzionali.
</p>
</div> </div>
</div> </div>
@@ -650,7 +691,11 @@
<h3>Varianti</h3> <h3>Varianti</h3>
<p>Colori, materiale interno, SKU e prezzi.</p> <p>Colori, materiale interno, SKU e prezzi.</p>
</div> </div>
<button type="button" class="ui-button ui-button--ghost" (click)="addVariant()"> <button
type="button"
class="ui-button ui-button--ghost"
(click)="addVariant()"
>
Aggiungi variante Aggiungi variante
</button> </button>
</div> </div>
@@ -658,12 +703,20 @@
<div class="variant-stack"> <div class="variant-stack">
<article <article
class="variant-card" class="variant-card"
*ngFor="let variant of productForm.variants; let index = index; trackBy: trackVariant" *ngFor="
let variant of productForm.variants;
let index = index;
trackBy: trackVariant
"
> >
<div class="variant-card__header"> <div class="variant-card__header">
<div> <div>
<h4> <h4>
{{ variant.variantLabel || variant.colorName || "Nuova variante" }} {{
variant.variantLabel ||
variant.colorName ||
"Nuova variante"
}}
</h4> </h4>
<p>Ordine {{ variant.sortOrder }}</p> <p>Ordine {{ variant.sortOrder }}</p>
</div> </div>
@@ -801,8 +854,8 @@
<div> <div>
<h3>Immagini e modello 3D</h3> <h3>Immagini e modello 3D</h3>
<p> <p>
Upload protetto con whitelist tipi file; il modello 3D è disponibile Upload protetto con whitelist tipi file; il modello 3D è
solo dopo il primo salvataggio del prodotto. disponibile solo dopo il primo salvataggio del prodotto.
</p> </p>
</div> </div>
</div> </div>
@@ -811,12 +864,17 @@
Salva prima il prodotto per collegare immagini e modello 3D. Salva prima il prodotto per collegare immagini e modello 3D.
</div> </div>
<div class="media-grid" *ngIf="productMode === 'edit' && selectedProduct"> <div
class="media-grid"
*ngIf="productMode === 'edit' && selectedProduct"
>
<section class="ui-subpanel ui-subpanel--soft"> <section class="ui-subpanel ui-subpanel--soft">
<div class="panel-heading"> <div class="panel-heading">
<div> <div>
<h4>Nuova immagine prodotto</h4> <h4>Nuova immagine prodotto</h4>
<p>JPG, PNG o WEBP con titolo e alt text in tutte le lingue.</p> <p>
JPG, PNG o WEBP con titolo e alt text in tutte le lingue.
</p>
</div> </div>
</div> </div>
@@ -840,7 +898,10 @@
</label> </label>
</div> </div>
<div class="preview-card form-field--wide" *ngIf="imageUploadState.previewUrl"> <div
class="preview-card form-field--wide"
*ngIf="imageUploadState.previewUrl"
>
<img [src]="imageUploadState.previewUrl" alt="" /> <img [src]="imageUploadState.previewUrl" alt="" />
</div> </div>
@@ -854,7 +915,9 @@
*ngFor="let language of mediaLanguages" *ngFor="let language of mediaLanguages"
type="button" type="button"
class="ui-language-toolbar__button" class="ui-language-toolbar__button"
[class.active]="imageUploadState.activeLanguage === language" [class.active]="
imageUploadState.activeLanguage === language
"
[class.complete]="isImageLanguageComplete(language)" [class.complete]="isImageLanguageComplete(language)"
[class.incomplete]="!isImageLanguageComplete(language)" [class.incomplete]="!isImageLanguageComplete(language)"
(click)="setActiveImageLanguage(language)" (click)="setActiveImageLanguage(language)"
@@ -872,7 +935,9 @@
class="ui-form-control" class="ui-form-control"
type="text" type="text"
[(ngModel)]=" [(ngModel)]="
imageUploadState.translations[imageUploadState.activeLanguage].title imageUploadState.translations[
imageUploadState.activeLanguage
].title
" "
name="productImageTitle" name="productImageTitle"
/> />
@@ -880,13 +945,16 @@
<label class="ui-form-field"> <label class="ui-form-field">
<span class="ui-form-caption"> <span class="ui-form-caption">
Alt text {{ languageLabels[imageUploadState.activeLanguage] }} Alt text
{{ languageLabels[imageUploadState.activeLanguage] }}
</span> </span>
<input <input
class="ui-form-control" class="ui-form-control"
type="text" type="text"
[(ngModel)]=" [(ngModel)]="
imageUploadState.translations[imageUploadState.activeLanguage].altText imageUploadState.translations[
imageUploadState.activeLanguage
].altText
" "
name="productImageAltText" name="productImageAltText"
/> />
@@ -922,7 +990,11 @@
(click)="uploadProductImage()" (click)="uploadProductImage()"
[disabled]="imageUploadState.saving || !imageUploadState.file" [disabled]="imageUploadState.saving || !imageUploadState.file"
> >
{{ imageUploadState.saving ? "Caricamento..." : "Carica immagine" }} {{
imageUploadState.saving
? "Caricamento..."
: "Carica immagine"
}}
</button> </button>
</div> </div>
</section> </section>
@@ -931,31 +1003,49 @@
<div class="panel-heading"> <div class="panel-heading">
<div> <div>
<h4>Immagini attive</h4> <h4>Immagini attive</h4>
<p>{{ productImages.length }} immagini collegate al prodotto.</p> <p>
{{ productImages.length }} immagini collegate al prodotto.
</p>
</div> </div>
</div> </div>
<div class="image-stack" *ngIf="productImages.length; else emptyImagesTpl"> <div
class="image-stack"
*ngIf="productImages.length; else emptyImagesTpl"
>
<article <article
class="image-item" class="image-item"
*ngFor="let image of productImages; trackBy: trackImage" *ngFor="let image of productImages; trackBy: trackImage"
> >
<div class="image-item__preview"> <div class="image-item__preview">
<img *ngIf="image.previewUrl; else noImagePreviewTpl" [src]="image.previewUrl" alt="" /> <img
*ngIf="image.previewUrl; else noImagePreviewTpl"
[src]="image.previewUrl"
alt=""
/>
</div> </div>
<div class="image-item__content"> <div class="image-item__content">
<div class="image-item__header"> <div class="image-item__header">
<strong> <strong>
{{ image.translations[imageUploadState.activeLanguage].title || "Senza titolo" }} {{
image.translations[imageUploadState.activeLanguage]
.title || "Senza titolo"
}}
</strong> </strong>
<span class="ui-pill ui-pill--soft" *ngIf="image.isPrimary"> <span
class="ui-pill ui-pill--soft"
*ngIf="image.isPrimary"
>
Primaria Primaria
</span> </span>
</div> </div>
<p class="image-meta"> <p class="image-meta">
{{ image.translations[imageUploadState.activeLanguage].altText || "Alt text mancante" }} {{
image.translations[imageUploadState.activeLanguage]
.altText || "Alt text mancante"
}}
</p> </p>
<div class="image-item__controls"> <div class="image-item__controls">
@@ -982,7 +1072,9 @@
type="button" type="button"
class="ui-button ui-button--ghost" class="ui-button ui-button--ghost"
(click)="setPrimaryImage(image)" (click)="setPrimaryImage(image)"
[disabled]="isImageBusy(image.usageId) || image.isPrimary" [disabled]="
isImageBusy(image.usageId) || image.isPrimary
"
> >
{{ image.isPrimary ? "Primaria" : "Rendi primaria" }} {{ image.isPrimary ? "Primaria" : "Rendi primaria" }}
</button> </button>
@@ -1012,7 +1104,10 @@
</div> </div>
</div> </div>
<div class="model-summary" *ngIf="selectedProduct.model3d as model; else noModelTpl"> <div
class="model-summary"
*ngIf="selectedProduct.model3d as model; else noModelTpl"
>
<div class="ui-meta-grid"> <div class="ui-meta-grid">
<div class="ui-meta-item"> <div class="ui-meta-item">
<strong>File</strong> <strong>File</strong>
@@ -1025,7 +1120,8 @@
<div class="ui-meta-item"> <div class="ui-meta-item">
<strong>Bounding box</strong> <strong>Bounding box</strong>
<span> <span>
{{ model.boundingBoxXMm || 0 }} × {{ model.boundingBoxYMm || 0 }} × {{ model.boundingBoxXMm || 0 }} ×
{{ model.boundingBoxYMm || 0 }} ×
{{ model.boundingBoxZMm || 0 }} mm {{ model.boundingBoxZMm || 0 }} mm
</span> </span>
</div> </div>
@@ -1053,7 +1149,9 @@
</div> </div>
<div class="ui-form-field"> <div class="ui-form-field">
<span class="ui-form-caption">Carica o sostituisci modello</span> <span class="ui-form-caption"
>Carica o sostituisci modello</span
>
<input <input
id="product-model-file" id="product-model-file"
class="sr-only" class="sr-only"
@@ -1127,5 +1225,7 @@
</ng-template> </ng-template>
<ng-template #noModelTpl> <ng-template #noModelTpl>
<div class="locked-panel">Nessun modello 3D caricato per questo prodotto.</div> <div class="locked-panel">
Nessun modello 3D caricato per questo prodotto.
</div>
</ng-template> </ng-template>

View File

@@ -170,7 +170,8 @@ export class AdminShopComponent implements OnInit, OnDestroy {
readonly categoryForm: CategoryFormState = this.createEmptyCategoryForm(); readonly categoryForm: CategoryFormState = this.createEmptyCategoryForm();
readonly productForm: ProductFormState = this.createEmptyProductForm(); readonly productForm: ProductFormState = this.createEmptyProductForm();
imageUploadState: ProductImageUploadState = this.createEmptyImageUploadState(); imageUploadState: ProductImageUploadState =
this.createEmptyImageUploadState();
modelUploadFile: File | null = null; modelUploadFile: File | null = null;
ngOnInit(): void { ngOnInit(): void {
@@ -220,7 +221,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
const targetProductId = const targetProductId =
preferredProductId ?? preferredProductId ??
(this.productMode === 'edit' ? this.selectedProductId : null); (this.productMode === 'edit' ? this.selectedProductId : null);
if (targetProductId && products.some((product) => product.id === targetProductId)) { if (
targetProductId &&
products.some((product) => product.id === targetProductId)
) {
this.openProduct(targetProductId); this.openProduct(targetProductId);
return; return;
} }
@@ -524,8 +528,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
addVariant(): void { addVariant(): void {
const sortOrder = const sortOrder = (this.productForm.variants.at(-1)?.sortOrder ?? -1) + 1;
(this.productForm.variants.at(-1)?.sortOrder ?? -1) + 1;
const firstVariant = this.productForm.variants.length === 0; const firstVariant = this.productForm.variants.length === 0;
this.productForm.variants = [ this.productForm.variants = [
...this.productForm.variants, ...this.productForm.variants,
@@ -621,11 +624,17 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
deleteModel(): void { deleteModel(): void {
if (!this.selectedProductId || this.deletingModel || !this.selectedProduct?.model3d) { if (
!this.selectedProductId ||
this.deletingModel ||
!this.selectedProduct?.model3d
) {
return; return;
} }
if (!window.confirm('Rimuovere il modello 3D associato a questo prodotto?')) { if (
!window.confirm('Rimuovere il modello 3D associato a questo prodotto?')
) {
return; return;
} }
@@ -709,11 +718,15 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
getActiveImageTranslation(): AdminMediaTranslation { getActiveImageTranslation(): AdminMediaTranslation {
return this.imageUploadState.translations[this.imageUploadState.activeLanguage]; return this.imageUploadState.translations[
this.imageUploadState.activeLanguage
];
} }
isImageLanguageComplete(language: AdminMediaLanguage): boolean { isImageLanguageComplete(language: AdminMediaLanguage): boolean {
return this.isTranslationComplete(this.imageUploadState.translations[language]); return this.isTranslationComplete(
this.imageUploadState.translations[language],
);
} }
uploadProductImage(): void { uploadProductImage(): void {
@@ -930,7 +943,8 @@ export class AdminShopComponent implements OnInit, OnDestroy {
const searchNeedle = this.productSearchTerm.trim().toLowerCase(); const searchNeedle = this.productSearchTerm.trim().toLowerCase();
this.filteredProducts = this.products.filter((product) => { this.filteredProducts = this.products.filter((product) => {
const matchesCategory = const matchesCategory =
this.categoryFilter === 'ALL' || product.categoryId === this.categoryFilter; this.categoryFilter === 'ALL' ||
product.categoryId === this.categoryFilter;
const matchesStatus = const matchesStatus =
this.productStatusFilter === 'ALL' || this.productStatusFilter === 'ALL' ||
(this.productStatusFilter === 'ACTIVE' && product.isActive) || (this.productStatusFilter === 'ACTIVE' && product.isActive) ||
@@ -1215,7 +1229,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
if (!Number.isFinite(price) || price < 0) { if (!Number.isFinite(price) || price < 0) {
return `La variante "${variant.colorName.trim()}" ha un prezzo non valido.`; return `La variante "${variant.colorName.trim()}" ha un prezzo non valido.`;
} }
if (variant.colorHex.trim() && !/^#[0-9A-Fa-f]{6}$/.test(variant.colorHex.trim())) { if (
variant.colorHex.trim() &&
!/^#[0-9A-Fa-f]{6}$/.test(variant.colorHex.trim())
) {
return `La variante "${variant.colorName.trim()}" ha un colore HEX non valido.`; return `La variante "${variant.colorName.trim()}" ha un colore HEX non valido.`;
} }
if (variant.isDefault) { if (variant.isDefault) {
@@ -1426,7 +1443,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
private deriveDefaultTitle(filename: string): string { private deriveDefaultTitle(filename: string): string {
return filename.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim(); return filename
.replace(/\.[^.]+$/, '')
.replace(/[-_]+/g, ' ')
.trim();
} }
private optionalValue(value: string): string | undefined { private optionalValue(value: string): string | undefined {
@@ -1445,7 +1465,9 @@ export class AdminShopComponent implements OnInit, OnDestroy {
private resolveFileExtension(filename: string): string { private resolveFileExtension(filename: string): string {
const lastDotIndex = filename.lastIndexOf('.'); const lastDotIndex = filename.lastIndexOf('.');
return lastDotIndex >= 0 ? filename.slice(lastDotIndex + 1).toLowerCase() : ''; return lastDotIndex >= 0
? filename.slice(lastDotIndex + 1).toLowerCase()
: '';
} }
private isAllowedImageType(mimeType: string, filename: string): boolean { private isAllowedImageType(mimeType: string, filename: string): boolean {

View File

@@ -152,7 +152,8 @@ export interface AdminShopProduct {
updatedAt: string; updatedAt: string;
} }
export interface AdminShopMediaUsage extends Omit<AdminMediaUsage, 'translations'> { export interface AdminShopMediaUsage
extends Omit<AdminMediaUsage, 'translations'> {
translations: Record<AdminMediaLanguage, AdminMediaTranslation>; translations: Record<AdminMediaLanguage, AdminMediaTranslation>;
} }
@@ -214,9 +215,12 @@ export class AdminShopService {
} }
getCategoryTree(): Observable<AdminShopCategory[]> { getCategoryTree(): Observable<AdminShopCategory[]> {
return this.http.get<AdminShopCategory[]>(`${this.categoriesBaseUrl}/tree`, { return this.http.get<AdminShopCategory[]>(
withCredentials: true, `${this.categoriesBaseUrl}/tree`,
}); {
withCredentials: true,
},
);
} }
getCategory(categoryId: string): Observable<AdminShopCategory> { getCategory(categoryId: string): Observable<AdminShopCategory> {
@@ -258,9 +262,12 @@ export class AdminShopService {
} }
getProduct(productId: string): Observable<AdminShopProduct> { getProduct(productId: string): Observable<AdminShopProduct> {
return this.http.get<AdminShopProduct>(`${this.productsBaseUrl}/${productId}`, { return this.http.get<AdminShopProduct>(
withCredentials: true, `${this.productsBaseUrl}/${productId}`,
}); {
withCredentials: true,
},
);
} }
createProduct( createProduct(
@@ -288,7 +295,10 @@ export class AdminShopService {
}); });
} }
uploadProductModel(productId: string, file: File): Observable<AdminShopProduct> { uploadProductModel(
productId: string,
file: File,
): Observable<AdminShopProduct> {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
return this.http.post<AdminShopProduct>( return this.http.post<AdminShopProduct>(
@@ -299,9 +309,12 @@ export class AdminShopService {
} }
deleteProductModel(productId: string): Observable<void> { deleteProductModel(productId: string): Observable<void> {
return this.http.delete<void>(`${this.productsBaseUrl}/${productId}/model`, { return this.http.delete<void>(
withCredentials: true, `${this.productsBaseUrl}/${productId}/model`,
}); {
withCredentials: true,
},
);
} }
listMediaAssets(): Observable<AdminMediaAsset[]> { listMediaAssets(): Observable<AdminMediaAsset[]> {

View File

@@ -40,7 +40,11 @@
align-items: flex-end; align-items: flex-end;
padding: var(--space-5); padding: var(--space-5);
background: background:
radial-gradient(circle at top right, rgba(250, 207, 10, 0.24), transparent 36%), radial-gradient(
circle at top right,
rgba(250, 207, 10, 0.24),
transparent 36%
),
linear-gradient(160deg, #f7f4ed 0%, #ece7db 100%); linear-gradient(160deg, #f7f4ed 0%, #ece7db 100%);
} }

View File

@@ -56,7 +56,11 @@
align-items: flex-end; align-items: flex-end;
padding: var(--space-6); padding: var(--space-6);
background: background:
radial-gradient(circle at top right, rgba(250, 207, 10, 0.24), transparent 34%), radial-gradient(
circle at top right,
rgba(250, 207, 10, 0.24),
transparent 34%
),
linear-gradient(160deg, #f8f4ea 0%, #eee8db 100%); linear-gradient(160deg, #f8f4ea 0%, #eee8db 100%);
} }
@@ -314,13 +318,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;
} }

View File

@@ -4,7 +4,9 @@
<p class="ui-simple-hero__subtitle"> <p class="ui-simple-hero__subtitle">
{{ {{
selectedCategory() selectedCategory()
? (selectedCategory()?.description || ("SHOP.CATEGORY_META" | translate: { count: selectedCategory()?.productCount || 0 })) ? selectedCategory()?.description ||
("SHOP.CATEGORY_META"
| translate: { count: selectedCategory()?.productCount || 0 })
: ("SHOP.CUSTOM_PART_CTA" | translate) : ("SHOP.CUSTOM_PART_CTA" | translate)
}} }}
</p> </p>
@@ -20,7 +22,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>
@@ -100,7 +104,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)"
> >
- -
@@ -108,7 +114,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)"
> >
+ +
@@ -134,7 +142,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>
@@ -202,7 +212,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)"

View File

@@ -253,13 +253,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;
} }

View File

@@ -11,7 +11,15 @@ 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, of, switchMap, tap } from 'rxjs'; import {
catchError,
combineLatest,
finalize,
forkJoin,
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';
@@ -68,7 +76,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);
@@ -86,7 +96,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(() => {
@@ -137,7 +149,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 {
@@ -253,12 +267,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,

View File

@@ -35,7 +35,6 @@
margin: var(--space-6) auto; margin: var(--space-6) auto;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 1.25rem; font-size: 1.25rem;
} }
.ui-simple-hero__actions { .ui-simple-hero__actions {