produzione 1 #9

Merged
JoeKung merged 135 commits from dev into main 2026-03-03 09:58:04 +01:00
23 changed files with 211 additions and 111 deletions
Showing only changes of commit 877171ceb1 - Show all commits

View File

@@ -12,22 +12,22 @@ export interface ColorCategory {
export const PRODUCT_COLORS: ColorCategory[] = [ export const PRODUCT_COLORS: ColorCategory[] = [
{ {
name: 'Lucidi', // Glossy name: 'COLOR.CATEGORY_GLOSSY',
colors: [ colors: [
{ label: 'Black', value: 'Black', hex: '#1a1a1a' }, // Not pure black for visibility { label: 'COLOR.NAME.BLACK', value: 'Black', hex: '#1a1a1a' }, // Not pure black for visibility
{ label: 'White', value: 'White', hex: '#f5f5f5' }, { label: 'COLOR.NAME.WHITE', value: 'White', hex: '#f5f5f5' },
{ label: 'Red', value: 'Red', hex: '#d32f2f', outOfStock: true }, { label: 'COLOR.NAME.RED', value: 'Red', hex: '#d32f2f', outOfStock: true },
{ label: 'Blue', value: 'Blue', hex: '#1976d2' }, { label: 'COLOR.NAME.BLUE', value: 'Blue', hex: '#1976d2' },
{ label: 'Green', value: 'Green', hex: '#388e3c' }, { label: 'COLOR.NAME.GREEN', value: 'Green', hex: '#388e3c' },
{ label: 'Yellow', value: 'Yellow', hex: '#fbc02d' } { label: 'COLOR.NAME.YELLOW', value: 'Yellow', hex: '#fbc02d' }
] ]
}, },
{ {
name: 'Opachi', // Matte name: 'COLOR.CATEGORY_MATTE',
colors: [ colors: [
{ label: 'Matte Black', value: 'Matte Black', hex: '#2c2c2c' }, // Lighter charcoal for matte { label: 'COLOR.NAME.MATTE_BLACK', value: 'Matte Black', hex: '#2c2c2c' }, // Lighter charcoal for matte
{ label: 'Matte White', value: 'Matte White', hex: '#e0e0e0' }, { label: 'COLOR.NAME.MATTE_WHITE', value: 'Matte White', hex: '#e0e0e0' },
{ label: 'Matte Gray', value: 'Matte Gray', hex: '#757575' } { label: 'COLOR.NAME.MATTE_GRAY', value: 'Matte Gray', hex: '#757575' }
] ]
} }
]; ];

View File

@@ -40,11 +40,11 @@
(keydown.space)="toggleSelectedMember('joe'); $event.preventDefault()" (keydown.space)="toggleSelectedMember('joe'); $event.preventDefault()"
> >
<div class="placeholder-img"> <div class="placeholder-img">
<img src="assets/images/joe.jpg" alt="Joe Küng"> <img src="assets/images/joe.jpg" [attr.alt]="'ABOUT.MEMBER_JOE_ALT' | translate">
</div> </div>
<div class="member-info"> <div class="member-info">
<span class="member-name">Joe Küng</span> <span class="member-name">{{ 'ABOUT.MEMBER_JOE_NAME' | translate }}</span>
<span class="member-role">Studente Ingegneria Informatica</span> <span class="member-role">{{ 'ABOUT.MEMBER_JOE_ROLE' | translate }}</span>
</div> </div>
</div> </div>
<div <div
@@ -63,11 +63,11 @@
(keydown.space)="toggleSelectedMember('matteo'); $event.preventDefault()" (keydown.space)="toggleSelectedMember('matteo'); $event.preventDefault()"
> >
<div class="placeholder-img"> <div class="placeholder-img">
<img src="assets/images/matteo.jpg" alt="Matteo Caletti"> <img src="assets/images/matteo.jpg" [attr.alt]="'ABOUT.MEMBER_MATTEO_ALT' | translate">
</div> </div>
<div class="member-info"> <div class="member-info">
<span class="member-name">Matteo Caletti</span> <span class="member-name">{{ 'ABOUT.MEMBER_MATTEO_NAME' | translate }}</span>
<span class="member-role">Studente Ingegneria Elettronica</span> <span class="member-role">{{ 'ABOUT.MEMBER_MATTEO_ROLE' | translate }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,8 +20,8 @@
<!-- Initial Dropzone (Visible only when no files) --> <!-- Initial Dropzone (Visible only when no files) -->
@if (items().length === 0) { @if (items().length === 0) {
<app-dropzone <app-dropzone
[label]="'CALC.UPLOAD_LABEL' | translate" [label]="'CALC.UPLOAD_LABEL'"
[subtext]="'CALC.UPLOAD_SUB' | translate" [subtext]="'CALC.UPLOAD_SUB'"
[accept]="acceptedFormats" [accept]="acceptedFormats"
[multiple]="true" [multiple]="true"
(filesDropped)="onFilesDropped($event)"> (filesDropped)="onFilesDropped($event)">
@@ -60,7 +60,11 @@
</div> </div>
</div> </div>
<button type="button" class="btn-remove" (click)="removeItem(i); $event.stopPropagation()" title="Remove file"> <button
type="button"
class="btn-remove"
(click)="removeItem(i); $event.stopPropagation()"
[attr.title]="'CALC.REMOVE_FILE' | translate">
X X
</button> </button>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { Component, input, output, signal, OnInit, inject } from '@angular/core'; import { Component, input, output, signal, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component'; import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component';
import { AppSelectComponent } from '../../../../shared/components/app-select/app-select.component'; import { AppSelectComponent } from '../../../../shared/components/app-select/app-select.component';
import { AppDropzoneComponent } from '../../../../shared/components/app-dropzone/app-dropzone.component'; import { AppDropzoneComponent } from '../../../../shared/components/app-dropzone/app-dropzone.component';
@@ -32,6 +32,7 @@ export class UploadFormComponent implements OnInit {
private estimator = inject(QuoteEstimatorService); private estimator = inject(QuoteEstimatorService);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
private translate = inject(TranslateService);
form: FormGroup; form: FormGroup;
@@ -127,8 +128,8 @@ export class UploadFormComponent implements OnInit {
error: (err) => { error: (err) => {
console.error('Failed to load options', err); console.error('Failed to load options', err);
// Fallback for debugging/offline dev // Fallback for debugging/offline dev
this.materials.set([{ label: 'PLA (Fallback)', value: 'PLA' }]); this.materials.set([{ label: this.translate.instant('CALC.FALLBACK_MATERIAL'), value: 'PLA' }]);
this.qualities.set([{ label: 'Standard', value: 'standard' }]); this.qualities.set([{ label: this.translate.instant('CALC.FALLBACK_QUALITY_STANDARD'), value: 'standard' }]);
this.nozzleDiameters.set([{ label: '0.4 mm', value: 0.4 }]); this.nozzleDiameters.set([{ label: '0.4 mm', value: 0.4 }]);
this.setDefaults(); this.setDefaults();
} }
@@ -171,7 +172,7 @@ export class UploadFormComponent implements OnInit {
} }
if (hasError) { if (hasError) {
alert("Alcuni file superano il limite di 200MB e non sono stati aggiunti."); alert(this.translate.instant('CALC.ERR_FILE_TOO_LARGE'));
} }
if (validItems.length > 0) { if (validItems.length > 0) {

View File

@@ -112,7 +112,7 @@ export class CheckoutComponent implements OnInit {
this.route.queryParams.subscribe(params => { this.route.queryParams.subscribe(params => {
this.sessionId = params['session']; this.sessionId = params['session'];
if (!this.sessionId) { if (!this.sessionId) {
this.error = 'No active session found. Please start a new quote.'; this.error = 'CHECKOUT.ERR_NO_SESSION_START';
this.router.navigate(['/']); // Redirect if no session this.router.navigate(['/']); // Redirect if no session
return; return;
} }
@@ -143,7 +143,7 @@ export class CheckoutComponent implements OnInit {
}, },
error: (err) => { error: (err) => {
console.error('Failed to load session', err); console.error('Failed to load session', err);
this.error = 'Failed to load session details. Please try again.'; this.error = 'CHECKOUT.ERR_LOAD_SESSION';
} }
}); });
} }
@@ -196,7 +196,7 @@ export class CheckoutComponent implements OnInit {
}; };
if (!this.sessionId) { if (!this.sessionId) {
this.error = 'No active session found. Cannot create order.'; this.error = 'CHECKOUT.ERR_NO_SESSION_CREATE_ORDER';
this.isSubmitting.set(false); this.isSubmitting.set(false);
return; return;
} }
@@ -208,7 +208,7 @@ export class CheckoutComponent implements OnInit {
error: (err) => { error: (err) => {
console.error('Order creation failed', err); console.error('Order creation failed', err);
this.isSubmitting.set(false); this.isSubmitting.set(false);
this.error = 'Failed to create order. Please try again.'; this.error = 'CHECKOUT.ERR_CREATE_ORDER';
} }
}); });
} }

View File

@@ -61,7 +61,11 @@
<div class="file-grid" *ngIf="files().length > 0"> <div class="file-grid" *ngIf="files().length > 0">
<div class="file-item" *ngFor="let file of files(); let i = index"> <div class="file-item" *ngFor="let file of files(); let i = index">
<button type="button" class="remove-btn" (click)="removeFile(i)">×</button> <button
type="button"
class="remove-btn"
(click)="removeFile(i)"
[attr.aria-label]="'CONTACT.REMOVE_FILE' | translate">×</button>
<img *ngIf="file.type === 'image'" [src]="file.url" class="preview-img"> <img *ngIf="file.type === 'image'" [src]="file.url" class="preview-img">
<video *ngIf="file.type === 'video'" [src]="file.url" class="preview-video" muted playsinline preload="metadata"></video> <video *ngIf="file.type === 'video'" [src]="file.url" class="preview-video" muted playsinline preload="metadata"></video>
<div *ngIf="file.type !== 'image' && file.type !== 'video'" class="file-icon"> <div *ngIf="file.type !== 'image' && file.type !== 'video'" class="file-icon">

View File

@@ -211,7 +211,7 @@ export class ContactFormComponent implements OnDestroy {
}, },
error: (err) => { error: (err) => {
console.error('Submission failed', err); console.error('Submission failed', err);
alert('Error submitting request. Please try again.'); alert(this.translate.instant('CONTACT.ERROR_SUBMIT'));
} }
}); });

View File

@@ -110,9 +110,9 @@
<app-button variant="outline" routerLink="/contact">{{ 'HOME.BTN_REQ_SOLUTION' | translate }}</app-button> <app-button variant="outline" routerLink="/contact">{{ 'HOME.BTN_REQ_SOLUTION' | translate }}</app-button>
</div> </div>
</div> </div>
<div class="shop-gallery" tabindex="0" aria-label="Gallery prodotti shop"> <div class="shop-gallery" tabindex="0" [attr.aria-label]="'HOME.SHOP_GALLERY_ARIA' | translate">
<figure class="shop-gallery-item" *ngFor="let image of shopGalleryImages"> <figure class="shop-gallery-item" *ngFor="let image of shopGalleryImages">
<img [src]="image.src" [alt]="image.alt"> <img [src]="image.src" [alt]="image.alt | translate">
</figure> </figure>
</div> </div>
<div class="shop-cards"> <div class="shop-cards">
@@ -149,7 +149,7 @@
<img <img
class="about-feature-photo" class="about-feature-photo"
[src]="founderImages[founderImageIndex].src" [src]="founderImages[founderImageIndex].src"
[alt]="founderImages[founderImageIndex].alt" [alt]="founderImages[founderImageIndex].alt | translate"
width="1200" width="1200"
height="900" height="900"
> >
@@ -157,7 +157,7 @@
type="button" type="button"
class="founder-nav founder-nav-prev" class="founder-nav founder-nav-prev"
(click)="prevFounderImage()" (click)="prevFounderImage()"
aria-label="Foto precedente" [attr.aria-label]="'HOME.FOUNDER_PREV_ARIA' | translate"
> >
</button> </button>
@@ -165,7 +165,7 @@
type="button" type="button"
class="founder-nav founder-nav-next" class="founder-nav founder-nav-next"
(click)="nextFounderImage()" (click)="nextFounderImage()"
aria-label="Foto successiva" [attr.aria-label]="'HOME.FOUNDER_NEXT_ARIA' | translate"
> >
</button> </button>

View File

@@ -16,18 +16,18 @@ export class HomeComponent {
readonly shopGalleryImages = [ readonly shopGalleryImages = [
{ {
src: 'assets/images/home/supporto-bici.jpg', src: 'assets/images/home/supporto-bici.jpg',
alt: 'Prodotto tecnico stampato in 3D' alt: 'HOME.SHOP_IMAGE_ALT_1'
} }
]; ];
readonly founderImages = [ readonly founderImages = [
{ {
src: 'assets/images/home/da-cambiare.jpg', src: 'assets/images/home/da-cambiare.jpg',
alt: 'Founder - da cambiare' alt: 'HOME.FOUNDER_IMAGE_ALT_1'
}, },
{ {
src: 'assets/images/home/vino.JPG', src: 'assets/images/home/vino.JPG',
alt: 'Founder - vino' alt: 'HOME.FOUNDER_IMAGE_ALT_2'
} }
]; ];

View File

@@ -68,7 +68,12 @@
<h4>{{ 'PAYMENT.TWINT_TITLE' | translate }}</h4> <h4>{{ 'PAYMENT.TWINT_TITLE' | translate }}</h4>
</div> </div>
<div class="qr-placeholder"> <div class="qr-placeholder">
<img *ngIf="twintQrUrl()" class="twint-qr" [src]="getTwintQrUrl()" (error)="onTwintQrError()" alt="TWINT payment QR" /> <img
*ngIf="twintQrUrl()"
class="twint-qr"
[src]="getTwintQrUrl()"
(error)="onTwintQrError()"
[attr.alt]="'PAYMENT.TWINT_QR_ALT' | translate" />
<p>{{ 'PAYMENT.TWINT_DESC' | translate }}</p> <p>{{ 'PAYMENT.TWINT_DESC' | translate }}</p>
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p> <p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
<div class="twint-mobile-action twint-button-container"> <div class="twint-mobile-action twint-button-container">
@@ -80,7 +85,10 @@ cursor: pointer;
background-color: transparent; background-color: transparent;
border: none; border: none;
align-items: center;" (click)="openTwintPayment()"> align-items: center;" (click)="openTwintPayment()">
<img style="width: auto; height: 58px" alt="Embedded TWINT button" [src]="getTwintButtonImageUrl()"/> <img
style="width: auto; height: 58px"
[attr.alt]="'PAYMENT.TWINT_BUTTON_ALT' | translate"
[src]="getTwintButtonImageUrl()"/>
</button> </button>
</div> </div>
<p class="amount">{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}</p> <p class="amount">{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}</p>
@@ -150,7 +158,7 @@ align-items: center;" (click)="openTwintPayment()">
<div *ngIf="error()" class="error-message"> <div *ngIf="error()" class="error-message">
<app-card> <app-card>
<p>{{ error() }}</p> <p>{{ error()! | translate }}</p>
</app-card> </app-card>
</div> </div>
</div> </div>

View File

@@ -34,7 +34,7 @@ export class OrderComponent implements OnInit {
this.loadOrder(); this.loadOrder();
this.loadTwintPayment(); this.loadTwintPayment();
} else { } else {
this.error.set('Order ID not found.'); this.error.set('ORDER.ERR_ID_NOT_FOUND');
this.loading.set(false); this.loading.set(false);
} }
} }
@@ -48,7 +48,7 @@ export class OrderComponent implements OnInit {
}, },
error: (err) => { error: (err) => {
console.error('Failed to load order', err); console.error('Failed to load order', err);
this.error.set('Failed to load order details.'); this.error.set('ORDER.ERR_LOAD_ORDER');
this.loading.set(false); this.loading.set(false);
} }
}); });
@@ -145,7 +145,7 @@ export class OrderComponent implements OnInit {
}, },
error: (err) => { error: (err) => {
console.error('Failed to report payment', err); console.error('Failed to report payment', err);
this.error.set('Failed to report payment. Please try again.'); this.error.set('ORDER.ERR_REPORT_PAYMENT');
} }
}); });
} }
@@ -157,7 +157,7 @@ export class OrderComponent implements OnInit {
if (order?.id) { if (order?.id) {
return this.extractOrderNumber(order.id); return this.extractOrderNumber(order.id);
} }
return 'N/A'; return this.translate.instant('ORDER.NOT_AVAILABLE');
} }
private extractOrderNumber(orderId: string): string { private extractOrderNumber(orderId: string): string {

View File

@@ -1,13 +1,13 @@
<div class="product-card"> <div class="product-card">
<div class="image-placeholder"></div> <div class="image-placeholder"></div>
<div class="content"> <div class="content">
<span class="category">{{ product().category }}</span> <span class="category">{{ product().category | translate }}</span>
<h3 class="name"> <h3 class="name">
<a [routerLink]="['/shop', product().id]">{{ product().name }}</a> <a [routerLink]="['/shop', product().id]">{{ product().name | translate }}</a>
</h3> </h3>
<div class="footer"> <div class="footer">
<span class="price">{{ product().price | currency:'EUR' }}</span> <span class="price">{{ product().price | currency:'EUR' }}</span>
<a [routerLink]="['/shop', product().id]" class="view-btn">Dettagli</a> <a [routerLink]="['/shop', product().id]" class="view-btn">{{ 'SHOP.DETAILS' | translate }}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,13 @@
import { Component, input } from '@angular/core'; import { Component, input } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { Product } from '../../services/shop.service'; import { Product } from '../../services/shop.service';
@Component({ @Component({
selector: 'app-product-card', selector: 'app-product-card',
standalone: true, standalone: true,
imports: [CommonModule, RouterLink], imports: [CommonModule, RouterLink, TranslateModule],
templateUrl: './product-card.component.html', templateUrl: './product-card.component.html',
styleUrl: './product-card.component.scss' styleUrl: './product-card.component.scss'
}) })

View File

@@ -6,11 +6,11 @@
<div class="image-box"></div> <div class="image-box"></div>
<div class="info"> <div class="info">
<span class="category">{{ p.category }}</span> <span class="category">{{ p.category | translate }}</span>
<h1>{{ p.name }}</h1> <h1>{{ p.name | translate }}</h1>
<p class="price">{{ p.price | currency:'EUR' }}</p> <p class="price">{{ p.price | currency:'EUR' }}</p>
<p class="desc">{{ p.description }}</p> <p class="desc">{{ p.description | translate }}</p>
<div class="actions"> <div class="actions">
<app-button variant="primary" (click)="addToCart()"> <app-button variant="primary" (click)="addToCart()">

View File

@@ -1,7 +1,7 @@
import { Component, input, signal } from '@angular/core'; import { Component, input, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ShopService, Product } from './services/shop.service'; import { ShopService, Product } from './services/shop.service';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
@@ -18,7 +18,10 @@ export class ProductDetailComponent {
product = signal<Product | undefined>(undefined); product = signal<Product | undefined>(undefined);
constructor(private shopService: ShopService) {} constructor(
private shopService: ShopService,
private translate: TranslateService
) {}
ngOnInit() { ngOnInit() {
const productId = this.id(); const productId = this.id();
@@ -28,6 +31,6 @@ export class ProductDetailComponent {
} }
addToCart() { addToCart() {
alert('Aggiunto al carrello (Mock)'); alert(this.translate.instant('SHOP.MOCK_ADD_CART'));
} }
} }

View File

@@ -17,24 +17,24 @@ export class ShopService {
private staticProducts: Product[] = [ private staticProducts: Product[] = [
{ {
id: '1', id: '1',
name: 'Filamento PLA Standard', name: 'SHOP.PRODUCTS.P1.NAME',
description: 'Il classico per ogni stampa, facile e affidabile.', description: 'SHOP.PRODUCTS.P1.DESC',
price: 24.90, price: 24.90,
category: 'Filamenti' category: 'SHOP.CATEGORIES.FILAMENTS'
}, },
{ {
id: '2', id: '2',
name: 'Filamento PETG Tough', name: 'SHOP.PRODUCTS.P2.NAME',
description: 'Resistente agli urti e alle temperature.', description: 'SHOP.PRODUCTS.P2.DESC',
price: 29.90, price: 29.90,
category: 'Filamenti' category: 'SHOP.CATEGORIES.FILAMENTS'
}, },
{ {
id: '3', id: '3',
name: 'Kit Ugelli (0.4mm)', name: 'SHOP.PRODUCTS.P3.NAME',
description: 'Set di ricambio per estrusore FDM.', description: 'SHOP.PRODUCTS.P3.DESC',
price: 15.00, price: 15.00,
category: 'Accessori' category: 'SHOP.CATEGORIES.ACCESSORIES'
} }
]; ];
@@ -45,4 +45,4 @@ export class ShopService {
getProductById(id: string): Observable<Product | undefined> { getProductById(id: string): Observable<Product | undefined> {
return of(this.staticProducts.find(p => p.id === id)); return of(this.staticProducts.find(p => p.id === id));
} }
} }

View File

@@ -12,8 +12,8 @@
<div class="icon"> <div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload-cloud"><polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline></svg> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload-cloud"><polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline></svg>
</div> </div>
<p class="text">{{ label() }}</p> <p class="text">{{ label() | translate }}</p>
<p class="subtext">{{ subtext() }}</p> <p class="subtext">{{ subtext() | translate }}</p>
@if (fileNames().length > 0) { @if (fileNames().length > 0) {
<div class="file-badges"> <div class="file-badges">

View File

@@ -1,16 +1,17 @@
import { Component, input, output, signal } from '@angular/core'; import { Component, input, output, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-dropzone', selector: 'app-dropzone',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, TranslateModule],
templateUrl: './app-dropzone.component.html', templateUrl: './app-dropzone.component.html',
styleUrl: './app-dropzone.component.scss' styleUrl: './app-dropzone.component.scss'
}) })
export class AppDropzoneComponent { export class AppDropzoneComponent {
label = input<string>('Drop files here or click to upload'); label = input<string>('DROPZONE.DEFAULT_LABEL');
subtext = input<string>('Supports .stl, .3mf, .step'); subtext = input<string>('DROPZONE.DEFAULT_SUBTEXT');
accept = input<string>('.stl,.3mf,.step,.stp'); accept = input<string>('.stl,.3mf,.step,.stp');
multiple = input<boolean>(true); multiple = input<boolean>(true);

View File

@@ -14,7 +14,7 @@
<div class="color-popup"> <div class="color-popup">
@for (category of categories(); track category.name) { @for (category of categories(); track category.name) {
<div class="category"> <div class="category">
<div class="category-name">{{ category.name }}</div> <div class="category-name">{{ category.name | translate }}</div>
<div class="colors-grid"> <div class="colors-grid">
@for (color of category.colors; track color.value) { @for (color of category.colors; track color.value) {
<div <div
@@ -28,7 +28,7 @@
<div class="color-circle small" [style.background-color]="color.hex"></div> <div class="color-circle small" [style.background-color]="color.hex"></div>
</div> </div>
<span class="color-name">{{ color.label }}</span> <span class="color-name">{{ color.label | translate }}</span>
</div> </div>
} }
</div> </div>

View File

@@ -24,7 +24,7 @@ export class ColorSelectorComponent {
// Flatten variants into a single category for now // Flatten variants into a single category for now
// We could try to group by extracting words, but "Colors" is fine. // We could try to group by extracting words, but "Colors" is fine.
return [{ return [{
name: 'Available Colors', name: 'COLOR.AVAILABLE_COLORS',
colors: vars.map(v => ({ colors: vars.map(v => ({
label: v.colorName, // Display "Red" label: v.colorName, // Display "Red"
value: v.colorName, // Send "Red" to backend value: v.colorName, // Send "Red" to backend

View File

@@ -2,7 +2,7 @@
@if (loading) { @if (loading) {
<div class="loading-overlay"> <div class="loading-overlay">
<div class="spinner"></div> <div class="spinner"></div>
<span>Loading 3D Model...</span> <span>{{ 'STL_VIEWER.LOADING' | translate }}</span>
</div> </div>
} }
@if (file && !loading) { @if (file && !loading) {

View File

@@ -1,5 +1,6 @@
import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild, SimpleChanges } from '@angular/core'; import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import * as THREE from 'three'; import * as THREE from 'three';
// @ts-ignore // @ts-ignore
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
@@ -9,7 +10,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
@Component({ @Component({
selector: 'app-stl-viewer', selector: 'app-stl-viewer',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, TranslateModule],
templateUrl: './stl-viewer.component.html', templateUrl: './stl-viewer.component.html',
styleUrl: './stl-viewer.component.scss' styleUrl: './stl-viewer.component.scss'
}) })

View File

@@ -15,7 +15,7 @@
"HERO_EYEBROW": "Stampa 3D tecnica per aziende, freelance e maker", "HERO_EYEBROW": "Stampa 3D tecnica per aziende, freelance e maker",
"HERO_TITLE": "Prezzo e tempi in pochi secondi.<br>Dal file 3D al pezzo finito.", "HERO_TITLE": "Prezzo e tempi in pochi secondi.<br>Dal file 3D al pezzo finito.",
"HERO_LEAD": "Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese.", "HERO_LEAD": "Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese.",
"HERO_SUBTITLE": "Offriamo anche servizi di cad, per pezzi personalizzati!", "HERO_SUBTITLE": "Offriamo anche servizi di CAD per pezzi personalizzati!",
"BTN_CALCULATE": "Calcola Preventivo", "BTN_CALCULATE": "Calcola Preventivo",
"BTN_SHOP": "Vai allo shop", "BTN_SHOP": "Vai allo shop",
"BTN_CONTACT": "Parla con noi", "BTN_CONTACT": "Parla con noi",
@@ -54,11 +54,17 @@
"CARD_SHOP_3_TEXT": "Non trovi quello che serve? Lo progettiamo e lo produciamo per te.", "CARD_SHOP_3_TEXT": "Non trovi quello che serve? Lo progettiamo e lo produciamo per te.",
"SEC_ABOUT_TITLE": "Su di noi", "SEC_ABOUT_TITLE": "Su di noi",
"SEC_ABOUT_TEXT": "Siamo due studenti di ingegneria: la stampa 3D ci ha conquistati per un motivo semplice, vedere un problema e costruire la soluzione. Da questa idea prendono forma prototipi, oggetti pensati per funzionare nella quotidianità. ", "SEC_ABOUT_TEXT": "Siamo due studenti di ingegneria: la stampa 3D ci ha conquistati per un motivo semplice, vedere un problema e costruire la soluzione. Da questa idea prendono forma prototipi, oggetti pensati per funzionare nella quotidianità. ",
"FOUNDERS_PHOTO": "Foto Founders" "FOUNDERS_PHOTO": "Foto Founders",
"SHOP_GALLERY_ARIA": "Galleria prodotti shop",
"FOUNDER_PREV_ARIA": "Foto precedente",
"FOUNDER_NEXT_ARIA": "Foto successiva",
"SHOP_IMAGE_ALT_1": "Prodotto tecnico stampato in 3D",
"FOUNDER_IMAGE_ALT_1": "Founder - foto 1",
"FOUNDER_IMAGE_ALT_2": "Founder - foto 2"
}, },
"CALC": { "CALC": {
"TITLE": "Calcola Preventivo 3D", "TITLE": "Calcola Preventivo 3D",
"SUBTITLE": "Carica il tuo file 3D (STL, 3MF, STEP), imposta la qualtà, colore e calcola immediatamente prezzo e tempi.", "SUBTITLE": "Carica il tuo file 3D (STL, 3MF, STEP), imposta la qualità, il colore e calcola immediatamente prezzo e tempi.",
"CTA_START": "Inizia Ora", "CTA_START": "Inizia Ora",
"BUSINESS": "Aziende", "BUSINESS": "Aziende",
"PRIVATE": "Privati", "PRIVATE": "Privati",
@@ -102,9 +108,13 @@
"UPLOADING": "Caricamento...", "UPLOADING": "Caricamento...",
"PROCESSING": "Elaborazione...", "PROCESSING": "Elaborazione...",
"NOTES_PLACEHOLDER": "Istruzioni specifiche...", "NOTES_PLACEHOLDER": "Istruzioni specifiche...",
"SETUP_NOTE": "* Include {{cost}} Costo di Setup", "SETUP_NOTE": "* Include {{cost}} come costo di setup",
"SHIPPING_NOTE": "** costi di spedizione esclusi, calcolati al prossimo passaggio", "SHIPPING_NOTE": "** Costi di spedizione esclusi, calcolati al passaggio successivo",
"STEP_WARNING": "La visualizzazione 3D non è compatibile con i file step e 3mf" "STEP_WARNING": "La visualizzazione 3D non è compatibile con i file step e 3mf",
"REMOVE_FILE": "Rimuovi file",
"FALLBACK_MATERIAL": "PLA (fallback)",
"FALLBACK_QUALITY_STANDARD": "Standard",
"ERR_FILE_TOO_LARGE": "Alcuni file superano il limite di 200MB e non sono stati aggiunti."
}, },
"QUOTE": { "QUOTE": {
"PROCEED_ORDER": "Procedi con l'ordine", "PROCEED_ORDER": "Procedi con l'ordine",
@@ -129,7 +139,8 @@
"CITY": "Città", "CITY": "Città",
"CITY_PLACEHOLDER": "Città", "CITY_PLACEHOLDER": "Città",
"SUBMIT": "Procedi", "SUBMIT": "Procedi",
"SUMMARY_TITLE": "Riepilogo" "SUMMARY_TITLE": "Riepilogo",
"DEFAULT_COLOR": "Predefinito"
}, },
"COMMON": { "COMMON": {
"REQUIRED": "Campo obbligatorio", "REQUIRED": "Campo obbligatorio",
@@ -145,10 +156,33 @@
"WIP_SUBTITLE": "Stiamo preparando uno shop con prodotti selezionati e funzionalità di creazione automatica!", "WIP_SUBTITLE": "Stiamo preparando uno shop con prodotti selezionati e funzionalità di creazione automatica!",
"WIP_CTA_CALC": "Vai al calcolatore", "WIP_CTA_CALC": "Vai al calcolatore",
"WIP_RETURN_LATER": "Torna tra un po'", "WIP_RETURN_LATER": "Torna tra un po'",
"WIP_NOTE": "Ci teniamo a fare le cose fatte bene: nel frattempo puoi calcolare subito prezzo e tempi di un file 3d con il nostro calcolatore.", "WIP_NOTE": "Ci teniamo a fare le cose fatte bene: nel frattempo puoi calcolare subito prezzo e tempi di un file 3D con il nostro calcolatore.",
"ADD_CART": "Aggiungi al Carrello", "ADD_CART": "Aggiungi al Carrello",
"BACK": "Torna allo Shop", "BACK": "Torna allo Shop",
"NOT_FOUND": "Prodotto non trovato." "NOT_FOUND": "Prodotto non trovato.",
"DETAILS": "Dettagli",
"MOCK_ADD_CART": "Aggiunto al carrello (Mock)",
"SUCCESS_TITLE": "Aggiunto al carrello",
"SUCCESS_DESC": "Il prodotto è stato aggiunto correttamente al carrello.",
"CONTINUE": "Continua",
"CATEGORIES": {
"FILAMENTS": "Filamenti",
"ACCESSORIES": "Accessori"
},
"PRODUCTS": {
"P1": {
"NAME": "Filamento PLA Standard",
"DESC": "Il classico per ogni stampa, facile e affidabile."
},
"P2": {
"NAME": "Filamento PETG Tough",
"DESC": "Resistente agli urti e alle temperature."
},
"P3": {
"NAME": "Kit Ugelli (0.4mm)",
"DESC": "Set di ricambio per estrusore FDM."
}
}
}, },
"ABOUT": { "ABOUT": {
"TITLE": "Chi Siamo", "TITLE": "Chi Siamo",
@@ -172,7 +206,13 @@
"SERVICES_TITLE": "Servizi principali", "SERVICES_TITLE": "Servizi principali",
"TARGET_TITLE": "Per chi è", "TARGET_TITLE": "Per chi è",
"TARGET_TEXT": "Piccole aziende, freelance, smanettoni e clienti che cercano un prodotto già pronto dallo shop.", "TARGET_TEXT": "Piccole aziende, freelance, smanettoni e clienti che cercano un prodotto già pronto dallo shop.",
"TEAM_TITLE": "Il Nostro Team" "TEAM_TITLE": "Il Nostro Team",
"MEMBER_JOE_NAME": "Joe Küng",
"MEMBER_JOE_ROLE": "Studente Ingegneria Informatica",
"MEMBER_JOE_ALT": "Joe Küng",
"MEMBER_MATTEO_NAME": "Matteo Caletti",
"MEMBER_MATTEO_ROLE": "Studente Ingegneria Elettronica",
"MEMBER_MATTEO_ALT": "Matteo Caletti"
}, },
"LOCATIONS": { "LOCATIONS": {
"TITLE": "Le Nostre Sedi", "TITLE": "Le Nostre Sedi",
@@ -210,7 +250,7 @@
"P2": "1.2. Quando carichi file 3D o allegati tecnici, i file vengono trattati solo per analisi, produzione, assistenza e gestione dell'ordine." "P2": "1.2. Quando carichi file 3D o allegati tecnici, i file vengono trattati solo per analisi, produzione, assistenza e gestione dell'ordine."
}, },
"S2": { "S2": {
"TITLE": "2. Finalita del trattamento", "TITLE": "2. Finalità del trattamento",
"P1": "2.1. Usiamo i dati esclusivamente per: preparare preventivi, confermare ordini, ricevere pagamenti, organizzare spedizioni e fornire supporto post-vendita.", "P1": "2.1. Usiamo i dati esclusivamente per: preparare preventivi, confermare ordini, ricevere pagamenti, organizzare spedizioni e fornire supporto post-vendita.",
"P2": "2.2. Non usiamo i dati per profilazione, marketing automatico o vendita a terzi." "P2": "2.2. Non usiamo i dati per profilazione, marketing automatico o vendita a terzi."
}, },
@@ -261,8 +301,8 @@
"S4": { "S4": {
"TITLE": "4. Conclusione del contratto e ordine", "TITLE": "4. Conclusione del contratto e ordine",
"P1": "4.1. Il contratto si conclude con conferma ordine (email/portale) o con avvio produzione dopo pagamento/anticipo.", "P1": "4.1. Il contratto si conclude con conferma ordine (email/portale) o con avvio produzione dopo pagamento/anticipo.",
"P2": "4.2. I preventivi automatici possono essere confermati o aggiornati dopo verifica tecnica minima (stampabilita, supporti, limiti macchina).", "P2": "4.2. I preventivi automatici possono essere confermati o aggiornati dopo verifica tecnica minima (stampabilità, supporti, limiti macchina).",
"P3": "4.3. Il fornitore puo rifiutare ordini tecnicamente non fattibili o non conformi ai presenti termini." "P3": "4.3. Il fornitore può rifiutare ordini tecnicamente non fattibili o non conformi ai presenti termini."
}, },
"S5": { "S5": {
"TITLE": "5. Prezzi, imposte e spedizione", "TITLE": "5. Prezzi, imposte e spedizione",
@@ -274,17 +314,17 @@
"TITLE": "6. Pagamenti", "TITLE": "6. Pagamenti",
"P1": "6.1. Metodi accettati: TWINT e bonifico bancario.", "P1": "6.1. Metodi accettati: TWINT e bonifico bancario.",
"P2": "6.2. Salvo accordi diversi, la produzione parte solo dopo il pagamento.", "P2": "6.2. Salvo accordi diversi, la produzione parte solo dopo il pagamento.",
"P3": "6.3. Per consulenza CAD e piccole serie puo essere richiesto un anticipo (30-100%) e/o saldo prima della consegna.", "P3": "6.3. Per consulenza CAD e piccole serie può essere richiesto un anticipo (30-100%) e/o saldo prima della consegna.",
"P4": "6.4. Dati bonifico: Joe Kung, IBAN CH74 0900 0000 1548 2158 1, Via G. Pioda 29a, 6710 Biasca." "P4": "6.4. Dati bonifico: Joe Kung, IBAN CH74 0900 0000 1548 2158 1, Via G. Pioda 29a, 6710 Biasca."
}, },
"S7": { "S7": {
"TITLE": "7. File del cliente e responsabilita tecnica", "TITLE": "7. File del cliente e responsabilità tecnica",
"P1": "7.1. Il cliente garantisce di avere i diritti sui file e che il loro uso non viola diritti di terzi (copyright, brevetti, licenze, segreti industriali).", "P1": "7.1. Il cliente garantisce di avere i diritti sui file e che il loro uso non viola diritti di terzi (copyright, brevetti, licenze, segreti industriali).",
"P2": "7.2. Il cliente e responsabile di correttezza del modello, scelta materiale, adeguatezza all'uso finale e rispetto delle norme applicabili.", "P2": "7.2. Il cliente è responsabile di correttezza del modello, scelta materiale, adeguatezza all'uso finale e rispetto delle norme applicabili.",
"P3": "7.3. Il fornitore puo suggerire modifiche per migliorare stampabilita e resa, ma non svolge funzione di certificazione del prodotto finale salvo accordo scritto." "P3": "7.3. Il fornitore può suggerire modifiche per migliorare stampabilità e resa, ma non svolge funzione di certificazione del prodotto finale salvo accordo scritto."
}, },
"S8": { "S8": {
"TITLE": "8. Qualita FDM/FFF e tolleranze", "TITLE": "8. Qualità FDM/FFF e tolleranze",
"P1": "8.1. La stampa FDM/FFF comporta caratteristiche intrinseche: linee di strato, anisotropia meccanica, micro-imperfezioni, variazioni cromatiche e possibili ritiri/deformazioni legati a geometria e materiale.", "P1": "8.1. La stampa FDM/FFF comporta caratteristiche intrinseche: linee di strato, anisotropia meccanica, micro-imperfezioni, variazioni cromatiche e possibili ritiri/deformazioni legati a geometria e materiale.",
"P2": "8.2. Tolleranze standard (salvo accordo scritto): fino a 100 mm +/-0.3 mm; oltre 100 mm +/-0.5% (minimo +/-0.3 mm).", "P2": "8.2. Tolleranze standard (salvo accordo scritto): fino a 100 mm +/-0.3 mm; oltre 100 mm +/-0.5% (minimo +/-0.3 mm).",
"P3": "8.3. Requisiti estetici o dimensionali critici devono essere concordati prima dell'ordine (es. campione, misure di controllo, finitura specifica)." "P3": "8.3. Requisiti estetici o dimensionali critici devono essere concordati prima dell'ordine (es. campione, misure di controllo, finitura specifica)."
@@ -292,19 +332,19 @@
"S9": { "S9": {
"TITLE": "9. Post-processing e lavorazioni aggiuntive", "TITLE": "9. Post-processing e lavorazioni aggiuntive",
"P1": "9.1. Lavorazioni come rimozione supporti, levigatura, primer/verniciatura o inserti filettati sono eseguite solo se concordate e possono introdurre variazioni dimensionali o estetiche.", "P1": "9.1. Lavorazioni come rimozione supporti, levigatura, primer/verniciatura o inserti filettati sono eseguite solo se concordate e possono introdurre variazioni dimensionali o estetiche.",
"P2": "9.2. Su richiesta puo essere prevista approvazione del primo pezzo o campione, eventualmente a pagamento." "P2": "9.2. Su richiesta può essere prevista approvazione del primo pezzo o campione, eventualmente a pagamento."
}, },
"S10": { "S10": {
"TITLE": "10. Tempi di produzione e consegna", "TITLE": "10. Tempi di produzione e consegna",
"P1": "10.1. I tempi indicati sono stime basate su carico di lavoro, complessita e disponibilita materiali.", "P1": "10.1. I tempi indicati sono stime basate su carico di lavoro, complessità e disponibilità materiali.",
"P2": "10.2. Ritardi dovuti a cause esterne (corrieri, fornitori, guasti, forza maggiore) non danno diritto automatico a penali salvo accordo scritto.", "P2": "10.2. Ritardi dovuti a cause esterne (corrieri, fornitori, guasti, forza maggiore) non danno diritto automatico a penali salvo accordo scritto.",
"P3": "10.3. Il cliente deve fornire indirizzi corretti e completi; eventuali costi di riconsegna per indirizzo errato restano a carico del cliente." "P3": "10.3. Il cliente deve fornire indirizzi corretti e completi; eventuali costi di riconsegna per indirizzo errato restano a carico del cliente."
}, },
"S11": { "S11": {
"TITLE": "11. Diritto di recesso e annullamento", "TITLE": "11. Diritto di recesso e annullamento",
"P1": "11.1. Salvo eccezioni confermate per iscritto, non e previsto diritto di recesso o restituzione dopo l'ordine.", "P1": "11.1. Salvo eccezioni confermate per iscritto, non è previsto diritto di recesso o restituzione dopo l'ordine.",
"P2": "11.2. I prodotti personalizzati non sono annullabili ne rimborsabili dopo l'avvio della produzione.", "P2": "11.2. I prodotti personalizzati non sono annullabili né rimborsabili dopo l'avvio della produzione.",
"P3": "11.3. Il fornitore puo accettare eccezioni prima dell'avvio produttivo, trattenendo eventuali costi gia sostenuti.", "P3": "11.3. Il fornitore può accettare eccezioni prima dell'avvio produttivo, trattenendo eventuali costi già sostenuti.",
"P4": "11.4. Eventuali condizioni speciali devono risultare nell'offerta, nella conferma d'ordine o in comunicazioni scritte." "P4": "11.4. Eventuali condizioni speciali devono risultare nell'offerta, nella conferma d'ordine o in comunicazioni scritte."
}, },
"S12": { "S12": {
@@ -312,15 +352,15 @@
"P1": "12.1. Il cliente deve controllare i prodotti alla consegna e segnalare eventuali difetti entro 7 giorni, per iscritto, con foto/video e descrizione.", "P1": "12.1. Il cliente deve controllare i prodotti alla consegna e segnalare eventuali difetti entro 7 giorni, per iscritto, con foto/video e descrizione.",
"P2": "12.2. Non costituiscono automaticamente difetto i segni tipici FDM, piccole variazioni cromatiche, imperfezioni non funzionali o tolleranze entro i limiti concordati.", "P2": "12.2. Non costituiscono automaticamente difetto i segni tipici FDM, piccole variazioni cromatiche, imperfezioni non funzionali o tolleranze entro i limiti concordati.",
"P3": "12.3. In caso di difetto imputabile al fornitore, i rimedi sono a scelta del fornitore: ristampa/sostituzione oppure rimborso limitato al valore del pezzo o ordine difettoso.", "P3": "12.3. In caso di difetto imputabile al fornitore, i rimedi sono a scelta del fornitore: ristampa/sostituzione oppure rimborso limitato al valore del pezzo o ordine difettoso.",
"P4": "12.4. Prima di ristampa o rimborso puo essere richiesta la restituzione del pezzo; se il difetto e confermato la restituzione e a carico del fornitore." "P4": "12.4. Prima di ristampa o rimborso può essere richiesta la restituzione del pezzo; se il difetto è confermato la restituzione è a carico del fornitore."
}, },
"S13": { "S13": {
"TITLE": "13. Usi vietati e conformita", "TITLE": "13. Usi vietati e conformità",
"P1": "13.1. Il fornitore puo rifiutare ordini relativi a oggetti illegali, armi o parti regolamentate, applicazioni safety-critical o medicali senza accordi e validazioni dedicate.", "P1": "13.1. Il fornitore può rifiutare ordini relativi a oggetti illegali, armi o parti regolamentate, applicazioni safety-critical o medicali senza accordi e validazioni dedicate.",
"P2": "13.2. Il cliente resta responsabile dell'uso finale e della conformita normativa del prodotto." "P2": "13.2. Il cliente resta responsabile dell'uso finale e della conformità normativa del prodotto."
}, },
"S14": { "S14": {
"TITLE": "14. Proprieta intellettuale e licenze", "TITLE": "14. Proprietà intellettuale e licenze",
"P1": "14.1. I diritti sui file del cliente restano al cliente o ai relativi titolari. Il cliente concede al fornitore una licenza limitata all'uso per valutazione tecnica e produzione.", "P1": "14.1. I diritti sui file del cliente restano al cliente o ai relativi titolari. Il cliente concede al fornitore una licenza limitata all'uso per valutazione tecnica e produzione.",
"P2": "14.2. Salvo accordi diversi, file e progetti CAD sviluppati dal fornitore vengono consegnati dopo pagamento, con diritto d'uso per gli scopi concordati.", "P2": "14.2. Salvo accordi diversi, file e progetti CAD sviluppati dal fornitore vengono consegnati dopo pagamento, con diritto d'uso per gli scopi concordati.",
"P3": "14.3. Il cliente manleva il fornitore da pretese di terzi legate a file o istruzioni fornite dal cliente." "P3": "14.3. Il cliente manleva il fornitore da pretese di terzi legate a file o istruzioni fornite dal cliente."
@@ -331,14 +371,14 @@
"P2": "15.2. I dati possono essere condivisi con fornitori tecnici e corrieri solo nella misura necessaria all'erogazione del servizio." "P2": "15.2. I dati possono essere condivisi con fornitori tecnici e corrieri solo nella misura necessaria all'erogazione del servizio."
}, },
"S16": { "S16": {
"TITLE": "16. Limitazione di responsabilita", "TITLE": "16. Limitazione di responsabilità",
"P1": "16.1. Nei limiti di legge, il fornitore risponde solo per danni diretti prevedibili e comunque non oltre il valore dell'ordine relativo al prodotto o servizio contestato.", "P1": "16.1. Nei limiti di legge, il fornitore risponde solo per danni diretti prevedibili e comunque non oltre il valore dell'ordine relativo al prodotto o servizio contestato.",
"P2": "16.2. Sono esclusi, nei limiti consentiti, danni indiretti, perdita di profitto, fermo attivita, perdita dati e danni consequenziali.", "P2": "16.2. Sono esclusi, nei limiti consentiti, danni indiretti, perdita di profitto, fermo attività, perdita dati e danni consequenziali.",
"P3": "16.3. Restano salve le responsabilita non escludibili per legge (es. dolo o colpa grave)." "P3": "16.3. Restano salve le responsabilità non escludibili per legge (es. dolo o colpa grave)."
}, },
"S17": { "S17": {
"TITLE": "17. Forza maggiore", "TITLE": "17. Forza maggiore",
"P1": "17.1. Eventi fuori dal controllo ragionevole del fornitore (guasti, blackout, ritardi fornitori, scioperi, provvedimenti autorita) possono causare proroghe o sospensioni senza responsabilita." "P1": "17.1. Eventi fuori dal controllo ragionevole del fornitore (guasti, blackout, ritardi fornitori, scioperi, provvedimenti autorità) possono causare proroghe o sospensioni senza responsabilità."
}, },
"S18": { "S18": {
"TITLE": "18. Legge applicabile e foro competente", "TITLE": "18. Legge applicabile e foro competente",
@@ -384,7 +424,9 @@
"FILE_TYPE_3D": "3D", "FILE_TYPE_3D": "3D",
"FILE_TYPE_VIDEO": "Video", "FILE_TYPE_VIDEO": "Video",
"FILE_TYPE_DOC": "DOC", "FILE_TYPE_DOC": "DOC",
"FILE_TYPE_FILE": "FILE" "FILE_TYPE_FILE": "FILE",
"ERROR_SUBMIT": "Errore durante l'invio della richiesta. Riprova.",
"REMOVE_FILE": "Rimuovi file allegato"
}, },
"CHECKOUT": { "CHECKOUT": {
"TITLE": "Checkout", "TITLE": "Checkout",
@@ -418,7 +460,11 @@
"COMPANY_OPTIONAL": "Nome Azienda (Opzionale)", "COMPANY_OPTIONAL": "Nome Azienda (Opzionale)",
"REF_PERSON_OPTIONAL": "Persona di Riferimento (Opzionale)", "REF_PERSON_OPTIONAL": "Persona di Riferimento (Opzionale)",
"SHIPPING_CALCULATED_NEXT_STEP": "il costo di spedizione viene calcolato al prossimo passaggio", "SHIPPING_CALCULATED_NEXT_STEP": "il costo di spedizione viene calcolato al prossimo passaggio",
"EXCLUDES_SHIPPING": "Escluso costo di spedizione" "EXCLUDES_SHIPPING": "Escluso costo di spedizione",
"ERR_NO_SESSION_START": "Nessuna sessione attiva trovata. Inizia un nuovo preventivo.",
"ERR_LOAD_SESSION": "Impossibile caricare i dettagli della sessione. Riprova.",
"ERR_NO_SESSION_CREATE_ORDER": "Nessuna sessione attiva trovata. Impossibile creare l'ordine.",
"ERR_CREATE_ORDER": "Impossibile creare l'ordine. Riprova."
}, },
"PAYMENT": { "PAYMENT": {
"TITLE": "Pagamento", "TITLE": "Pagamento",
@@ -444,8 +490,10 @@
"METHOD_TWINT": "TWINT", "METHOD_TWINT": "TWINT",
"METHOD_BANK": "Fattura QR / Bonifico", "METHOD_BANK": "Fattura QR / Bonifico",
"STATUS_REPORTED_TITLE": "Ordine in lavorazione", "STATUS_REPORTED_TITLE": "Ordine in lavorazione",
"STATUS_REPORTED_DESC": "Abbiamo registrato la tua operazione. Appena confermiamo il pagamento l'ordine entrà in produzione", "STATUS_REPORTED_DESC": "Abbiamo registrato la tua operazione. Appena confermiamo il pagamento l'ordine entrerà in produzione",
"IN_VERIFICATION": "Pagamento Segnalato" "IN_VERIFICATION": "Pagamento Segnalato",
"TWINT_QR_ALT": "QR di pagamento TWINT",
"TWINT_BUTTON_ALT": "Pulsante TWINT incorporato"
}, },
"TRACKING": { "TRACKING": {
"TITLE": "Stato dell'Ordine", "TITLE": "Stato dell'Ordine",
@@ -464,5 +512,34 @@
"PROCESSING_TEXT": "Non appena confermiamo il pagamento, il tuo ordine passerà in produzione.", "PROCESSING_TEXT": "Non appena confermiamo il pagamento, il tuo ordine passerà in produzione.",
"EMAIL_TEXT": "Ti invieremo una email con aggiornamento stato e prossimi step.", "EMAIL_TEXT": "Ti invieremo una email con aggiornamento stato e prossimi step.",
"BACK_HOME": "Torna alla Home" "BACK_HOME": "Torna alla Home"
},
"STL_VIEWER": {
"LOADING": "Caricamento modello 3D..."
},
"ORDER": {
"ERR_ID_NOT_FOUND": "ID ordine non trovato.",
"ERR_LOAD_ORDER": "Impossibile caricare i dettagli dell'ordine.",
"ERR_REPORT_PAYMENT": "Impossibile segnalare il pagamento. Riprova.",
"NOT_AVAILABLE": "N/D"
},
"DROPZONE": {
"DEFAULT_LABEL": "Trascina i file qui o clicca per caricare",
"DEFAULT_SUBTEXT": "Supporta .STL, .3MF, .STEP"
},
"COLOR": {
"AVAILABLE_COLORS": "Colori disponibili",
"CATEGORY_GLOSSY": "Lucidi",
"CATEGORY_MATTE": "Opachi",
"NAME": {
"BLACK": "Nero",
"WHITE": "Bianco",
"RED": "Rosso",
"BLUE": "Blu",
"GREEN": "Verde",
"YELLOW": "Giallo",
"MATTE_BLACK": "Nero opaco",
"MATTE_WHITE": "Bianco opaco",
"MATTE_GRAY": "Grigio opaco"
}
} }
} }