feat(back-end): add ClamAV service remember to add env and compose.deploy on server
This commit is contained in:
@@ -67,6 +67,16 @@ public class OrderItem {
|
|||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private OffsetDateTime createdAt;
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
private void onCreate() {
|
||||||
|
if (createdAt == null) {
|
||||||
|
createdAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
if (quantity == null) {
|
||||||
|
quantity = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public UUID getId() {
|
public UUID getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -195,4 +205,4 @@ public class OrderItem {
|
|||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="checkout-page">
|
<div class="checkout-page">
|
||||||
<h2 class="section-title">Checkout</h2>
|
<h2 class="section-title">{{ 'CHECKOUT.TITLE' | translate }}</h2>
|
||||||
|
|
||||||
<div class="checkout-layout">
|
<div class="checkout-layout">
|
||||||
|
|
||||||
@@ -15,23 +15,12 @@
|
|||||||
<!-- Contact Info Card -->
|
<!-- Contact Info Card -->
|
||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Contact Information</h3>
|
<h3>{{ 'CHECKOUT.CONTACT_INFO' | translate }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|
||||||
<!-- User Type Selector -->
|
|
||||||
<div class="user-type-selector">
|
|
||||||
<div class="type-option" [class.selected]="!isCompany" (click)="setCustomerType(false)">
|
|
||||||
Private
|
|
||||||
</div>
|
|
||||||
<div class="type-option" [class.selected]="isCompany" (click)="setCustomerType(true)">
|
|
||||||
Company
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<app-input formControlName="email" type="email" label="Email" [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') ? 'Invalid email' : null"></app-input>
|
||||||
<app-input formControlName="phone" type="tel" label="Phone" [required]="true"></app-input>
|
<app-input formControlName="phone" type="tel" [label]="'CHECKOUT.PHONE' | translate" [required]="true"></app-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,24 +28,37 @@
|
|||||||
<!-- Billing Address Card -->
|
<!-- Billing Address Card -->
|
||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Billing Address</h3>
|
<h3>{{ 'CHECKOUT.BILLING_ADDR' | translate }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content" formGroupName="billingAddress">
|
<div class="card-content" formGroupName="billingAddress">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<app-input formControlName="firstName" label="First Name" [required]="true"></app-input>
|
<app-input formControlName="firstName" [label]="'CHECKOUT.FIRST_NAME' | translate" [required]="true"></app-input>
|
||||||
<app-input formControlName="lastName" label="Last Name" [required]="true"></app-input>
|
<app-input formControlName="lastName" [label]="'CHECKOUT.LAST_NAME' | translate" [required]="true"></app-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Company Name (Conditional) -->
|
<app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate" [required]="true"></app-input>
|
||||||
<app-input *ngIf="isCompany" formControlName="companyName" label="Company Name" [required]="true"></app-input>
|
<app-input formControlName="addressLine2" [label]="'CHECKOUT.ADDRESS_2' | translate"></app-input>
|
||||||
|
|
||||||
<app-input formControlName="addressLine1" label="Address Line 1" [required]="true"></app-input>
|
|
||||||
<app-input formControlName="addressLine2" label="Address Line 2 (Optional)"></app-input>
|
|
||||||
|
|
||||||
<div class="form-row three-cols">
|
<div class="form-row three-cols">
|
||||||
<app-input formControlName="zip" label="ZIP Code" [required]="true"></app-input>
|
<app-input formControlName="zip" [label]="'CHECKOUT.ZIP' | translate" [required]="true"></app-input>
|
||||||
<app-input formControlName="city" label="City" class="city-field" [required]="true"></app-input>
|
<app-input formControlName="city" [label]="'CHECKOUT.CITY' | translate" class="city-field" [required]="true"></app-input>
|
||||||
<app-input formControlName="countryCode" label="Country" [disabled]="true" [required]="true"></app-input>
|
<app-input formControlName="countryCode" [label]="'CHECKOUT.COUNTRY' | translate" [disabled]="true" [required]="true"></app-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Type Selector -->
|
||||||
|
<div class="user-type-selector">
|
||||||
|
<div class="type-option" [class.selected]="!isCompany" (click)="setCustomerType(false)">
|
||||||
|
{{ 'CONTACT.TYPE_PRIVATE' | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="type-option" [class.selected]="isCompany" (click)="setCustomerType(true)">
|
||||||
|
{{ 'CONTACT.TYPE_COMPANY' | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Company Fields (Indented with left border) -->
|
||||||
|
<div *ngIf="isCompany" class="company-fields">
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,36 +68,39 @@
|
|||||||
<label class="checkbox-container">
|
<label class="checkbox-container">
|
||||||
<input type="checkbox" formControlName="shippingSameAsBilling">
|
<input type="checkbox" formControlName="shippingSameAsBilling">
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
Shipping address same as billing
|
{{ 'CHECKOUT.SHIPPING_SAME' | translate }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Shipping Address Card (Conditional) -->
|
<!-- Shipping Address Card (Conditional) -->
|
||||||
<div class="form-card" *ngIf="!checkoutForm.get('shippingSameAsBilling')?.value">
|
<div class="form-card" *ngIf="!checkoutForm.get('shippingSameAsBilling')?.value">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Shipping Address</h3>
|
<h3>{{ 'CHECKOUT.SHIPPING_ADDR' | translate }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content" formGroupName="shippingAddress">
|
<div class="card-content" formGroupName="shippingAddress">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<app-input formControlName="firstName" label="First Name"></app-input>
|
<app-input formControlName="firstName" [label]="'CHECKOUT.FIRST_NAME' | translate"></app-input>
|
||||||
<app-input formControlName="lastName" label="Last Name"></app-input>
|
<app-input formControlName="lastName" [label]="'CHECKOUT.LAST_NAME' | translate"></app-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-input formControlName="companyName" label="Company (Optional)"></app-input>
|
<div *ngIf="isCompany" class="company-fields">
|
||||||
<app-input formControlName="addressLine1" label="Address Line 1"></app-input>
|
<app-input formControlName="companyName" [label]="('CHECKOUT.COMPANY_NAME' | translate) + ' (Optional)'"></app-input>
|
||||||
<app-input formControlName="zip" label="ZIP Code"></app-input>
|
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' (Optional)'"></app-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate"></app-input>
|
||||||
|
|
||||||
<div class="form-row three-cols">
|
<div class="form-row three-cols">
|
||||||
<app-input formControlName="zip" label="ZIP Code"></app-input>
|
<app-input formControlName="zip" [label]="'CHECKOUT.ZIP' | translate"></app-input>
|
||||||
<app-input formControlName="city" label="City" class="city-field"></app-input>
|
<app-input formControlName="city" [label]="'CHECKOUT.CITY' | translate" class="city-field"></app-input>
|
||||||
<app-input formControlName="countryCode" label="Country" [disabled]="true"></app-input>
|
<app-input formControlName="countryCode" [label]="'CHECKOUT.COUNTRY' | translate" [disabled]="true"></app-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<app-button type="submit" [disabled]="checkoutForm.invalid || isSubmitting()" [fullWidth]="true">
|
<app-button type="submit" [disabled]="checkoutForm.invalid || isSubmitting()" [fullWidth]="true">
|
||||||
{{ isSubmitting() ? 'Processing...' : 'Place Order' }}
|
{{ isSubmitting() ? ('CHECKOUT.PROCESSING' | translate) : ('CHECKOUT.PLACE_ORDER' | translate) }}
|
||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@
|
|||||||
<div class="checkout-summary-section">
|
<div class="checkout-summary-section">
|
||||||
<div class="form-card sticky-card">
|
<div class="form-card sticky-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Order Summary</h3>
|
<h3>{{ 'CHECKOUT.SUMMARY_TITLE' | translate }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
@@ -115,7 +120,7 @@
|
|||||||
<div class="item-details">
|
<div class="item-details">
|
||||||
<span class="item-name">{{ item.originalFilename }}</span>
|
<span class="item-name">{{ item.originalFilename }}</span>
|
||||||
<div class="item-specs">
|
<div class="item-specs">
|
||||||
<span>Qty: {{ item.quantity }}</span>
|
<span>{{ 'CHECKOUT.QTY' | translate }}: {{ item.quantity }}</span>
|
||||||
<span *ngIf="item.colorCode" class="color-dot" [style.background-color]="item.colorCode"></span>
|
<span *ngIf="item.colorCode" class="color-dot" [style.background-color]="item.colorCode"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-specs-sub">
|
<div class="item-specs-sub">
|
||||||
@@ -130,17 +135,17 @@
|
|||||||
|
|
||||||
<div class="summary-totals" *ngIf="quoteSession() as session">
|
<div class="summary-totals" *ngIf="quoteSession() as session">
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
<span>Subtotal</span>
|
<span>{{ 'CHECKOUT.SUBTOTAL' | translate }}</span>
|
||||||
<span>{{ (session.totalPrice - session.setupCostChf) | currency:'CHF' }}</span>
|
<span>{{ session.itemsTotalChf | currency:'CHF' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
<span>Setup Fee</span>
|
<span>{{ 'CHECKOUT.SETUP_FEE' | translate }}</span>
|
||||||
<span>{{ session.setupCostChf | currency:'CHF' }}</span>
|
<span>{{ session.session.setupCostChf | currency:'CHF' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="total-row grand-total">
|
<div class="total-row grand-total">
|
||||||
<span>Total</span>
|
<span>{{ 'CHECKOUT.TOTAL' | translate }}</span>
|
||||||
<span>{{ session.totalPrice | currency:'CHF' }}</span>
|
<span>{{ session.grandTotalChf | currency:'CHF' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,15 +72,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* User Type Selector Styles */
|
/* User Type Selector Styles - Matched with Contact Form */
|
||||||
.user-type-selector {
|
.user-type-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--color-bg-subtle);
|
background-color: var(--color-neutral-100);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-option {
|
.type-option {
|
||||||
@@ -105,6 +106,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.company-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding-left: var(--space-4);
|
||||||
|
border-left: 2px solid var(--color-border);
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.shipping-option {
|
.shipping-option {
|
||||||
margin: var(--space-6) 0;
|
margin: var(--space-6) 0;
|
||||||
}
|
}
|
||||||
@@ -239,9 +250,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-totals {
|
.summary-totals {
|
||||||
background: var(--color-bg-subtle);
|
padding-top: var(--space-4);
|
||||||
padding: var(--space-4);
|
border-top: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-top: var(--space-4);
|
margin-top: var(--space-4);
|
||||||
|
|
||||||
.total-row {
|
.total-row {
|
||||||
@@ -249,21 +259,18 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
|
||||||
&.grand-total {
|
&.grand-total {
|
||||||
color: var(--color-heading);
|
color: var(--color-heading);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin-top: var(--space-3);
|
margin-top: var(--space-4);
|
||||||
padding-top: var(--space-3);
|
padding-top: var(--space-4);
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: none; // Handled by border-top in grand-total
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, inject, OnInit, signal } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
|
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
|
||||||
import { AppInputComponent } from '../../shared/components/app-input/app-input.component';
|
import { AppInputComponent } from '../../shared/components/app-input/app-input.component';
|
||||||
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
||||||
@@ -12,6 +13,7 @@ import { AppButtonComponent } from '../../shared/components/app-button/app-butto
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
TranslateModule,
|
||||||
AppInputComponent,
|
AppInputComponent,
|
||||||
AppButtonComponent
|
AppButtonComponent
|
||||||
],
|
],
|
||||||
@@ -43,6 +45,7 @@ export class CheckoutComponent implements OnInit {
|
|||||||
firstName: ['', Validators.required],
|
firstName: ['', Validators.required],
|
||||||
lastName: ['', Validators.required],
|
lastName: ['', Validators.required],
|
||||||
companyName: [''],
|
companyName: [''],
|
||||||
|
referencePerson: [''],
|
||||||
addressLine1: ['', Validators.required],
|
addressLine1: ['', Validators.required],
|
||||||
addressLine2: [''],
|
addressLine2: [''],
|
||||||
zip: ['', Validators.required],
|
zip: ['', Validators.required],
|
||||||
@@ -54,6 +57,7 @@ export class CheckoutComponent implements OnInit {
|
|||||||
firstName: [''],
|
firstName: [''],
|
||||||
lastName: [''],
|
lastName: [''],
|
||||||
companyName: [''],
|
companyName: [''],
|
||||||
|
referencePerson: [''],
|
||||||
addressLine1: [''],
|
addressLine1: [''],
|
||||||
addressLine2: [''],
|
addressLine2: [''],
|
||||||
zip: [''],
|
zip: [''],
|
||||||
@@ -74,13 +78,17 @@ export class CheckoutComponent implements OnInit {
|
|||||||
// Update validators based on type
|
// Update validators based on type
|
||||||
const billingGroup = this.checkoutForm.get('billingAddress') as FormGroup;
|
const billingGroup = this.checkoutForm.get('billingAddress') as FormGroup;
|
||||||
const companyControl = billingGroup.get('companyName');
|
const companyControl = billingGroup.get('companyName');
|
||||||
|
const referenceControl = billingGroup.get('referencePerson');
|
||||||
|
|
||||||
if (isCompany) {
|
if (isCompany) {
|
||||||
companyControl?.setValidators([Validators.required]);
|
companyControl?.setValidators([Validators.required]);
|
||||||
|
referenceControl?.setValidators([Validators.required]);
|
||||||
} else {
|
} else {
|
||||||
companyControl?.clearValidators();
|
companyControl?.clearValidators();
|
||||||
|
referenceControl?.clearValidators();
|
||||||
}
|
}
|
||||||
companyControl?.updateValueAndValidity();
|
companyControl?.updateValueAndValidity();
|
||||||
|
referenceControl?.updateValueAndValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -147,6 +155,7 @@ export class CheckoutComponent implements OnInit {
|
|||||||
firstName: formVal.billingAddress.firstName,
|
firstName: formVal.billingAddress.firstName,
|
||||||
lastName: formVal.billingAddress.lastName,
|
lastName: formVal.billingAddress.lastName,
|
||||||
companyName: formVal.billingAddress.companyName,
|
companyName: formVal.billingAddress.companyName,
|
||||||
|
contactPerson: formVal.billingAddress.referencePerson,
|
||||||
addressLine1: formVal.billingAddress.addressLine1,
|
addressLine1: formVal.billingAddress.addressLine1,
|
||||||
addressLine2: formVal.billingAddress.addressLine2,
|
addressLine2: formVal.billingAddress.addressLine2,
|
||||||
zip: formVal.billingAddress.zip,
|
zip: formVal.billingAddress.zip,
|
||||||
@@ -157,6 +166,7 @@ export class CheckoutComponent implements OnInit {
|
|||||||
firstName: formVal.shippingAddress.firstName,
|
firstName: formVal.shippingAddress.firstName,
|
||||||
lastName: formVal.shippingAddress.lastName,
|
lastName: formVal.shippingAddress.lastName,
|
||||||
companyName: formVal.shippingAddress.companyName,
|
companyName: formVal.shippingAddress.companyName,
|
||||||
|
contactPerson: formVal.shippingAddress.referencePerson,
|
||||||
addressLine1: formVal.shippingAddress.addressLine1,
|
addressLine1: formVal.shippingAddress.addressLine1,
|
||||||
addressLine2: formVal.shippingAddress.addressLine2,
|
addressLine2: formVal.shippingAddress.addressLine2,
|
||||||
zip: formVal.shippingAddress.zip,
|
zip: formVal.shippingAddress.zip,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
private controls!: OrbitControls;
|
private controls!: OrbitControls;
|
||||||
private animationId: number | null = null;
|
private animationId: number | null = null;
|
||||||
private currentMesh: THREE.Mesh | null = null;
|
private currentMesh: THREE.Mesh | null = null;
|
||||||
|
private autoRotate = true;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
@@ -38,14 +39,14 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (changes['color'] && this.currentMesh && !changes['file']) {
|
if (changes['color'] && this.currentMesh && !changes['file']) {
|
||||||
// Update existing mesh color if only color changed
|
this.applyColorStyle(this.color);
|
||||||
const mat = this.currentMesh.material as THREE.MeshPhongMaterial;
|
|
||||||
mat.color.set(this.color);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
if (this.animationId) cancelAnimationFrame(this.animationId);
|
if (this.animationId) cancelAnimationFrame(this.animationId);
|
||||||
|
this.clearCurrentMesh();
|
||||||
|
if (this.controls) this.controls.dispose();
|
||||||
if (this.renderer) this.renderer.dispose();
|
if (this.renderer) this.renderer.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,28 +55,51 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
const height = this.rendererContainer.nativeElement.clientHeight;
|
const height = this.rendererContainer.nativeElement.clientHeight;
|
||||||
|
|
||||||
this.scene = new THREE.Scene();
|
this.scene = new THREE.Scene();
|
||||||
this.scene.background = new THREE.Color(0xf7f6f2); // Neutral-50
|
this.scene.background = new THREE.Color(0xf4f8fc);
|
||||||
|
|
||||||
// Lights
|
// Lights
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
|
||||||
this.scene.add(ambientLight);
|
this.scene.add(ambientLight);
|
||||||
|
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
const hemiLight = new THREE.HemisphereLight(0xf8fbff, 0xc8d3df, 0.95);
|
||||||
directionalLight.position.set(1, 1, 1);
|
hemiLight.position.set(0, 30, 0);
|
||||||
this.scene.add(directionalLight);
|
this.scene.add(hemiLight);
|
||||||
|
|
||||||
|
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.35);
|
||||||
|
directionalLight1.position.set(6, 8, 6);
|
||||||
|
this.scene.add(directionalLight1);
|
||||||
|
|
||||||
|
const directionalLight2 = new THREE.DirectionalLight(0xe8f0ff, 0.85);
|
||||||
|
directionalLight2.position.set(-7, 4, -5);
|
||||||
|
this.scene.add(directionalLight2);
|
||||||
|
|
||||||
|
const directionalLight3 = new THREE.DirectionalLight(0xffffff, 0.55);
|
||||||
|
directionalLight3.position.set(0, 5, -9);
|
||||||
|
this.scene.add(directionalLight3);
|
||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
this.camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
|
this.camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
|
||||||
this.camera.position.z = 100;
|
this.camera.position.z = 100;
|
||||||
|
|
||||||
// Renderer
|
// Renderer
|
||||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
|
||||||
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
this.renderer.toneMappingExposure = 1.2;
|
||||||
this.renderer.setSize(width, height);
|
this.renderer.setSize(width, height);
|
||||||
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
|
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
this.controls.enableDamping = true;
|
this.controls.enableDamping = true;
|
||||||
|
this.controls.dampingFactor = 0.06;
|
||||||
|
this.controls.enablePan = false;
|
||||||
|
this.controls.minDistance = 10;
|
||||||
|
this.controls.maxDistance = 600;
|
||||||
|
this.controls.addEventListener('start', () => {
|
||||||
|
this.autoRotate = false;
|
||||||
|
});
|
||||||
|
|
||||||
this.animate();
|
this.animate();
|
||||||
|
|
||||||
@@ -95,24 +119,27 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
|
|
||||||
private loadFile(file: File) {
|
private loadFile(file: File) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.autoRotate = true;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
try {
|
try {
|
||||||
const loader = new STLLoader();
|
const loader = new STLLoader();
|
||||||
const geometry = loader.parse(event.target?.result as ArrayBuffer);
|
const geometry = loader.parse(event.target?.result as ArrayBuffer);
|
||||||
|
|
||||||
if (this.currentMesh) {
|
this.clearCurrentMesh();
|
||||||
this.scene.remove(this.currentMesh);
|
|
||||||
this.currentMesh.geometry.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
const material = new THREE.MeshPhongMaterial({
|
geometry.computeVertexNormals();
|
||||||
color: this.color,
|
|
||||||
specular: 0x111111,
|
const material = new THREE.MeshStandardMaterial({
|
||||||
shininess: 200
|
color: this.color,
|
||||||
|
roughness: 0.42,
|
||||||
|
metalness: 0.05,
|
||||||
|
emissive: 0x000000,
|
||||||
|
emissiveIntensity: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentMesh = new THREE.Mesh(geometry, material);
|
this.currentMesh = new THREE.Mesh(geometry, material);
|
||||||
|
this.applyColorStyle(this.color);
|
||||||
|
|
||||||
// Center geometry
|
// Center geometry
|
||||||
geometry.computeBoundingBox();
|
geometry.computeBoundingBox();
|
||||||
@@ -140,9 +167,10 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
|
|
||||||
// Calculate distance towards camera (z-axis)
|
// Calculate distance towards camera (z-axis)
|
||||||
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
||||||
cameraZ *= 1.5; // Tighter zoom (reduced from 2.5)
|
cameraZ *= 1.72;
|
||||||
|
|
||||||
this.camera.position.z = cameraZ;
|
this.camera.position.set(cameraZ * 0.65, cameraZ * 0.95, cameraZ * 1.1);
|
||||||
|
this.camera.lookAt(0, 0, 0);
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
this.controls.update();
|
this.controls.update();
|
||||||
|
|
||||||
@@ -157,9 +185,63 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
|
|
||||||
private animate() {
|
private animate() {
|
||||||
this.animationId = requestAnimationFrame(() => this.animate());
|
this.animationId = requestAnimationFrame(() => this.animate());
|
||||||
|
|
||||||
|
if (this.currentMesh && this.autoRotate) {
|
||||||
|
this.currentMesh.rotation.z += 0.0025;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.controls) this.controls.update();
|
if (this.controls) this.controls.update();
|
||||||
if (this.renderer && this.scene && this.camera) {
|
if (this.renderer && this.scene && this.camera) {
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private clearCurrentMesh() {
|
||||||
|
if (!this.currentMesh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scene.remove(this.currentMesh);
|
||||||
|
this.currentMesh.geometry.dispose();
|
||||||
|
|
||||||
|
const meshMaterial = this.currentMesh.material;
|
||||||
|
if (Array.isArray(meshMaterial)) {
|
||||||
|
meshMaterial.forEach((m) => m.dispose());
|
||||||
|
} else {
|
||||||
|
meshMaterial.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentMesh = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyColorStyle(color: string) {
|
||||||
|
if (!this.currentMesh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkColor = this.isDarkColor(color);
|
||||||
|
const meshMaterial = this.currentMesh.material;
|
||||||
|
|
||||||
|
if (meshMaterial instanceof THREE.MeshStandardMaterial) {
|
||||||
|
meshMaterial.color.set(color);
|
||||||
|
if (darkColor) {
|
||||||
|
meshMaterial.emissive.set(0x2a2f36);
|
||||||
|
meshMaterial.emissiveIntensity = 0.28;
|
||||||
|
meshMaterial.roughness = 0.5;
|
||||||
|
meshMaterial.metalness = 0.03;
|
||||||
|
} else {
|
||||||
|
meshMaterial.emissive.set(0x000000);
|
||||||
|
meshMaterial.emissiveIntensity = 0;
|
||||||
|
meshMaterial.roughness = 0.42;
|
||||||
|
meshMaterial.metalness = 0.05;
|
||||||
|
}
|
||||||
|
meshMaterial.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDarkColor(color: string): boolean {
|
||||||
|
const c = new THREE.Color(color);
|
||||||
|
const luminance = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
|
||||||
|
return luminance < 0.22;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,5 +148,29 @@
|
|||||||
"SUCCESS_TITLE": "Message Sent Successfully",
|
"SUCCESS_TITLE": "Message Sent Successfully",
|
||||||
"SUCCESS_DESC": "Thank you for contacting us. We have received your message and will send you a recap email shortly.",
|
"SUCCESS_DESC": "Thank you for contacting us. We have received your message and will send you a recap email shortly.",
|
||||||
"SEND_ANOTHER": "Send Another Message"
|
"SEND_ANOTHER": "Send Another Message"
|
||||||
|
},
|
||||||
|
"CHECKOUT": {
|
||||||
|
"TITLE": "Checkout",
|
||||||
|
"CONTACT_INFO": "Contact Information",
|
||||||
|
"BILLING_ADDR": "Billing Address",
|
||||||
|
"SHIPPING_ADDR": "Shipping Address",
|
||||||
|
"FIRST_NAME": "First Name",
|
||||||
|
"LAST_NAME": "Last Name",
|
||||||
|
"EMAIL": "Email",
|
||||||
|
"PHONE": "Phone",
|
||||||
|
"COMPANY_NAME": "Company Name",
|
||||||
|
"ADDRESS_1": "Address Line 1",
|
||||||
|
"ADDRESS_2": "Address Line 2 (Optional)",
|
||||||
|
"ZIP": "ZIP Code",
|
||||||
|
"CITY": "City",
|
||||||
|
"COUNTRY": "Country",
|
||||||
|
"SHIPPING_SAME": "Shipping address same as billing",
|
||||||
|
"PLACE_ORDER": "Place Order",
|
||||||
|
"PROCESSING": "Processing...",
|
||||||
|
"SUMMARY_TITLE": "Order Summary",
|
||||||
|
"SUBTOTAL": "Subtotal",
|
||||||
|
"SETUP_FEE": "Setup Fee",
|
||||||
|
"TOTAL": "Total",
|
||||||
|
"QTY": "Qty"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,5 +127,29 @@
|
|||||||
"SUCCESS_TITLE": "Messaggio Inviato con Successo",
|
"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.",
|
"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"
|
||||||
|
},
|
||||||
|
"CHECKOUT": {
|
||||||
|
"TITLE": "Checkout",
|
||||||
|
"CONTACT_INFO": "Informazioni di Contatto",
|
||||||
|
"BILLING_ADDR": "Indirizzo di Fatturazione",
|
||||||
|
"SHIPPING_ADDR": "Indirizzo di Spedizione",
|
||||||
|
"FIRST_NAME": "Nome",
|
||||||
|
"LAST_NAME": "Cognome",
|
||||||
|
"EMAIL": "Email",
|
||||||
|
"PHONE": "Telefono",
|
||||||
|
"COMPANY_NAME": "Nome Azienda",
|
||||||
|
"ADDRESS_1": "Indirizzo (Via e numero)",
|
||||||
|
"ADDRESS_2": "Informazioni aggiuntive (opzionale)",
|
||||||
|
"ZIP": "CAP",
|
||||||
|
"CITY": "Città",
|
||||||
|
"COUNTRY": "Paese",
|
||||||
|
"SHIPPING_SAME": "L'indirizzo di spedizione è lo stesso di quello di fatturazione",
|
||||||
|
"PLACE_ORDER": "Invia Ordine",
|
||||||
|
"PROCESSING": "Elaborazione...",
|
||||||
|
"SUMMARY_TITLE": "Riepilogo Ordine",
|
||||||
|
"SUBTOTAL": "Subtotale",
|
||||||
|
"SETUP_FEE": "Costo di Avvio",
|
||||||
|
"TOTAL": "Totale",
|
||||||
|
"QTY": "Qtà"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user