style: apply prettier formatting
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
<div class="alert" [ngClass]="type()">
|
||||
<div class="icon">
|
||||
@if(type() === 'info') { ℹ️ }
|
||||
@if(type() === 'warning') { ⚠️ }
|
||||
@if(type() === 'error') { ❌ }
|
||||
@if(type() === 'success') { ✅ }
|
||||
@if (type() === "info") {
|
||||
ℹ️
|
||||
}
|
||||
@if (type() === "warning") {
|
||||
⚠️
|
||||
}
|
||||
@if (type() === "error") {
|
||||
❌
|
||||
}
|
||||
@if (type() === "success") {
|
||||
✅
|
||||
}
|
||||
</div>
|
||||
<div class="content"><ng-content></ng-content></div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,22 @@
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.info { background: var(--color-neutral-100); color: var(--color-neutral-800); }
|
||||
.warning { background: #fefce8; color: #854d0e; border: 1px solid #fde047; }
|
||||
.error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
|
||||
.success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
|
||||
.info {
|
||||
background: var(--color-neutral-100);
|
||||
color: var(--color-neutral-800);
|
||||
}
|
||||
.warning {
|
||||
background: #fefce8;
|
||||
color: #854d0e;
|
||||
border: 1px solid #fde047;
|
||||
}
|
||||
.error {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
.success {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common';
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './app-alert.component.html',
|
||||
styleUrl: './app-alert.component.scss'
|
||||
styleUrl: './app-alert.component.scss',
|
||||
})
|
||||
export class AppAlertComponent {
|
||||
type = input<'info' | 'warning' | 'error' | 'success'>('info');
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<button
|
||||
[type]="type()"
|
||||
<button
|
||||
[type]="type()"
|
||||
[class]="'btn btn-' + variant() + ' ' + (fullWidth() ? 'w-full' : '')"
|
||||
[disabled]="disabled()"
|
||||
(click)="handleClick($event)">
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</button>
|
||||
|
||||
@@ -6,28 +6,37 @@
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.w-full { width: 100%; }
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--color-brand);
|
||||
color: var(--color-neutral-900);
|
||||
&:hover:not(:disabled) { background-color: var(--color-brand-hover); }
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--color-neutral-200);
|
||||
color: var(--color-neutral-900);
|
||||
&:hover:not(:disabled) { background-color: var(--color-neutral-300); }
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-neutral-300);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@@ -37,8 +46,8 @@
|
||||
padding: calc(0.5rem - 1px) calc(1rem - 1px);
|
||||
color: var(--color-neutral-900);
|
||||
font-weight: 600;
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-brand);
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-brand);
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
}
|
||||
@@ -47,5 +56,7 @@
|
||||
background-color: transparent;
|
||||
color: var(--color-text-muted);
|
||||
padding: 0.5rem;
|
||||
&:hover:not(:disabled) { color: var(--color-text); }
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common';
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './app-button.component.html',
|
||||
styleUrl: './app-button.component.scss'
|
||||
styleUrl: './app-button.component.scss',
|
||||
})
|
||||
export class AppButtonComponent {
|
||||
variant = input<'primary' | 'secondary' | 'outline' | 'text'>('primary');
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: var(--space-6);
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
@@ -4,6 +4,6 @@ import { Component } from '@angular/core';
|
||||
selector: 'app-card',
|
||||
standalone: true,
|
||||
templateUrl: './app-card.component.html',
|
||||
styleUrl: './app-card.component.scss'
|
||||
styleUrl: './app-card.component.scss',
|
||||
})
|
||||
export class AppCardComponent {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div
|
||||
<div
|
||||
class="dropzone"
|
||||
[class.dragover]="isDragOver()"
|
||||
(dragover)="onDragOver($event)"
|
||||
@@ -6,21 +6,44 @@
|
||||
(drop)="onDrop($event)"
|
||||
(click)="fileInput.click()"
|
||||
>
|
||||
<input #fileInput type="file" (change)="onFileSelected($event)" hidden [accept]="accept()" [multiple]="multiple()">
|
||||
|
||||
<input
|
||||
#fileInput
|
||||
type="file"
|
||||
(change)="onFileSelected($event)"
|
||||
hidden
|
||||
[accept]="accept()"
|
||||
[multiple]="multiple()"
|
||||
/>
|
||||
|
||||
<div class="content">
|
||||
<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>
|
||||
<p class="text">{{ label() | translate }}</p>
|
||||
<p class="subtext">{{ subtext() | translate }}</p>
|
||||
|
||||
|
||||
@if (fileNames().length > 0) {
|
||||
<div class="file-badges">
|
||||
@for (name of fileNames(); track name) {
|
||||
<div class="file-badge">{{ name }}</div>
|
||||
}
|
||||
</div>
|
||||
<div class="file-badges">
|
||||
@for (name of fileNames(); track name) {
|
||||
<div class="file-badge">{{ name }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,27 +6,37 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background-color: var(--color-neutral-50);
|
||||
|
||||
&:hover, &.dragover {
|
||||
|
||||
&:hover,
|
||||
&.dragover {
|
||||
border-color: var(--color-brand);
|
||||
background-color: var(--color-neutral-100);
|
||||
}
|
||||
}
|
||||
.icon { color: var(--color-brand); margin-bottom: var(--space-4); }
|
||||
.text { font-weight: 600; margin-bottom: var(--space-2); }
|
||||
.subtext { font-size: 0.875rem; color: var(--color-text-muted); }
|
||||
.icon {
|
||||
color: var(--color-brand);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.text {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.subtext {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.file-badges {
|
||||
margin-top: var(--space-4);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
justify-content: center;
|
||||
margin-top: var(--space-4);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
justify-content: center;
|
||||
}
|
||||
.file-badge {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background: var(--color-neutral-200);
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-700);
|
||||
font-size: 0.85rem;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background: var(--color-neutral-200);
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-700);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule],
|
||||
templateUrl: './app-dropzone.component.html',
|
||||
styleUrl: './app-dropzone.component.scss'
|
||||
styleUrl: './app-dropzone.component.scss',
|
||||
})
|
||||
export class AppDropzoneComponent {
|
||||
label = input<string>('DROPZONE.DEFAULT_LABEL');
|
||||
subtext = input<string>('DROPZONE.DEFAULT_SUBTEXT');
|
||||
accept = input<string>('.stl,.3mf,.step,.stp');
|
||||
multiple = input<boolean>(true);
|
||||
|
||||
|
||||
filesDropped = output<File[]>();
|
||||
|
||||
|
||||
isDragOver = signal(false);
|
||||
fileNames = signal<string[]>([]);
|
||||
|
||||
@@ -51,8 +51,8 @@ export class AppDropzoneComponent {
|
||||
}
|
||||
|
||||
handleFiles(files: File[]) {
|
||||
const newNames = files.map(f => f.name);
|
||||
this.fileNames.update(current => [...current, ...newNames]);
|
||||
const newNames = files.map((f) => f.name);
|
||||
this.fileNames.update((current) => [...current, ...newNames]);
|
||||
this.filesDropped.emit(files);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div class="form-group">
|
||||
@if (label()) {
|
||||
@if (label()) {
|
||||
<label [for]="id()">
|
||||
{{ label() }}
|
||||
@if (required()) { <span class="required-mark">*</span> }
|
||||
</label>
|
||||
@if (required()) {
|
||||
<span class="required-mark">*</span>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<input
|
||||
[id]="id()"
|
||||
@@ -15,5 +17,7 @@
|
||||
[disabled]="disabled"
|
||||
class="form-control"
|
||||
/>
|
||||
@if (error()) { <span class="error-text">{{ error() }}</span> }
|
||||
@if (error()) {
|
||||
<span class="error-text">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
.form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); }
|
||||
label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); }
|
||||
.required-mark { color: var(--color-text); margin-left: 2px; }
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.required-mark {
|
||||
color: var(--color-text);
|
||||
margin-left: 2px;
|
||||
}
|
||||
.form-control {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -9,7 +21,18 @@ label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); co
|
||||
width: 100%;
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text);
|
||||
&:focus { outline: none; border-color: var(--color-brand); box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25); }
|
||||
&:disabled { background: var(--color-neutral-100); cursor: not-allowed; }
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25);
|
||||
}
|
||||
&:disabled {
|
||||
background: var(--color-neutral-100);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.error-text {
|
||||
color: var(--color-danger-500);
|
||||
font-size: 0.75rem;
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); }
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Component, input, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
@@ -10,11 +14,11 @@ import { CommonModule } from '@angular/common';
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => AppInputComponent),
|
||||
multi: true
|
||||
}
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
templateUrl: './app-input.component.html',
|
||||
styleUrl: './app-input.component.scss'
|
||||
styleUrl: './app-input.component.scss',
|
||||
})
|
||||
export class AppInputComponent implements ControlValueAccessor {
|
||||
label = input<string>('');
|
||||
@@ -30,11 +34,19 @@ export class AppInputComponent implements ControlValueAccessor {
|
||||
onChange: any = () => {};
|
||||
onTouched: any = () => {};
|
||||
|
||||
writeValue(obj: any): void { this.value = obj || ''; }
|
||||
registerOnChange(fn: any): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: any): void { this.onTouched = fn; }
|
||||
setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; }
|
||||
|
||||
writeValue(obj: any): void {
|
||||
this.value = obj || '';
|
||||
}
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
onInput(event: Event) {
|
||||
const val = (event.target as HTMLInputElement).value;
|
||||
this.value = val;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<section class="locations-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>{{ 'LOCATIONS.TITLE' | translate }}</h2>
|
||||
<p class="subtitle">{{ 'LOCATIONS.SUBTITLE' | translate }}</p>
|
||||
<h2>{{ "LOCATIONS.TITLE" | translate }}</h2>
|
||||
<p class="subtitle">{{ "LOCATIONS.SUBTITLE" | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="locations-grid">
|
||||
@@ -11,23 +11,24 @@
|
||||
<app-toggle-selector
|
||||
[options]="locationOptions"
|
||||
[selectedValue]="selectedLocation"
|
||||
(selectionChange)="selectLocation($event)">
|
||||
(selectionChange)="selectLocation($event)"
|
||||
>
|
||||
</app-toggle-selector>
|
||||
</div>
|
||||
|
||||
<div class="location-details">
|
||||
<div *ngIf="selectedLocation === 'ticino'" class="details-card">
|
||||
<h3>{{ 'LOCATIONS.BIASCA' | translate }}</h3>
|
||||
<p>{{ 'LOCATIONS.ADDRESS_TICINO' | translate }}</p>
|
||||
<h3>{{ "LOCATIONS.BIASCA" | translate }}</h3>
|
||||
<p>{{ "LOCATIONS.ADDRESS_TICINO" | translate }}</p>
|
||||
</div>
|
||||
<div *ngIf="selectedLocation === 'bienne'" class="details-card">
|
||||
<h3>{{ 'LOCATIONS.BIENNE' | translate }}</h3>
|
||||
<p>{{ 'LOCATIONS.ADDRESS_BIENNE' | translate }}</p>
|
||||
<h3>{{ "LOCATIONS.BIENNE" | translate }}</h3>
|
||||
<p>{{ "LOCATIONS.ADDRESS_BIENNE" | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a routerLink="/contact" class="contact-btn">
|
||||
{{ 'LOCATIONS.CONTACT_US' | translate }}
|
||||
{{ "LOCATIONS.CONTACT_US" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,12 +38,24 @@
|
||||
<iframe
|
||||
*ngIf="selectedLocation === 'ticino'"
|
||||
src="https://www.google.com/maps?q=Via%20G.%20Pioda%2029a%2C%20Biasca&output=embed"
|
||||
width="100%" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade">
|
||||
width="100%"
|
||||
height="450"
|
||||
style="border: 0"
|
||||
allowfullscreen=""
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
>
|
||||
</iframe>
|
||||
<iframe
|
||||
*ngIf="selectedLocation === 'bienne'"
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2729.0438104193587!2d7.240752176735282!3d47.126435979155985!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x478e1eb84efba295%3A0x95924d5ba8b6f3b0!2sLyss-Strasse%2071%2C%202560%20Nidau%2C%20Switzerland!5e0!3m2!1sen!2sch!4v1700000000000!5m2!1sen!2sch"
|
||||
width="100%" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade">
|
||||
width="100%"
|
||||
height="450"
|
||||
style="border: 0"
|
||||
allowfullscreen=""
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
gap: 3rem;
|
||||
align-items: start;
|
||||
|
||||
@media(min-width: 992px) {
|
||||
@media (min-width: 992px) {
|
||||
grid-template-columns: repeat(2, minmax(320px, 420px));
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -44,7 +44,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.location-details {
|
||||
padding: 2rem;
|
||||
background: var(--color-bg);
|
||||
|
||||
@@ -2,14 +2,22 @@ import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { AppToggleSelectorComponent, ToggleOption } from '../app-toggle-selector/app-toggle-selector.component';
|
||||
import {
|
||||
AppToggleSelectorComponent,
|
||||
ToggleOption,
|
||||
} from '../app-toggle-selector/app-toggle-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-locations',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule, RouterLink, AppToggleSelectorComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
RouterLink,
|
||||
AppToggleSelectorComponent,
|
||||
],
|
||||
templateUrl: './app-locations.component.html',
|
||||
styleUrl: './app-locations.component.scss'
|
||||
styleUrl: './app-locations.component.scss',
|
||||
})
|
||||
export class AppLocationsComponent {
|
||||
selectedLocation: 'ticino' | 'bienne' = 'ticino';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<div class="form-group">
|
||||
@if (label()) { <label [for]="id()">{{ label() }}</label> }
|
||||
@if (label()) {
|
||||
<label [for]="id()">{{ label() }}</label>
|
||||
}
|
||||
<select
|
||||
[id]="id()"
|
||||
[ngModel]="value"
|
||||
@@ -12,5 +14,7 @@
|
||||
<option [ngValue]="opt.value">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
@if (error()) { <span class="error-text">{{ error() }}</span> }
|
||||
@if (error()) {
|
||||
<span class="error-text">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
.form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); }
|
||||
label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); }
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.form-control {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -8,6 +17,13 @@ label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); co
|
||||
width: 100%;
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text);
|
||||
&:focus { outline: none; border-color: var(--color-brand); }
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
}
|
||||
.error-text {
|
||||
color: var(--color-danger-500);
|
||||
font-size: 0.75rem;
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); }
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Component, input, output, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
} from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
@@ -10,16 +15,16 @@ import { CommonModule } from '@angular/common';
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => AppSelectComponent),
|
||||
multi: true
|
||||
}
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
templateUrl: './app-select.component.html',
|
||||
styleUrl: './app-select.component.scss'
|
||||
styleUrl: './app-select.component.scss',
|
||||
})
|
||||
export class AppSelectComponent implements ControlValueAccessor {
|
||||
label = input<string>('');
|
||||
id = input<string>('select-' + Math.random().toString(36).substr(2, 9));
|
||||
options = input<{label: string, value: any}[]>([]);
|
||||
options = input<{ label: string; value: any }[]>([]);
|
||||
error = input<string | null>(null);
|
||||
|
||||
value: any = '';
|
||||
@@ -28,13 +33,21 @@ export class AppSelectComponent implements ControlValueAccessor {
|
||||
onChange: any = () => {};
|
||||
onTouched: any = () => {};
|
||||
|
||||
writeValue(obj: any): void { this.value = obj; }
|
||||
registerOnChange(fn: any): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: any): void { this.onTouched = fn; }
|
||||
setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; }
|
||||
|
||||
writeValue(obj: any): void {
|
||||
this.value = obj;
|
||||
}
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
onModelChange(val: any) {
|
||||
this.value = val;
|
||||
this.onChange(val);
|
||||
this.value = val;
|
||||
this.onChange(val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<div class="tabs">
|
||||
@for (tab of tabs(); track tab.value) {
|
||||
<button
|
||||
class="tab"
|
||||
<button
|
||||
class="tab"
|
||||
[class.active]="activeTab() === tab.value"
|
||||
(click)="selectTab(tab.value)">
|
||||
(click)="selectTab(tab.value)"
|
||||
>
|
||||
{{ tab.label | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover { color: var(--color-text); }
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
&.active {
|
||||
color: var(--color-brand);
|
||||
border-bottom-color: var(--color-brand);
|
||||
|
||||
@@ -7,10 +7,10 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule],
|
||||
templateUrl: './app-tabs.component.html',
|
||||
styleUrl: './app-tabs.component.scss'
|
||||
styleUrl: './app-tabs.component.scss',
|
||||
})
|
||||
export class AppTabsComponent {
|
||||
tabs = input<{label: string, value: string}[]>([]);
|
||||
tabs = input<{ label: string; value: string }[]>([]);
|
||||
activeTab = input<string>('');
|
||||
tabChange = output<string>();
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<div class="user-type-selector">
|
||||
@for (option of options(); track option.value) {
|
||||
<div class="type-option"
|
||||
[class.selected]="selectedValue() === option.value"
|
||||
(click)="selectOption(option.value)">
|
||||
<div
|
||||
class="type-option"
|
||||
[class.selected]="selectedValue() === option.value"
|
||||
(click)="selectOption(option.value)"
|
||||
>
|
||||
{{ option.label | translate }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@
|
||||
color: var(--color-text-muted);
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover { color: var(--color-text); }
|
||||
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--color-brand);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ export interface ToggleOption {
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule],
|
||||
templateUrl: './app-toggle-selector.component.html',
|
||||
styleUrl: './app-toggle-selector.component.scss'
|
||||
styleUrl: './app-toggle-selector.component.scss',
|
||||
})
|
||||
export class AppToggleSelectorComponent {
|
||||
options = input.required<ToggleOption[]>();
|
||||
selectedValue = input.required<any>();
|
||||
|
||||
|
||||
selectionChange = output<any>();
|
||||
|
||||
selectOption(value: any) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<div class="color-selector-container">
|
||||
@if (isOpen()) {
|
||||
<div class="backdrop" (click)="close()"></div>
|
||||
<div class="backdrop" (click)="close()"></div>
|
||||
}
|
||||
|
||||
<div
|
||||
class="color-circle trigger"
|
||||
<div
|
||||
class="color-circle trigger"
|
||||
[style.background-color]="getCurrentHex()"
|
||||
[title]="selectedColor()"
|
||||
(click)="toggleOpen()">
|
||||
</div>
|
||||
(click)="toggleOpen()"
|
||||
></div>
|
||||
|
||||
@if (isOpen()) {
|
||||
<div class="color-popup">
|
||||
@@ -17,17 +17,26 @@
|
||||
<div class="category-name">{{ category.name | translate }}</div>
|
||||
<div class="colors-grid">
|
||||
@for (color of category.colors; track color.value) {
|
||||
<div
|
||||
class="color-item"
|
||||
<div
|
||||
class="color-item"
|
||||
(click)="selectColor(color)"
|
||||
[class.disabled]="color.outOfStock">
|
||||
|
||||
<div class="selection-ring"
|
||||
[class.active]="selectedVariantId() ? selectedVariantId() === color.variantId : selectedColor() === color.value"
|
||||
[class.out-of-stock]="color.outOfStock">
|
||||
<div class="color-circle small" [style.background-color]="color.hex"></div>
|
||||
[class.disabled]="color.outOfStock"
|
||||
>
|
||||
<div
|
||||
class="selection-ring"
|
||||
[class.active]="
|
||||
selectedVariantId()
|
||||
? selectedVariantId() === color.variantId
|
||||
: selectedColor() === color.value
|
||||
"
|
||||
[class.out-of-stock]="color.outOfStock"
|
||||
>
|
||||
<div
|
||||
class="color-circle small"
|
||||
[style.background-color]="color.hex"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
|
||||
<span class="color-name">{{ color.label | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.color-circle {
|
||||
@@ -21,12 +21,14 @@
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ddd;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.trigger:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.small {
|
||||
@@ -44,17 +46,17 @@
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px; /* Slightly tighter radius */
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
width: 230px; /* Increased size */
|
||||
max-height: min(62vh, 360px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
|
||||
// Little triangle arrow
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 8px; // Align arrow to left side near trigger
|
||||
@@ -72,7 +74,7 @@
|
||||
width: 280px; /* Provide enough width for touch targets */
|
||||
max-width: 90vw; /* Safety constraint */
|
||||
max-height: min(72vh, 420px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.2); /* Stronger shadow for modal feel */
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); /* Stronger shadow for modal feel */
|
||||
|
||||
/* Hide arrow on mobile since it's detached from trigger */
|
||||
&::before {
|
||||
@@ -89,7 +91,7 @@
|
||||
|
||||
.category {
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -117,15 +119,17 @@
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
||||
|
||||
&:hover .selection-ring {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
&:hover .selection-ring { transform: none; }
|
||||
&:hover .selection-ring {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,29 +139,29 @@
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
padding: 2px; /* Space for ring */
|
||||
|
||||
|
||||
&.active {
|
||||
border-color: var(--color-brand, #facf0a);
|
||||
box-shadow: 0 0 0 1px var(--color-brand, #facf0a);
|
||||
}
|
||||
|
||||
|
||||
&.out-of-stock::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #cc0000;
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #cc0000;
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-size: 0.65rem;
|
||||
color: #444;
|
||||
line-height: 1.1;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.65rem;
|
||||
color: #444;
|
||||
line-height: 1.1;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Component, input, output, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { PRODUCT_COLORS, getColorHex, ColorCategory, ColorOption } from '../../../core/constants/colors.const';
|
||||
import {
|
||||
PRODUCT_COLORS,
|
||||
getColorHex,
|
||||
ColorCategory,
|
||||
ColorOption,
|
||||
} from '../../../core/constants/colors.const';
|
||||
import { VariantOption } from '../../../features/calculator/services/quote-estimator.service';
|
||||
|
||||
@Component({
|
||||
@@ -9,7 +14,7 @@ import { VariantOption } from '../../../features/calculator/services/quote-estim
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule],
|
||||
templateUrl: './color-selector.component.html',
|
||||
styleUrl: './color-selector.component.scss'
|
||||
styleUrl: './color-selector.component.scss',
|
||||
})
|
||||
export class ColorSelectorComponent {
|
||||
selectedColor = input<string>('Black');
|
||||
@@ -20,32 +25,32 @@ export class ColorSelectorComponent {
|
||||
isOpen = signal(false);
|
||||
|
||||
categories = computed(() => {
|
||||
const vars = this.variants();
|
||||
if (vars && vars.length > 0) {
|
||||
const byFinish = new Map<string, ColorOption[]>();
|
||||
vars.forEach(v => {
|
||||
const finish = v.finishType || 'AVAILABLE_COLORS';
|
||||
const bucket = byFinish.get(finish) || [];
|
||||
bucket.push({
|
||||
label: v.colorName,
|
||||
value: v.colorName,
|
||||
hex: v.hexColor,
|
||||
variantId: v.id,
|
||||
outOfStock: v.isOutOfStock
|
||||
});
|
||||
byFinish.set(finish, bucket);
|
||||
});
|
||||
const vars = this.variants();
|
||||
if (vars && vars.length > 0) {
|
||||
const byFinish = new Map<string, ColorOption[]>();
|
||||
vars.forEach((v) => {
|
||||
const finish = v.finishType || 'AVAILABLE_COLORS';
|
||||
const bucket = byFinish.get(finish) || [];
|
||||
bucket.push({
|
||||
label: v.colorName,
|
||||
value: v.colorName,
|
||||
hex: v.hexColor,
|
||||
variantId: v.id,
|
||||
outOfStock: v.isOutOfStock,
|
||||
});
|
||||
byFinish.set(finish, bucket);
|
||||
});
|
||||
|
||||
return Array.from(byFinish.entries()).map(([finish, colors]) => ({
|
||||
name: finish,
|
||||
colors
|
||||
})) as ColorCategory[];
|
||||
}
|
||||
return PRODUCT_COLORS;
|
||||
return Array.from(byFinish.entries()).map(([finish, colors]) => ({
|
||||
name: finish,
|
||||
colors,
|
||||
})) as ColorCategory[];
|
||||
}
|
||||
return PRODUCT_COLORS;
|
||||
});
|
||||
|
||||
toggleOpen() {
|
||||
this.isOpen.update(v => !v);
|
||||
this.isOpen.update((v) => !v);
|
||||
}
|
||||
|
||||
selectColor(color: ColorOption) {
|
||||
@@ -53,24 +58,24 @@ export class ColorSelectorComponent {
|
||||
|
||||
this.colorSelected.emit({
|
||||
colorName: color.value,
|
||||
filamentVariantId: color.variantId
|
||||
filamentVariantId: color.variantId,
|
||||
});
|
||||
this.isOpen.set(false);
|
||||
}
|
||||
|
||||
// Helper to find hex for the current selected value
|
||||
getCurrentHex(): string {
|
||||
// Check in dynamic variants first
|
||||
const vars = this.variants();
|
||||
if (vars && vars.length > 0) {
|
||||
const found = vars.find(v => v.colorName === this.selectedColor());
|
||||
if (found) return found.hexColor;
|
||||
}
|
||||
// Check in dynamic variants first
|
||||
const vars = this.variants();
|
||||
if (vars && vars.length > 0) {
|
||||
const found = vars.find((v) => v.colorName === this.selectedColor());
|
||||
if (found) return found.hexColor;
|
||||
}
|
||||
|
||||
return getColorHex(this.selectedColor());
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen.set(false);
|
||||
this.isOpen.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
@if (loading) {
|
||||
<div class="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
<span>{{ 'STL_VIEWER.LOADING' | translate }}</span>
|
||||
<span>{{ "STL_VIEWER.LOADING" | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (file && !loading) {
|
||||
<div class="dims-overlay">
|
||||
{{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm
|
||||
{{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -28,17 +28,19 @@
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.dims-overlay {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0,0,0,0.6);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
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 { TranslateModule } from '@ngx-translate/core';
|
||||
import * as THREE from 'three';
|
||||
@@ -12,13 +21,14 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule],
|
||||
templateUrl: './stl-viewer.component.html',
|
||||
styleUrl: './stl-viewer.component.scss'
|
||||
styleUrl: './stl-viewer.component.scss',
|
||||
})
|
||||
export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() file: File | null = null;
|
||||
@Input() color: string = '#facf0a'; // Default Brand Color
|
||||
|
||||
@ViewChild('rendererContainer', { static: true }) rendererContainer!: ElementRef;
|
||||
@ViewChild('rendererContainer', { static: true })
|
||||
rendererContainer!: ElementRef;
|
||||
|
||||
private scene!: THREE.Scene;
|
||||
private camera!: THREE.PerspectiveCamera;
|
||||
@@ -38,7 +48,7 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
if (changes['file'] && this.file) {
|
||||
this.loadFile(this.file);
|
||||
}
|
||||
|
||||
|
||||
if (changes['color'] && this.currentMesh && !changes['file']) {
|
||||
this.applyColorStyle(this.color);
|
||||
}
|
||||
@@ -83,7 +93,11 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
this.camera.position.z = 100;
|
||||
|
||||
// Renderer
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
|
||||
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;
|
||||
@@ -106,12 +120,12 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
// Handle resize
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (!this.rendererContainer) return;
|
||||
const w = this.rendererContainer.nativeElement.clientWidth;
|
||||
const h = this.rendererContainer.nativeElement.clientHeight;
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(w, h);
|
||||
if (!this.rendererContainer) return;
|
||||
const w = this.rendererContainer.nativeElement.clientWidth;
|
||||
const h = this.rendererContainer.nativeElement.clientHeight;
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(w, h);
|
||||
});
|
||||
resizeObserver.observe(this.rendererContainer.nativeElement);
|
||||
}
|
||||
@@ -126,7 +140,7 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
try {
|
||||
const loader = new STLLoader();
|
||||
const geometry = loader.parse(event.target?.result as ArrayBuffer);
|
||||
|
||||
|
||||
this.clearCurrentMesh();
|
||||
|
||||
geometry.computeVertexNormals();
|
||||
@@ -136,12 +150,12 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
roughness: 0.42,
|
||||
metalness: 0.05,
|
||||
emissive: 0x000000,
|
||||
emissiveIntensity: 0
|
||||
emissiveIntensity: 0,
|
||||
});
|
||||
|
||||
|
||||
this.currentMesh = new THREE.Mesh(geometry, material);
|
||||
this.applyColorStyle(this.color);
|
||||
|
||||
|
||||
// Center geometry
|
||||
geometry.computeBoundingBox();
|
||||
geometry.center();
|
||||
@@ -150,11 +164,11 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
const boundingBox = geometry.boundingBox!;
|
||||
const size = new THREE.Vector3();
|
||||
boundingBox.getSize(size);
|
||||
|
||||
|
||||
this.dimensions = {
|
||||
x: Math.round(size.x * 10) / 10,
|
||||
y: Math.round(size.y * 10) / 10,
|
||||
z: Math.round(size.z * 10) / 10
|
||||
x: Math.round(size.x * 10) / 10,
|
||||
y: Math.round(size.y * 10) / 10,
|
||||
z: Math.round(size.z * 10) / 10,
|
||||
};
|
||||
|
||||
// Rotate to stand upright (usually necessary for STLs)
|
||||
@@ -165,16 +179,15 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
// Adjust camera to fit object
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const fov = this.camera.fov * (Math.PI / 180);
|
||||
|
||||
|
||||
// Calculate distance towards camera (z-axis)
|
||||
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
||||
cameraZ *= 1.72;
|
||||
|
||||
|
||||
this.camera.position.set(cameraZ * 0.65, cameraZ * 0.95, cameraZ * 1.1);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.controls.update();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading STL:', err);
|
||||
} finally {
|
||||
@@ -186,14 +199,14 @@ export class StlViewerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
private 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.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
<div class="success-state">
|
||||
<div class="success-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
@switch (context()) {
|
||||
@case ('contact') {
|
||||
<h3>{{ 'CONTACT.SUCCESS_TITLE' | translate }}</h3>
|
||||
<p>{{ 'CONTACT.SUCCESS_DESC' | translate }}</p>
|
||||
<app-button (click)="action.emit()">{{ 'CONTACT.SEND_ANOTHER' | translate }}</app-button>
|
||||
@case ("contact") {
|
||||
<h3>{{ "CONTACT.SUCCESS_TITLE" | translate }}</h3>
|
||||
<p>{{ "CONTACT.SUCCESS_DESC" | translate }}</p>
|
||||
<app-button (click)="action.emit()">{{
|
||||
"CONTACT.SEND_ANOTHER" | translate
|
||||
}}</app-button>
|
||||
}
|
||||
@case ('calc') {
|
||||
<h3>{{ 'CALC.ORDER_SUCCESS_TITLE' | translate }}</h3>
|
||||
<p>{{ 'CALC.ORDER_SUCCESS_DESC' | translate }}</p>
|
||||
<app-button (click)="action.emit()">{{ 'CALC.NEW_QUOTE' | translate }}</app-button>
|
||||
@case ("calc") {
|
||||
<h3>{{ "CALC.ORDER_SUCCESS_TITLE" | translate }}</h3>
|
||||
<p>{{ "CALC.ORDER_SUCCESS_DESC" | translate }}</p>
|
||||
<app-button (click)="action.emit()">{{
|
||||
"CALC.NEW_QUOTE" | translate
|
||||
}}</app-button>
|
||||
}
|
||||
@case ('shop') {
|
||||
<h3>{{ 'SHOP.SUCCESS_TITLE' | translate }}</h3>
|
||||
<p>{{ 'SHOP.SUCCESS_DESC' | translate }}</p>
|
||||
<app-button (click)="action.emit()">{{ 'SHOP.CONTINUE' | translate }}</app-button>
|
||||
@case ("shop") {
|
||||
<h3>{{ "SHOP.SUCCESS_TITLE" | translate }}</h3>
|
||||
<p>{{ "SHOP.SUCCESS_DESC" | translate }}</p>
|
||||
<app-button (click)="action.emit()">{{
|
||||
"SHOP.CONTINUE" | translate
|
||||
}}</app-button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
height: 64px;
|
||||
color: var(--color-success, #10b981);
|
||||
margin-bottom: var(--space-2);
|
||||
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -10,7 +10,7 @@ export type SuccessContext = 'contact' | 'calc' | 'shop';
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule, AppButtonComponent],
|
||||
templateUrl: './success-state.component.html',
|
||||
styleUrl: './success-state.component.scss'
|
||||
styleUrl: './success-state.component.scss',
|
||||
})
|
||||
export class SuccessStateComponent {
|
||||
context = input.required<SuccessContext>();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common';
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './summary-card.component.html',
|
||||
styleUrl: './summary-card.component.scss'
|
||||
styleUrl: './summary-card.component.scss',
|
||||
})
|
||||
export class SummaryCardComponent {
|
||||
label = input.required<string>();
|
||||
|
||||
Reference in New Issue
Block a user