dev #8
@@ -7,6 +7,7 @@ import com.printcalculator.service.InvoicePdfRenderingService;
|
||||
import com.printcalculator.service.OrderService;
|
||||
import com.printcalculator.service.QrBillService;
|
||||
import com.printcalculator.service.StorageService;
|
||||
import com.printcalculator.service.TwintPaymentService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -24,7 +25,9 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Base64;
|
||||
import java.util.stream.Collectors;
|
||||
import java.net.URI;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
@@ -39,6 +42,7 @@ public class OrderController {
|
||||
private final StorageService storageService;
|
||||
private final InvoicePdfRenderingService invoiceService;
|
||||
private final QrBillService qrBillService;
|
||||
private final TwintPaymentService twintPaymentService;
|
||||
|
||||
|
||||
public OrderController(OrderService orderService,
|
||||
@@ -49,7 +53,8 @@ public class OrderController {
|
||||
CustomerRepository customerRepo,
|
||||
StorageService storageService,
|
||||
InvoicePdfRenderingService invoiceService,
|
||||
QrBillService qrBillService) {
|
||||
QrBillService qrBillService,
|
||||
TwintPaymentService twintPaymentService) {
|
||||
this.orderService = orderService;
|
||||
this.orderRepo = orderRepo;
|
||||
this.orderItemRepo = orderItemRepo;
|
||||
@@ -59,6 +64,7 @@ public class OrderController {
|
||||
this.storageService = storageService;
|
||||
this.invoiceService = invoiceService;
|
||||
this.qrBillService = qrBillService;
|
||||
this.twintPaymentService = twintPaymentService;
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +192,51 @@ public class OrderController {
|
||||
.body(pdf);
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/twint")
|
||||
public ResponseEntity<Map<String, String>> getTwintPayment(@PathVariable UUID orderId) {
|
||||
if (!orderRepo.existsById(orderId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
byte[] qrPng = twintPaymentService.generateQrPng(360);
|
||||
String qrDataUri = "data:image/png;base64," + Base64.getEncoder().encodeToString(qrPng);
|
||||
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("paymentUrl", twintPaymentService.getTwintPaymentUrl());
|
||||
data.put("openUrl", "/api/orders/" + orderId + "/twint/open");
|
||||
data.put("qrImageUrl", "/api/orders/" + orderId + "/twint/qr");
|
||||
data.put("qrImageDataUri", qrDataUri);
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/twint/open")
|
||||
public ResponseEntity<Void> openTwintPayment(@PathVariable UUID orderId) {
|
||||
if (!orderRepo.existsById(orderId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.status(302)
|
||||
.location(URI.create(twintPaymentService.getTwintPaymentUrl()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/twint/qr")
|
||||
public ResponseEntity<byte[]> getTwintQr(
|
||||
@PathVariable UUID orderId,
|
||||
@RequestParam(defaultValue = "320") int size
|
||||
) {
|
||||
if (!orderRepo.existsById(orderId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
int normalizedSize = Math.max(200, Math.min(size, 600));
|
||||
byte[] png = twintPaymentService.generateQrPng(normalizedSize);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.IMAGE_PNG)
|
||||
.body(png);
|
||||
}
|
||||
|
||||
private String getExtension(String filename) {
|
||||
if (filename == null) return "stl";
|
||||
int i = filename.lastIndexOf('.');
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import io.nayuki.qrcodegen.QrCode;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
@Service
|
||||
public class TwintPaymentService {
|
||||
|
||||
private final String twintPaymentUrl;
|
||||
|
||||
public TwintPaymentService(
|
||||
@Value("${payment.twint.url:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.}")
|
||||
String twintPaymentUrl
|
||||
) {
|
||||
this.twintPaymentUrl = twintPaymentUrl;
|
||||
}
|
||||
|
||||
public String getTwintPaymentUrl() {
|
||||
return twintPaymentUrl;
|
||||
}
|
||||
|
||||
public byte[] generateQrPng(int sizePx) {
|
||||
try {
|
||||
// Use High Error Correction for financial QR codes
|
||||
QrCode qrCode = QrCode.encodeText(twintPaymentUrl, QrCode.Ecc.HIGH);
|
||||
|
||||
// Standard QR quiet zone is 4 modules
|
||||
int borderModules = 4;
|
||||
int fullModules = qrCode.size + borderModules * 2;
|
||||
int scale = Math.max(1, sizePx / fullModules);
|
||||
int imageSize = fullModules * scale;
|
||||
|
||||
BufferedImage image = new BufferedImage(imageSize, imageSize, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
try {
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, imageSize, imageSize);
|
||||
graphics.setColor(Color.BLACK);
|
||||
|
||||
for (int y = 0; y < qrCode.size; y++) {
|
||||
for (int x = 0; x < qrCode.size; x++) {
|
||||
if (qrCode.getModule(x, y)) {
|
||||
int px = (x + borderModules) * scale;
|
||||
int py = (y + borderModules) * scale;
|
||||
graphics.fillRect(px, py, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "png", outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to generate TWINT QR image.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,3 +23,6 @@ spring.servlet.multipart.max-request-size=200MB
|
||||
clamav.host=${CLAMAV_HOST:clamav}
|
||||
clamav.port=${CLAMAV_PORT:3310}
|
||||
clamav.enabled=${CLAMAV_ENABLED:false}
|
||||
|
||||
# TWINT Configuration
|
||||
payment.twint.url=${TWINT_PAYMENT_URL:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.}
|
||||
|
||||
@@ -58,7 +58,10 @@ services:
|
||||
container_name: print-calculator-clamav
|
||||
ports:
|
||||
- "3310:3310"
|
||||
volumes:
|
||||
- clamav_db:/var/lib/clamav
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
clamav_db:
|
||||
|
||||
@@ -33,6 +33,10 @@ export const routes: Routes = [
|
||||
path: 'payment/:orderId',
|
||||
loadComponent: () => import('./features/payment/payment.component').then(m => m.PaymentComponent)
|
||||
},
|
||||
{
|
||||
path: 'order-confirmed/:orderId',
|
||||
loadComponent: () => import('./features/order-confirmed/order-confirmed.component').then(m => m.OrderConfirmedComponent)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./features/legal/legal.routes').then(m => m.LEGAL_ROUTES)
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
<app-card class="loading-state">
|
||||
<div class="loader-content">
|
||||
<div class="spinner"></div>
|
||||
<h3 class="loading-title">Analisi in corso...</h3>
|
||||
<p class="loading-text">Stiamo analizzando la geometria e calcolando il percorso utensile.</p>
|
||||
<h3 class="loading-title">{{ 'CALC.ANALYZING_TITLE' | translate }}</h3>
|
||||
<p class="loading-text">{{ 'CALC.ANALYZING_TEXT' | translate }}</p>
|
||||
</div>
|
||||
</app-card>
|
||||
} @else if (result()) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="setup-note">
|
||||
<small>* Include {{ result().setupCost | currency:result().currency }} Setup Cost</small>
|
||||
<small>{{ 'CALC.SETUP_NOTE' | translate:{cost: (result().setupCost | currency:result().currency)} }}</small>
|
||||
</div>
|
||||
|
||||
@if (result().notes) {
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<div class="item-controls">
|
||||
<div class="qty-control">
|
||||
<label>Qtà:</label>
|
||||
<label>{{ 'CHECKOUT.QTY' | translate }}:</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="card-body">
|
||||
<div class="card-controls">
|
||||
<div class="qty-group">
|
||||
<label>QTÀ</label>
|
||||
<label>{{ 'CALC.QTY_SHORT' | translate }}</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
|
||||
<div class="color-group">
|
||||
<label>COLORE</label>
|
||||
<label>{{ 'CALC.COLOR_LABEL' | translate }}</label>
|
||||
<app-color-selector
|
||||
[selectedColor]="item.color"
|
||||
[variants]="currentMaterialVariants()"
|
||||
@@ -134,7 +134,7 @@
|
||||
<app-input
|
||||
formControlName="notes"
|
||||
[label]="'CALC.NOTES' | translate"
|
||||
placeholder="Istruzioni specifiche..."
|
||||
[placeholder]="'CALC.NOTES_PLACEHOLDER' | translate"
|
||||
></app-input>
|
||||
|
||||
<div class="actions">
|
||||
@@ -151,7 +151,7 @@
|
||||
type="submit"
|
||||
[disabled]="items().length === 0 || loading()"
|
||||
[fullWidth]="true">
|
||||
{{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }}
|
||||
{{ loading() ? (uploadProgress() < 100 ? ('CALC.UPLOADING' | translate) : ('CALC.PROCESSING' | translate)) : ('CALC.CALCULATE' | translate) }}
|
||||
</app-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<div class="col-md-6">
|
||||
<app-input
|
||||
formControlName="name"
|
||||
label="USER_DETAILS.NAME"
|
||||
placeholder="USER_DETAILS.NAME_PLACEHOLDER"
|
||||
[label]="'USER_DETAILS.NAME' | translate"
|
||||
[placeholder]="'USER_DETAILS.NAME_PLACEHOLDER' | translate"
|
||||
[required]="true"
|
||||
[error]="form.get('name')?.invalid && form.get('name')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||
</app-input>
|
||||
@@ -18,8 +18,8 @@
|
||||
<div class="col-md-6">
|
||||
<app-input
|
||||
formControlName="surname"
|
||||
label="USER_DETAILS.SURNAME"
|
||||
placeholder="USER_DETAILS.SURNAME_PLACEHOLDER"
|
||||
[label]="'USER_DETAILS.SURNAME' | translate"
|
||||
[placeholder]="'USER_DETAILS.SURNAME_PLACEHOLDER' | translate"
|
||||
[required]="true"
|
||||
[error]="form.get('surname')?.invalid && form.get('surname')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||
</app-input>
|
||||
@@ -31,9 +31,9 @@
|
||||
<div class="col-md-6">
|
||||
<app-input
|
||||
formControlName="email"
|
||||
label="USER_DETAILS.EMAIL"
|
||||
[label]="'USER_DETAILS.EMAIL' | translate"
|
||||
type="email"
|
||||
placeholder="USER_DETAILS.EMAIL_PLACEHOLDER"
|
||||
[placeholder]="'USER_DETAILS.EMAIL_PLACEHOLDER' | translate"
|
||||
[required]="true"
|
||||
[error]="form.get('email')?.invalid && form.get('email')?.touched ? ('COMMON.INVALID_EMAIL' | translate) : null">
|
||||
</app-input>
|
||||
@@ -41,9 +41,9 @@
|
||||
<div class="col-md-6">
|
||||
<app-input
|
||||
formControlName="phone"
|
||||
label="USER_DETAILS.PHONE"
|
||||
[label]="'USER_DETAILS.PHONE' | translate"
|
||||
type="tel"
|
||||
placeholder="USER_DETAILS.PHONE_PLACEHOLDER"
|
||||
[placeholder]="'USER_DETAILS.PHONE_PLACEHOLDER' | translate"
|
||||
[required]="true"
|
||||
[error]="form.get('phone')?.invalid && form.get('phone')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||
</app-input>
|
||||
@@ -53,8 +53,8 @@
|
||||
<!-- Address -->
|
||||
<app-input
|
||||
formControlName="address"
|
||||
label="USER_DETAILS.ADDRESS"
|
||||
placeholder="USER_DETAILS.ADDRESS_PLACEHOLDER"
|
||||
[label]="'USER_DETAILS.ADDRESS' | translate"
|
||||
[placeholder]="'USER_DETAILS.ADDRESS_PLACEHOLDER' | translate"
|
||||
[required]="true"
|
||||
[error]="form.get('address')?.invalid && form.get('address')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||
</app-input>
|
||||
@@ -64,8 +64,8 @@
|
||||
<div class="col-md-4">
|
||||
<app-input
|
||||
formControlName="zip"
|
||||
label="USER_DETAILS.ZIP"
|
||||
placeholder="USER_DETAILS.ZIP_PLACEHOLDER"
|
||||
[label]="'USER_DETAILS.ZIP' | translate"
|
||||
[placeholder]="'USER_DETAILS.ZIP_PLACEHOLDER' | translate"
|
||||
[required]="true"
|
||||
[error]="form.get('zip')?.invalid && form.get('zip')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||
</app-input>
|
||||
@@ -73,8 +73,8 @@
|
||||
<div class="col-md-8">
|
||||
<app-input
|
||||
formControlName="city"
|
||||
label="USER_DETAILS.CITY"
|
||||
placeholder="USER_DETAILS.CITY_PLACEHOLDER"
|
||||
[label]="'USER_DETAILS.CITY' | translate"
|
||||
[placeholder]="'USER_DETAILS.CITY_PLACEHOLDER' | translate"
|
||||
[required]="true"
|
||||
[error]="form.get('city')?.invalid && form.get('city')?.touched ? ('COMMON.REQUIRED' | translate) : null">
|
||||
</app-input>
|
||||
|
||||
@@ -162,6 +162,13 @@ export class QuoteEstimatorService {
|
||||
});
|
||||
}
|
||||
|
||||
getTwintPayment(orderId: string): Observable<any> {
|
||||
const headers: any = {};
|
||||
// @ts-ignore
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}/twint`, { headers });
|
||||
}
|
||||
|
||||
calculate(request: QuoteRequest): Observable<number | QuoteResult> {
|
||||
console.log('QuoteEstimatorService: Calculating quote...', request);
|
||||
if (request.items.length === 0) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<h3>{{ 'CHECKOUT.CONTACT_INFO' | translate }}</h3>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<app-input formControlName="email" type="email" [label]="'CHECKOUT.EMAIL' | translate" [required]="true" [error]="checkoutForm.get('email')?.hasError('email') ? 'Invalid email' : null"></app-input>
|
||||
<app-input formControlName="email" type="email" [label]="'CHECKOUT.EMAIL' | translate" [required]="true" [error]="checkoutForm.get('email')?.hasError('email') ? ('CHECKOUT.INVALID_EMAIL' | translate) : null"></app-input>
|
||||
<app-input formControlName="phone" type="tel" [label]="'CHECKOUT.PHONE' | translate" [required]="true"></app-input>
|
||||
</div>
|
||||
</app-card>
|
||||
@@ -41,8 +41,8 @@
|
||||
|
||||
<!-- Company Fields -->
|
||||
<div *ngIf="isCompany" class="company-fields mb-4">
|
||||
<app-input formControlName="companyName" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
|
||||
<app-input formControlName="companyName" [label]="'CHECKOUT.COMPANY_NAME' | translate" [required]="true" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="'CONTACT.REF_PERSON' | translate" [required]="true" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
|
||||
</div>
|
||||
|
||||
<!-- User Type Selector -->
|
||||
@@ -87,8 +87,8 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="isCompany" class="company-fields">
|
||||
<app-input formControlName="companyName" [label]="('CHECKOUT.COMPANY_NAME' | translate) + ' (Optional)'"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' (Optional)'"></app-input>
|
||||
<app-input formControlName="companyName" [label]="'CHECKOUT.COMPANY_OPTIONAL' | translate"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="'CHECKOUT.REF_PERSON_OPTIONAL' | translate"></app-input>
|
||||
</div>
|
||||
|
||||
<app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate"></app-input>
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
<button type="button" class="remove-btn" (click)="removeFile(i)">×</button>
|
||||
<img *ngIf="file.type === 'image'" [src]="file.url" class="preview-img">
|
||||
<div *ngIf="file.type !== 'image'" class="file-icon">
|
||||
<span *ngIf="file.type === 'pdf'">PDF</span>
|
||||
<span *ngIf="file.type === '3d'">3D</span>
|
||||
<span *ngIf="file.type === 'pdf'">{{ 'CONTACT.FILE_TYPE_PDF' | translate }}</span>
|
||||
<span *ngIf="file.type === '3d'">{{ 'CONTACT.FILE_TYPE_3D' | translate }}</span>
|
||||
</div>
|
||||
<div class="file-name" [title]="file.file.name">{{ file.file.name }}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<section class="contact-hero">
|
||||
<div class="container">
|
||||
<h1>{{ 'CONTACT.TITLE' | translate }}</h1>
|
||||
<p class="subtitle">Siamo qui per aiutarti. Compila il modulo sottostante per qualsiasi richiesta.</p>
|
||||
<p class="subtitle">{{ 'CONTACT.HERO_SUBTITLE' | translate }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -2,22 +2,18 @@
|
||||
<section class="hero">
|
||||
<div class="container hero-grid">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Stampa 3D tecnica per aziende, freelance e maker</p>
|
||||
<h1 class="hero-title">
|
||||
Prezzo e tempi in pochi secondi.<br>
|
||||
Dal file 3D al pezzo finito.
|
||||
</h1>
|
||||
<p class="eyebrow">{{ 'HOME.HERO_EYEBROW' | translate }}</p>
|
||||
<h1 class="hero-title" [innerHTML]="'HOME.HERO_TITLE' | translate"></h1>
|
||||
<p class="hero-lead">
|
||||
Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese.
|
||||
{{ 'HOME.HERO_LEAD' | translate }}
|
||||
</p>
|
||||
<p class="hero-subtitle">
|
||||
Realizziamo pezzi unici e produzioni in serie. Se hai il file, il preventivo è istantaneo.
|
||||
Se devi ancora crearlo, il nostro team di design lo progetterà per te.
|
||||
{{ 'HOME.HERO_SUBTITLE' | translate }}
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<app-button variant="primary" routerLink="/calculator/basic">Calcola Preventivo</app-button>
|
||||
<app-button variant="outline" routerLink="/shop">Vai allo shop</app-button>
|
||||
<app-button variant="text" routerLink="/contact">Parla con noi</app-button>
|
||||
<app-button variant="primary" routerLink="/calculator/basic">{{ 'HOME.BTN_CALCULATE' | translate }}</app-button>
|
||||
<app-button variant="outline" routerLink="/shop">{{ 'HOME.BTN_SHOP' | translate }}</app-button>
|
||||
<app-button variant="text" routerLink="/contact">{{ 'HOME.BTN_CONTACT' | translate }}</app-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,31 +22,31 @@
|
||||
<section class="section calculator">
|
||||
<div class="container calculator-grid">
|
||||
<div class="calculator-copy">
|
||||
<h2 class="section-title">Preventivo immediato in pochi secondi</h2>
|
||||
<h2 class="section-title">{{ 'HOME.SEC_CALC_TITLE' | translate }}</h2>
|
||||
<p class="section-subtitle">
|
||||
Nessuna registrazione necessaria. Non salviamo il tuo file fino al momento dell'ordine e la stima è effettuata tramite vero slicing.
|
||||
{{ 'HOME.SEC_CALC_SUBTITLE' | translate }}
|
||||
</p>
|
||||
<ul class="calculator-list">
|
||||
<li>Formati supportati: STL, 3MF, STEP, OBJ</li>
|
||||
<li>Qualità: bozza, standard, alta definizione</li>
|
||||
<li>{{ 'HOME.SEC_CALC_LIST_1' | translate }}</li>
|
||||
<li>{{ 'HOME.SEC_CALC_LIST_2' | translate }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<app-card class="quote-card">
|
||||
<div class="quote-header">
|
||||
<div>
|
||||
<p class="quote-eyebrow">Calcolo automatico</p>
|
||||
<h3 class="quote-title">Prezzo e tempi in un click</h3>
|
||||
<p class="quote-eyebrow">{{ 'HOME.CARD_CALC_EYEBROW' | translate }}</p>
|
||||
<h3 class="quote-title">{{ 'HOME.CARD_CALC_TITLE' | translate }}</h3>
|
||||
</div>
|
||||
<span class="quote-tag">Senza registrazione</span>
|
||||
<span class="quote-tag">{{ 'HOME.CARD_CALC_TAG' | translate }}</span>
|
||||
</div>
|
||||
<ul class="quote-steps">
|
||||
<li>Carica il file 3D</li>
|
||||
<li>Scegli materiale e qualità</li>
|
||||
<li>Ricevi subito costo e tempo</li>
|
||||
<li>{{ 'HOME.CARD_CALC_STEP_1' | translate }}</li>
|
||||
<li>{{ 'HOME.CARD_CALC_STEP_2' | translate }}</li>
|
||||
<li>{{ 'HOME.CARD_CALC_STEP_3' | translate }}</li>
|
||||
</ul>
|
||||
<div class="quote-actions">
|
||||
<app-button variant="primary" [fullWidth]="true" routerLink="/calculator/basic">Apri calcolatore</app-button>
|
||||
<app-button variant="outline" [fullWidth]="true" routerLink="/contact">Parla con noi</app-button>
|
||||
<app-button variant="primary" [fullWidth]="true" routerLink="/calculator/basic">{{ 'HOME.BTN_OPEN_CALC' | translate }}</app-button>
|
||||
<app-button variant="outline" [fullWidth]="true" routerLink="/contact">{{ 'HOME.BTN_CONTACT' | translate }}</app-button>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
@@ -60,9 +56,9 @@
|
||||
<div class="capabilities-bg"></div>
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">Cosa puoi ottenere</h2>
|
||||
<h2 class="section-title">{{ 'HOME.SEC_CAP_TITLE' | translate }}</h2>
|
||||
<p class="section-subtitle">
|
||||
Produzione su misura per prototipi, piccole serie e pezzi personalizzati.
|
||||
{{ 'HOME.SEC_CAP_SUBTITLE' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="cap-cards">
|
||||
@@ -70,29 +66,29 @@
|
||||
<div class="card-image-placeholder">
|
||||
<!-- <img src="..." alt="..."> -->
|
||||
</div>
|
||||
<h3>Prototipazione veloce</h3>
|
||||
<p class="text-muted">Dal file digitale al modello fisico in 24/48 ore. Verifica ergonomia, incastri e funzionamento prima dello stampo definitivo.</p>
|
||||
<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="..." alt="..."> -->
|
||||
</div>
|
||||
<h3>Pezzi personalizzati</h3>
|
||||
<p class="text-muted">Componenti unici impossibili da trovare in commercio. Riproduciamo parti rotte o creiamo adattamenti su misura.</p>
|
||||
<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="..." alt="..."> -->
|
||||
</div>
|
||||
<h3>Piccole serie</h3>
|
||||
<p class="text-muted">Produzione ponte o serie limitate (10-500 pezzi) con qualità ripetibile. Ideale per validazione di mercato.</p>
|
||||
<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="..." alt="..."> -->
|
||||
</div>
|
||||
<h3>Consulenza e CAD</h3>
|
||||
<p class="text-muted">Non hai il file 3D? Ti aiutiamo a progettarlo, ottimizzarlo per la stampa e scegliere il materiale giusto.</p>
|
||||
<h3>{{ 'HOME.CAP_4_TITLE' | translate }}</h3>
|
||||
<p class="text-muted">{{ 'HOME.CAP_4_TEXT' | translate }}</p>
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,33 +97,32 @@
|
||||
<section class="section shop">
|
||||
<div class="container split">
|
||||
<div class="shop-copy">
|
||||
<h2 class="section-title">Shop di soluzioni tecniche pronte</h2>
|
||||
<h2 class="section-title">{{ 'HOME.SEC_SHOP_TITLE' | translate }}</h2>
|
||||
<p>
|
||||
Prodotti selezionati, testati in laboratorio e pronti all'uso. Risolvono problemi reali con
|
||||
funzionalità concrete.
|
||||
{{ 'HOME.SEC_SHOP_TEXT' | translate }}
|
||||
</p>
|
||||
<ul class="shop-list">
|
||||
<li>Accessori funzionali per officine e laboratori</li>
|
||||
<li>Ricambi e componenti difficili da reperire</li>
|
||||
<li>Supporti e organizzatori per migliorare i flussi di lavoro</li>
|
||||
<li>{{ 'HOME.SEC_SHOP_LIST_1' | translate }}</li>
|
||||
<li>{{ 'HOME.SEC_SHOP_LIST_2' | translate }}</li>
|
||||
<li>{{ 'HOME.SEC_SHOP_LIST_3' | translate }}</li>
|
||||
</ul>
|
||||
<div class="shop-actions">
|
||||
<app-button variant="primary" routerLink="/shop">Scopri i prodotti</app-button>
|
||||
<app-button variant="outline" routerLink="/contact">Richiedi una soluzione</app-button>
|
||||
<app-button variant="primary" routerLink="/shop">{{ 'HOME.BTN_DISCOVER' | translate }}</app-button>
|
||||
<app-button variant="outline" routerLink="/contact">{{ 'HOME.BTN_REQ_SOLUTION' | translate }}</app-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-cards">
|
||||
<app-card>
|
||||
<h3>Best seller tecnici</h3>
|
||||
<p class="text-muted">Soluzioni provate sul campo e già pronte alla spedizione.</p>
|
||||
<h3>{{ 'HOME.CARD_SHOP_1_TITLE' | translate }}</h3>
|
||||
<p class="text-muted">{{ 'HOME.CARD_SHOP_1_TEXT' | translate }}</p>
|
||||
</app-card>
|
||||
<app-card>
|
||||
<h3>Kit pronti all'uso</h3>
|
||||
<p class="text-muted">Componenti compatibili e facili da montare senza sorprese.</p>
|
||||
<h3>{{ 'HOME.CARD_SHOP_2_TITLE' | translate }}</h3>
|
||||
<p class="text-muted">{{ 'HOME.CARD_SHOP_2_TEXT' | translate }}</p>
|
||||
</app-card>
|
||||
<app-card>
|
||||
<h3>Su richiesta</h3>
|
||||
<p class="text-muted">Non trovi quello che serve? Lo progettiamo e lo produciamo per te.</p>
|
||||
<h3>{{ 'HOME.CARD_SHOP_3_TITLE' | translate }}</h3>
|
||||
<p class="text-muted">{{ 'HOME.CARD_SHOP_3_TEXT' | translate }}</p>
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,17 +131,16 @@
|
||||
<section class="section about">
|
||||
<div class="container about-grid">
|
||||
<div class="about-copy">
|
||||
<h2 class="section-title">Su di noi</h2>
|
||||
<h2 class="section-title">{{ 'HOME.SEC_ABOUT_TITLE' | translate }}</h2>
|
||||
<p>
|
||||
3D fab è un laboratorio tecnico di stampa 3D. Seguiamo progetti dalla consulenza iniziale
|
||||
alla produzione, con tempi chiari e supporto diretto.
|
||||
{{ 'HOME.SEC_ABOUT_TEXT' | translate }}
|
||||
</p>
|
||||
<app-button variant="outline" routerLink="/contact">Contattaci</app-button>
|
||||
<app-button variant="outline" routerLink="/contact">{{ 'HOME.BTN_CONTACT' | translate }}</app-button>
|
||||
</div>
|
||||
<div class="about-media">
|
||||
<div class="about-feature-image">
|
||||
<!-- Foto founders -->
|
||||
<span class="text-sm">Foto Founders</span>
|
||||
<span class="text-sm">{{ 'HOME.FOUNDERS_PHOTO' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="container hero">
|
||||
<h1>{{ 'ORDER_CONFIRMED.TITLE' | translate }}</h1>
|
||||
<p class="subtitle">{{ 'ORDER_CONFIRMED.SUBTITLE' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="confirmation-layout">
|
||||
<app-card class="status-card">
|
||||
<div class="status-badge">{{ 'ORDER_CONFIRMED.STATUS' | translate }}</div>
|
||||
<h2>{{ 'ORDER_CONFIRMED.HEADING' | translate }}</h2>
|
||||
<p class="order-ref" *ngIf="orderId">
|
||||
{{ 'ORDER_CONFIRMED.ORDER_REF' | translate }}: <strong>#{{ orderId.substring(0, 8) }}</strong>
|
||||
</p>
|
||||
|
||||
<div class="message-block">
|
||||
<p>{{ 'ORDER_CONFIRMED.PROCESSING_TEXT' | translate }}</p>
|
||||
<p>{{ 'ORDER_CONFIRMED.EMAIL_TEXT' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<app-button (click)="goHome()">{{ 'ORDER_CONFIRMED.BACK_HOME' | translate }}</app-button>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
.hero {
|
||||
padding: var(--space-12) 0 var(--space-8);
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-size: 2.4rem;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text-muted);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.confirmation-layout {
|
||||
max-width: 760px;
|
||||
margin: 0 auto var(--space-12);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: #eef8f0;
|
||||
color: #136f2d;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
|
||||
.order-ref {
|
||||
margin: 0 0 var(--space-4);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.message-block {
|
||||
background: var(--color-neutral-100);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-5);
|
||||
margin-bottom: var(--space-6);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
max-width: 320px;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
||||
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-order-confirmed',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule, AppButtonComponent, AppCardComponent],
|
||||
templateUrl: './order-confirmed.component.html',
|
||||
styleUrl: './order-confirmed.component.scss'
|
||||
})
|
||||
export class OrderConfirmedComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
|
||||
orderId: string | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.orderId = this.route.snapshot.paramMap.get('orderId');
|
||||
}
|
||||
|
||||
goHome(): void {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
@@ -17,26 +17,35 @@
|
||||
class="type-option"
|
||||
[class.selected]="selectedPaymentMethod === 'twint'"
|
||||
(click)="selectPayment('twint')">
|
||||
<span class="method-name">TWINT</span>
|
||||
<span class="method-name">{{ 'PAYMENT.METHOD_TWINT' | translate }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="type-option"
|
||||
[class.selected]="selectedPaymentMethod === 'bill'"
|
||||
(click)="selectPayment('bill')">
|
||||
<span class="method-name">QR Bill / Bank Transfer</span>
|
||||
<span class="method-name">{{ 'PAYMENT.METHOD_BANK' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-details fade-in" *ngIf="selectedPaymentMethod === 'twint'">
|
||||
<div class="payment-details payment-details-twint fade-in" *ngIf="selectedPaymentMethod === 'twint'">
|
||||
<div class="details-header">
|
||||
<h4>{{ 'PAYMENT.TWINT_TITLE' | translate }}</h4>
|
||||
</div>
|
||||
<div class="qr-placeholder">
|
||||
<div class="qr-box">
|
||||
<span>QR CODE</span>
|
||||
</div>
|
||||
<img
|
||||
*ngIf="twintQrUrl()"
|
||||
class="twint-qr"
|
||||
[src]="getTwintQrUrl()"
|
||||
(error)="onTwintQrError()"
|
||||
alt="TWINT payment QR" />
|
||||
<p>{{ 'PAYMENT.TWINT_DESC' | translate }}</p>
|
||||
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
|
||||
<div class="twint-mobile-action">
|
||||
<app-button (click)="openTwintPayment()" [fullWidth]="true">
|
||||
{{ 'PAYMENT.TWINT_OPEN' | translate }}
|
||||
</app-button>
|
||||
</div>
|
||||
<p class="amount">{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,6 +58,7 @@
|
||||
<p><strong>{{ 'PAYMENT.BANK_OWNER' | translate }}:</strong> 3D Fab Switzerland</p>
|
||||
<p><strong>{{ 'PAYMENT.BANK_IBAN' | translate }}:</strong> CH98 0000 0000 0000 0000 0</p>
|
||||
<p><strong>{{ 'PAYMENT.BANK_REF' | translate }}:</strong> {{ o.id }}</p>
|
||||
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
|
||||
|
||||
<div class="qr-bill-actions">
|
||||
<app-button variant="outline" (click)="downloadInvoice()">
|
||||
|
||||
@@ -105,23 +105,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.payment-details-twint {
|
||||
border: 1px solid #dcd5ff;
|
||||
background: linear-gradient(180deg, #fcfbff 0%, #f6f4ff 100%);
|
||||
}
|
||||
|
||||
.qr-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.qr-box {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background-color: white;
|
||||
border: 2px solid var(--color-neutral-900);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: var(--space-4);
|
||||
.twint-qr {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
object-fit: contain;
|
||||
box-shadow: 0 6px 18px rgba(44, 37, 84, 0.08);
|
||||
}
|
||||
|
||||
.twint-mobile-action {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.amount {
|
||||
@@ -132,6 +142,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.billing-hint {
|
||||
margin-top: var(--space-3);
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.bank-details {
|
||||
p {
|
||||
margin-bottom: var(--space-2);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AppButtonComponent } from '../../shared/components/app-button/app-butto
|
||||
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
|
||||
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment',
|
||||
@@ -23,11 +24,14 @@ export class PaymentComponent implements OnInit {
|
||||
order = signal<any>(null);
|
||||
loading = signal(true);
|
||||
error = signal<string | null>(null);
|
||||
twintOpenUrl = signal<string | null>(null);
|
||||
twintQrUrl = signal<string | null>(null);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.orderId = this.route.snapshot.paramMap.get('orderId');
|
||||
if (this.orderId) {
|
||||
this.loadOrder();
|
||||
this.loadTwintPayment();
|
||||
} else {
|
||||
this.error.set('Order ID not found.');
|
||||
this.loading.set(false);
|
||||
@@ -68,8 +72,51 @@ export class PaymentComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
loadTwintPayment() {
|
||||
if (!this.orderId) return;
|
||||
this.quoteService.getTwintPayment(this.orderId).subscribe({
|
||||
next: (res) => {
|
||||
const qrPath = typeof res.qrImageUrl === 'string' ? `${res.qrImageUrl}?size=360` : null;
|
||||
const qrDataUri = typeof res.qrImageDataUri === 'string' ? res.qrImageDataUri : null;
|
||||
this.twintOpenUrl.set(this.resolveApiUrl(res.openUrl));
|
||||
this.twintQrUrl.set(qrDataUri ?? this.resolveApiUrl(qrPath));
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to load TWINT payment details', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openTwintPayment(): void {
|
||||
const openUrl = this.twintOpenUrl();
|
||||
if (typeof window !== 'undefined' && openUrl) {
|
||||
window.location.href = openUrl;
|
||||
}
|
||||
}
|
||||
|
||||
getTwintQrUrl(): string {
|
||||
return this.twintQrUrl() ?? '';
|
||||
}
|
||||
|
||||
onTwintQrError(): void {
|
||||
this.twintQrUrl.set(null);
|
||||
}
|
||||
|
||||
private resolveApiUrl(urlOrPath: string | null | undefined): string | null {
|
||||
if (!urlOrPath) return null;
|
||||
if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
|
||||
return urlOrPath;
|
||||
}
|
||||
const base = (environment.apiUrl || '').replace(/\/$/, '');
|
||||
const path = urlOrPath.startsWith('/') ? urlOrPath : `/${urlOrPath}`;
|
||||
return `${base}${path}`;
|
||||
}
|
||||
|
||||
completeOrder(): void {
|
||||
alert('Payment Simulated! Order marked as PAID.');
|
||||
this.router.navigate(['/']);
|
||||
if (!this.orderId) {
|
||||
this.router.navigate(['/']);
|
||||
return;
|
||||
}
|
||||
this.router.navigate(['/order-confirmed', this.orderId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<p>Prodotto non trovato.</p>
|
||||
<p>{{ 'SHOP.NOT_FOUND' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -180,10 +180,13 @@
|
||||
"METHOD": "Payment Method",
|
||||
"TWINT_TITLE": "Pay with TWINT",
|
||||
"TWINT_DESC": "Scan the code with your TWINT app",
|
||||
"TWINT_OPEN": "Open directly in TWINT",
|
||||
"TWINT_LINK": "Open payment link",
|
||||
"BANK_TITLE": "Bank Transfer",
|
||||
"BANK_OWNER": "Owner",
|
||||
"BANK_IBAN": "IBAN",
|
||||
"BANK_REF": "Reference",
|
||||
"BILLING_INFO_HINT": "Add the same information used in billing.",
|
||||
"DOWNLOAD_QR": "Download QR-Invoice (PDF)",
|
||||
"CONFIRM": "Confirm Order",
|
||||
"SUMMARY_TITLE": "Order Summary",
|
||||
@@ -192,5 +195,15 @@
|
||||
"SETUP_FEE": "Setup Fee",
|
||||
"TOTAL": "Total",
|
||||
"LOADING": "Loading order details..."
|
||||
},
|
||||
"ORDER_CONFIRMED": {
|
||||
"TITLE": "Order Confirmed",
|
||||
"SUBTITLE": "Payment received. Your order is now being processed.",
|
||||
"STATUS": "Processing",
|
||||
"HEADING": "We are preparing your order",
|
||||
"ORDER_REF": "Order reference",
|
||||
"PROCESSING_TEXT": "As soon as payment is confirmed, your order will move to production.",
|
||||
"EMAIL_TEXT": "We will send you an email update with status and next steps.",
|
||||
"BACK_HOME": "Back to Home"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,52 @@
|
||||
"TERMS": "Termini & Condizioni",
|
||||
"CONTACT": "Contattaci"
|
||||
},
|
||||
"HOME": {
|
||||
"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_LEAD": "Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese.",
|
||||
"HERO_SUBTITLE": "Realizziamo pezzi unici e produzioni in serie. Se hai il file, il preventivo è istantaneo. Se devi ancora crearlo, il nostro team di design lo progetterà per te.",
|
||||
"BTN_CALCULATE": "Calcola Preventivo",
|
||||
"BTN_SHOP": "Vai allo shop",
|
||||
"BTN_CONTACT": "Parla con noi",
|
||||
"SEC_CALC_TITLE": "Preventivo immediato in pochi secondi",
|
||||
"SEC_CALC_SUBTITLE": "Nessuna registrazione necessaria. Non salviamo il tuo file fino al momento dell'ordine e la stima è effettuata tramite vero slicing.",
|
||||
"SEC_CALC_LIST_1": "Formati supportati: STL, 3MF, STEP, OBJ",
|
||||
"SEC_CALC_LIST_2": "Qualità: bozza, standard, alta definizione",
|
||||
"CARD_CALC_EYEBROW": "Calcolo automatico",
|
||||
"CARD_CALC_TITLE": "Prezzo e tempi in un click",
|
||||
"CARD_CALC_TAG": "Senza registrazione",
|
||||
"CARD_CALC_STEP_1": "Carica il file 3D",
|
||||
"CARD_CALC_STEP_2": "Scegli materiale e qualità",
|
||||
"CARD_CALC_STEP_3": "Ricevi subito costo e tempo",
|
||||
"BTN_OPEN_CALC": "Apri calcolatore",
|
||||
"SEC_CAP_TITLE": "Cosa puoi ottenere",
|
||||
"SEC_CAP_SUBTITLE": "Produzione su misura per prototipi, piccole serie e pezzi personalizzati.",
|
||||
"CAP_1_TITLE": "Prototipazione veloce",
|
||||
"CAP_1_TEXT": "Dal file digitale al modello fisico in 24/48 ore. Verifica ergonomia, incastri e funzionamento prima dello stampo definitivo.",
|
||||
"CAP_2_TITLE": "Pezzi personalizzati",
|
||||
"CAP_2_TEXT": "Componenti unici impossibili da trovare in commercio. Riproduciamo parti rotte o creiamo adattamenti su misura.",
|
||||
"CAP_3_TITLE": "Piccole serie",
|
||||
"CAP_3_TEXT": "Produzione ponte o serie limitate (10-500 pezzi) con qualità ripetibile. Ideale per validazione di mercato.",
|
||||
"CAP_4_TITLE": "Consulenza e CAD",
|
||||
"CAP_4_TEXT": "Non hai il file 3D? Ti aiutiamo a progettarlo, ottimizzarlo per la stampa e scegliere il materiale giusto.",
|
||||
"SEC_SHOP_TITLE": "Shop di soluzioni tecniche pronte",
|
||||
"SEC_SHOP_TEXT": "Prodotti selezionati, testati in laboratorio e pronti all'uso. Risolvono problemi reali con funzionalità concrete.",
|
||||
"SEC_SHOP_LIST_1": "Accessori funzionali per officine e laboratori",
|
||||
"SEC_SHOP_LIST_2": "Ricambi e componenti difficili da reperire",
|
||||
"SEC_SHOP_LIST_3": "Supporti e organizzatori per migliorare i flussi di lavoro",
|
||||
"BTN_DISCOVER": "Scopri i prodotti",
|
||||
"BTN_REQ_SOLUTION": "Richiedi una soluzione",
|
||||
"CARD_SHOP_1_TITLE": "Best seller tecnici",
|
||||
"CARD_SHOP_1_TEXT": "Soluzioni provate sul campo e già pronte alla spedizione.",
|
||||
"CARD_SHOP_2_TITLE": "Kit pronti all'uso",
|
||||
"CARD_SHOP_2_TEXT": "Componenti compatibili e facili da montare senza sorprese.",
|
||||
"CARD_SHOP_3_TITLE": "Su richiesta",
|
||||
"CARD_SHOP_3_TEXT": "Non trovi quello che serve? Lo progettiamo e lo produciamo per te.",
|
||||
"SEC_ABOUT_TITLE": "Su di noi",
|
||||
"SEC_ABOUT_TEXT": "3D fab è un laboratorio tecnico di stampa 3D. Seguiamo progetti dalla consulenza iniziale alla produzione, con tempi chiari e supporto diretto.",
|
||||
"FOUNDERS_PHOTO": "Foto Founders"
|
||||
},
|
||||
"CALC": {
|
||||
"TITLE": "Calcola Preventivo 3D",
|
||||
"SUBTITLE": "Carica il tuo file 3D (STL, 3MF, STEP...) e ricevi una stima immediata di costi e tempi di stampa.",
|
||||
@@ -47,13 +93,53 @@
|
||||
"BENEFITS_1": "Preventivo automatico con costo e tempo immediati",
|
||||
"BENEFITS_2": "Materiali selezionati e qualità controllata",
|
||||
"BENEFITS_3": "Consulenza CAD se il file ha bisogno di modifiche",
|
||||
"ERR_FILE_REQUIRED": "Il file è obbligatorio."
|
||||
"ERR_FILE_REQUIRED": "Il file è obbligatorio.",
|
||||
"ANALYZING_TITLE": "Analisi in corso...",
|
||||
"ANALYZING_TEXT": "Stiamo analizzando la geometria e calcolando il percorso utensile.",
|
||||
"QTY_SHORT": "QTÀ",
|
||||
"COLOR_LABEL": "COLORE",
|
||||
"ADD_FILES": "Aggiungi file",
|
||||
"UPLOADING": "Caricamento...",
|
||||
"PROCESSING": "Elaborazione...",
|
||||
"NOTES_PLACEHOLDER": "Istruzioni specifiche...",
|
||||
"SETUP_NOTE": "* Include {{cost}} Costo di Setup"
|
||||
},
|
||||
"QUOTE": {
|
||||
"PROCEED_ORDER": "Procedi con l'ordine",
|
||||
"CONSULT": "Richiedi Consulenza",
|
||||
"TOTAL": "Totale"
|
||||
},
|
||||
"USER_DETAILS": {
|
||||
"TITLE": "I tuoi dati",
|
||||
"NAME": "Nome",
|
||||
"NAME_PLACEHOLDER": "Il tuo nome",
|
||||
"SURNAME": "Cognome",
|
||||
"SURNAME_PLACEHOLDER": "Il tuo cognome",
|
||||
"EMAIL": "Email",
|
||||
"EMAIL_PLACEHOLDER": "tua@email.com",
|
||||
"PHONE": "Telefono",
|
||||
"PHONE_PLACEHOLDER": "+41 ...",
|
||||
"ADDRESS": "Indirizzo",
|
||||
"ADDRESS_PLACEHOLDER": "Via e numero",
|
||||
"ZIP": "CAP",
|
||||
"ZIP_PLACEHOLDER": "0000",
|
||||
"CITY": "Città",
|
||||
"CITY_PLACEHOLDER": "Città",
|
||||
"SUBMIT": "Procedi",
|
||||
"SUMMARY_TITLE": "Riepilogo"
|
||||
},
|
||||
"COMMON": {
|
||||
"REQUIRED": "Campo obbligatorio",
|
||||
"INVALID_EMAIL": "Email non valida",
|
||||
"BACK": "Indietro",
|
||||
"OPTIONAL": "(Opzionale)"
|
||||
},
|
||||
"SHOP": {
|
||||
"TITLE": "Soluzioni tecniche",
|
||||
"SUBTITLE": "Prodotti pronti che risolvono problemi pratici",
|
||||
"ADD_CART": "Aggiungi al Carrello",
|
||||
"BACK": "Torna allo Shop"
|
||||
"BACK": "Torna allo Shop",
|
||||
"NOT_FOUND": "Prodotto non trovato."
|
||||
},
|
||||
"ABOUT": {
|
||||
"TITLE": "Chi Siamo",
|
||||
@@ -126,7 +212,10 @@
|
||||
"ERR_MAX_FILES": "Limite massimo di 15 file raggiunto.",
|
||||
"SUCCESS_TITLE": "Messaggio Inviato con Successo",
|
||||
"SUCCESS_DESC": "Grazie per averci contattato. Abbiamo ricevuto il tuo messaggio e ti invieremo una email di riepilogo a breve.",
|
||||
"SEND_ANOTHER": "Invia un altro messaggio"
|
||||
"SEND_ANOTHER": "Invia un altro messaggio",
|
||||
"HERO_SUBTITLE": "Siamo qui per aiutarti. Compila il modulo sottostante per qualsiasi richiesta.",
|
||||
"FILE_TYPE_PDF": "PDF",
|
||||
"FILE_TYPE_3D": "3D"
|
||||
},
|
||||
"CHECKOUT": {
|
||||
"TITLE": "Checkout",
|
||||
@@ -152,17 +241,23 @@
|
||||
"SETUP_FEE": "Costo di Avvio",
|
||||
"TOTAL": "Totale",
|
||||
"QTY": "Qtà",
|
||||
"SHIPPING": "Spedizione"
|
||||
"SHIPPING": "Spedizione",
|
||||
"INVALID_EMAIL": "Email non valida",
|
||||
"COMPANY_OPTIONAL": "Nome Azienda (Opzionale)",
|
||||
"REF_PERSON_OPTIONAL": "Persona di Riferimento (Opzionale)"
|
||||
},
|
||||
"PAYMENT": {
|
||||
"TITLE": "Pagamento",
|
||||
"METHOD": "Metodo di Pagamento",
|
||||
"TWINT_TITLE": "Paga con TWINT",
|
||||
"TWINT_DESC": "Inquadra il codice con l'app TWINT",
|
||||
"TWINT_OPEN": "Apri direttamente in TWINT",
|
||||
"TWINT_LINK": "Apri link di pagamento",
|
||||
"BANK_TITLE": "Bonifico Bancario",
|
||||
"BANK_OWNER": "Titolare",
|
||||
"BANK_IBAN": "IBAN",
|
||||
"BANK_REF": "Riferimento",
|
||||
"BILLING_INFO_HINT": "Aggiungi le informazioni uguali a quelle della fatturazione.",
|
||||
"DOWNLOAD_QR": "Scarica QR-Fattura (PDF)",
|
||||
"CONFIRM": "Conferma Ordine",
|
||||
"SUMMARY_TITLE": "Riepilogo Ordine",
|
||||
@@ -170,6 +265,18 @@
|
||||
"SHIPPING": "Spedizione",
|
||||
"SETUP_FEE": "Costo Setup",
|
||||
"TOTAL": "Totale",
|
||||
"LOADING": "Caricamento dettagli ordine..."
|
||||
"LOADING": "Caricamento dettagli ordine...",
|
||||
"METHOD_TWINT": "TWINT",
|
||||
"METHOD_BANK": "Fattura QR / Bonifico"
|
||||
},
|
||||
"ORDER_CONFIRMED": {
|
||||
"TITLE": "Ordine Confermato",
|
||||
"SUBTITLE": "Pagamento registrato. Il tuo ordine è ora in elaborazione.",
|
||||
"STATUS": "In elaborazione",
|
||||
"HEADING": "Stiamo preparando il tuo ordine",
|
||||
"ORDER_REF": "Riferimento ordine",
|
||||
"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.",
|
||||
"BACK_HOME": "Torna alla Home"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user