From 5bc698815cb82c6918d25e3ef5f78c136d882126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Thu, 5 Feb 2026 17:12:18 +0100 Subject: [PATCH 01/17] fix(back-end): update profile inheritance --- .../quote-result/quote-result.component.ts | 166 ++++++++++++++- .../services/quote-estimator.service.ts | 192 +++++++----------- 2 files changed, 230 insertions(+), 128 deletions(-) diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index 1a2e9da..134a7f7 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -1,36 +1,73 @@ -import { Component, input, output } from '@angular/core'; +import { Component, input, output, signal, computed, effect } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component'; import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component'; import { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.component'; -import { QuoteResult } from '../../services/quote-estimator.service'; +import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; @Component({ selector: 'app-quote-result', standalone: true, - imports: [CommonModule, TranslateModule, AppCardComponent, AppButtonComponent, SummaryCardComponent], + imports: [CommonModule, FormsModule, TranslateModule, AppCardComponent, AppButtonComponent, SummaryCardComponent], template: `

{{ 'CALC.RESULT' | translate }}

+ +
+ @for (item of items(); track item.fileName; let i = $index) { +
+
+ {{ item.fileName }} + + {{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g + +
+ +
+
+ + +
+
+ {{ (item.unitPrice * item.quantity) | currency:result().currency }} +
+
+
+ } +
+ +
+ +
- {{ result().price | currency:result().currency }} + {{ totals().price | currency:result().currency }} - {{ result().printTimeHours }}h {{ result().printTimeMinutes }}m + {{ totals().hours }}h {{ totals().minutes }}m - {{ result().materialUsageGrams }}g + {{ totals().weight }}g
+ +
+ * Include {{ result().setupCost | currency:result().currency }} Setup Cost +
{{ 'CALC.ORDER' | translate }} @@ -40,18 +77,133 @@ import { QuoteResult } from '../../services/quote-estimator.service'; `, styles: [` .title { margin-bottom: var(--space-6); text-align: center; } + + .divider { + height: 1px; + background: var(--color-border); + margin: var(--space-4) 0; + } + + .items-list { + display: flex; + flex-direction: column; + gap: var(--space-3); + margin-bottom: var(--space-4); + } + + .item-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-3); + background: var(--color-neutral-50); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + } + + .item-info { + display: flex; + flex-direction: column; + } + + .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); } + .file-details { font-size: 0.8rem; color: var(--color-text-muted); } + + .item-controls { + display: flex; + align-items: center; + gap: var(--space-4); + } + + .qty-control { + display: flex; + align-items: center; + gap: var(--space-2); + + label { font-size: 0.8rem; color: var(--color-text-muted); } + } + + .qty-input { + width: 60px; + padding: 4px 8px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + text-align: center; + &:focus { outline: none; border-color: var(--color-brand); } + } + + .item-price { + font-weight: 600; + min-width: 60px; + text-align: right; + } + .result-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); - margin-bottom: var(--space-6); + margin-bottom: var(--space-2); } .full-width { grid-column: span 2; } + .setup-note { + text-align: center; + margin-bottom: var(--space-6); + color: var(--color-text-muted); + font-size: 0.8rem; + } + .actions { display: flex; flex-direction: column; gap: var(--space-3); } `] }) export class QuoteResultComponent { result = input.required(); consult = output(); + + // Local mutable state for items to handle quantity changes + items = signal([]); + + constructor() { + effect(() => { + // Initialize local items when result inputs change + // We map to new objects to avoid mutating the input directly if it was a reference + this.items.set(this.result().items.map(i => ({...i}))); + }, { allowSignalWrites: true }); + } + + updateQuantity(index: number, newQty: number | string) { + const qty = typeof newQty === 'string' ? parseInt(newQty, 10) : newQty; + if (qty < 1 || isNaN(qty)) return; + + this.items.update(current => { + const updated = [...current]; + updated[index] = { ...updated[index], quantity: qty }; + return updated; + }); + } + + totals = computed(() => { + const currentItems = this.items(); + const setup = this.result().setupCost; + + let price = setup; + let time = 0; + let weight = 0; + + currentItems.forEach(i => { + price += i.unitPrice * i.quantity; + time += i.unitTime * i.quantity; + weight += i.unitWeight * i.quantity; + }); + + const hours = Math.floor(time / 3600); + const minutes = Math.ceil((time % 3600) / 60); + + return { + price: Math.round(price * 100) / 100, + hours, + minutes, + weight: Math.ceil(weight) + }; + }); } diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index 64d434c..0d17653 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -5,10 +5,9 @@ import { map, catchError } from 'rxjs/operators'; import { environment } from '../../../../environments/environment'; export interface QuoteRequest { - files: File[]; + items: { file: File, quantity: number }[]; material: string; quality: string; - quantity: number; notes?: string; color?: string; infillDensity?: number; @@ -17,13 +16,24 @@ export interface QuoteRequest { mode: 'easy' | 'advanced'; } +export interface QuoteItem { + fileName: string; + unitPrice: number; + unitTime: number; // seconds + unitWeight: number; // grams + quantity: number; + // Computed values for UI convenience (optional, can be done in component) +} + export interface QuoteResult { - price: number; - currency: string; - printTimeHours: number; - printTimeMinutes: number; - materialUsageGrams: number; + items: QuoteItem[]; setupCost: number; + currency: string; + // The following are aggregations that can be re-calculated + totalPrice: number; + totalTimeHours: number; + totalTimeMinutes: number; + totalWeight: number; } interface BackendResponse { @@ -45,80 +55,17 @@ export class QuoteEstimatorService { private http = inject(HttpClient); calculate(request: QuoteRequest): Observable { - const formData = new FormData(); - // Assuming single file primarily for now, or aggregating. - // The current UI seems to select one "active" file or handle multiple. - // The logic below was mapping multiple files to multiple requests. - // To support progress seamlessly for the "main" action, let's focus on the processing flow. - // If multiple files, we might need a more complex progress tracking or just track the first/total. - // Given the UI shows one big "Analyse" button, let's treat it as a batch or single. - - // NOTE: The previous logic did `request.files.map(...)`. - // If we want a global progress, we can mistakenly complexity it. - // Let's assume we upload all files in one request if the API supported it, but the API seems to be 1 file per request from previous code? - // "formData.append('file', file)" inside the map implies multiple requests. - // To keep it simple and working with the progress bar which is global: - // We will emit progress for the *current* file being processed or average them. - // OR simpler: The user typically uploads one file for a quote? - // The UI `files: File[]` allows multiple. - // Let's stick to the previous logic but wrap it to emit progress. - // However, forkJoin waits for all. We can't easily get specialized progress for "overall upload" with forkJoin of distinct requests easily without merging. - - // Refined approach: - // We will process files IN PARALLEL (forkJoin) but we can't easily track aggregated upload progress of multiple requests in a single simple number without extra code. - // BUT, the user wants "la barra di upload". - // If we assume standard use case is 1 file, it's easy. - // If multiple, we can emit progress as "average of all uploads" or just "uploading...". - // Let's modify the signature to return `Observable<{ type: 'progress' | 'result', value: any }>` or similar? - // The plan said `Observable` originally, now we need progress. - // Let's change return type to `Observable` or a specific union. - - // Let's handle just the first file for progress visualization simplicity if multiple are present, - // or better, create a wrapper that merges the progress. - - // Actually, looking at the previous code: `const requests = request.files.map(...)`. - // If we have 3 files, we have 3 requests. - // We can emit progress events. - - // START implementation for generalized progress: - - const file = request.files[0]; // Primary target for now to ensure we have a progress to show. - // Ideally we should upload all. - - // For this task, to satisfy "bar disappears after upload", we really need to know when upload finishes. - - // Let's keep it robust: - // If multiple files, we likely want to just process them. - // Let's stick to the previous logic but capture progress events for at least one or all. - - if (request.files.length === 0) return of(); - - // We will change the architecture slightly: - // We will execute requests and for EACH, we track progress. - // But we only have one boolean 'loading' and one 'progress' bar in UI. - // Let's average the progress? - - // Simplification: The user probably uploads one file to check quote. - // Let's implement support for the first file's progress to drive the UI bar, handling the rest in background/parallel. - - // Re-implementing the single file logic from the map, but enabled for progress. + if (request.items.length === 0) return of(); return new Observable(observer => { - let completed = 0; - let total = request.files.length; - const results: BackendResponse[] = []; - let grandTotal = 0; // For progress calculation if we wanted to average - - // We'll just track the "upload phase" of the bundle. - // Actually, let's just use `concat` or `merge`? - // Let's simplify: We will only track progress for the first file or "active" file. - // But the previous code sent ALL files. - - // Let's change the return type to emit events. - - const uploads = request.files.map(file => { + const totalItems = request.items.length; + const allProgress: number[] = new Array(totalItems).fill(0); + const finalResponses: any[] = []; + let completedRequests = 0; + + const uploads = request.items.map((item, index) => { const formData = new FormData(); - formData.append('file', file); + formData.append('file', item.file); formData.append('machine', 'bambu_a1'); formData.append('filament', this.mapMaterial(request.material)); formData.append('quality', this.mapQuality(request.quality)); @@ -138,27 +85,19 @@ export class QuoteEstimatorService { reportProgress: true, observe: 'events' }).pipe( - map(event => ({ file, event })), - catchError(err => of({ file, error: err })) + map(event => ({ item, event, index })), + catchError(err => of({ item, error: err, index })) ); }); - // We process all uploads. - // We want to emit: - // 1. Progress updates (average of all files?) - // 2. Final QuoteResult - - const allProgress: number[] = new Array(request.files.length).fill(0); - let completedRequests = 0; - const finalResponses: any[] = []; - // Subscribe to all - uploads.forEach((obs, index) => { + uploads.forEach((obs) => { obs.subscribe({ next: (wrapper: any) => { + const idx = wrapper.index; + if (wrapper.error) { - // handled in final calculation - finalResponses[index] = { success: false, data: { cost: { total:0 }, print_time_seconds:0, material_grams:0 } }; + finalResponses[idx] = { success: false, fileName: wrapper.item.file.name }; return; } @@ -166,64 +105,75 @@ export class QuoteEstimatorService { if (event.type === 1) { // HttpEventType.UploadProgress if (event.total) { const percent = Math.round((100 * event.loaded) / event.total); - allProgress[index] = percent; + allProgress[idx] = percent; // Emit average progress - const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / total); - observer.next(avg); // Emit number for progress + const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / totalItems); + observer.next(avg); } } else if (event.type === 4) { // HttpEventType.Response - allProgress[index] = 100; - finalResponses[index] = event.body; + allProgress[idx] = 100; + finalResponses[idx] = { ...event.body, fileName: wrapper.item.file.name, originalQty: wrapper.item.quantity }; completedRequests++; - if (completedRequests === total) { + if (completedRequests === totalItems) { // All done - observer.next(100); // Ensure complete + observer.next(100); - // Calculate Totals - const valid = finalResponses.filter(r => r && r.success); - if (valid.length === 0 && finalResponses.length > 0) { + // Calculate Results + const setupCost = 10; + + const items: QuoteItem[] = []; + + finalResponses.forEach(res => { + if (res && res.success) { + items.push({ + fileName: res.fileName, + unitPrice: res.data.cost.total, + unitTime: res.data.print_time_seconds, + unitWeight: res.data.material_grams, + quantity: res.originalQty // Use the requested quantity + }); + } + }); + + if (items.length === 0) { observer.error('All calculations failed.'); return; } - let totalPrice = 0; + // Initial Aggregation + let grandTotal = setupCost; let totalTime = 0; let totalWeight = 0; - let setupCost = 10; - - valid.forEach(res => { - totalPrice += res.data.cost.total; - totalTime += res.data.print_time_seconds; - totalWeight += res.data.material_grams; + + items.forEach(item => { + grandTotal += item.unitPrice * item.quantity; + totalTime += item.unitTime * item.quantity; + totalWeight += item.unitWeight * item.quantity; }); - totalPrice = (totalPrice * request.quantity) + setupCost; - totalWeight = totalWeight * request.quantity; - totalTime = totalTime * request.quantity; - const totalHours = Math.floor(totalTime / 3600); const totalMinutes = Math.ceil((totalTime % 3600) / 60); const result: QuoteResult = { - price: Math.round(totalPrice * 100) / 100, + items, + setupCost, currency: 'CHF', - printTimeHours: totalHours, - printTimeMinutes: totalMinutes, - materialUsageGrams: Math.ceil(totalWeight), - setupCost + totalPrice: Math.round(grandTotal * 100) / 100, + totalTimeHours: totalHours, + totalTimeMinutes: totalMinutes, + totalWeight: Math.ceil(totalWeight) }; - observer.next(result); // Emit final object + observer.next(result); observer.complete(); } } }, error: (err) => { console.error('Error in request', err); - finalResponses[index] = { success: false }; completedRequests++; - if (completedRequests === total) { + if (completedRequests === totalItems) { observer.error('Requests failed'); } } -- 2.49.1 From cecdfacd33e2a651008823a7ac503b3a4b69de87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Thu, 5 Feb 2026 17:13:56 +0100 Subject: [PATCH 02/17] feat(front-end): multiple file upload --- .../upload-form/upload-form.component.ts | 212 +++++++++++++----- 1 file changed, 154 insertions(+), 58 deletions(-) diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index 1a92133..99eb7b5 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -1,4 +1,4 @@ -import { Component, input, output, signal } from '@angular/core'; +import { Component, input, output, signal, effect } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; @@ -9,6 +9,11 @@ import { AppButtonComponent } from '../../../../shared/components/app-button/app import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component'; import { QuoteRequest } from '../../services/quote-estimator.service'; +interface FormItem { + file: File; + quantity: number; +} + @Component({ selector: 'app-upload-form', standalone: true, @@ -20,28 +25,53 @@ import { QuoteRequest } from '../../services/quote-estimator.service'; @if (selectedFile()) {
-
-
- @for (f of files(); track f.name) { -
- {{ f.name }} -
- } -
- } @else { - + + - + + + + @if (items().length > 0) { +
+ @for (item of items(); track item.file.name; let i = $index) { +
+
+ {{ item.file.name }} + {{ (item.file.size / 1024 / 1024) | number:'1.1-2' }} MB +
+ +
+
+ + +
+ + +
+
+ } +
} - @if (form.get('files')?.invalid && form.get('files')?.touched) { + @if (items().length === 0 && form.get('itemsTouched')?.value) {
{{ 'CALC.ERR_FILE_REQUIRED' | translate }}
}
@@ -59,12 +89,8 @@ import { QuoteRequest } from '../../services/quote-estimator.service'; [options]="qualities" > - - + + @if (mode() === 'advanced') {
@@ -113,7 +139,7 @@ import { QuoteRequest } from '../../services/quote-estimator.service'; {{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }} @@ -127,43 +153,95 @@ import { QuoteRequest } from '../../services/quote-estimator.service'; .error-msg { color: var(--color-danger-500); font-size: 0.875rem; margin-top: var(--space-2); text-align: center; } .viewer-wrapper { position: relative; margin-bottom: var(--space-4); } - .btn-clear { + .btn-clear-viewer { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.5); color: white; border: none; - width: 32px; - height: 32px; - border-radius: 50%; + padding: 4px 8px; + border-radius: 4px; cursor: pointer; z-index: 10; &:hover { background: rgba(0,0,0,0.7); } } - .file-list { + .items-list { display: flex; + flex-direction: column; gap: var(--space-2); - overflow-x: auto; - padding-bottom: var(--space-2); + margin-top: var(--space-4); } - .file-item { - padding: 0.5rem 1rem; + + .file-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-3); background: var(--color-neutral-100); border: 1px solid var(--color-border); border-radius: var(--radius-md); - font-size: 0.85rem; - cursor: pointer; - white-space: nowrap; - &:hover { background: var(--color-neutral-200); } - &.active { - border-color: var(--color-brand); - background: rgba(250, 207, 10, 0.1); - font-weight: 600; + transition: all 0.2s; + + &.active { + border-color: var(--color-brand); + background: rgba(250, 207, 10, 0.05); } } + .file-info { + flex: 1; + display: flex; + flex-direction: column; + cursor: pointer; + } + .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); } + .file-size { font-size: 0.75rem; color: var(--color-text-muted); } + + .file-actions { + display: flex; + align-items: center; + gap: var(--space-3); + } + + .qty-control { + display: flex; + align-items: center; + gap: var(--space-2); + label { font-size: 0.8rem; color: var(--color-text-muted); } + } + + .qty-input { + width: 50px; + padding: 4px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + text-align: center; + font-size: 0.9rem; + &:focus { outline: none; border-color: var(--color-brand); } + } + + .btn-remove { + width: 28px; + height: 28px; + border-radius: 50%; + border: none; + background: transparent; // var(--color-neutral-200); + color: var(--color-text-muted); // var(--color-danger-500); + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + + &:hover { + background: var(--color-danger-100); + color: var(--color-danger-500); + } + } + .checkbox-row { display: flex; align-items: center; @@ -185,9 +263,6 @@ import { QuoteRequest } from '../../services/quote-estimator.service'; /* Progress Bar */ .progress-container { margin-bottom: var(--space-3); - /* padding: var(--space-2); */ - /* background: var(--color-neutral-100); */ - /* border-radius: var(--radius-md); */ text-align: center; width: 100%; } @@ -206,7 +281,6 @@ import { QuoteRequest } from '../../services/quote-estimator.service'; width: 0%; transition: width 0.2s ease-out; } - .progress-text { font-size: 0.875rem; color: var(--color-text-muted); } `] }) export class UploadFormComponent { @@ -217,7 +291,7 @@ export class UploadFormComponent { form: FormGroup; - files = signal([]); + items = signal([]); selectedFile = signal(null); materials = [ @@ -252,10 +326,9 @@ export class UploadFormComponent { constructor(private fb: FormBuilder) { this.form = this.fb.group({ - files: [[], Validators.required], + itemsTouched: [false], // Hack to track touched state for custom items list material: ['PLA', Validators.required], quality: ['Standard', Validators.required], - quantity: [1, [Validators.required, Validators.min(1)]], notes: [''], // Advanced fields color: ['Black'], @@ -267,14 +340,14 @@ export class UploadFormComponent { onFilesDropped(newFiles: File[]) { const MAX_SIZE = 200 * 1024 * 1024; // 200MB - const validFiles: File[] = []; + const validItems: FormItem[] = []; let hasError = false; for (const file of newFiles) { if (file.size > MAX_SIZE) { hasError = true; } else { - validFiles.push(file); + validItems.push({ file, quantity: 1 }); } } @@ -282,32 +355,55 @@ export class UploadFormComponent { alert("Alcuni file superano il limite di 200MB e non sono stati aggiunti."); } - if (validFiles.length > 0) { - this.files.update(current => [...current, ...validFiles]); - this.form.patchValue({ files: this.files() }); - this.form.get('files')?.markAsTouched(); - this.selectedFile.set(validFiles[validFiles.length - 1]); + if (validItems.length > 0) { + this.items.update(current => [...current, ...validItems]); + this.form.get('itemsTouched')?.setValue(true); + // Auto select last added + this.selectedFile.set(validItems[validItems.length - 1].file); } } selectFile(file: File) { - this.selectedFile.set(file); + if (this.selectedFile() === file) { + // toggle off? no, keep active + } else { + this.selectedFile.set(file); + } } - clearFiles() { - this.files.set([]); - this.selectedFile.set(null); - this.form.patchValue({ files: [] }); + updateItemQuantity(index: number, event: Event) { + const input = event.target as HTMLInputElement; + let val = parseInt(input.value, 10); + if (isNaN(val) || val < 1) val = 1; + + this.items.update(current => { + const updated = [...current]; + updated[index] = { ...updated[index], quantity: val }; + return updated; + }); + } + + removeItem(index: number) { + this.items.update(current => { + const updated = [...current]; + const removed = updated.splice(index, 1)[0]; + if (this.selectedFile() === removed.file) { + this.selectedFile.set(null); + } + return updated; + }); } onSubmit() { - if (this.form.valid) { + if (this.form.valid && this.items().length > 0) { this.submitRequest.emit({ + items: this.items(), // Pass the items array ...this.form.value, mode: this.mode() }); } else { this.form.markAllAsTouched(); + this.form.get('itemsTouched')?.setValue(true); } } } -- 2.49.1 From fcf439e36969eba54660c4d230f2acb4f776fcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Thu, 5 Feb 2026 17:14:26 +0100 Subject: [PATCH 03/17] feat(front-end): multiple file upload --- .../app/features/calculator/calculator-page.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index 5b76d7d..afc5aea 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -214,7 +214,11 @@ export class CalculatorPageComponent { let details = `Richiesta Preventivo:\n`; details += `- Materiale: ${req.material}\n`; details += `- Qualità: ${req.quality}\n`; - details += `- Quantità: ${req.quantity}\n`; + + details += `- File:\n`; + req.items.forEach(item => { + details += ` * ${item.file.name} (Qtà: ${item.quantity})\n`; + }); if (req.mode === 'advanced') { if (req.color) details += `- Colore: ${req.color}\n`; @@ -224,7 +228,7 @@ export class CalculatorPageComponent { if (req.notes) details += `\nNote: ${req.notes}`; this.estimator.setPendingConsultation({ - files: req.files, + files: req.items.map(i => i.file), message: details }); -- 2.49.1 From 99ae6db064d2d9ba320c40e78025047e463eb6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Thu, 5 Feb 2026 17:21:52 +0100 Subject: [PATCH 04/17] feat(front-end): multiple file upload --- .../calculator/calculator-page.component.ts | 11 +++- .../quote-result/quote-result.component.ts | 58 ++++++++++--------- .../upload-form/upload-form.component.ts | 47 ++++++++++++--- 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index afc5aea..c6d469c 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -1,4 +1,4 @@ -import { Component, signal } from '@angular/core'; +import { Component, signal, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; @@ -37,6 +37,7 @@ import { Router } from '@angular/router';
} @else if (result()) { - + } @else {

{{ 'CALC.BENEFITS_TITLE' | translate }}

@@ -177,6 +182,8 @@ export class CalculatorPageComponent { uploadProgress = signal(0); result = signal(null); error = signal(false); + + @ViewChild('uploadForm') uploadForm!: UploadFormComponent; constructor(private estimator: QuoteEstimatorService, private router: Router) {} diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index 134a7f7..cf325c4 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -15,7 +15,32 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';

{{ 'CALC.RESULT' | translate }}

- + +
+ + {{ totals().price | currency:result().currency }} + + + + {{ totals().hours }}h {{ totals().minutes }}m + + + + {{ totals().weight }}g + +
+ +
+ * Include {{ result().setupCost | currency:result().currency }} Setup Cost +
+ +
+ +
@for (item of items(); track item.fileName; let i = $index) {
@@ -44,31 +69,6 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; }
-
- - -
- - {{ totals().price | currency:result().currency }} - - - - {{ totals().hours }}h {{ totals().minutes }}m - - - - {{ totals().weight }}g - -
- -
- * Include {{ result().setupCost | currency:result().currency }} Setup Cost -
-
{{ 'CALC.ORDER' | translate }} {{ 'CALC.CONSULT' | translate }} @@ -159,6 +159,7 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; export class QuoteResultComponent { result = input.required(); consult = output(); + itemChange = output<{fileName: string, quantity: number}>(); // Local mutable state for items to handle quantity changes items = signal([]); @@ -180,6 +181,11 @@ export class QuoteResultComponent { updated[index] = { ...updated[index], quantity: qty }; return updated; }); + + this.itemChange.emit({ + fileName: this.items()[index].fileName, + quantity: qty + }); } totals = computed(() => { diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index 99eb7b5..f3319f5 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -31,15 +31,16 @@ interface FormItem {
} - - - - + + @if (items().length === 0) { + + + } @if (items().length > 0) { @@ -69,6 +70,14 @@ interface FormItem {
} + + +
+ + + + {{ 'CALC.ADD_FILES' | translate }} + +
} @if (items().length === 0 && form.get('itemsTouched')?.value) { @@ -363,6 +372,26 @@ export class UploadFormComponent { } } + onAdditionalFilesSelected(event: Event) { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + this.onFilesDropped(Array.from(input.files)); + // Reset input so same files can be selected again if needed + input.value = ''; + } + } + + updateItemQuantityByName(fileName: string, quantity: number) { + this.items.update(current => { + return current.map(item => { + if (item.file.name === fileName) { + return { ...item, quantity }; + } + return item; + }); + }); + } + selectFile(file: File) { if (this.selectedFile() === file) { // toggle off? no, keep active -- 2.49.1 From cb7b44073c03f93288bc4d415a2c15030a2f2bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 6 Feb 2026 11:09:46 +0100 Subject: [PATCH 05/17] feat(front-end): responsive layout --- .../calculator/calculator-page.component.ts | 19 +- .../quote-result/quote-result.component.ts | 13 +- .../upload-form/upload-form.component.ts | 182 +++++++++--------- 3 files changed, 121 insertions(+), 93 deletions(-) diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index c6d469c..dc33a4b 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -1,4 +1,4 @@ -import { Component, signal, ViewChild } from '@angular/core'; +import { Component, signal, ViewChild, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; @@ -47,7 +47,7 @@ import { Router } from '@angular/router'; -
+
@if (error()) { Si è verificato un errore durante il calcolo del preventivo. } @@ -86,9 +86,10 @@ import { Router } from '@angular/router'; .content-grid { display: grid; grid-template-columns: 1fr; - gap: var(--space-8); + gap: var(--space-6); @media(min-width: 768px) { grid-template-columns: 1.5fr 1fr; + gap: var(--space-8); } } @@ -98,6 +99,10 @@ import { Router } from '@angular/router'; align-self: center; } } + + .col-input, .col-result { + min-width: 0; /* Prevent grid blowout */ + } /* Mode Selector (Segmented Control style) */ .mode-selector { @@ -184,6 +189,7 @@ export class CalculatorPageComponent { error = signal(false); @ViewChild('uploadForm') uploadForm!: UploadFormComponent; + @ViewChild('resultCol') resultCol!: ElementRef; constructor(private estimator: QuoteEstimatorService, private router: Router) {} @@ -194,6 +200,13 @@ export class CalculatorPageComponent { this.error.set(false); this.result.set(null); + // Auto-scroll on mobile to make analysis visible + setTimeout(() => { + if (this.resultCol && window.innerWidth < 768) { + this.resultCol.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, 100); + this.estimator.calculate(req).subscribe({ next: (event) => { if (typeof event === 'number') { diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index cf325c4..7d41ec0 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -104,9 +104,11 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; .item-info { display: flex; flex-direction: column; + min-width: 0; + flex: 1; /* Ensure it takes available space */ } - .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); } + .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .file-details { font-size: 0.8rem; color: var(--color-text-muted); } .item-controls { @@ -140,9 +142,14 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; .result-grid { display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-4); + grid-template-columns: 1fr; + gap: var(--space-3); margin-bottom: var(--space-2); + + @media(min-width: 500px) { + grid-template-columns: 1fr 1fr; + gap: var(--space-4); + } } .full-width { grid-column: span 2; } diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index f3319f5..399d1c7 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -25,9 +25,7 @@ interface FormItem { @if (selectedFile()) {
- +
} @@ -44,26 +42,26 @@ interface FormItem { @if (items().length > 0) { -
+
@for (item of items(); track item.file.name; let i = $index) { -
-
- {{ item.file.name }} - {{ (item.file.size / 1024 / 1024) | number:'1.1-2' }} MB +
+
+ {{ item.file.name }}
-
-
+
+
+ class="qty-input" + (click)="$event.stopPropagation()">
-
@@ -74,9 +72,10 @@ interface FormItem {
- + +
} @@ -102,38 +101,7 @@ interface FormItem { @if (mode() === 'advanced') { -
- - - -
- -
- - -
- - -
-
- - + }
@@ -157,100 +125,140 @@ interface FormItem { `, styles: [` .section { margin-bottom: var(--space-6); } - .grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } + .grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-4); + + @media(min-width: 640px) { + grid-template-columns: 1fr 1fr; + } + } .actions { margin-top: var(--space-6); } .error-msg { color: var(--color-danger-500); font-size: 0.875rem; margin-top: var(--space-2); text-align: center; } .viewer-wrapper { position: relative; margin-bottom: var(--space-4); } - .btn-clear-viewer { - position: absolute; - top: 10px; - right: 10px; - background: rgba(0,0,0,0.5); - color: white; - border: none; - padding: 4px 8px; - border-radius: 4px; - cursor: pointer; - z-index: 10; - &:hover { background: rgba(0,0,0,0.7); } - } - .items-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + /* Grid Layout for Files */ + .items-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-3); margin-top: var(--space-4); + margin-bottom: var(--space-4); + + @media(min-width: 640px) { + grid-template-columns: 1fr 1fr; + } } - .file-row { - display: flex; - justify-content: space-between; - align-items: center; + .file-card { padding: var(--space-3); background: var(--color-neutral-100); border: 1px solid var(--color-border); border-radius: var(--radius-md); transition: all 0.2s; + cursor: pointer; + display: flex; + flex-direction: column; + gap: var(--space-2); + &:hover { border-color: var(--color-neutral-300); } &.active { border-color: var(--color-brand); background: rgba(250, 207, 10, 0.05); + box-shadow: 0 0 0 1px var(--color-brand); } } - .file-info { - flex: 1; - display: flex; - flex-direction: column; - cursor: pointer; + .card-header { + overflow: hidden; } - .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); } - .file-size { font-size: 0.75rem; color: var(--color-text-muted); } - .file-actions { + .file-name { + font-weight: 500; + font-size: 0.85rem; + color: var(--color-text); + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .card-body { display: flex; + justify-content: space-between; align-items: center; - gap: var(--space-3); } - .qty-control { + .qty-group { display: flex; align-items: center; gap: var(--space-2); - label { font-size: 0.8rem; color: var(--color-text-muted); } + label { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } } .qty-input { - width: 50px; - padding: 4px; + width: 40px; + padding: 2px 4px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); text-align: center; font-size: 0.9rem; + background: white; &:focus { outline: none; border-color: var(--color-brand); } } .btn-remove { - width: 28px; - height: 28px; - border-radius: 50%; - border: none; - background: transparent; // var(--color-neutral-200); - color: var(--color-text-muted); // var(--color-danger-500); + width: 24px; + height: 24px; + border-radius: 4px; + border: 1px solid transparent; // var(--color-border); + background: transparent; // white; + color: var(--color-text-muted); font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; + font-size: 0.9rem; &:hover { background: var(--color-danger-100); color: var(--color-danger-500); + border-color: var(--color-danger-200); } } + /* Prominent Add Button */ + .add-more-container { + margin-top: var(--space-2); + } + + .btn-add-more { + width: 100%; + padding: var(--space-3); + background: var(--color-neutral-800); + color: white; + border: none; + border-radius: var(--radius-md); + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + + &:hover { + background: var(--color-neutral-900); + transform: translateY(-1px); + } + &:active { transform: translateY(0); } + } + .checkbox-row { display: flex; align-items: center; -- 2.49.1 From 7978884ca6896ae55dbd7de540bd2aa44feba99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 6 Feb 2026 11:18:57 +0100 Subject: [PATCH 06/17] feat(front-end): fix advanced view --- .../upload-form/upload-form.component.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index 399d1c7..0928ee5 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -101,7 +101,38 @@ interface FormItem { @if (mode() === 'advanced') { - +
+ + + +
+ +
+ + +
+ + +
+
+ + }
-- 2.49.1 From bcdeafe1194cf1ad9d78010585e40c6dcc30d907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 6 Feb 2026 11:33:25 +0100 Subject: [PATCH 07/17] chore(web): refractor --- GEMINI.md | 4 + frontend/src/app/app.component.scss | 0 frontend/src/app/app.component.ts | 3 +- .../src/app/core/layout/layout.component.html | 7 + .../src/app/core/layout/layout.component.scss | 9 + .../src/app/core/layout/layout.component.ts | 22 +- .../features/about/about-page.component.html | 42 +++ .../features/about/about-page.component.scss | 157 +++++++++ .../features/about/about-page.component.ts | 205 +----------- .../calculator/calculator-page.component.html | 64 ++++ .../calculator/calculator-page.component.scss | 99 ++++++ .../calculator/calculator-page.component.ts | 169 +--------- .../quote-result/quote-result.component.html | 62 ++++ .../quote-result/quote-result.component.scss | 85 +++++ .../quote-result/quote-result.component.ts | 153 +-------- .../upload-form/upload-form.component.html | 134 ++++++++ .../upload-form/upload-form.component.scss | 174 ++++++++++ .../upload-form/upload-form.component.ts | 314 +----------------- .../contact-form/contact-form.component.html | 73 ++++ .../contact-form/contact-form.component.scss | 133 ++++++++ .../contact-form/contact-form.component.ts | 212 +----------- .../contact/contact-page.component.html | 12 + .../contact/contact-page.component.scss | 14 + .../contact/contact-page.component.ts | 32 +- .../product-card/product-card.component.html | 13 + .../product-card/product-card.component.scss | 18 + .../product-card/product-card.component.ts | 37 +-- .../shop/product-detail.component.html | 25 ++ .../shop/product-detail.component.scss | 20 ++ .../features/shop/product-detail.component.ts | 51 +-- .../features/shop/shop-page.component.html | 12 + .../features/shop/shop-page.component.scss | 7 + .../app/features/shop/shop-page.component.ts | 25 +- .../app-alert/app-alert.component.html | 9 + .../app-alert/app-alert.component.scss | 12 + .../app-alert/app-alert.component.ts | 27 +- .../app-button/app-button.component.html | 7 + .../app-button/app-button.component.scss | 49 +++ .../app-button/app-button.component.ts | 62 +--- .../app-card/app-card.component.html | 3 + .../app-card/app-card.component.scss | 12 + .../components/app-card/app-card.component.ts | 21 +- .../app-dropzone/app-dropzone.component.html | 26 ++ .../app-dropzone/app-dropzone.component.scss | 32 ++ .../app-dropzone/app-dropzone.component.ts | 64 +--- .../app-input/app-input.component.html | 14 + .../app-input/app-input.component.scss | 14 + .../app-input/app-input.component.ts | 34 +- .../app-select/app-select.component.html | 16 + .../app-select/app-select.component.scss | 13 + .../app-select/app-select.component.ts | 35 +- .../app-tabs/app-tabs.component.html | 10 + .../app-tabs/app-tabs.component.scss | 21 ++ .../components/app-tabs/app-tabs.component.ts | 37 +-- .../stl-viewer/stl-viewer.component.html | 13 + .../stl-viewer/stl-viewer.component.scss | 44 +++ .../stl-viewer/stl-viewer.component.ts | 63 +--- .../summary-card/summary-card.component.html | 6 + .../summary-card/summary-card.component.scss | 29 ++ .../summary-card/summary-card.component.ts | 41 +-- 60 files changed, 1534 insertions(+), 1567 deletions(-) create mode 100644 frontend/src/app/app.component.scss create mode 100644 frontend/src/app/core/layout/layout.component.html create mode 100644 frontend/src/app/core/layout/layout.component.scss create mode 100644 frontend/src/app/features/about/about-page.component.html create mode 100644 frontend/src/app/features/about/about-page.component.scss create mode 100644 frontend/src/app/features/calculator/calculator-page.component.html create mode 100644 frontend/src/app/features/calculator/calculator-page.component.scss create mode 100644 frontend/src/app/features/calculator/components/quote-result/quote-result.component.html create mode 100644 frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss create mode 100644 frontend/src/app/features/calculator/components/upload-form/upload-form.component.html create mode 100644 frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss create mode 100644 frontend/src/app/features/contact/components/contact-form/contact-form.component.html create mode 100644 frontend/src/app/features/contact/components/contact-form/contact-form.component.scss create mode 100644 frontend/src/app/features/contact/contact-page.component.html create mode 100644 frontend/src/app/features/contact/contact-page.component.scss create mode 100644 frontend/src/app/features/shop/components/product-card/product-card.component.html create mode 100644 frontend/src/app/features/shop/components/product-card/product-card.component.scss create mode 100644 frontend/src/app/features/shop/product-detail.component.html create mode 100644 frontend/src/app/features/shop/product-detail.component.scss create mode 100644 frontend/src/app/features/shop/shop-page.component.html create mode 100644 frontend/src/app/features/shop/shop-page.component.scss create mode 100644 frontend/src/app/shared/components/app-alert/app-alert.component.html create mode 100644 frontend/src/app/shared/components/app-alert/app-alert.component.scss create mode 100644 frontend/src/app/shared/components/app-button/app-button.component.html create mode 100644 frontend/src/app/shared/components/app-button/app-button.component.scss create mode 100644 frontend/src/app/shared/components/app-card/app-card.component.html create mode 100644 frontend/src/app/shared/components/app-card/app-card.component.scss create mode 100644 frontend/src/app/shared/components/app-dropzone/app-dropzone.component.html create mode 100644 frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss create mode 100644 frontend/src/app/shared/components/app-input/app-input.component.html create mode 100644 frontend/src/app/shared/components/app-input/app-input.component.scss create mode 100644 frontend/src/app/shared/components/app-select/app-select.component.html create mode 100644 frontend/src/app/shared/components/app-select/app-select.component.scss create mode 100644 frontend/src/app/shared/components/app-tabs/app-tabs.component.html create mode 100644 frontend/src/app/shared/components/app-tabs/app-tabs.component.scss create mode 100644 frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html create mode 100644 frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss create mode 100644 frontend/src/app/shared/components/summary-card/summary-card.component.html create mode 100644 frontend/src/app/shared/components/summary-card/summary-card.component.scss diff --git a/GEMINI.md b/GEMINI.md index 043579f..997d781 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -36,3 +36,7 @@ Questo file serve a dare contesto all'AI (Antigravity/Gemini) sulla struttura e - Per eseguire il backend serve `uvicorn`. - Il frontend richiede `npm install` al primo avvio. - Le configurazioni di stampa (layer height, wall thickness, infill) sono attualmente hardcoded o con valori di default nel backend, ma potrebbero essere esposte come parametri API in futuro. + +## AI Agent Rules +- **No Inline Code**: Tutti i componenti Angular DEVONO usare file separati per HTML (`templateUrl`) e SCSS (`styleUrl`). È vietato usare `template` o `styles` inline nel decoratore `@Component`. + diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 3c3d410..966ecc2 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -5,6 +5,7 @@ import { RouterOutlet } from '@angular/router'; selector: 'app-root', standalone: true, imports: [RouterOutlet], - template: `` + templateUrl: './app.component.html', + styleUrl: './app.component.scss' }) export class AppComponent {} diff --git a/frontend/src/app/core/layout/layout.component.html b/frontend/src/app/core/layout/layout.component.html new file mode 100644 index 0000000..a1775b8 --- /dev/null +++ b/frontend/src/app/core/layout/layout.component.html @@ -0,0 +1,7 @@ +
+ +
+ +
+ +
diff --git a/frontend/src/app/core/layout/layout.component.scss b/frontend/src/app/core/layout/layout.component.scss new file mode 100644 index 0000000..dbb9226 --- /dev/null +++ b/frontend/src/app/core/layout/layout.component.scss @@ -0,0 +1,9 @@ +.layout-wrapper { + display: flex; + flex-direction: column; + min-height: 100vh; +} +.main-content { + flex: 1; + padding-bottom: var(--space-12); +} diff --git a/frontend/src/app/core/layout/layout.component.ts b/frontend/src/app/core/layout/layout.component.ts index ac27e33..7e90e14 100644 --- a/frontend/src/app/core/layout/layout.component.ts +++ b/frontend/src/app/core/layout/layout.component.ts @@ -7,25 +7,7 @@ import { FooterComponent } from './footer.component'; selector: 'app-layout', standalone: true, imports: [RouterOutlet, NavbarComponent, FooterComponent], - template: ` -
- -
- -
- -
- `, - styles: [` - .layout-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; - } - .main-content { - flex: 1; - padding-bottom: var(--space-12); - } - `] + templateUrl: './layout.component.html', + styleUrl: './layout.component.scss' }) export class LayoutComponent {} diff --git a/frontend/src/app/features/about/about-page.component.html b/frontend/src/app/features/about/about-page.component.html new file mode 100644 index 0000000..51321c3 --- /dev/null +++ b/frontend/src/app/features/about/about-page.component.html @@ -0,0 +1,42 @@ +
+
+ + +
+

{{ 'ABOUT.EYEBROW' | translate }}

+

{{ 'ABOUT.TITLE' | translate }}

+

{{ 'ABOUT.SUBTITLE' | translate }}

+ +
+ +

{{ 'ABOUT.HOW_TEXT' | translate }}

+ +
+ {{ 'ABOUT.PILL_1' | translate }} + {{ 'ABOUT.PILL_2' | translate }} + {{ 'ABOUT.PILL_3' | translate }} + {{ 'ABOUT.SERVICE_1' | translate }} + {{ 'ABOUT.SERVICE_2' | translate }} +
+
+ + +
+
+
+
+ Member 1 + Founder +
+
+
+
+
+ Member 2 + Co-Founder +
+
+
+ +
+
diff --git a/frontend/src/app/features/about/about-page.component.scss b/frontend/src/app/features/about/about-page.component.scss new file mode 100644 index 0000000..1a926a5 --- /dev/null +++ b/frontend/src/app/features/about/about-page.component.scss @@ -0,0 +1,157 @@ +.about-section { + padding: 6rem 0; + background: var(--color-bg); + min-height: 80vh; + display: flex; + align-items: center; +} + +.split-layout { + display: grid; + grid-template-columns: 1fr; + gap: 4rem; + align-items: center; + text-align: center; /* Center on mobile */ + + @media(min-width: 992px) { + grid-template-columns: 1fr 1fr; + gap: 6rem; + text-align: left; /* Reset to left on desktop */ + } +} + +/* Left Column */ +.text-content { + /* text-align: left; Removed to inherit from parent */ +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.15em; + font-size: 0.875rem; + color: var(--color-primary-500); + font-weight: 700; + margin-bottom: var(--space-2); + display: block; +} + +h1 { + font-size: 3rem; + line-height: 1.1; + margin-bottom: var(--space-4); + color: var(--color-text-main); +} + +.subtitle { + font-size: 1.25rem; + color: var(--color-text-muted); + margin-bottom: var(--space-6); + font-weight: 300; +} + +.divider { + height: 4px; + width: 60px; + background: var(--color-primary-500); + border-radius: 2px; + margin-bottom: var(--space-6); + /* Center divider on mobile */ + margin-left: auto; + margin-right: auto; + + @media(min-width: 992px) { + margin-left: 0; + margin-right: 0; + } +} + +.description { + font-size: 1.1rem; + line-height: 1.7; + color: var(--color-text-main); + margin-bottom: var(--space-8); +} + +.tags-container { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; /* Center tags on mobile */ + + @media(min-width: 992px) { + justify-content: flex-start; + } +} + +.tag { + padding: 0.5rem 1rem; + border-radius: 99px; + background: var(--color-surface-card); + border: 1px solid var(--color-border); + color: var(--color-text-main); + font-weight: 500; + font-size: 0.9rem; + box-shadow: var(--shadow-sm); + transition: all 0.2s ease; +} + +.tag:hover { + transform: translateY(-2px); + border-color: var(--color-primary-500); + color: var(--color-primary-500); + box-shadow: var(--shadow-md); +} + +/* Right Column */ +.visual-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 2rem; + + @media(min-width: 768px) { + display: grid; + grid-template-columns: repeat(2, 1fr); + align-items: start; + justify-items: center; + } +} + +.photo-card { + background: var(--color-surface-card); + padding: 1rem; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 260px; + position: relative; +} + +.placeholder-img { + width: 100%; + aspect-ratio: 3/4; + background: linear-gradient(45deg, var(--color-neutral-200), var(--color-neutral-100)); + border-radius: var(--radius-md); + margin-bottom: 1rem; +} + +.member-info { + text-align: center; +} + +.member-name { + display: block; + font-weight: 700; + color: var(--color-text-main); + font-size: 1.1rem; +} + +.member-role { + display: block; + font-size: 0.85rem; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-top: 0.25rem; +} diff --git a/frontend/src/app/features/about/about-page.component.ts b/frontend/src/app/features/about/about-page.component.ts index e06f003..edb3d90 100644 --- a/frontend/src/app/features/about/about-page.component.ts +++ b/frontend/src/app/features/about/about-page.component.ts @@ -6,209 +6,8 @@ import { TranslateModule } from '@ngx-translate/core'; selector: 'app-about-page', standalone: true, imports: [TranslateModule], - template: ` -
-
- - -
-

{{ 'ABOUT.EYEBROW' | translate }}

-

{{ 'ABOUT.TITLE' | translate }}

-

{{ 'ABOUT.SUBTITLE' | translate }}

- -
- -

{{ 'ABOUT.HOW_TEXT' | translate }}

- -
- {{ 'ABOUT.PILL_1' | translate }} - {{ 'ABOUT.PILL_2' | translate }} - {{ 'ABOUT.PILL_3' | translate }} - {{ 'ABOUT.SERVICE_1' | translate }} - {{ 'ABOUT.SERVICE_2' | translate }} -
-
- - -
-
-
-
- Member 1 - Founder -
-
-
-
-
- Member 2 - Co-Founder -
-
-
- -
-
- `, - styles: [` - .about-section { - padding: 6rem 0; - background: var(--color-bg); - min-height: 80vh; - display: flex; - align-items: center; - } - - .split-layout { - display: grid; - grid-template-columns: 1fr; - gap: 4rem; - align-items: center; - text-align: center; /* Center on mobile */ - - @media(min-width: 992px) { - grid-template-columns: 1fr 1fr; - gap: 6rem; - text-align: left; /* Reset to left on desktop */ - } - } - - /* Left Column */ - .text-content { - /* text-align: left; Removed to inherit from parent */ - } - - .eyebrow { - text-transform: uppercase; - letter-spacing: 0.15em; - font-size: 0.875rem; - color: var(--color-primary-500); - font-weight: 700; - margin-bottom: var(--space-2); - display: block; - } - - h1 { - font-size: 3rem; - line-height: 1.1; - margin-bottom: var(--space-4); - color: var(--color-text-main); - } - - .subtitle { - font-size: 1.25rem; - color: var(--color-text-muted); - margin-bottom: var(--space-6); - font-weight: 300; - } - - .divider { - height: 4px; - width: 60px; - background: var(--color-primary-500); - border-radius: 2px; - margin-bottom: var(--space-6); - /* Center divider on mobile */ - margin-left: auto; - margin-right: auto; - - @media(min-width: 992px) { - margin-left: 0; - margin-right: 0; - } - } - - .description { - font-size: 1.1rem; - line-height: 1.7; - color: var(--color-text-main); - margin-bottom: var(--space-8); - } - - .tags-container { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - justify-content: center; /* Center tags on mobile */ - - @media(min-width: 992px) { - justify-content: flex-start; - } - } - - .tag { - padding: 0.5rem 1rem; - border-radius: 99px; - background: var(--color-surface-card); - border: 1px solid var(--color-border); - color: var(--color-text-main); - font-weight: 500; - font-size: 0.9rem; - box-shadow: var(--shadow-sm); - transition: all 0.2s ease; - } - - .tag:hover { - transform: translateY(-2px); - border-color: var(--color-primary-500); - color: var(--color-primary-500); - box-shadow: var(--shadow-md); - } - - /* Right Column */ - .visual-content { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 2rem; - - @media(min-width: 768px) { - display: grid; - grid-template-columns: repeat(2, 1fr); - align-items: start; - justify-items: center; - } - } - - .photo-card { - background: var(--color-surface-card); - padding: 1rem; - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - width: 100%; - max-width: 260px; - position: relative; - } - - .placeholder-img { - width: 100%; - aspect-ratio: 3/4; - background: linear-gradient(45deg, var(--color-neutral-200), var(--color-neutral-100)); - border-radius: var(--radius-md); - margin-bottom: 1rem; - } - - .member-info { - text-align: center; - } - - .member-name { - display: block; - font-weight: 700; - color: var(--color-text-main); - font-size: 1.1rem; - } - - .member-role { - display: block; - font-size: 0.85rem; - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-top: 0.25rem; - } - `] + templateUrl: './about-page.component.html', + styleUrl: './about-page.component.scss' }) export class AboutPageComponent {} diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html new file mode 100644 index 0000000..a589735 --- /dev/null +++ b/frontend/src/app/features/calculator/calculator-page.component.html @@ -0,0 +1,64 @@ +
+

{{ 'CALC.TITLE' | translate }}

+

{{ 'CALC.SUBTITLE' | translate }}

+
+ +
+ +
+ +
+
+ {{ 'CALC.MODE_EASY' | translate }} +
+
+ {{ 'CALC.MODE_ADVANCED' | translate }} +
+
+ + +
+
+ + +
+ @if (error()) { + Si è verificato un errore durante il calcolo del preventivo. + } + + @if (loading()) { + +
+
+

Analisi in corso...

+

Stiamo analizzando la geometria e calcolando il percorso utensile.

+
+
+ } @else if (result()) { + + } @else { + +

{{ 'CALC.BENEFITS_TITLE' | translate }}

+
    +
  • {{ 'CALC.BENEFITS_1' | translate }}
  • +
  • {{ 'CALC.BENEFITS_2' | translate }}
  • +
  • {{ 'CALC.BENEFITS_3' | translate }}
  • +
+
+ } +
+
diff --git a/frontend/src/app/features/calculator/calculator-page.component.scss b/frontend/src/app/features/calculator/calculator-page.component.scss new file mode 100644 index 0000000..8d2160a --- /dev/null +++ b/frontend/src/app/features/calculator/calculator-page.component.scss @@ -0,0 +1,99 @@ +.hero { padding: var(--space-12) 0; text-align: center; } +.subtitle { font-size: 1.25rem; color: var(--color-text-muted); max-width: 600px; margin: 0 auto; } + +.content-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-6); + @media(min-width: 768px) { + grid-template-columns: 1.5fr 1fr; + gap: var(--space-8); + } +} + +.centered-col { + align-self: flex-start; /* Default */ + @media(min-width: 768px) { + align-self: center; + } +} + +.col-input, .col-result { + min-width: 0; /* Prevent grid blowout */ +} + +/* Mode Selector (Segmented Control style) */ +.mode-selector { + display: flex; + background-color: var(--color-neutral-100); + border-radius: var(--radius-md); + padding: 4px; + margin-bottom: var(--space-6); + gap: 4px; + width: 100%; +} + +.mode-option { + flex: 1; + text-align: center; + padding: 8px 16px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-muted); + transition: all 0.2s ease; + user-select: none; + + &:hover { color: var(--color-text); } + + &.active { + background-color: var(--color-brand); + color: #000; + font-weight: 600; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + } +} + +.benefits { padding-left: var(--space-4); color: var(--color-text-muted); line-height: 2; } + +.loading-state { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; /* Match typical result height */ +} + +.loader-content { + text-align: center; + max-width: 300px; + margin: 0 auto; +} + +.loading-title { + font-size: 1.1rem; + font-weight: 600; + margin: var(--space-4) 0 var(--space-2); + color: var(--color-text); +} + +.loading-text { + font-size: 0.9rem; + color: var(--color-text-muted); + line-height: 1.5; +} + +.spinner { + border: 3px solid var(--color-neutral-200); + border-left-color: var(--color-brand); + border-radius: 50%; + width: 48px; + height: 48px; + animation: spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index dc33a4b..ac8aefe 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -13,173 +13,8 @@ import { Router } from '@angular/router'; selector: 'app-calculator-page', standalone: true, imports: [CommonModule, TranslateModule, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent], - template: ` -
-

{{ 'CALC.TITLE' | translate }}

-

{{ 'CALC.SUBTITLE' | translate }}

-
- -
- -
- -
-
- {{ 'CALC.MODE_EASY' | translate }} -
-
- {{ 'CALC.MODE_ADVANCED' | translate }} -
-
- - -
-
- - -
- @if (error()) { - Si è verificato un errore durante il calcolo del preventivo. - } - - @if (loading()) { - -
-
-

Analisi in corso...

-

Stiamo analizzando la geometria e calcolando il percorso utensile.

-
-
- } @else if (result()) { - - } @else { - -

{{ 'CALC.BENEFITS_TITLE' | translate }}

-
    -
  • {{ 'CALC.BENEFITS_1' | translate }}
  • -
  • {{ 'CALC.BENEFITS_2' | translate }}
  • -
  • {{ 'CALC.BENEFITS_3' | translate }}
  • -
-
- } -
-
- `, - styles: [` - .hero { padding: var(--space-12) 0; text-align: center; } - .subtitle { font-size: 1.25rem; color: var(--color-text-muted); max-width: 600px; margin: 0 auto; } - - .content-grid { - display: grid; - grid-template-columns: 1fr; - gap: var(--space-6); - @media(min-width: 768px) { - grid-template-columns: 1.5fr 1fr; - gap: var(--space-8); - } - } - - .centered-col { - align-self: flex-start; /* Default */ - @media(min-width: 768px) { - align-self: center; - } - } - - .col-input, .col-result { - min-width: 0; /* Prevent grid blowout */ - } - - /* Mode Selector (Segmented Control style) */ - .mode-selector { - display: flex; - background-color: var(--color-neutral-100); - border-radius: var(--radius-md); - padding: 4px; - margin-bottom: var(--space-6); - gap: 4px; - width: 100%; - } - - .mode-option { - flex: 1; - text-align: center; - padding: 8px 16px; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: 0.875rem; - font-weight: 500; - color: var(--color-text-muted); - transition: all 0.2s ease; - user-select: none; - - &:hover { color: var(--color-text); } - - &.active { - background-color: var(--color-brand); - color: #000; - font-weight: 600; - box-shadow: 0 1px 2px rgba(0,0,0,0.05); - } - } - - .benefits { padding-left: var(--space-4); color: var(--color-text-muted); line-height: 2; } - - .loading-state { - display: flex; - align-items: center; - justify-content: center; - min-height: 300px; /* Match typical result height */ - } - - .loader-content { - text-align: center; - max-width: 300px; - margin: 0 auto; - } - - .loading-title { - font-size: 1.1rem; - font-weight: 600; - margin: var(--space-4) 0 var(--space-2); - color: var(--color-text); - } - - .loading-text { - font-size: 0.9rem; - color: var(--color-text-muted); - line-height: 1.5; - } - - .spinner { - border: 3px solid var(--color-neutral-200); - border-left-color: var(--color-brand); - border-radius: 50%; - width: 48px; - height: 48px; - animation: spin 1s linear infinite; - margin: 0 auto; - } - - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - `] + templateUrl: './calculator-page.component.html', + styleUrl: './calculator-page.component.scss' }) export class CalculatorPageComponent { mode = signal('easy'); diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html new file mode 100644 index 0000000..bc78d4e --- /dev/null +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html @@ -0,0 +1,62 @@ + +

{{ 'CALC.RESULT' | translate }}

+ + +
+ + {{ totals().price | currency:result().currency }} + + + + {{ totals().hours }}h {{ totals().minutes }}m + + + + {{ totals().weight }}g + +
+ +
+ * Include {{ result().setupCost | currency:result().currency }} Setup Cost +
+ +
+ + +
+ @for (item of items(); track item.fileName; let i = $index) { +
+
+ {{ item.fileName }} + + {{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g + +
+ +
+
+ + +
+
+ {{ (item.unitPrice * item.quantity) | currency:result().currency }} +
+
+
+ } +
+ +
+ {{ 'CALC.ORDER' | translate }} + {{ 'CALC.CONSULT' | translate }} +
+
diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss new file mode 100644 index 0000000..0c16042 --- /dev/null +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss @@ -0,0 +1,85 @@ +.title { margin-bottom: var(--space-6); text-align: center; } + +.divider { + height: 1px; + background: var(--color-border); + margin: var(--space-4) 0; +} + +.items-list { + display: flex; + flex-direction: column; + gap: var(--space-3); + margin-bottom: var(--space-4); +} + +.item-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-3); + background: var(--color-neutral-50); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); +} + +.item-info { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; /* Ensure it takes available space */ +} + +.file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.file-details { font-size: 0.8rem; color: var(--color-text-muted); } + +.item-controls { + display: flex; + align-items: center; + gap: var(--space-4); +} + +.qty-control { + display: flex; + align-items: center; + gap: var(--space-2); + + label { font-size: 0.8rem; color: var(--color-text-muted); } +} + +.qty-input { + width: 60px; + padding: 4px 8px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + text-align: center; + &:focus { outline: none; border-color: var(--color-brand); } +} + +.item-price { + font-weight: 600; + min-width: 60px; + text-align: right; +} + +.result-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-3); + margin-bottom: var(--space-2); + + @media(min-width: 500px) { + grid-template-columns: 1fr 1fr; + gap: var(--space-4); + } +} +.full-width { grid-column: span 2; } + +.setup-note { + text-align: center; + margin-bottom: var(--space-6); + color: var(--color-text-muted); + font-size: 0.8rem; +} + +.actions { display: flex; flex-direction: column; gap: var(--space-3); } diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index 7d41ec0..eda5a6f 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -11,157 +11,8 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; selector: 'app-quote-result', standalone: true, imports: [CommonModule, FormsModule, TranslateModule, AppCardComponent, AppButtonComponent, SummaryCardComponent], - template: ` - -

{{ 'CALC.RESULT' | translate }}

- - -
- - {{ totals().price | currency:result().currency }} - - - - {{ totals().hours }}h {{ totals().minutes }}m - - - - {{ totals().weight }}g - -
- -
- * Include {{ result().setupCost | currency:result().currency }} Setup Cost -
- -
- - -
- @for (item of items(); track item.fileName; let i = $index) { -
-
- {{ item.fileName }} - - {{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g - -
- -
-
- - -
-
- {{ (item.unitPrice * item.quantity) | currency:result().currency }} -
-
-
- } -
- -
- {{ 'CALC.ORDER' | translate }} - {{ 'CALC.CONSULT' | translate }} -
-
- `, - styles: [` - .title { margin-bottom: var(--space-6); text-align: center; } - - .divider { - height: 1px; - background: var(--color-border); - margin: var(--space-4) 0; - } - - .items-list { - display: flex; - flex-direction: column; - gap: var(--space-3); - margin-bottom: var(--space-4); - } - - .item-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--space-3); - background: var(--color-neutral-50); - border-radius: var(--radius-md); - border: 1px solid var(--color-border); - } - - .item-info { - display: flex; - flex-direction: column; - min-width: 0; - flex: 1; /* Ensure it takes available space */ - } - - .file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .file-details { font-size: 0.8rem; color: var(--color-text-muted); } - - .item-controls { - display: flex; - align-items: center; - gap: var(--space-4); - } - - .qty-control { - display: flex; - align-items: center; - gap: var(--space-2); - - label { font-size: 0.8rem; color: var(--color-text-muted); } - } - - .qty-input { - width: 60px; - padding: 4px 8px; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - text-align: center; - &:focus { outline: none; border-color: var(--color-brand); } - } - - .item-price { - font-weight: 600; - min-width: 60px; - text-align: right; - } - - .result-grid { - display: grid; - grid-template-columns: 1fr; - gap: var(--space-3); - margin-bottom: var(--space-2); - - @media(min-width: 500px) { - grid-template-columns: 1fr 1fr; - gap: var(--space-4); - } - } - .full-width { grid-column: span 2; } - - .setup-note { - text-align: center; - margin-bottom: var(--space-6); - color: var(--color-text-muted); - font-size: 0.8rem; - } - - .actions { display: flex; flex-direction: column; gap: var(--space-3); } - `] + templateUrl: './quote-result.component.html', + styleUrl: './quote-result.component.scss' }) export class QuoteResultComponent { result = input.required(); diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html new file mode 100644 index 0000000..334fd93 --- /dev/null +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html @@ -0,0 +1,134 @@ +
+ +
+ @if (selectedFile()) { +
+ + +
+ } + + + @if (items().length === 0) { + + + } + + + @if (items().length > 0) { +
+ @for (item of items(); track item.file.name; let i = $index) { +
+
+ {{ item.file.name }} +
+ +
+
+ + +
+ + +
+
+ } +
+ + +
+ + + +
+ } + + @if (items().length === 0 && form.get('itemsTouched')?.value) { +
{{ 'CALC.ERR_FILE_REQUIRED' | translate }}
+ } +
+ +
+ + + +
+ + + + @if (mode() === 'advanced') { +
+ + + +
+ +
+ + +
+ + +
+
+ + + } + +
+ + @if (loading() && uploadProgress() < 100) { +
+
+
+
+
+ } + + + {{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }} + +
+
diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss new file mode 100644 index 0000000..1cee5e1 --- /dev/null +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss @@ -0,0 +1,174 @@ +.section { margin-bottom: var(--space-6); } +.grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-4); + + @media(min-width: 640px) { + grid-template-columns: 1fr 1fr; + } +} +.actions { margin-top: var(--space-6); } +.error-msg { color: var(--color-danger-500); font-size: 0.875rem; margin-top: var(--space-2); text-align: center; } + +.viewer-wrapper { position: relative; margin-bottom: var(--space-4); } + +/* Grid Layout for Files */ +.items-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-3); + margin-top: var(--space-4); + margin-bottom: var(--space-4); + + @media(min-width: 640px) { + grid-template-columns: 1fr 1fr; + } +} + +.file-card { + padding: var(--space-3); + background: var(--color-neutral-100); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + transition: all 0.2s; + cursor: pointer; + display: flex; + flex-direction: column; + gap: var(--space-2); + + &:hover { border-color: var(--color-neutral-300); } + &.active { + border-color: var(--color-brand); + background: rgba(250, 207, 10, 0.05); + box-shadow: 0 0 0 1px var(--color-brand); + } +} + +.card-header { + overflow: hidden; +} + +.file-name { + font-weight: 500; + font-size: 0.85rem; + color: var(--color-text); + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.card-body { + display: flex; + justify-content: space-between; + align-items: center; +} + +.qty-group { + display: flex; + align-items: center; + gap: var(--space-2); + label { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } +} + +.qty-input { + width: 40px; + padding: 2px 4px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + text-align: center; + font-size: 0.9rem; + background: white; + &:focus { outline: none; border-color: var(--color-brand); } +} + +.btn-remove { + width: 24px; + height: 24px; + border-radius: 4px; + border: 1px solid transparent; // var(--color-border); + background: transparent; // white; + color: var(--color-text-muted); + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + font-size: 0.9rem; + + &:hover { + background: var(--color-danger-100); + color: var(--color-danger-500); + border-color: var(--color-danger-200); + } +} + +/* Prominent Add Button */ +.add-more-container { + margin-top: var(--space-2); +} + +.btn-add-more { + width: 100%; + padding: var(--space-3); + background: var(--color-neutral-800); + color: white; + border: none; + border-radius: var(--radius-md); + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + + &:hover { + background: var(--color-neutral-900); + transform: translateY(-1px); + } + &:active { transform: translateY(0); } +} + +.checkbox-row { + display: flex; + align-items: center; + gap: var(--space-3); + height: 100%; + padding-top: var(--space-4); + + input[type="checkbox"] { + width: 20px; + height: 20px; + accent-color: var(--color-brand); + } + label { + font-weight: 500; + cursor: pointer; + } +} + +/* Progress Bar */ +.progress-container { + margin-bottom: var(--space-3); + text-align: center; + width: 100%; +} +.progress-bar { + height: 4px; + background: var(--color-border); + border-radius: 2px; + overflow: hidden; + margin-bottom: 0; + position: relative; + width: 100%; +} +.progress-fill { + height: 100%; + background: var(--color-brand); + width: 0%; + transition: width 0.2s ease-out; +} diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index 0928ee5..4229e3e 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -18,318 +18,8 @@ interface FormItem { selector: 'app-upload-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppSelectComponent, AppDropzoneComponent, AppButtonComponent, StlViewerComponent], - template: ` -
- -
- @if (selectedFile()) { -
- - -
- } - - - @if (items().length === 0) { - - - } - - - @if (items().length > 0) { -
- @for (item of items(); track item.file.name; let i = $index) { -
-
- {{ item.file.name }} -
- -
-
- - -
- - -
-
- } -
- - -
- - - -
- } - - @if (items().length === 0 && form.get('itemsTouched')?.value) { -
{{ 'CALC.ERR_FILE_REQUIRED' | translate }}
- } -
- -
- - - -
- - - - @if (mode() === 'advanced') { -
- - - -
- -
- - -
- - -
-
- - - } - -
- - @if (loading() && uploadProgress() < 100) { -
-
-
-
-
- } - - - {{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }} - -
-
- `, - styles: [` - .section { margin-bottom: var(--space-6); } - .grid { - display: grid; - grid-template-columns: 1fr; - gap: var(--space-4); - - @media(min-width: 640px) { - grid-template-columns: 1fr 1fr; - } - } - .actions { margin-top: var(--space-6); } - .error-msg { color: var(--color-danger-500); font-size: 0.875rem; margin-top: var(--space-2); text-align: center; } - - .viewer-wrapper { position: relative; margin-bottom: var(--space-4); } - - /* Grid Layout for Files */ - .items-grid { - display: grid; - grid-template-columns: 1fr; - gap: var(--space-3); - margin-top: var(--space-4); - margin-bottom: var(--space-4); - - @media(min-width: 640px) { - grid-template-columns: 1fr 1fr; - } - } - - .file-card { - padding: var(--space-3); - background: var(--color-neutral-100); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - transition: all 0.2s; - cursor: pointer; - display: flex; - flex-direction: column; - gap: var(--space-2); - - &:hover { border-color: var(--color-neutral-300); } - &.active { - border-color: var(--color-brand); - background: rgba(250, 207, 10, 0.05); - box-shadow: 0 0 0 1px var(--color-brand); - } - } - - .card-header { - overflow: hidden; - } - - .file-name { - font-weight: 500; - font-size: 0.85rem; - color: var(--color-text); - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .card-body { - display: flex; - justify-content: space-between; - align-items: center; - } - - .qty-group { - display: flex; - align-items: center; - gap: var(--space-2); - label { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } - } - - .qty-input { - width: 40px; - padding: 2px 4px; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - text-align: center; - font-size: 0.9rem; - background: white; - &:focus { outline: none; border-color: var(--color-brand); } - } - - .btn-remove { - width: 24px; - height: 24px; - border-radius: 4px; - border: 1px solid transparent; // var(--color-border); - background: transparent; // white; - color: var(--color-text-muted); - font-weight: bold; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; - font-size: 0.9rem; - - &:hover { - background: var(--color-danger-100); - color: var(--color-danger-500); - border-color: var(--color-danger-200); - } - } - - /* Prominent Add Button */ - .add-more-container { - margin-top: var(--space-2); - } - - .btn-add-more { - width: 100%; - padding: var(--space-3); - background: var(--color-neutral-800); - color: white; - border: none; - border-radius: var(--radius-md); - font-weight: 600; - font-size: 0.9rem; - cursor: pointer; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-2); - - &:hover { - background: var(--color-neutral-900); - transform: translateY(-1px); - } - &:active { transform: translateY(0); } - } - - .checkbox-row { - display: flex; - align-items: center; - gap: var(--space-3); - height: 100%; - padding-top: var(--space-4); - - input[type="checkbox"] { - width: 20px; - height: 20px; - accent-color: var(--color-brand); - } - label { - font-weight: 500; - cursor: pointer; - } - } - - /* Progress Bar */ - .progress-container { - margin-bottom: var(--space-3); - text-align: center; - width: 100%; - } - .progress-bar { - height: 4px; - background: var(--color-border); - border-radius: 2px; - overflow: hidden; - margin-bottom: 0; - position: relative; - width: 100%; - } - .progress-fill { - height: 100%; - background: var(--color-brand); - width: 0%; - transition: width 0.2s ease-out; - } - `] + templateUrl: './upload-form.component.html', + styleUrl: './upload-form.component.scss' }) export class UploadFormComponent { mode = input<'easy' | 'advanced'>('easy'); diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html new file mode 100644 index 0000000..69b02af --- /dev/null +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html @@ -0,0 +1,73 @@ +
+ +
+ + +
+ +
+ + + + +
+ + +
+
+ {{ 'CONTACT.TYPE_PRIVATE' | translate }} +
+
+ {{ 'CONTACT.TYPE_COMPANY' | translate }} +
+
+ + + + + +
+ + +
+ +
+ + +
+ + +
+ +

{{ 'CONTACT.UPLOAD_HINT' | translate }}

+ +
+ +

{{ 'CONTACT.DROP_FILES' | translate }}

+
+ +
+
+ + +
+ PDF + 3D +
+
{{ file.file.name }}
+
+
+
+ +
+ + {{ sent() ? ('CONTACT.MSG_SENT' | translate) : ('CONTACT.SEND' | translate) }} + +
+
diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss b/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss new file mode 100644 index 0000000..299a2d0 --- /dev/null +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss @@ -0,0 +1,133 @@ +.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); } +.hint { font-size: 0.75rem; color: var(--color-text-muted); margin-bottom: var(--space-2); } + +.form-control { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + width: 100%; + background: var(--color-bg-card); + color: var(--color-text); + font-family: inherit; + &:focus { outline: none; border-color: var(--color-brand); } +} + +select.form-control { + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg 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'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 1rem center; + background-size: 1em; +} + +.row { + display: flex; + flex-direction: column; + gap: var(--space-4); + margin-bottom: var(--space-4); + @media(min-width: 768px) { + flex-direction: row; + .col { flex: 1; margin-bottom: 0; } + } +} + +app-input.col { width: 100%; } + +/* User Type Selector Styles */ +.user-type-selector { + display: flex; + background-color: var(--color-neutral-100); + border-radius: var(--radius-md); + padding: 4px; + margin-bottom: var(--space-4); + gap: 4px; + width: 100%; /* Full width */ + max-width: 400px; /* Limit on desktop */ +} + +.type-option { + flex: 1; /* Equal width */ + text-align: center; + padding: 8px 16px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-muted); + transition: all 0.2s ease; + user-select: none; + + &: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); + } +} + +.company-fields { + display: flex; + flex-direction: column; + gap: var(--space-4); + padding-left: var(--space-4); + border-left: 2px solid var(--color-border); + margin-bottom: var(--space-4); +} + +/* File Upload Styles */ +.drop-zone { + border: 2px dashed var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-6); + text-align: center; + cursor: pointer; + color: var(--color-text-muted); + transition: all 0.2s; + &:hover { border-color: var(--color-brand); color: var(--color-brand); } +} + +.file-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: var(--space-3); + margin-top: var(--space-3); +} + +.file-item { + position: relative; + background: var(--color-neutral-100); + border-radius: var(--radius-sm); + padding: var(--space-2); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + aspect-ratio: 1; + overflow: hidden; +} + +.preview-img { + width: 100%; height: 100%; object-fit: cover; position: absolute; top:0; left:0; + border-radius: var(--radius-sm); +} + +.file-icon { + font-weight: 700; color: var(--color-text-muted); font-size: 0.8rem; +} + +.file-name { + font-size: 0.65rem; color: var(--color-text); white-space: nowrap; overflow: hidden; + text-overflow: ellipsis; width: 100%; text-align: center; position: absolute; bottom: 2px; + padding: 0 4px; z-index: 2; background: rgba(255,255,255,0.8); +} + +.remove-btn { + position: absolute; top: 2px; right: 2px; z-index: 10; + background: rgba(0,0,0,0.5); color: white; border: none; border-radius: 50%; + width: 18px; height: 18px; font-size: 12px; cursor: pointer; + display: flex; align-items: center; justify-content: center; line-height: 1; + &:hover { background: red; } +} diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts index 30bf471..8889620 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts @@ -16,216 +16,8 @@ interface FilePreview { selector: 'app-contact-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppButtonComponent], - template: ` -
- -
- - -
- -
- - - - -
- - -
-
- {{ 'CONTACT.TYPE_PRIVATE' | translate }} -
-
- {{ 'CONTACT.TYPE_COMPANY' | translate }} -
-
- - - - - -
- - -
- -
- - -
- - -
- -

{{ 'CONTACT.UPLOAD_HINT' | translate }}

- -
- -

{{ 'CONTACT.DROP_FILES' | translate }}

-
- -
-
- - -
- PDF - 3D -
-
{{ file.file.name }}
-
-
-
- -
- - {{ sent() ? ('CONTACT.MSG_SENT' | translate) : ('CONTACT.SEND' | translate) }} - -
-
- `, - styles: [` - .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); } - .hint { font-size: 0.75rem; color: var(--color-text-muted); margin-bottom: var(--space-2); } - - .form-control { - padding: 0.5rem 0.75rem; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - width: 100%; - background: var(--color-bg-card); - color: var(--color-text); - font-family: inherit; - &:focus { outline: none; border-color: var(--color-brand); } - } - - select.form-control { - appearance: none; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg 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'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right 1rem center; - background-size: 1em; - } - - .row { - display: flex; - flex-direction: column; - gap: var(--space-4); - margin-bottom: var(--space-4); - @media(min-width: 768px) { - flex-direction: row; - .col { flex: 1; margin-bottom: 0; } - } - } - - app-input.col { width: 100%; } - - /* User Type Selector Styles */ - .user-type-selector { - display: flex; - background-color: var(--color-neutral-100); - border-radius: var(--radius-md); - padding: 4px; - margin-bottom: var(--space-4); - gap: 4px; - width: 100%; /* Full width */ - max-width: 400px; /* Limit on desktop */ - } - - .type-option { - flex: 1; /* Equal width */ - text-align: center; - padding: 8px 16px; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: 0.875rem; - font-weight: 500; - color: var(--color-text-muted); - transition: all 0.2s ease; - user-select: none; - - &: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); - } - } - - .company-fields { - display: flex; - flex-direction: column; - gap: var(--space-4); - padding-left: var(--space-4); - border-left: 2px solid var(--color-border); - margin-bottom: var(--space-4); - } - - /* File Upload Styles */ - .drop-zone { - border: 2px dashed var(--color-border); - border-radius: var(--radius-md); - padding: var(--space-6); - text-align: center; - cursor: pointer; - color: var(--color-text-muted); - transition: all 0.2s; - &:hover { border-color: var(--color-brand); color: var(--color-brand); } - } - - .file-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); - gap: var(--space-3); - margin-top: var(--space-3); - } - - .file-item { - position: relative; - background: var(--color-neutral-100); - border-radius: var(--radius-sm); - padding: var(--space-2); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - aspect-ratio: 1; - overflow: hidden; - } - - .preview-img { - width: 100%; height: 100%; object-fit: cover; position: absolute; top:0; left:0; - border-radius: var(--radius-sm); - } - - .file-icon { - font-weight: 700; color: var(--color-text-muted); font-size: 0.8rem; - } - - .file-name { - font-size: 0.65rem; color: var(--color-text); white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; width: 100%; text-align: center; position: absolute; bottom: 2px; - padding: 0 4px; z-index: 2; background: rgba(255,255,255,0.8); - } - - .remove-btn { - position: absolute; top: 2px; right: 2px; z-index: 10; - background: rgba(0,0,0,0.5); color: white; border: none; border-radius: 50%; - width: 18px; height: 18px; font-size: 12px; cursor: pointer; - display: flex; align-items: center; justify-content: center; line-height: 1; - &:hover { background: red; } - } - `] + templateUrl: './contact-form.component.html', + styleUrl: './contact-form.component.scss' }) export class ContactFormComponent { form: FormGroup; diff --git a/frontend/src/app/features/contact/contact-page.component.html b/frontend/src/app/features/contact/contact-page.component.html new file mode 100644 index 0000000..75d5c3c --- /dev/null +++ b/frontend/src/app/features/contact/contact-page.component.html @@ -0,0 +1,12 @@ +
+
+

{{ 'CONTACT.TITLE' | translate }}

+

Siamo qui per aiutarti. Compila il modulo sottostante per qualsiasi richiesta.

+
+
+ +
+ + + +
diff --git a/frontend/src/app/features/contact/contact-page.component.scss b/frontend/src/app/features/contact/contact-page.component.scss new file mode 100644 index 0000000..f495fe5 --- /dev/null +++ b/frontend/src/app/features/contact/contact-page.component.scss @@ -0,0 +1,14 @@ +.contact-hero { + padding: 3rem 0 2rem; + background: var(--color-bg); + text-align: center; +} +.subtitle { + color: var(--color-text-muted); + max-width: 640px; + margin: var(--space-3) auto 0; +} +.content { + padding: 2rem 0 5rem; + max-width: 800px; +} diff --git a/frontend/src/app/features/contact/contact-page.component.ts b/frontend/src/app/features/contact/contact-page.component.ts index c0b4630..0a27260 100644 --- a/frontend/src/app/features/contact/contact-page.component.ts +++ b/frontend/src/app/features/contact/contact-page.component.ts @@ -8,35 +8,7 @@ import { AppCardComponent } from '../../shared/components/app-card/app-card.comp selector: 'app-contact-page', standalone: true, imports: [CommonModule, TranslateModule, ContactFormComponent, AppCardComponent], - template: ` -
-
-

{{ 'CONTACT.TITLE' | translate }}

-

Siamo qui per aiutarti. Compila il modulo sottostante per qualsiasi richiesta.

-
-
- -
- - - -
- `, - styles: [` - .contact-hero { - padding: 3rem 0 2rem; - background: var(--color-bg); - text-align: center; - } - .subtitle { - color: var(--color-text-muted); - max-width: 640px; - margin: var(--space-3) auto 0; - } - .content { - padding: 2rem 0 5rem; - max-width: 800px; - } - `] + templateUrl: './contact-page.component.html', + styleUrl: './contact-page.component.scss' }) export class ContactPageComponent {} diff --git a/frontend/src/app/features/shop/components/product-card/product-card.component.html b/frontend/src/app/features/shop/components/product-card/product-card.component.html new file mode 100644 index 0000000..cc862a0 --- /dev/null +++ b/frontend/src/app/features/shop/components/product-card/product-card.component.html @@ -0,0 +1,13 @@ +
+
+
+ {{ product().category }} +

+ {{ product().name }} +

+ +
+
diff --git a/frontend/src/app/features/shop/components/product-card/product-card.component.scss b/frontend/src/app/features/shop/components/product-card/product-card.component.scss new file mode 100644 index 0000000..db3d954 --- /dev/null +++ b/frontend/src/app/features/shop/components/product-card/product-card.component.scss @@ -0,0 +1,18 @@ +.product-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; + transition: box-shadow 0.2s; + &:hover { box-shadow: var(--shadow-md); } +} +.image-placeholder { + height: 200px; + background-color: var(--color-neutral-200); +} +.content { padding: var(--space-4); } +.category { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; } +.name { font-size: 1.125rem; margin: var(--space-2) 0; a { color: var(--color-text); text-decoration: none; &:hover { color: var(--color-brand); } } } +.footer { display: flex; justify-content: space-between; align-items: center; margin-top: var(--space-4); } +.price { font-weight: 700; color: var(--color-brand); } +.view-btn { font-size: 0.875rem; font-weight: 500; } diff --git a/frontend/src/app/features/shop/components/product-card/product-card.component.ts b/frontend/src/app/features/shop/components/product-card/product-card.component.ts index 34d61bc..aa7833d 100644 --- a/frontend/src/app/features/shop/components/product-card/product-card.component.ts +++ b/frontend/src/app/features/shop/components/product-card/product-card.component.ts @@ -7,41 +7,8 @@ import { Product } from '../../services/shop.service'; selector: 'app-product-card', standalone: true, imports: [CommonModule, RouterLink], - template: ` -
-
-
- {{ product().category }} -

- {{ product().name }} -

- -
-
- `, - styles: [` - .product-card { - background: var(--color-bg-card); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - overflow: hidden; - transition: box-shadow 0.2s; - &:hover { box-shadow: var(--shadow-md); } - } - .image-placeholder { - height: 200px; - background-color: var(--color-neutral-200); - } - .content { padding: var(--space-4); } - .category { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; } - .name { font-size: 1.125rem; margin: var(--space-2) 0; a { color: var(--color-text); text-decoration: none; &:hover { color: var(--color-brand); } } } - .footer { display: flex; justify-content: space-between; align-items: center; margin-top: var(--space-4); } - .price { font-weight: 700; color: var(--color-brand); } - .view-btn { font-size: 0.875rem; font-weight: 500; } - `] + templateUrl: './product-card.component.html', + styleUrl: './product-card.component.scss' }) export class ProductCardComponent { product = input.required(); diff --git a/frontend/src/app/features/shop/product-detail.component.html b/frontend/src/app/features/shop/product-detail.component.html new file mode 100644 index 0000000..a08b543 --- /dev/null +++ b/frontend/src/app/features/shop/product-detail.component.html @@ -0,0 +1,25 @@ +
+ ← {{ 'SHOP.BACK' | translate }} + + @if (product(); as p) { +
+
+ +
+ {{ p.category }} +

{{ p.name }}

+

{{ p.price | currency:'EUR' }}

+ +

{{ p.description }}

+ +
+ + {{ 'SHOP.ADD_CART' | translate }} + +
+
+
+ } @else { +

Prodotto non trovato.

+ } +
diff --git a/frontend/src/app/features/shop/product-detail.component.scss b/frontend/src/app/features/shop/product-detail.component.scss new file mode 100644 index 0000000..4e81bb4 --- /dev/null +++ b/frontend/src/app/features/shop/product-detail.component.scss @@ -0,0 +1,20 @@ +.wrapper { padding-top: var(--space-8); } +.back-link { display: inline-block; margin-bottom: var(--space-6); color: var(--color-text-muted); } + +.detail-grid { + display: grid; + gap: var(--space-8); + @media(min-width: 768px) { + grid-template-columns: 1fr 1fr; + } +} + +.image-box { + background-color: var(--color-neutral-200); + border-radius: var(--radius-lg); + aspect-ratio: 1; +} + +.category { color: var(--color-brand); font-weight: 600; text-transform: uppercase; font-size: 0.875rem; } +.price { font-size: 1.5rem; font-weight: 700; color: var(--color-text); margin: var(--space-4) 0; } +.desc { color: var(--color-text-muted); line-height: 1.6; margin-bottom: var(--space-8); } diff --git a/frontend/src/app/features/shop/product-detail.component.ts b/frontend/src/app/features/shop/product-detail.component.ts index 8049f7d..1e8fa74 100644 --- a/frontend/src/app/features/shop/product-detail.component.ts +++ b/frontend/src/app/features/shop/product-detail.component.ts @@ -9,55 +9,8 @@ import { AppButtonComponent } from '../../shared/components/app-button/app-butto selector: 'app-product-detail', standalone: true, imports: [CommonModule, RouterLink, TranslateModule, AppButtonComponent], - template: ` -
- ← {{ 'SHOP.BACK' | translate }} - - @if (product(); as p) { -
-
- -
- {{ p.category }} -

{{ p.name }}

-

{{ p.price | currency:'EUR' }}

- -

{{ p.description }}

- -
- - {{ 'SHOP.ADD_CART' | translate }} - -
-
-
- } @else { -

Prodotto non trovato.

- } -
- `, - styles: [` - .wrapper { padding-top: var(--space-8); } - .back-link { display: inline-block; margin-bottom: var(--space-6); color: var(--color-text-muted); } - - .detail-grid { - display: grid; - gap: var(--space-8); - @media(min-width: 768px) { - grid-template-columns: 1fr 1fr; - } - } - - .image-box { - background-color: var(--color-neutral-200); - border-radius: var(--radius-lg); - aspect-ratio: 1; - } - - .category { color: var(--color-brand); font-weight: 600; text-transform: uppercase; font-size: 0.875rem; } - .price { font-size: 1.5rem; font-weight: 700; color: var(--color-text); margin: var(--space-4) 0; } - .desc { color: var(--color-text-muted); line-height: 1.6; margin-bottom: var(--space-8); } - `] + templateUrl: './product-detail.component.html', + styleUrl: './product-detail.component.scss' }) export class ProductDetailComponent { // Input binding from router diff --git a/frontend/src/app/features/shop/shop-page.component.html b/frontend/src/app/features/shop/shop-page.component.html new file mode 100644 index 0000000..f43032e --- /dev/null +++ b/frontend/src/app/features/shop/shop-page.component.html @@ -0,0 +1,12 @@ +
+

{{ 'SHOP.TITLE' | translate }}

+

{{ 'SHOP.SUBTITLE' | translate }}

+
+ +
+
+ @for (product of products(); track product.id) { + + } +
+
diff --git a/frontend/src/app/features/shop/shop-page.component.scss b/frontend/src/app/features/shop/shop-page.component.scss new file mode 100644 index 0000000..507fea1 --- /dev/null +++ b/frontend/src/app/features/shop/shop-page.component.scss @@ -0,0 +1,7 @@ +.hero { padding: var(--space-8) 0; text-align: center; } +.subtitle { color: var(--color-text-muted); margin-bottom: var(--space-8); } +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--space-6); +} diff --git a/frontend/src/app/features/shop/shop-page.component.ts b/frontend/src/app/features/shop/shop-page.component.ts index 53bc452..8f89098 100644 --- a/frontend/src/app/features/shop/shop-page.component.ts +++ b/frontend/src/app/features/shop/shop-page.component.ts @@ -8,29 +8,8 @@ import { ProductCardComponent } from './components/product-card/product-card.com selector: 'app-shop-page', standalone: true, imports: [CommonModule, TranslateModule, ProductCardComponent], - template: ` -
-

{{ 'SHOP.TITLE' | translate }}

-

{{ 'SHOP.SUBTITLE' | translate }}

-
- -
-
- @for (product of products(); track product.id) { - - } -
-
- `, - styles: [` - .hero { padding: var(--space-8) 0; text-align: center; } - .subtitle { color: var(--color-text-muted); margin-bottom: var(--space-8); } - .grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: var(--space-6); - } - `] + templateUrl: './shop-page.component.html', + styleUrl: './shop-page.component.scss' }) export class ShopPageComponent { products = signal([]); diff --git a/frontend/src/app/shared/components/app-alert/app-alert.component.html b/frontend/src/app/shared/components/app-alert/app-alert.component.html new file mode 100644 index 0000000..e377c49 --- /dev/null +++ b/frontend/src/app/shared/components/app-alert/app-alert.component.html @@ -0,0 +1,9 @@ +
+
+ @if(type() === 'info') { ℹ️ } + @if(type() === 'warning') { ⚠️ } + @if(type() === 'error') { ❌ } + @if(type() === 'success') { ✅ } +
+
+
diff --git a/frontend/src/app/shared/components/app-alert/app-alert.component.scss b/frontend/src/app/shared/components/app-alert/app-alert.component.scss new file mode 100644 index 0000000..2d4285b --- /dev/null +++ b/frontend/src/app/shared/components/app-alert/app-alert.component.scss @@ -0,0 +1,12 @@ +.alert { + padding: var(--space-4); + border-radius: var(--radius-md); + display: flex; + gap: var(--space-3); + 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; } diff --git a/frontend/src/app/shared/components/app-alert/app-alert.component.ts b/frontend/src/app/shared/components/app-alert/app-alert.component.ts index c187d52..9ad0b51 100644 --- a/frontend/src/app/shared/components/app-alert/app-alert.component.ts +++ b/frontend/src/app/shared/components/app-alert/app-alert.component.ts @@ -5,31 +5,8 @@ import { CommonModule } from '@angular/common'; selector: 'app-alert', standalone: true, imports: [CommonModule], - template: ` -
-
- @if(type() === 'info') { ℹ️ } - @if(type() === 'warning') { ⚠️ } - @if(type() === 'error') { ❌ } - @if(type() === 'success') { ✅ } -
-
-
- `, - styles: [` - .alert { - padding: var(--space-4); - border-radius: var(--radius-md); - display: flex; - gap: var(--space-3); - 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; } - `] + templateUrl: './app-alert.component.html', + styleUrl: './app-alert.component.scss' }) export class AppAlertComponent { type = input<'info' | 'warning' | 'error' | 'success'>('info'); diff --git a/frontend/src/app/shared/components/app-button/app-button.component.html b/frontend/src/app/shared/components/app-button/app-button.component.html new file mode 100644 index 0000000..c4328a6 --- /dev/null +++ b/frontend/src/app/shared/components/app-button/app-button.component.html @@ -0,0 +1,7 @@ + diff --git a/frontend/src/app/shared/components/app-button/app-button.component.scss b/frontend/src/app/shared/components/app-button/app-button.component.scss new file mode 100644 index 0000000..407d667 --- /dev/null +++ b/frontend/src/app/shared/components/app-button/app-button.component.scss @@ -0,0 +1,49 @@ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 500; + cursor: pointer; + 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%; } + +.btn-primary { + background-color: var(--color-brand); + color: var(--color-neutral-900); + &: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); } +} + +.btn-outline { + background-color: transparent; + border-color: var(--color-border); + color: var(--color-text); + &:hover:not(:disabled) { + border-color: var(--color-brand); + color: var(--color-neutral-900); + background-color: rgba(250, 207, 10, 0.1); /* Low opacity brand color */ + } +} + +.btn-text { + background-color: transparent; + color: var(--color-text-muted); + padding: 0.5rem; + &:hover:not(:disabled) { color: var(--color-text); } +} diff --git a/frontend/src/app/shared/components/app-button/app-button.component.ts b/frontend/src/app/shared/components/app-button/app-button.component.ts index ac003a9..7ea1863 100644 --- a/frontend/src/app/shared/components/app-button/app-button.component.ts +++ b/frontend/src/app/shared/components/app-button/app-button.component.ts @@ -5,66 +5,8 @@ import { CommonModule } from '@angular/common'; selector: 'app-button', standalone: true, imports: [CommonModule], - template: ` - - `, - styles: [` - .btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 500; - cursor: pointer; - 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%; } - - .btn-primary { - background-color: var(--color-brand); - color: var(--color-neutral-900); - &: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); } - } - - .btn-outline { - background-color: transparent; - border-color: var(--color-border); - color: var(--color-text); - &:hover:not(:disabled) { - border-color: var(--color-brand); - color: var(--color-neutral-900); - background-color: rgba(250, 207, 10, 0.1); /* Low opacity brand color */ - } - } - - .btn-text { - background-color: transparent; - color: var(--color-text-muted); - padding: 0.5rem; - &:hover:not(:disabled) { color: var(--color-text); } - } - `] + templateUrl: './app-button.component.html', + styleUrl: './app-button.component.scss' }) export class AppButtonComponent { variant = input<'primary' | 'secondary' | 'outline' | 'text'>('primary'); diff --git a/frontend/src/app/shared/components/app-card/app-card.component.html b/frontend/src/app/shared/components/app-card/app-card.component.html new file mode 100644 index 0000000..c883bea --- /dev/null +++ b/frontend/src/app/shared/components/app-card/app-card.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/frontend/src/app/shared/components/app-card/app-card.component.scss b/frontend/src/app/shared/components/app-card/app-card.component.scss new file mode 100644 index 0000000..4488777 --- /dev/null +++ b/frontend/src/app/shared/components/app-card/app-card.component.scss @@ -0,0 +1,12 @@ +.card { + background-color: var(--color-bg-card); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); + padding: var(--space-6); + transition: box-shadow 0.2s; + + &:hover { + box-shadow: var(--shadow-md); + } +} diff --git a/frontend/src/app/shared/components/app-card/app-card.component.ts b/frontend/src/app/shared/components/app-card/app-card.component.ts index 05dc74b..9c5bcf2 100644 --- a/frontend/src/app/shared/components/app-card/app-card.component.ts +++ b/frontend/src/app/shared/components/app-card/app-card.component.ts @@ -3,24 +3,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-card', standalone: true, - template: ` -
- -
- `, - styles: [` - .card { - background-color: var(--color-bg-card); - border-radius: var(--radius-lg); - border: 1px solid var(--color-border); - box-shadow: var(--shadow-sm); - padding: var(--space-6); - transition: box-shadow 0.2s; - - &:hover { - box-shadow: var(--shadow-md); - } - } - `] + templateUrl: './app-card.component.html', + styleUrl: './app-card.component.scss' }) export class AppCardComponent {} diff --git a/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.html b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.html new file mode 100644 index 0000000..692d3a4 --- /dev/null +++ b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.html @@ -0,0 +1,26 @@ +
+ + +
+
+ +
+

{{ label() }}

+

{{ subtext() }}

+ + @if (fileNames().length > 0) { +
+ @for (name of fileNames(); track name) { +
{{ name }}
+ } +
+ } +
+
diff --git a/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss new file mode 100644 index 0000000..42c3b1c --- /dev/null +++ b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss @@ -0,0 +1,32 @@ +.dropzone { + border: 2px dashed var(--color-border); + border-radius: var(--radius-lg); + padding: var(--space-8); + text-align: center; + cursor: pointer; + transition: all 0.2s; + background-color: var(--color-neutral-50); + + &: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); } +.file-badges { + 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; +} diff --git a/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts index fd02157..fd1e563 100644 --- a/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts +++ b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts @@ -5,68 +5,8 @@ import { CommonModule } from '@angular/common'; selector: 'app-dropzone', standalone: true, imports: [CommonModule], - template: ` -
- - -
-
- -
-

{{ label() }}

-

{{ subtext() }}

- - @if (fileNames().length > 0) { -
- @for (name of fileNames(); track name) { -
{{ name }}
- } -
- } -
-
- `, - styles: [` - .dropzone { - border: 2px dashed var(--color-border); - border-radius: var(--radius-lg); - padding: var(--space-8); - text-align: center; - cursor: pointer; - transition: all 0.2s; - background-color: var(--color-neutral-50); - - &: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); } - .file-badges { - 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; - } - `] + templateUrl: './app-dropzone.component.html', + styleUrl: './app-dropzone.component.scss' }) export class AppDropzoneComponent { label = input('Drop files here or click to upload'); diff --git a/frontend/src/app/shared/components/app-input/app-input.component.html b/frontend/src/app/shared/components/app-input/app-input.component.html new file mode 100644 index 0000000..7e30cf2 --- /dev/null +++ b/frontend/src/app/shared/components/app-input/app-input.component.html @@ -0,0 +1,14 @@ +
+ @if (label()) { } + + @if (error()) { {{ error() }} } +
diff --git a/frontend/src/app/shared/components/app-input/app-input.component.scss b/frontend/src/app/shared/components/app-input/app-input.component.scss new file mode 100644 index 0000000..8919bfb --- /dev/null +++ b/frontend/src/app/shared/components/app-input/app-input.component.scss @@ -0,0 +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-control { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-size: 1rem; + 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; } +} +.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } diff --git a/frontend/src/app/shared/components/app-input/app-input.component.ts b/frontend/src/app/shared/components/app-input/app-input.component.ts index 929fe11..6723696 100644 --- a/frontend/src/app/shared/components/app-input/app-input.component.ts +++ b/frontend/src/app/shared/components/app-input/app-input.component.ts @@ -13,38 +13,8 @@ import { CommonModule } from '@angular/common'; multi: true } ], - template: ` -
- @if (label()) { } - - @if (error()) { {{ error() }} } -
- `, - styles: [` - .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); - border-radius: var(--radius-md); - font-size: 1rem; - 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; } - } - .error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } - `] + templateUrl: './app-input.component.html', + styleUrl: './app-input.component.scss' }) export class AppInputComponent implements ControlValueAccessor { label = input(''); diff --git a/frontend/src/app/shared/components/app-select/app-select.component.html b/frontend/src/app/shared/components/app-select/app-select.component.html new file mode 100644 index 0000000..ddb857a --- /dev/null +++ b/frontend/src/app/shared/components/app-select/app-select.component.html @@ -0,0 +1,16 @@ +
+ @if (label()) { } + + @if (error()) { {{ error() }} } +
diff --git a/frontend/src/app/shared/components/app-select/app-select.component.scss b/frontend/src/app/shared/components/app-select/app-select.component.scss new file mode 100644 index 0000000..bcf488a --- /dev/null +++ b/frontend/src/app/shared/components/app-select/app-select.component.scss @@ -0,0 +1,13 @@ +.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); + border-radius: var(--radius-md); + font-size: 1rem; + width: 100%; + background: var(--color-bg-card); + color: var(--color-text); + &:focus { outline: none; border-color: var(--color-brand); } +} +.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } diff --git a/frontend/src/app/shared/components/app-select/app-select.component.ts b/frontend/src/app/shared/components/app-select/app-select.component.ts index 84f0ead..e4e5e0d 100644 --- a/frontend/src/app/shared/components/app-select/app-select.component.ts +++ b/frontend/src/app/shared/components/app-select/app-select.component.ts @@ -13,39 +13,8 @@ import { CommonModule } from '@angular/common'; multi: true } ], - template: ` -
- @if (label()) { } - - @if (error()) { {{ error() }} } -
- `, - styles: [` - .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); - border-radius: var(--radius-md); - font-size: 1rem; - width: 100%; - background: var(--color-bg-card); - color: var(--color-text); - &:focus { outline: none; border-color: var(--color-brand); } - } - .error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } - `] + templateUrl: './app-select.component.html', + styleUrl: './app-select.component.scss' }) export class AppSelectComponent implements ControlValueAccessor { label = input(''); diff --git a/frontend/src/app/shared/components/app-tabs/app-tabs.component.html b/frontend/src/app/shared/components/app-tabs/app-tabs.component.html new file mode 100644 index 0000000..d85c910 --- /dev/null +++ b/frontend/src/app/shared/components/app-tabs/app-tabs.component.html @@ -0,0 +1,10 @@ +
+ @for (tab of tabs(); track tab.value) { + + } +
diff --git a/frontend/src/app/shared/components/app-tabs/app-tabs.component.scss b/frontend/src/app/shared/components/app-tabs/app-tabs.component.scss new file mode 100644 index 0000000..2825a0e --- /dev/null +++ b/frontend/src/app/shared/components/app-tabs/app-tabs.component.scss @@ -0,0 +1,21 @@ +.tabs { + display: flex; + border-bottom: 1px solid var(--color-border); + gap: var(--space-4); +} +.tab { + background: none; + border: none; + padding: var(--space-3) var(--space-4); + cursor: pointer; + font-weight: 500; + color: var(--color-text-muted); + border-bottom: 2px solid transparent; + transition: all 0.2s; + + &:hover { color: var(--color-text); } + &.active { + color: var(--color-brand); + border-bottom-color: var(--color-brand); + } +} diff --git a/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts b/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts index 27d093b..28ed4ec 100644 --- a/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts +++ b/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts @@ -6,41 +6,8 @@ import { TranslateModule } from '@ngx-translate/core'; selector: 'app-tabs', standalone: true, imports: [CommonModule, TranslateModule], - template: ` -
- @for (tab of tabs(); track tab.value) { - - } -
- `, - styles: [` - .tabs { - display: flex; - border-bottom: 1px solid var(--color-border); - gap: var(--space-4); - } - .tab { - background: none; - border: none; - padding: var(--space-3) var(--space-4); - cursor: pointer; - font-weight: 500; - color: var(--color-text-muted); - border-bottom: 2px solid transparent; - transition: all 0.2s; - - &:hover { color: var(--color-text); } - &.active { - color: var(--color-brand); - border-bottom-color: var(--color-brand); - } - } - `] + templateUrl: './app-tabs.component.html', + styleUrl: './app-tabs.component.scss' }) export class AppTabsComponent { tabs = input<{label: string, value: string}[]>([]); diff --git a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html new file mode 100644 index 0000000..afeb836 --- /dev/null +++ b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html @@ -0,0 +1,13 @@ +
+ @if (loading) { +
+
+ Loading 3D Model... +
+ } + @if (file && !loading) { +
+ {{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm +
+ } +
diff --git a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss new file mode 100644 index 0000000..32c4c4a --- /dev/null +++ b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss @@ -0,0 +1,44 @@ +.viewer-container { + width: 100%; + height: 300px; + background: var(--color-neutral-50); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + overflow: hidden; + position: relative; +} +.loading-overlay { + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + z-index: 10; + color: var(--color-text-muted); +} +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--color-neutral-200); + border-top-color: var(--color-brand); + border-radius: 50%; + animation: spin 1s linear infinite; +} +@keyframes spin { + 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; +} diff --git a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts index 45a9ee5..038c6b8 100644 --- a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts +++ b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts @@ -10,67 +10,8 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; selector: 'app-stl-viewer', standalone: true, imports: [CommonModule], - template: ` -
- @if (loading) { -
-
- Loading 3D Model... -
- } - @if (file && !loading) { -
- {{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm -
- } -
- `, - styles: [` - .viewer-container { - width: 100%; - height: 300px; - background: var(--color-neutral-50); - border-radius: var(--radius-lg); - border: 1px solid var(--color-border); - overflow: hidden; - position: relative; - } - .loading-overlay { - position: absolute; - inset: 0; - background: rgba(255, 255, 255, 0.8); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - z-index: 10; - color: var(--color-text-muted); - } - .spinner { - width: 32px; - height: 32px; - border: 3px solid var(--color-neutral-200); - border-top-color: var(--color-brand); - border-radius: 50%; - animation: spin 1s linear infinite; - } - @keyframes spin { - 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; - } - `] + templateUrl: './stl-viewer.component.html', + styleUrl: './stl-viewer.component.scss' }) export class StlViewerComponent implements OnInit, OnDestroy, OnChanges { @Input() file: File | null = null; diff --git a/frontend/src/app/shared/components/summary-card/summary-card.component.html b/frontend/src/app/shared/components/summary-card/summary-card.component.html new file mode 100644 index 0000000..adb2df4 --- /dev/null +++ b/frontend/src/app/shared/components/summary-card/summary-card.component.html @@ -0,0 +1,6 @@ +
+ {{ label() }} + + + +
diff --git a/frontend/src/app/shared/components/summary-card/summary-card.component.scss b/frontend/src/app/shared/components/summary-card/summary-card.component.scss new file mode 100644 index 0000000..3c8c0f9 --- /dev/null +++ b/frontend/src/app/shared/components/summary-card/summary-card.component.scss @@ -0,0 +1,29 @@ +.summary-card { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--space-3); + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + height: 100%; + justify-content: center; +} +.highlight { + background: var(--color-neutral-100); + border-color: var(--color-border); +} +.label { + font-size: 0.875rem; + color: var(--color-text-muted); + margin-bottom: var(--space-1); +} +.value { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-text); +} +.large { + font-size: 2rem; + color: var(--color-brand); +} diff --git a/frontend/src/app/shared/components/summary-card/summary-card.component.ts b/frontend/src/app/shared/components/summary-card/summary-card.component.ts index a15faa4..68a8f3f 100644 --- a/frontend/src/app/shared/components/summary-card/summary-card.component.ts +++ b/frontend/src/app/shared/components/summary-card/summary-card.component.ts @@ -5,45 +5,8 @@ import { CommonModule } from '@angular/common'; selector: 'app-summary-card', standalone: true, imports: [CommonModule], - template: ` -
- {{ label() }} - - - -
- `, - styles: [` - .summary-card { - display: flex; - flex-direction: column; - align-items: center; - padding: var(--space-3); - background: var(--color-bg-card); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - height: 100%; - justify-content: center; - } - .highlight { - background: var(--color-neutral-100); - border-color: var(--color-border); - } - .label { - font-size: 0.875rem; - color: var(--color-text-muted); - margin-bottom: var(--space-1); - } - .value { - font-size: 1.25rem; - font-weight: 700; - color: var(--color-text); - } - .large { - font-size: 2rem; - color: var(--color-brand); - } - `] + templateUrl: './summary-card.component.html', + styleUrl: './summary-card.component.scss' }) export class SummaryCardComponent { label = input.required(); -- 2.49.1 From 13790f205566fd1743ee4268fdd946a53a9bc402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 6 Feb 2026 13:25:41 +0100 Subject: [PATCH 08/17] feat(web): update color selector --- .../src/app/core/constants/colors.const.ts | 41 ++++++ .../calculator/calculator-page.component.ts | 7 +- .../upload-form/upload-form.component.html | 39 ++--- .../upload-form/upload-form.component.scss | 73 +++++++--- .../upload-form/upload-form.component.ts | 41 ++++-- .../services/quote-estimator.service.ts | 9 +- .../color-selector.component.html | 39 +++++ .../color-selector.component.scss | 136 ++++++++++++++++++ .../color-selector.component.ts | 40 ++++++ .../stl-viewer/stl-viewer.component.ts | 10 +- 10 files changed, 380 insertions(+), 55 deletions(-) create mode 100644 frontend/src/app/core/constants/colors.const.ts create mode 100644 frontend/src/app/shared/components/color-selector/color-selector.component.html create mode 100644 frontend/src/app/shared/components/color-selector/color-selector.component.scss create mode 100644 frontend/src/app/shared/components/color-selector/color-selector.component.ts diff --git a/frontend/src/app/core/constants/colors.const.ts b/frontend/src/app/core/constants/colors.const.ts new file mode 100644 index 0000000..66b1336 --- /dev/null +++ b/frontend/src/app/core/constants/colors.const.ts @@ -0,0 +1,41 @@ +export interface ColorOption { + label: string; + value: string; + hex: string; + outOfStock?: boolean; +} + +export interface ColorCategory { + name: string; // 'Glossy' | 'Matte' + colors: ColorOption[]; +} + +export const PRODUCT_COLORS: ColorCategory[] = [ + { + name: 'Lucidi', // Glossy + colors: [ + { label: 'Black', value: 'Black', hex: '#1a1a1a' }, // Not pure black for visibility + { label: 'White', value: 'White', hex: '#f5f5f5' }, + { label: 'Red', value: 'Red', hex: '#d32f2f', outOfStock: true }, + { label: 'Blue', value: 'Blue', hex: '#1976d2' }, + { label: 'Green', value: 'Green', hex: '#388e3c' }, + { label: 'Yellow', value: 'Yellow', hex: '#fbc02d' } + ] + }, + { + name: 'Opachi', // Matte + colors: [ + { label: 'Matte Black', value: 'Matte Black', hex: '#2c2c2c' }, // Lighter charcoal for matte + { label: 'Matte White', value: 'Matte White', hex: '#e0e0e0' }, + { label: 'Matte Gray', value: 'Matte Gray', hex: '#757575' } + ] + } +]; + +export function getColorHex(value: string): string { + for (const cat of PRODUCT_COLORS) { + const found = cat.colors.find(c => c.value === value); + if (found) return found.hex; + } + return '#facf0a'; // Default Brand Color if not found +} diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index ac8aefe..7122a4b 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -72,11 +72,14 @@ export class CalculatorPageComponent { details += `- File:\n`; req.items.forEach(item => { - details += ` * ${item.file.name} (Qtà: ${item.quantity})\n`; + details += ` * ${item.file.name} (Qtà: ${item.quantity}`; + if (item.color) { + details += `, Colore: ${item.color}`; + } + details += `)\n`; }); if (req.mode === 'advanced') { - if (req.color) details += `- Colore: ${req.color}\n`; if (req.infillDensity) details += `- Infill: ${req.infillDensity}%\n`; } diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html index 334fd93..e89b46d 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html @@ -3,7 +3,10 @@
@if (selectedFile()) {
- + +
} @@ -29,15 +32,25 @@
-
- - +
+
+ + +
+ +
+ + + +
diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index 6415414..c2ec404 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -46,6 +46,12 @@ export class UploadFormComponent { { label: 'Standard', value: 'Standard' }, { label: 'Alta definizione', value: 'High' } ]; + + printSpeeds = [ + { label: 'Slow (High Quality)', value: 'Slow' }, + { label: 'Standard', value: 'Standard' }, + { label: 'Fast (Draft)', value: 'Fast' } + ]; infillPatterns = [ { label: 'Grid', value: 'grid' }, @@ -53,6 +59,15 @@ export class UploadFormComponent { { label: 'Cubic', value: 'cubic' }, { label: 'Triangles', value: 'triangles' } ]; + + layerHeights = [ + { label: '0.08 mm', value: 0.08 }, + { label: '0.12 mm (High Quality - Slow)', value: 0.12 }, + { label: '0.16 mm', value: 0.16 }, + { label: '0.20 mm (Standard)', value: 0.20 }, + { label: '0.24 mm', value: 0.24 }, + { label: '0.28 mm (Draft - Fast)', value: 0.28 } + ]; acceptedFormats = '.stl,.3mf,.step,.stp,.obj,.amf,.ply,.igs,.iges'; @@ -61,10 +76,12 @@ export class UploadFormComponent { itemsTouched: [false], // Hack to track touched state for custom items list material: ['PLA', Validators.required], quality: ['Standard', Validators.required], + printSpeed: ['Standard', Validators.required], notes: [''], // Advanced fields // Color removed from global form infillDensity: [20, [Validators.min(0), Validators.max(100)]], + layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]], infillPattern: ['grid'], supportEnabled: [false] }); diff --git a/frontend/src/app/shared/components/app-select/app-select.component.html b/frontend/src/app/shared/components/app-select/app-select.component.html index ddb857a..dee40ad 100644 --- a/frontend/src/app/shared/components/app-select/app-select.component.html +++ b/frontend/src/app/shared/components/app-select/app-select.component.html @@ -2,14 +2,14 @@ @if (label()) { } @if (error()) { {{ error() }} } diff --git a/frontend/src/app/shared/components/app-select/app-select.component.ts b/frontend/src/app/shared/components/app-select/app-select.component.ts index e4e5e0d..4f49c97 100644 --- a/frontend/src/app/shared/components/app-select/app-select.component.ts +++ b/frontend/src/app/shared/components/app-select/app-select.component.ts @@ -1,11 +1,11 @@ import { Component, input, output, forwardRef } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-select', standalone: true, - imports: [CommonModule, ReactiveFormsModule], + imports: [CommonModule, ReactiveFormsModule, FormsModule], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -33,9 +33,8 @@ export class AppSelectComponent implements ControlValueAccessor { registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } - onSelect(event: Event) { - const val = (event.target as HTMLSelectElement).value; - this.value = val; - this.onChange(val); + onModelChange(val: any) { + this.value = val; + this.onChange(val); } } diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index c7abd1b..56fad45 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -17,17 +17,19 @@ "CTA_START": "Inizia Ora", "BUSINESS": "Aziende", "PRIVATE": "Privati", - "MODE_EASY": "Rapida", + "MODE_EASY": "Base", "MODE_ADVANCED": "Avanzata", "UPLOAD_LABEL": "Trascina il tuo file 3D qui", "UPLOAD_SUB": "Supportiamo STL, 3MF, STEP, OBJ fino a 50MB", "MATERIAL": "Materiale", "QUALITY": "Qualità", + "PRINT_SPEED": "Velocità di Stampa", "QUANTITY": "Quantità", "NOTES": "Note aggiuntive", "COLOR": "Colore", "INFILL": "Riempimento (%)", "PATTERN": "Pattern di riempimento", + "LAYER_HEIGHT": "Altezza Layer", "SUPPORT": "Supporti", "SUPPORT_DESC": "Abilita supporti per sporgenze", "CALCULATE": "Calcola Preventivo", -- 2.49.1 From 4f301b1652ffdd7a4d8d3006b185293c1bd508dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 16:06:19 +0100 Subject: [PATCH 11/17] feat(web): update quality print advanced and base --- .../upload-form/upload-form.component.html | 6 +++--- .../upload-form/upload-form.component.ts | 14 ++++++++------ .../calculator/services/quote-estimator.service.ts | 12 +++++++++++- frontend/src/assets/i18n/en.json | 6 ++++++ frontend/src/assets/i18n/it.json | 1 + 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html index d5a7876..594415a 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.html @@ -91,9 +91,9 @@ > } @else { }
diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index c2ec404..4dd76ab 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -47,10 +47,11 @@ export class UploadFormComponent { { label: 'Alta definizione', value: 'High' } ]; - printSpeeds = [ - { label: 'Slow (High Quality)', value: 'Slow' }, - { label: 'Standard', value: 'Standard' }, - { label: 'Fast (Draft)', value: 'Fast' } + nozzleDiameters = [ + { label: '0.2 mm (+2 CHF)', value: 0.2 }, + { label: '0.4 mm (Standard)', value: 0.4 }, + { label: '0.6 mm (+2 CHF)', value: 0.6 }, + { label: '0.8 mm (+2 CHF)', value: 0.8 } ]; infillPatterns = [ @@ -66,7 +67,7 @@ export class UploadFormComponent { { label: '0.16 mm', value: 0.16 }, { label: '0.20 mm (Standard)', value: 0.20 }, { label: '0.24 mm', value: 0.24 }, - { label: '0.28 mm (Draft - Fast)', value: 0.28 } + { label: '0.28 mm', value: 0.28 } ]; acceptedFormats = '.stl,.3mf,.step,.stp,.obj,.amf,.ply,.igs,.iges'; @@ -76,12 +77,13 @@ export class UploadFormComponent { itemsTouched: [false], // Hack to track touched state for custom items list material: ['PLA', Validators.required], quality: ['Standard', Validators.required], - printSpeed: ['Standard', Validators.required], + // Print Speed removed notes: [''], // Advanced fields // Color removed from global form infillDensity: [20, [Validators.min(0), Validators.max(100)]], layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]], + nozzleDiameter: [0.4, Validators.required], infillPattern: ['grid'], supportEnabled: [false] }); diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index de25a77..e8e3e1c 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -13,6 +13,8 @@ export interface QuoteRequest { infillDensity?: number; infillPattern?: string; supportEnabled?: boolean; + layerHeight?: number; + nozzleDiameter?: number; mode: 'easy' | 'advanced'; } @@ -77,6 +79,8 @@ export class QuoteEstimatorService { if (request.infillDensity) formData.append('infill_density', request.infillDensity.toString()); if (request.infillPattern) formData.append('infill_pattern', request.infillPattern); if (request.supportEnabled) formData.append('support_enabled', 'true'); + if (request.layerHeight) formData.append('layer_height', request.layerHeight.toString()); + if (request.nozzleDiameter) formData.append('nozzle_diameter', request.nozzleDiameter.toString()); } const headers: any = {}; @@ -123,7 +127,13 @@ export class QuoteEstimatorService { observer.next(100); // Calculate Results - const setupCost = 10; + // Base setup cost + let setupCost = 10; + + // Surcharge for non-standard nozzle + if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) { + setupCost += 2; + } const items: QuoteItem[] = []; diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 826df25..0566e16 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -25,6 +25,12 @@ "QUALITY": "Quality", "QUANTITY": "Quantity", "NOTES": "Additional Notes", + "NOZZLE": "Nozzle Diameter", + "INFILL": "Infill (%)", + "PATTERN": "Infill Pattern", + "LAYER_HEIGHT": "Layer Height", + "SUPPORT": "Supports", + "SUPPORT_DESC": "Enable supports for overhangs", "CALCULATE": "Calculate Quote", "RESULT": "Estimated Quote", "TIME": "Print Time", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 56fad45..d1ee0c5 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -30,6 +30,7 @@ "INFILL": "Riempimento (%)", "PATTERN": "Pattern di riempimento", "LAYER_HEIGHT": "Altezza Layer", + "NOZZLE": "Diametro Ugello", "SUPPORT": "Supporti", "SUPPORT_DESC": "Abilita supporti per sporgenze", "CALCULATE": "Calcola Preventivo", -- 2.49.1 From b3c0413b7cc10b698cccdb30e1353767de0017d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 17:38:02 +0100 Subject: [PATCH 12/17] feat(web): improvements in home and about us --- .../features/about/about-page.component.html | 2 + .../features/about/about-page.component.ts | 4 +- .../src/app/features/home/home.component.html | 70 +++++------ .../src/app/features/home/home.component.scss | 78 ++++++------ .../app-locations.component.html | 57 +++++++++ .../app-locations.component.scss | 116 ++++++++++++++++++ .../app-locations/app-locations.component.ts | 19 +++ frontend/src/assets/i18n/en.json | 9 ++ frontend/src/assets/i18n/it.json | 9 ++ 9 files changed, 288 insertions(+), 76 deletions(-) create mode 100644 frontend/src/app/shared/components/app-locations/app-locations.component.html create mode 100644 frontend/src/app/shared/components/app-locations/app-locations.component.scss create mode 100644 frontend/src/app/shared/components/app-locations/app-locations.component.ts diff --git a/frontend/src/app/features/about/about-page.component.html b/frontend/src/app/features/about/about-page.component.html index 51321c3..9dd3f12 100644 --- a/frontend/src/app/features/about/about-page.component.html +++ b/frontend/src/app/features/about/about-page.component.html @@ -40,3 +40,5 @@
+ + diff --git a/frontend/src/app/features/about/about-page.component.ts b/frontend/src/app/features/about/about-page.component.ts index edb3d90..dcb93f8 100644 --- a/frontend/src/app/features/about/about-page.component.ts +++ b/frontend/src/app/features/about/about-page.component.ts @@ -1,11 +1,11 @@ import { Component } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; - +import { AppLocationsComponent } from '../../shared/components/app-locations/app-locations.component'; @Component({ selector: 'app-about-page', standalone: true, - imports: [TranslateModule], + imports: [TranslateModule, AppLocationsComponent], templateUrl: './about-page.component.html', styleUrl: './about-page.component.scss' }) diff --git a/frontend/src/app/features/home/home.component.html b/frontend/src/app/features/home/home.component.html index 8380ff0..f1c0826 100644 --- a/frontend/src/app/features/home/home.component.html +++ b/frontend/src/app/features/home/home.component.html @@ -7,13 +7,17 @@ Prezzo e tempi in pochi secondi.
Dal file 3D al pezzo finito. +

+ Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese. +

- Lavoriamo con trasparenza su costi, qualità e tempi. Produciamo prototipi, pezzi personalizzati - e piccole serie con supporto tecnico reale. + 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.

- Parla con noi + Calcola Preventivo Vai allo shop + Parla con noi
@@ -22,13 +26,12 @@
-

Preventivo immediato

+

Preventivo immediato in pochi secondi

- Carica il file 3D e ottieni subito costo e tempo di stampa. Nessuna registrazione. + Nessuna registrazione necessaria. Non salviamo il tuo file fino al momento dell'ordine e la stima è effettuata tramite vero slicing.

  • Formati supportati: STL, 3MF, STEP, OBJ
  • -
  • Materiali disponibili: PLA, PETG, TPU
  • Qualità: bozza, standard, alta definizione
@@ -45,19 +48,9 @@
  • Scegli materiale e qualità
  • Ricevi subito costo e tempo
  • -
    -
    - Modalità - Rapida / Avanzata -
    -
    - Output - Ordina o richiedi consulenza -
    -
    Apri calcolatore - Parla con noi + Parla con noi
    @@ -74,20 +67,32 @@
    +
    + +

    Prototipazione veloce

    -

    Valida idee e funzioni in pochi giorni con preventivo immediato.

    +

    Dal file digitale al modello fisico in 24/48 ore. Verifica ergonomia, incastri e funzionamento prima dello stampo definitivo.

    +
    + +

    Pezzi personalizzati

    -

    Componenti unici o in mini serie per clienti, macchine e prodotti.

    +

    Componenti unici impossibili da trovare in commercio. Riproduciamo parti rotte o creiamo adattamenti su misura.

    +
    + +

    Piccole serie

    -

    Produzione controllata fino a 500 pezzi con qualità costante.

    +

    Produzione ponte o serie limitate (10-500 pezzi) con qualità ripetibile. Ideale per validazione di mercato.

    +
    + +

    Consulenza e CAD

    -

    Supporto tecnico per progettazione, modifiche e ottimizzazione.

    +

    Non hai il file 3D? Ti aiutiamo a progettarlo, ottimizzarlo per la stampa e scegliere il materiale giusto.

    @@ -108,7 +113,7 @@
    Scopri i prodotti - Richiedi una soluzione + Richiedi una soluzione
    @@ -136,25 +141,12 @@ 3D fab è un laboratorio tecnico di stampa 3D. Seguiamo progetti dalla consulenza iniziale alla produzione, con tempi chiari e supporto diretto.

    -

    - Qui puoi inserire descrizioni più dettagliate del team, del laboratorio e dei progetti in corso. -

    - Contattaci + Contattaci
    -
    -
    -
    -

    Foto laboratorio / stampanti

    -
    -
    -
    -

    Dettagli qualità e finiture

    -
    -
    -
    -

    Team, prototipi o casi studio

    -
    +
    + + Foto Founders
    diff --git a/frontend/src/app/features/home/home.component.scss b/frontend/src/app/features/home/home.component.scss index 5e406d9..147d469 100644 --- a/frontend/src/app/features/home/home.component.scss +++ b/frontend/src/app/features/home/home.component.scss @@ -15,10 +15,10 @@ position: absolute; inset: 0; @include patterns.pattern-grid(var(--color-neutral-900), 40px, 1px); - opacity: 0.12; + opacity: 0.06; z-index: 0; pointer-events: none; - mask-image: linear-gradient(to bottom, black 40%, transparent 100%); + mask-image: linear-gradient(to bottom, black 70%, transparent 100%); } } @@ -43,6 +43,7 @@ position: relative; z-index: 1; } + .hero-copy { animation: fadeUp 0.8s ease both; } .hero-panel { animation: fadeUp 0.8s ease 0.15s both; } @@ -61,10 +62,18 @@ letter-spacing: -0.02em; margin-bottom: var(--space-4); } + .hero-lead { + font-size: 1.35rem; + font-weight: 500; + color: var(--color-neutral-900); + margin-bottom: var(--space-3); + max-width: 600px; + } .hero-subtitle { - font-size: 1.2rem; + font-size: 1.1rem; color: var(--color-text-muted); max-width: 560px; + line-height: 1.6; } .hero-actions { display: flex; @@ -135,6 +144,9 @@ padding: 0.35rem 0.75rem; font-size: 0.8rem; font-weight: 600; + color: var(--color-brand-600); + background: var(--color-brand-50); + border-color: var(--color-brand-200); } .quote-steps { list-style: none; @@ -177,14 +189,10 @@ .capabilities { position: relative; + border-bottom: 1px solid var(--color-border); } .capabilities-bg { - position: absolute; - inset: 0; - @include patterns.pattern-rectilinear(var(--color-neutral-900), 24px, 1px); - opacity: 0.05; - pointer-events: none; - z-index: 0; + display: none; } .section { padding: 5.5rem 0; position: relative; } @@ -194,24 +202,13 @@ .text-muted { color: var(--color-text-muted); } .calculator { - background: var(--color-neutral-50); - border-top: 1px solid var(--color-border); - border-bottom: 1px solid var(--color-border); position: relative; - // Honeycomb Pattern - &::before { - content: ''; - position: absolute; - inset: 0; - @include patterns.pattern-honeycomb(var(--color-neutral-900), 24px); - opacity: 0.04; - pointer-events: none; - } + border-bottom: 1px solid var(--color-border); } .calculator-grid { display: grid; gap: var(--space-10); - align-items: center; + align-items: start; position: relative; z-index: 1; } @@ -225,6 +222,19 @@ gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } + + .card-image-placeholder { + width: 100%; + height: 160px; + background: var(--color-neutral-100); + margin: -1.5rem -1.5rem 1.5rem -1.5rem; /* Negative margins to bleed to edge */ + width: calc(100% + 3rem); + border-bottom: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-neutral-400); + } .shop { background: var(--color-neutral-50); @@ -282,24 +292,21 @@ align-items: center; } .about-media { - display: grid; - gap: var(--space-4); + position: relative; } - .media-grid { - display: grid; - gap: var(--space-4); - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - } - .media-tile { - display: grid; - gap: var(--space-2); - } - .media-photo { + + .about-feature-image { width: 100%; - aspect-ratio: 4 / 3; + height: 100%; + min-height: 320px; + object-fit: cover; border-radius: var(--radius-lg); background: var(--color-neutral-100); border: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-text-muted); } .media-tile p { margin: 0; @@ -313,6 +320,7 @@ @media (min-width: 960px) { .hero-grid { grid-template-columns: 1.1fr 0.9fr; } .calculator-grid { grid-template-columns: 1.1fr 0.9fr; } + .calculator-grid { grid-template-columns: 1.1fr 0.9fr; } .split { grid-template-columns: 1.1fr 0.9fr; } .about-grid { grid-template-columns: 1.1fr 0.9fr; } } diff --git a/frontend/src/app/shared/components/app-locations/app-locations.component.html b/frontend/src/app/shared/components/app-locations/app-locations.component.html new file mode 100644 index 0000000..e8a827c --- /dev/null +++ b/frontend/src/app/shared/components/app-locations/app-locations.component.html @@ -0,0 +1,57 @@ +
    +
    +
    +

    {{ 'LOCATIONS.TITLE' | translate }}

    +

    {{ 'LOCATIONS.SUBTITLE' | translate }}

    +
    + +
    +
    +
    + + +
    + +
    +
    +

    {{ 'LOCATIONS.TICINO' | translate }}

    +

    {{ 'LOCATIONS.ADDRESS_TICINO' | translate }}

    +
    +
    +

    {{ 'LOCATIONS.BIENNE' | translate }}

    +

    {{ 'LOCATIONS.ADDRESS_BIENNE' | translate }}

    +
    + + +
    +
    + +
    + + +
    +
    +
    +
    diff --git a/frontend/src/app/shared/components/app-locations/app-locations.component.scss b/frontend/src/app/shared/components/app-locations/app-locations.component.scss new file mode 100644 index 0000000..2396f8d --- /dev/null +++ b/frontend/src/app/shared/components/app-locations/app-locations.component.scss @@ -0,0 +1,116 @@ +.locations-section { + padding: 6rem 0; + background: var(--color-surface-card); + border-top: 1px solid var(--color-border); +} + +.section-header { + text-align: center; + margin-bottom: 4rem; + + h2 { + font-size: 2.5rem; + margin-bottom: 1rem; + color: var(--color-text-main); + } + + .subtitle { + font-size: 1.1rem; + color: var(--color-text-muted); + max-width: 600px; + margin: 0 auto; + } +} + +.locations-grid { + display: grid; + grid-template-columns: 1fr; + gap: 3rem; + align-items: start; + + @media(min-width: 992px) { + grid-template-columns: 1fr 2fr; + } +} + +.location-tabs { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + background: var(--color-bg); + padding: 0.5rem; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); +} + +.tab-btn { + flex: 1; + padding: 0.75rem; + border: none; + background: transparent; + border-radius: var(--radius-sm); + color: var(--color-text-muted); + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + color: var(--color-text-main); + } + + &.active { + background: var(--color-primary-500); + color: var(--color-neutral-900); + box-shadow: var(--shadow-sm); + } +} + +.location-details { + padding: 2rem; + background: var(--color-bg); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-md); + + h3 { + margin-bottom: 1rem; + font-size: 1.5rem; + } + + p { + color: var(--color-text-muted); + margin-bottom: 2rem; + line-height: 1.6; + } +} + +.contact-btn { + display: inline-block; + padding: 0.75rem 2rem; + background: var(--color-primary-500); + color: var(--color-neutral-900); + text-decoration: none; + border-radius: var(--radius-md); + font-weight: 600; + transition: all 0.2s ease; + + &:hover { + background: var(--color-primary-600); + transform: translateY(-2px); + box-shadow: var(--shadow-md); + } +} + +.map-container { + border-radius: var(--radius-lg); + overflow: hidden; + border: 1px solid var(--color-border); + box-shadow: var(--shadow-lg); + background: var(--color-bg); + height: 450px; + + iframe { + width: 100%; + height: 100%; + } +} diff --git a/frontend/src/app/shared/components/app-locations/app-locations.component.ts b/frontend/src/app/shared/components/app-locations/app-locations.component.ts new file mode 100644 index 0000000..89988ff --- /dev/null +++ b/frontend/src/app/shared/components/app-locations/app-locations.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-locations', + standalone: true, + imports: [CommonModule, TranslateModule, RouterLink], + templateUrl: './app-locations.component.html', + styleUrl: './app-locations.component.scss' +}) +export class AppLocationsComponent { + selectedLocation: 'ticino' | 'bienne' = 'ticino'; + + selectLocation(location: 'ticino' | 'bienne') { + this.selectedLocation = location; + } +} diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 0566e16..8091b49 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -68,6 +68,15 @@ "TARGET_TEXT": "Small businesses, freelancers, makers and customers looking for a ready-made product from the shop.", "TEAM_TITLE": "Our Team" }, + "LOCATIONS": { + "TITLE": "Our Locations", + "SUBTITLE": "We have two locations to serve you better. Select a location to see details.", + "TICINO": "Ticino", + "BIENNE": "Bienne", + "ADDRESS_TICINO": "Ticino Office, Switzerland", + "ADDRESS_BIENNE": "Bienne Office, Switzerland", + "CONTACT_US": "Contact Us" + }, "CONTACT": { "TITLE": "Contact Us", "SEND": "Send Message", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index d1ee0c5..98e4dc8 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -70,6 +70,15 @@ "TARGET_TEXT": "Piccole aziende, freelance, smanettoni e clienti che cercano un prodotto già pronto dallo shop.", "TEAM_TITLE": "Il Nostro Team" }, + "LOCATIONS": { + "TITLE": "Le Nostre Sedi", + "SUBTITLE": "Siamo presenti in due sedi per coprire meglio il territorio. Seleziona la sede per vedere i dettagli.", + "TICINO": "Ticino", + "BIENNE": "Bienne", + "ADDRESS_TICINO": "Sede Ticino, Svizzera", + "ADDRESS_BIENNE": "Sede Bienne, Svizzera", + "CONTACT_US": "Contattaci" + }, "CONTACT": { "TITLE": "Contattaci", "SEND": "Invia Messaggio", -- 2.49.1 From 78af87ac3cd54288e31480a1d4d25deb8d651d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 17:49:36 +0100 Subject: [PATCH 13/17] feat(web): new step for user details --- .../calculator/calculator-page.component.ts | 28 +++- .../quote-result/quote-result.component.html | 9 +- .../quote-result/quote-result.component.ts | 1 + .../user-details/user-details.component.html | 120 ++++++++++++++++++ .../user-details/user-details.component.scss | 102 +++++++++++++++ .../user-details/user-details.component.ts | 59 +++++++++ .../services/quote-estimator.service.ts | 113 ++--------------- frontend/src/assets/i18n/en.json | 29 ++++- 8 files changed, 353 insertions(+), 108 deletions(-) create mode 100644 frontend/src/app/features/calculator/components/user-details/user-details.component.html create mode 100644 frontend/src/app/features/calculator/components/user-details/user-details.component.scss create mode 100644 frontend/src/app/features/calculator/components/user-details/user-details.component.ts diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts index 7122a4b..1982bbf 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.ts +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -6,23 +6,28 @@ import { AppCardComponent } from '../../shared/components/app-card/app-card.comp import { AppAlertComponent } from '../../shared/components/app-alert/app-alert.component'; import { UploadFormComponent } from './components/upload-form/upload-form.component'; import { QuoteResultComponent } from './components/quote-result/quote-result.component'; +import { UserDetailsComponent } from './components/user-details/user-details.component'; import { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quote-estimator.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-calculator-page', standalone: true, - imports: [CommonModule, TranslateModule, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent], + imports: [CommonModule, TranslateModule, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent, UserDetailsComponent], templateUrl: './calculator-page.component.html', styleUrl: './calculator-page.component.scss' }) export class CalculatorPageComponent { mode = signal('easy'); + step = signal<'upload' | 'quote' | 'details'>('upload'); + loading = signal(false); uploadProgress = signal(0); result = signal(null); error = signal(false); + orderSuccess = signal(false); + @ViewChild('uploadForm') uploadForm!: UploadFormComponent; @ViewChild('resultCol') resultCol!: ElementRef; @@ -34,6 +39,7 @@ export class CalculatorPageComponent { this.uploadProgress.set(0); this.error.set(false); this.result.set(null); + this.orderSuccess.set(false); // Auto-scroll on mobile to make analysis visible setTimeout(() => { @@ -51,6 +57,7 @@ export class CalculatorPageComponent { this.result.set(event as QuoteResult); this.loading.set(false); this.uploadProgress.set(100); + this.step.set('quote'); } }, error: () => { @@ -60,6 +67,25 @@ export class CalculatorPageComponent { }); } + onProceed() { + this.step.set('details'); + } + + onCancelDetails() { + this.step.set('quote'); + } + + onSubmitOrder(orderData: any) { + console.log('Order Submitted:', orderData); + this.orderSuccess.set(true); + this.step.set('upload'); // Reset to start, or show success page? + // For now, let's show success message and reset + setTimeout(() => { + this.orderSuccess.set(false); + }, 5000); + this.result.set(null); + } + private currentRequest: QuoteRequest | null = null; onConsult() { diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html index bc78d4e..f41ecb8 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html @@ -56,7 +56,12 @@
    - {{ 'CALC.ORDER' | translate }} - {{ 'CALC.CONSULT' | translate }} + + {{ 'QUOTE.CONSULT' | translate }} + + + + {{ 'QUOTE.PROCEED_ORDER' | translate }} +
    diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts index eda5a6f..daeb3cd 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -17,6 +17,7 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service'; export class QuoteResultComponent { result = input.required(); consult = output(); + proceed = output(); itemChange = output<{fileName: string, quantity: number}>(); // Local mutable state for items to handle quantity changes diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.html b/frontend/src/app/features/calculator/components/user-details/user-details.component.html new file mode 100644 index 0000000..a080cd6 --- /dev/null +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.html @@ -0,0 +1,120 @@ +
    +
    +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + + + + +
    +
    + + +
    +
    + + +
    +
    + +
    + + {{ 'COMMON.BACK' | translate }} + + + {{ 'USER_DETAILS.SUBMIT' | translate }} + +
    + +
    +
    +
    + + +
    + + +
    +
    +
    + {{ item.fileName }} + {{ item.material }} - {{ item.color || 'Default' }} +
    +
    x{{ item.quantity }}
    +
    {{ (item.unitPrice * item.quantity) | currency:'CHF' }}
    +
    + +
    + +
    + {{ 'QUOTE.TOTAL' | translate }} + {{ quote()!.totalPrice | currency:'CHF' }} +
    +
    + +
    +
    +
    +
    diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.scss b/frontend/src/app/features/calculator/components/user-details/user-details.component.scss new file mode 100644 index 0000000..734880a --- /dev/null +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.scss @@ -0,0 +1,102 @@ +.user-details-container { + padding: 1rem 0; +} + +.row { + display: flex; + flex-wrap: wrap; + margin: 0 -0.5rem; + + > [class*='col-'] { + padding: 0 0.5rem; + } +} + +.col-md-6 { + width: 100%; + + @media (min-width: 768px) { + width: 50%; + } +} + +.col-md-4 { + width: 100%; + + @media (min-width: 768px) { + width: 33.333%; + } +} + +.col-md-8 { + width: 100%; + + @media (min-width: 768px) { + width: 66.666%; + } +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 1.5rem; +} + +// Summary Styles +.summary-content { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.summary-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + &:last-child { + border-bottom: none; + } +} + +.item-info { + display: flex; + flex-direction: column; + flex: 1; +} + +.item-name { + font-weight: 500; +} + +.item-meta { + font-size: 0.85rem; + opacity: 0.7; +} + +.item-qty { + margin: 0 1rem; + opacity: 0.8; +} + +.item-price { + font-weight: 600; +} + +.total-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 1.2rem; + font-weight: 700; + margin-top: 1rem; + padding-top: 1rem; + border-top: 2px solid rgba(255, 255, 255, 0.2); + + .total-price { + color: var(--primary-color, #00C853); // Fallback color + } +} diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.ts b/frontend/src/app/features/calculator/components/user-details/user-details.component.ts new file mode 100644 index 0000000..9656001 --- /dev/null +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.ts @@ -0,0 +1,59 @@ +import { Component, input, output, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component'; +import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component'; +import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component'; +import { QuoteResult } from '../../services/quote-estimator.service'; + +@Component({ + selector: 'app-user-details', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppCardComponent, AppInputComponent, AppButtonComponent], + templateUrl: './user-details.component.html', + styleUrl: './user-details.component.scss' +}) +export class UserDetailsComponent { + quote = input(); + submitOrder = output(); + cancel = output(); + + form: FormGroup; + submitting = signal(false); + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + name: ['', Validators.required], + surname: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + phone: ['', Validators.required], + address: ['', Validators.required], + zip: ['', Validators.required], + city: ['', Validators.required] + }); + } + + onSubmit() { + if (this.form.valid) { + this.submitting.set(true); + + const orderData = { + customer: this.form.value, + quote: this.quote() + }; + + // Simulate API delay + setTimeout(() => { + this.submitOrder.emit(orderData); + this.submitting.set(false); + }, 1000); + } else { + this.form.markAllAsTouched(); + } + } + + onCancel() { + this.cancel.emit(); + } +} diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index e8e3e1c..3b4de2a 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -24,6 +24,8 @@ export interface QuoteItem { unitTime: number; // seconds unitWeight: number; // grams quantity: number; + material?: string; + color?: string; // Computed values for UI convenience (optional, can be done in component) } @@ -37,114 +39,21 @@ export interface QuoteResult { totalTimeMinutes: number; totalWeight: number; } - -interface BackendResponse { - success: boolean; - data: { - print_time_seconds: number; - material_grams: number; - cost: { - total: number; - }; - }; - error?: string; -} - -@Injectable({ - providedIn: 'root' -}) -export class QuoteEstimatorService { - private http = inject(HttpClient); - - calculate(request: QuoteRequest): Observable { - if (request.items.length === 0) return of(); - - return new Observable(observer => { - const totalItems = request.items.length; - const allProgress: number[] = new Array(totalItems).fill(0); - const finalResponses: any[] = []; - let completedRequests = 0; - - const uploads = request.items.map((item, index) => { - const formData = new FormData(); - formData.append('file', item.file); - formData.append('machine', 'bambu_a1'); - formData.append('filament', this.mapMaterial(request.material)); - formData.append('quality', this.mapQuality(request.quality)); - - // Send color for both modes if present, defaulting to Black - formData.append('material_color', item.color || 'Black'); - - if (request.mode === 'advanced') { - if (request.infillDensity) formData.append('infill_density', request.infillDensity.toString()); - if (request.infillPattern) formData.append('infill_pattern', request.infillPattern); - if (request.supportEnabled) formData.append('support_enabled', 'true'); - if (request.layerHeight) formData.append('layer_height', request.layerHeight.toString()); - if (request.nozzleDiameter) formData.append('nozzle_diameter', request.nozzleDiameter.toString()); - } - - const headers: any = {}; - // @ts-ignore - if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); - - return this.http.post(`${environment.apiUrl}/api/quote`, formData, { - headers, - reportProgress: true, - observe: 'events' - }).pipe( - map(event => ({ item, event, index })), - catchError(err => of({ item, error: err, index })) - ); - }); - - // Subscribe to all - uploads.forEach((obs) => { - obs.subscribe({ - next: (wrapper: any) => { - const idx = wrapper.index; - - if (wrapper.error) { - finalResponses[idx] = { success: false, fileName: wrapper.item.file.name }; - return; - } - - const event = wrapper.event; - if (event.type === 1) { // HttpEventType.UploadProgress - if (event.total) { - const percent = Math.round((100 * event.loaded) / event.total); - allProgress[idx] = percent; - // Emit average progress - const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / totalItems); - observer.next(avg); - } - } else if (event.type === 4) { // HttpEventType.Response - allProgress[idx] = 100; - finalResponses[idx] = { ...event.body, fileName: wrapper.item.file.name, originalQty: wrapper.item.quantity }; - completedRequests++; - - if (completedRequests === totalItems) { - // All done - observer.next(100); - - // Calculate Results - // Base setup cost - let setupCost = 10; - - // Surcharge for non-standard nozzle - if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) { - setupCost += 2; - } - - const items: QuoteItem[] = []; - - finalResponses.forEach(res => { +// ... (skip down to calculate logic) + finalResponses.forEach((res, idx) => { if (res && res.success) { + // Find original item to get color + const originalItem = request.items[idx]; + // Note: responses and request.items are index-aligned because we mapped them + items.push({ fileName: res.fileName, unitPrice: res.data.cost.total, unitTime: res.data.print_time_seconds, unitWeight: res.data.material_grams, - quantity: res.originalQty // Use the requested quantity + quantity: res.originalQty, + material: request.material, + color: originalItem.color || 'Default' }); } }); diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 8091b49..938928f 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -2,9 +2,32 @@ "NAV": { "HOME": "Home", "CALCULATOR": "Calculator", - "SHOP": "Shop", - "ABOUT": "About", - "CONTACT": "Contact Us" + "SHOP": "Shop" + }, + "QUOTE": { + "CONSULT": "Request Consultation", + "PROCEED_ORDER": "Proceed to Order", + "TOTAL": "Total Estimate" + }, + "USER_DETAILS": { + "TITLE": "Shipping Details", + "SUMMARY_TITLE": "Order Summary", + "NAME": "First Name", + "NAME_PLACEHOLDER": "Enter your first name", + "SURNAME": "Last Name", + "SURNAME_PLACEHOLDER": "Enter your last name", + "EMAIL": "Email", + "EMAIL_PLACEHOLDER": "your@email.com", + "PHONE": "Phone", + "PHONE_PLACEHOLDER": "+41 79 123 45 67", + "ADDRESS": "Address", + "ADDRESS_PLACEHOLDER": "Street and Number", + "ZIP": "ZIP", + "ZIP_PLACEHOLDER": "8000", + "CITY": "City", + "CITY_PLACEHOLDER": "Zurich", + "SUBMIT": "Submit Order", + "ORDER_SUCCESS": "Order submitted successfully! We will contact you shortly." }, "FOOTER": { "PRIVACY": "Privacy", -- 2.49.1 From 83b3008234c3007266b170ab3e8307d51da9594b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 17:52:34 +0100 Subject: [PATCH 14/17] feat(web): new step for user details --- .../services/quote-estimator.service.ts | 129 ++++++++++++++++-- 1 file changed, 116 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index 3b4de2a..e7c8620 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject, signal } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, forkJoin, of } from 'rxjs'; +import { HttpClient, HttpEventType } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; import { environment } from '../../../../environments/environment'; @@ -9,7 +9,6 @@ export interface QuoteRequest { material: string; quality: string; notes?: string; - // color removed from global scope infillDensity?: number; infillPattern?: string; supportEnabled?: boolean; @@ -26,32 +25,133 @@ export interface QuoteItem { quantity: number; material?: string; color?: string; - // Computed values for UI convenience (optional, can be done in component) } export interface QuoteResult { items: QuoteItem[]; setupCost: number; currency: string; - // The following are aggregations that can be re-calculated totalPrice: number; totalTimeHours: number; totalTimeMinutes: number; totalWeight: number; } -// ... (skip down to calculate logic) + +interface BackendResponse { + success: boolean; + data: { + print_time_seconds: number; + material_grams: number; + cost: { + total: number; + }; + }; + error?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class QuoteEstimatorService { + private http = inject(HttpClient); + + calculate(request: QuoteRequest): Observable { + if (request.items.length === 0) return of(); + + return new Observable(observer => { + const totalItems = request.items.length; + const allProgress: number[] = new Array(totalItems).fill(0); + const finalResponses: any[] = []; + let completedRequests = 0; + + const uploads = request.items.map((item, index) => { + const formData = new FormData(); + formData.append('file', item.file); + formData.append('machine', 'bambu_a1'); + formData.append('filament', this.mapMaterial(request.material)); + formData.append('quality', this.mapQuality(request.quality)); + + // Send color for both modes if present, defaulting to Black + formData.append('material_color', item.color || 'Black'); + + if (request.mode === 'advanced') { + if (request.infillDensity) formData.append('infill_density', request.infillDensity.toString()); + if (request.infillPattern) formData.append('infill_pattern', request.infillPattern); + if (request.supportEnabled) formData.append('support_enabled', 'true'); + if (request.layerHeight) formData.append('layer_height', request.layerHeight.toString()); + if (request.nozzleDiameter) formData.append('nozzle_diameter', request.nozzleDiameter.toString()); + } + + const headers: any = {}; + // @ts-ignore + if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); + + return this.http.post(`${environment.apiUrl}/api/quote`, formData, { + headers, + reportProgress: true, + observe: 'events' + }).pipe( + map(event => ({ item, event, index })), + catchError(err => of({ item, error: err, index })) + ); + }); + + // Subscribe to all + uploads.forEach((obs) => { + obs.subscribe({ + next: (wrapper: any) => { + const idx = wrapper.index; + + if (wrapper.error) { + finalResponses[idx] = { success: false, fileName: wrapper.item.file.name }; + // Even if error, we count as complete + // But we need to handle completion logic carefully. + // For simplicity, let's treat it as complete but check later. + } + + const event = wrapper.event; + if (event && event.type === HttpEventType.UploadProgress) { + if (event.total) { + const percent = Math.round((100 * event.loaded) / event.total); + allProgress[idx] = percent; + // Emit average progress + const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / totalItems); + observer.next(avg); + } + } else if ((event && event.type === HttpEventType.Response) || wrapper.error) { + // It's done (either response or error caught above) + if (!finalResponses[idx]) { // only if not already set by error + allProgress[idx] = 100; + if (wrapper.error) { + finalResponses[idx] = { success: false, fileName: wrapper.item.file.name }; + } else { + finalResponses[idx] = { ...event.body, fileName: wrapper.item.file.name, originalQty: wrapper.item.quantity }; + } + completedRequests++; + } + + if (completedRequests === totalItems) { + // All done + observer.next(100); + + // Calculate Results + let setupCost = 10; + + if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) { + setupCost += 2; + } + + const items: QuoteItem[] = []; + finalResponses.forEach((res, idx) => { if (res && res.success) { - // Find original item to get color - const originalItem = request.items[idx]; - // Note: responses and request.items are index-aligned because we mapped them - + const originalItem = request.items[idx]; items.push({ fileName: res.fileName, unitPrice: res.data.cost.total, unitTime: res.data.print_time_seconds, unitWeight: res.data.material_grams, - quantity: res.originalQty, + quantity: res.originalQty, // Use the requested quantity material: request.material, color: originalItem.color || 'Default' }); @@ -59,6 +159,8 @@ export interface QuoteResult { }); if (items.length === 0) { + // If at least one failed? Or all? + // For now if NO items succeeded, error. observer.error('All calculations failed.'); return; } @@ -93,9 +195,10 @@ export interface QuoteResult { } }, error: (err) => { - console.error('Error in request', err); + console.error('Error in request subscription', err); + // Should be caught by inner pipe, but safety net completedRequests++; - if (completedRequests === totalItems) { + if (completedRequests === totalItems) { observer.error('Requests failed'); } } -- 2.49.1 From 44d99b0a6882e1672c6566168192de5d7b12bd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 17:53:43 +0100 Subject: [PATCH 15/17] feat(web): new step for user details --- .../calculator/calculator-page.component.html | 129 ++++++++++-------- 1 file changed, 72 insertions(+), 57 deletions(-) diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html index a589735..e2c8597 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.html +++ b/frontend/src/app/features/calculator/calculator-page.component.html @@ -1,64 +1,79 @@

    {{ 'CALC.TITLE' | translate }}

    {{ 'CALC.SUBTITLE' | translate }}

    + + @if (orderSuccess()) { + {{ 'USER_DETAILS.ORDER_SUCCESS' | translate }} + } + @if (error()) { + {{ 'CALC.ERROR_GENERIC' | translate }} + }
    -
    - -
    - -
    -
    - {{ 'CALC.MODE_EASY' | translate }} -
    -
    - {{ 'CALC.MODE_ADVANCED' | translate }} -
    -
    - - -
    -
    - - -
    - @if (error()) { - Si è verificato un errore durante il calcolo del preventivo. - } - - @if (loading()) { - -
    -
    -

    Analisi in corso...

    -

    Stiamo analizzando la geometria e calcolando il percorso utensile.

    +@if (step() === 'details' && result()) { +
    + + +
    +} @else { +
    + +
    + +
    +
    + {{ 'CALC.MODE_EASY' | translate }}
    +
    + {{ 'CALC.MODE_ADVANCED' | translate }} +
    +
    + +
    - } @else if (result()) { - - } @else { - -

    {{ 'CALC.BENEFITS_TITLE' | translate }}

    -
      -
    • {{ 'CALC.BENEFITS_1' | translate }}
    • -
    • {{ 'CALC.BENEFITS_2' | translate }}
    • -
    • {{ 'CALC.BENEFITS_3' | translate }}
    • -
    -
    - } -
    -
    +
    + + +
    + + @if (loading()) { + +
    +
    +

    Analisi in corso...

    +

    Stiamo analizzando la geometria e calcolando il percorso utensile.

    +
    +
    + } @else if (result()) { + + } @else { + +

    {{ 'CALC.BENEFITS_TITLE' | translate }}

    +
      +
    • {{ 'CALC.BENEFITS_1' | translate }}
    • +
    • {{ 'CALC.BENEFITS_2' | translate }}
    • +
    • {{ 'CALC.BENEFITS_3' | translate }}
    • +
    +
    + } +
    +
    +} -- 2.49.1 From f1636d9057a4785e0cda9b128a01cd109bb314d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 17:59:51 +0100 Subject: [PATCH 16/17] feat(web): success message contact us --- .../contact-form/contact-form.component.html | 146 ++++++++++-------- .../contact-form/contact-form.component.scss | 38 +++++ .../contact-form/contact-form.component.ts | 11 +- frontend/src/assets/i18n/en.json | 5 +- frontend/src/assets/i18n/it.json | 5 +- 5 files changed, 132 insertions(+), 73 deletions(-) diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html index 69b02af..23bba83 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html @@ -1,73 +1,87 @@ -
    - -
    - - -
    - -
    - - - - -
    - - -
    -
    - {{ 'CONTACT.TYPE_PRIVATE' | translate }} -
    -
    - {{ 'CONTACT.TYPE_COMPANY' | translate }} +@if (sent()) { +
    +
    + + + +
    +

    {{ 'CONTACT.SUCCESS_TITLE' | translate }}

    +

    {{ 'CONTACT.SUCCESS_DESC' | translate }}

    + {{ 'CONTACT.SEND_ANOTHER' | translate }}
    - - - - - -
    - - -
    - -
    - - -
    - - -
    - -

    {{ 'CONTACT.UPLOAD_HINT' | translate }}

    - -
    - -

    {{ 'CONTACT.DROP_FILES' | translate }}

    +} @else { + + +
    + +
    -
    -
    - - -
    - PDF - 3D -
    -
    {{ file.file.name }}
    +
    + + + + +
    + + +
    +
    + {{ 'CONTACT.TYPE_PRIVATE' | translate }} +
    +
    + {{ 'CONTACT.TYPE_COMPANY' | translate }}
    -
    -
    - - {{ sent() ? ('CONTACT.MSG_SENT' | translate) : ('CONTACT.SEND' | translate) }} - -
    - + + + + +
    + + +
    + +
    + + +
    + + +
    + +

    {{ 'CONTACT.UPLOAD_HINT' | translate }}

    + +
    + +

    {{ 'CONTACT.DROP_FILES' | translate }}

    +
    + +
    +
    + + +
    + PDF + 3D +
    +
    {{ file.file.name }}
    +
    +
    +
    + +
    + + {{ sent() ? ('CONTACT.MSG_SENT' | translate) : ('CONTACT.SEND' | translate) }} + +
    + +} diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss b/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss index 299a2d0..e8437e2 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss @@ -131,3 +131,41 @@ app-input.col { width: 100%; } display: flex; align-items: center; justify-content: center; line-height: 1; &:hover { background: red; } } + +/* Success State */ +.success-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: var(--space-8) var(--space-4); + gap: var(--space-4); + min-height: 300px; /* Ensure visual balance */ + + .success-icon { + width: 64px; + height: 64px; + color: var(--color-success, #10b981); + margin-bottom: var(--space-2); + + svg { + width: 100%; + height: 100%; + } + } + + h3 { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-text); + margin: 0; + } + + p { + color: var(--color-text-muted); + max-width: 400px; + margin-bottom: var(--space-4); + line-height: 1.5; + } +} diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts index 8889620..f46b7da 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts @@ -161,13 +161,14 @@ export class ContactFormComponent { console.log('Form Submit:', formData); this.sent.set(true); - setTimeout(() => { - this.sent.set(false); - this.form.reset({ requestType: 'custom', isCompany: false }); - this.files.set([]); - }, 3000); } else { this.form.markAllAsTouched(); } } + + resetForm() { + this.sent.set(false); + this.form.reset({ requestType: 'custom', isCompany: false }); + this.files.set([]); + } } diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 938928f..a44cb04 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -126,6 +126,9 @@ "LABEL_EMAIL": "Email *", "LABEL_NAME": "Name *", "MSG_SENT": "Sent!", - "ERR_MAX_FILES": "Max 15 files limit reached." + "ERR_MAX_FILES": "Max 15 files limit reached.", + "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.", + "SEND_ANOTHER": "Send Another Message" } } diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 98e4dc8..70a7190 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -105,6 +105,9 @@ "LABEL_EMAIL": "Email *", "LABEL_NAME": "Nome *", "MSG_SENT": "Inviato!", - "ERR_MAX_FILES": "Limite massimo di 15 file raggiunto." + "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" } } -- 2.49.1 From 05e1c224f0d52a90acce6c3fd1bd15ed6917d98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Feb 2026 18:07:29 +0100 Subject: [PATCH 17/17] feat(web): success comnponent --- .../calculator/calculator-page.component.html | 9 +++-- .../calculator/calculator-page.component.ts | 22 ++++++----- .../contact-form/contact-form.component.html | 12 +----- .../contact-form/contact-form.component.scss | 38 +------------------ .../contact-form/contact-form.component.ts | 4 +- .../success-state.component.html | 26 +++++++++++++ .../success-state.component.scss | 36 ++++++++++++++++++ .../success-state/success-state.component.ts | 18 +++++++++ frontend/src/assets/i18n/en.json | 3 ++ frontend/src/assets/i18n/it.json | 3 ++ 10 files changed, 109 insertions(+), 62 deletions(-) create mode 100644 frontend/src/app/shared/components/success-state/success-state.component.html create mode 100644 frontend/src/app/shared/components/success-state/success-state.component.scss create mode 100644 frontend/src/app/shared/components/success-state/success-state.component.ts diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html index e2c8597..3de51d9 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.html +++ b/frontend/src/app/features/calculator/calculator-page.component.html @@ -2,15 +2,16 @@

    {{ 'CALC.TITLE' | translate }}

    {{ 'CALC.SUBTITLE' | translate }}

    - @if (orderSuccess()) { - {{ 'USER_DETAILS.ORDER_SUCCESS' | translate }} - } @if (error()) { {{ 'CALC.ERROR_GENERIC' | translate }} }
    -@if (step() === 'details' && result()) { +@if (step() === 'success') { +
    + +
    +} @else if (step() === 'details' && result()) {
    ('easy'); - step = signal<'upload' | 'quote' | 'details'>('upload'); + step = signal<'upload' | 'quote' | 'details' | 'success'>('upload'); loading = signal(false); uploadProgress = signal(0); @@ -34,6 +35,7 @@ export class CalculatorPageComponent { constructor(private estimator: QuoteEstimatorService, private router: Router) {} onCalculate(req: QuoteRequest) { + // ... (logic remains the same, simplified for diff) this.currentRequest = req; this.loading.set(true); this.uploadProgress.set(0); @@ -78,12 +80,14 @@ export class CalculatorPageComponent { onSubmitOrder(orderData: any) { console.log('Order Submitted:', orderData); this.orderSuccess.set(true); - this.step.set('upload'); // Reset to start, or show success page? - // For now, let's show success message and reset - setTimeout(() => { - this.orderSuccess.set(false); - }, 5000); - this.result.set(null); + this.step.set('success'); + } + + onNewQuote() { + this.step.set('upload'); + this.result.set(null); + this.orderSuccess.set(false); + this.mode.set('easy'); // Reset to default } private currentRequest: QuoteRequest | null = null; diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html index 23bba83..b0f7bed 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html @@ -1,15 +1,5 @@ @if (sent()) { -
    -
    - - - - -
    -

    {{ 'CONTACT.SUCCESS_TITLE' | translate }}

    -

    {{ 'CONTACT.SUCCESS_DESC' | translate }}

    - {{ 'CONTACT.SEND_ANOTHER' | translate }} -
    + } @else {
    diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss b/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss index e8437e2..76186ad 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.scss @@ -132,40 +132,4 @@ app-input.col { width: 100%; } &:hover { background: red; } } -/* Success State */ -.success-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: var(--space-8) var(--space-4); - gap: var(--space-4); - min-height: 300px; /* Ensure visual balance */ - - .success-icon { - width: 64px; - height: 64px; - color: var(--color-success, #10b981); - margin-bottom: var(--space-2); - - svg { - width: 100%; - height: 100%; - } - } - - h3 { - font-size: 1.5rem; - font-weight: 600; - color: var(--color-text); - margin: 0; - } - - p { - color: var(--color-text-muted); - max-width: 400px; - margin-bottom: var(--space-4); - line-height: 1.5; - } -} +/* Success State styles moved to shared component */ diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts index f46b7da..30eec60 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts @@ -12,10 +12,12 @@ interface FilePreview { type: 'image' | 'pdf' | '3d' | 'other'; } +import { SuccessStateComponent } from '../../../../shared/components/success-state/success-state.component'; + @Component({ selector: 'app-contact-form', standalone: true, - imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppButtonComponent], + imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppButtonComponent, SuccessStateComponent], templateUrl: './contact-form.component.html', styleUrl: './contact-form.component.scss' }) diff --git a/frontend/src/app/shared/components/success-state/success-state.component.html b/frontend/src/app/shared/components/success-state/success-state.component.html new file mode 100644 index 0000000..d118121 --- /dev/null +++ b/frontend/src/app/shared/components/success-state/success-state.component.html @@ -0,0 +1,26 @@ +
    +
    + + + + +
    + + @switch (context()) { + @case ('contact') { +

    {{ 'CONTACT.SUCCESS_TITLE' | translate }}

    +

    {{ 'CONTACT.SUCCESS_DESC' | translate }}

    + {{ 'CONTACT.SEND_ANOTHER' | translate }} + } + @case ('calc') { +

    {{ 'CALC.ORDER_SUCCESS_TITLE' | translate }}

    +

    {{ 'CALC.ORDER_SUCCESS_DESC' | translate }}

    + {{ 'CALC.NEW_QUOTE' | translate }} + } + @case ('shop') { +

    {{ 'SHOP.SUCCESS_TITLE' | translate }}

    +

    {{ 'SHOP.SUCCESS_DESC' | translate }}

    + {{ 'SHOP.CONTINUE' | translate }} + } + } +
    diff --git a/frontend/src/app/shared/components/success-state/success-state.component.scss b/frontend/src/app/shared/components/success-state/success-state.component.scss new file mode 100644 index 0000000..dc86115 --- /dev/null +++ b/frontend/src/app/shared/components/success-state/success-state.component.scss @@ -0,0 +1,36 @@ +.success-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: var(--space-8) var(--space-4); + gap: var(--space-4); + min-height: 300px; /* Ensure visual balance */ + + .success-icon { + width: 64px; + height: 64px; + color: var(--color-success, #10b981); + margin-bottom: var(--space-2); + + svg { + width: 100%; + height: 100%; + } + } + + h3 { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-text); + margin: 0; + } + + p { + color: var(--color-text-muted); + max-width: 400px; + margin-bottom: var(--space-4); + line-height: 1.5; + } +} diff --git a/frontend/src/app/shared/components/success-state/success-state.component.ts b/frontend/src/app/shared/components/success-state/success-state.component.ts new file mode 100644 index 0000000..3cf4048 --- /dev/null +++ b/frontend/src/app/shared/components/success-state/success-state.component.ts @@ -0,0 +1,18 @@ +import { Component, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppButtonComponent } from '../app-button/app-button.component'; + +export type SuccessContext = 'contact' | 'calc' | 'shop'; + +@Component({ + selector: 'app-success-state', + standalone: true, + imports: [CommonModule, TranslateModule, AppButtonComponent], + templateUrl: './success-state.component.html', + styleUrl: './success-state.component.scss' +}) +export class SuccessStateComponent { + context = input.required(); + action = output(); +} diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index a44cb04..fd65237 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -61,6 +61,9 @@ "ORDER": "Order Now", "CONSULT": "Request Consultation", "ERROR_GENERIC": "An error occurred while calculating the quote.", + "NEW_QUOTE": "Calculate New Quote", + "ORDER_SUCCESS_TITLE": "Order Submitted Successfully", + "ORDER_SUCCESS_DESC": "We have received your order details. You will receive a confirmation email shortly with the payment details.", "BENEFITS_TITLE": "Why choose us?", "BENEFITS_1": "Automatic quote with instant cost and time", "BENEFITS_2": "Selected materials and quality control", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 70a7190..0f1221b 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -40,6 +40,9 @@ "ORDER": "Ordina Ora", "CONSULT": "Richiedi Consulenza", "ERROR_GENERIC": "Si è verificato un errore durante il calcolo del preventivo.", + "NEW_QUOTE": "Calcola Nuovo Preventivo", + "ORDER_SUCCESS_TITLE": "Ordine Inviato con Successo", + "ORDER_SUCCESS_DESC": "Abbiamo ricevuto i dettagli del tuo ordine. Riceverai a breve una email di conferma con i dettagli per il pagamento.", "BENEFITS_TITLE": "Perché scegliere noi?", "BENEFITS_1": "Preventivo automatico con costo e tempo immediati", "BENEFITS_2": "Materiali selezionati e qualità controllata", -- 2.49.1