feat/calculator-options #23
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { SeoService } from './core/services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -8,4 +9,6 @@ import { RouterOutlet } from '@angular/router';
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
})
|
||||
export class AppComponent {}
|
||||
export class AppComponent {
|
||||
private readonly seoService = inject(SeoService);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ const appChildRoutes: Routes = [
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('./features/home/home.component').then((m) => m.HomeComponent),
|
||||
data: {
|
||||
seoTitle: '3D fab | Stampa 3D su misura',
|
||||
seoDescription:
|
||||
'Servizio di stampa 3D con preventivo online immediato per prototipi, piccole serie e pezzi personalizzati.',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'calculator',
|
||||
@@ -12,21 +17,42 @@ const appChildRoutes: Routes = [
|
||||
import('./features/calculator/calculator.routes').then(
|
||||
(m) => m.CALCULATOR_ROUTES,
|
||||
),
|
||||
data: {
|
||||
seoTitle: 'Calcolatore preventivo stampa 3D | 3D fab',
|
||||
seoDescription:
|
||||
'Carica il file 3D e ottieni prezzo e tempi in pochi secondi con slicing reale.',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'shop',
|
||||
loadChildren: () =>
|
||||
import('./features/shop/shop.routes').then((m) => m.SHOP_ROUTES),
|
||||
data: {
|
||||
seoTitle: 'Shop 3D fab',
|
||||
seoDescription:
|
||||
'Catalogo prodotti stampati in 3D e soluzioni tecniche pronte all uso.',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadChildren: () =>
|
||||
import('./features/about/about.routes').then((m) => m.ABOUT_ROUTES),
|
||||
data: {
|
||||
seoTitle: 'Chi siamo | 3D fab',
|
||||
seoDescription:
|
||||
'Scopri il team 3D fab e il laboratorio di stampa 3D con sedi in Ticino e Bienne.',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'contact',
|
||||
loadChildren: () =>
|
||||
import('./features/contact/contact.routes').then((m) => m.CONTACT_ROUTES),
|
||||
data: {
|
||||
seoTitle: 'Contatti | 3D fab',
|
||||
seoDescription:
|
||||
'Contatta 3D fab per preventivi, supporto tecnico e richieste personalizzate di stampa 3D.',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'checkout/cad',
|
||||
@@ -34,6 +60,10 @@ const appChildRoutes: Routes = [
|
||||
import('./features/checkout/checkout.component').then(
|
||||
(m) => m.CheckoutComponent,
|
||||
),
|
||||
data: {
|
||||
seoTitle: 'Checkout | 3D fab',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'checkout',
|
||||
@@ -41,16 +71,28 @@ const appChildRoutes: Routes = [
|
||||
import('./features/checkout/checkout.component').then(
|
||||
(m) => m.CheckoutComponent,
|
||||
),
|
||||
data: {
|
||||
seoTitle: 'Checkout | 3D fab',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId',
|
||||
loadComponent: () =>
|
||||
import('./features/order/order.component').then((m) => m.OrderComponent),
|
||||
data: {
|
||||
seoTitle: 'Ordine | 3D fab',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'co/:orderId',
|
||||
loadComponent: () =>
|
||||
import('./features/order/order.component').then((m) => m.OrderComponent),
|
||||
data: {
|
||||
seoTitle: 'Ordine | 3D fab',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
@@ -61,6 +103,10 @@ const appChildRoutes: Routes = [
|
||||
path: 'admin',
|
||||
loadChildren: () =>
|
||||
import('./features/admin/admin.routes').then((m) => m.ADMIN_ROUTES),
|
||||
data: {
|
||||
seoTitle: 'Admin | 3D fab',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
|
||||
@@ -2,5 +2,13 @@ import { Routes } from '@angular/router';
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
export const ABOUT_ROUTES: Routes = [
|
||||
{ path: '', component: AboutPageComponent },
|
||||
{
|
||||
path: '',
|
||||
component: AboutPageComponent,
|
||||
data: {
|
||||
seoTitle: 'Chi siamo | 3D fab',
|
||||
seoDescription:
|
||||
'Siamo un laboratorio di stampa 3D orientato a prototipi, ricambi e produzioni su misura.',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,10 +3,24 @@ import { CalculatorPageComponent } from './calculator-page.component';
|
||||
|
||||
export const CALCULATOR_ROUTES: Routes = [
|
||||
{ path: '', redirectTo: 'basic', pathMatch: 'full' },
|
||||
{ path: 'basic', component: CalculatorPageComponent, data: { mode: 'easy' } },
|
||||
{
|
||||
path: 'basic',
|
||||
component: CalculatorPageComponent,
|
||||
data: {
|
||||
mode: 'easy',
|
||||
seoTitle: 'Calcolatore stampa 3D base | 3D fab',
|
||||
seoDescription:
|
||||
'Calcola rapidamente il prezzo della tua stampa 3D in modalita base.',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'advanced',
|
||||
component: CalculatorPageComponent,
|
||||
data: { mode: 'advanced' },
|
||||
data: {
|
||||
mode: 'advanced',
|
||||
seoTitle: 'Calcolatore stampa 3D avanzato | 3D fab',
|
||||
seoDescription:
|
||||
'Configura parametri avanzati e ottieni un preventivo preciso con slicing reale.',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
<p>{{ result().notes }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (recalculationRequired()) {
|
||||
<div class="recalc-banner">
|
||||
Hai modificato i parametri di stampa. Ricalcola il preventivo prima di
|
||||
procedere con l'ordine.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -104,7 +110,10 @@
|
||||
|
||||
<div class="actions-right">
|
||||
@if (!hasQuantityOverLimit()) {
|
||||
<app-button (click)="proceed.emit()">
|
||||
<app-button
|
||||
[disabled]="recalculationRequired()"
|
||||
(click)="proceed.emit()"
|
||||
>
|
||||
{{ "QUOTE.PROCEED_ORDER" | translate }}
|
||||
</app-button>
|
||||
} @else {
|
||||
@@ -112,6 +121,11 @@
|
||||
"QUOTE.MAX_QTY_NOTICE" | translate: { max: directOrderLimit }
|
||||
}}</small>
|
||||
}
|
||||
@if (recalculationRequired()) {
|
||||
<small class="limit-note">
|
||||
Ricalcola il preventivo per riattivare il checkout.
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
@@ -184,3 +184,14 @@
|
||||
white-space: pre-wrap; /* Preserve line breaks */
|
||||
}
|
||||
}
|
||||
|
||||
.recalc-banner {
|
||||
margin-top: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid #f0c95a;
|
||||
background: #fff8e1;
|
||||
border-radius: var(--radius-md);
|
||||
color: #6f5b1a;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export class QuoteResultComponent implements OnDestroy {
|
||||
readonly quantityAutoRefreshMs = 2000;
|
||||
|
||||
result = input.required<QuoteResult>();
|
||||
recalculationRequired = input<boolean>(false);
|
||||
consult = output<void>();
|
||||
proceed = output<void>();
|
||||
itemChange = output<{
|
||||
@@ -43,6 +44,12 @@ export class QuoteResultComponent implements OnDestroy {
|
||||
fileName: string;
|
||||
quantity: number;
|
||||
}>();
|
||||
itemQuantityPreviewChange = output<{
|
||||
id?: string;
|
||||
index: number;
|
||||
fileName: string;
|
||||
quantity: number;
|
||||
}>();
|
||||
|
||||
// Local mutable state for items to handle quantity changes
|
||||
items = signal<QuoteItem[]>([]);
|
||||
@@ -87,6 +94,13 @@ export class QuoteResultComponent implements OnDestroy {
|
||||
return updated;
|
||||
});
|
||||
|
||||
this.itemQuantityPreviewChange.emit({
|
||||
id: item.id,
|
||||
index,
|
||||
fileName: item.fileName,
|
||||
quantity: normalizedQty,
|
||||
});
|
||||
|
||||
this.scheduleQuantityRefresh(index, key);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,10 @@ export const CONTACT_ROUTES: Routes = [
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('./contact-page.component').then((m) => m.ContactPageComponent),
|
||||
data: {
|
||||
seoTitle: 'Contatti | 3D fab',
|
||||
seoDescription:
|
||||
'Richiedi informazioni, preventivi personalizzati o supporto per progetti di stampa 3D.',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -37,28 +37,31 @@
|
||||
<div class="cap-cards">
|
||||
<app-card>
|
||||
<div class="card-image-placeholder">
|
||||
<img src="assets/images/home/prototipi.jpg" alt="" />
|
||||
<img src="assets/images/home/prototipi.jpg" alt="Prototipazione 3D" />
|
||||
</div>
|
||||
<h3>{{ "HOME.CAP_1_TITLE" | translate }}</h3>
|
||||
<p class="text-muted">{{ "HOME.CAP_1_TEXT" | translate }}</p>
|
||||
</app-card>
|
||||
<app-card>
|
||||
<div class="card-image-placeholder">
|
||||
<img src="assets/images/home/original-vs-3dprinted.jpg" alt="" />
|
||||
<img
|
||||
src="assets/images/home/original-vs-3dprinted.jpg"
|
||||
alt="Confronto pezzo originale e stampato in 3D"
|
||||
/>
|
||||
</div>
|
||||
<h3>{{ "HOME.CAP_2_TITLE" | translate }}</h3>
|
||||
<p class="text-muted">{{ "HOME.CAP_2_TEXT" | translate }}</p>
|
||||
</app-card>
|
||||
<app-card>
|
||||
<div class="card-image-placeholder">
|
||||
<img src="assets/images/home/serie.jpg" alt="" />
|
||||
<img src="assets/images/home/serie.jpg" alt="Piccole serie stampate in 3D" />
|
||||
</div>
|
||||
<h3>{{ "HOME.CAP_3_TITLE" | translate }}</h3>
|
||||
<p class="text-muted">{{ "HOME.CAP_3_TEXT" | translate }}</p>
|
||||
</app-card>
|
||||
<app-card>
|
||||
<div class="card-image-placeholder">
|
||||
<img src="assets/images/home/cad.jpg" alt="" />
|
||||
<img src="assets/images/home/cad.jpg" alt="Servizio CAD per stampa 3D" />
|
||||
</div>
|
||||
<h3>{{ "HOME.CAP_4_TITLE" | translate }}</h3>
|
||||
<p class="text-muted">{{ "HOME.CAP_4_TEXT" | translate }}</p>
|
||||
|
||||
@@ -5,10 +5,20 @@ export const LEGAL_ROUTES: Routes = [
|
||||
path: 'privacy',
|
||||
loadComponent: () =>
|
||||
import('./privacy/privacy.component').then((m) => m.PrivacyComponent),
|
||||
data: {
|
||||
seoTitle: 'Privacy Policy | 3D fab',
|
||||
seoDescription:
|
||||
'Informativa privacy di 3D fab: trattamento dati, finalita e contatti.',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'terms',
|
||||
loadComponent: () =>
|
||||
import('./terms/terms.component').then((m) => m.TermsComponent),
|
||||
data: {
|
||||
seoTitle: 'Termini e condizioni | 3D fab',
|
||||
seoDescription:
|
||||
'Termini e condizioni del servizio di stampa 3D e del calcolatore preventivi.',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,6 +3,22 @@ import { ShopPageComponent } from './shop-page.component';
|
||||
import { ProductDetailComponent } from './product-detail.component';
|
||||
|
||||
export const SHOP_ROUTES: Routes = [
|
||||
{ path: '', component: ShopPageComponent },
|
||||
{ path: ':id', component: ProductDetailComponent },
|
||||
{
|
||||
path: '',
|
||||
component: ShopPageComponent,
|
||||
data: {
|
||||
seoTitle: 'Shop 3D fab',
|
||||
seoDescription:
|
||||
'Lo shop 3D fab e in allestimento. Intanto puoi usare il calcolatore per ottenere un preventivo.',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: ProductDetailComponent,
|
||||
data: {
|
||||
seoTitle: 'Prodotto | 3D fab',
|
||||
seoRobots: 'noindex, nofollow',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>3D fab</title>
|
||||
<title>3D fab | Stampa 3D su misura</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Stampa 3D su misura con preventivo online immediato. Carica il file, scegli materiale e qualità, ricevi prezzo e tempi in pochi secondi."
|
||||
/>
|
||||
<meta name="robots" content="index, follow" />
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/png" href="assets/images/Fav-icon.png" />
|
||||
|
||||
Reference in New Issue
Block a user