From 2c658d00c196b94fcff19a71e628c18bd7aa3e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 2 Feb 2026 17:38:03 +0100 Subject: [PATCH 001/135] feat(web): vibe coding pazzo --- frontend/README.md | 84 +++--- frontend/package-lock.json | 28 ++ frontend/package.json | 2 + frontend/src/app/app.component.html | 2 +- frontend/src/app/app.component.scss | 0 frontend/src/app/app.component.spec.ts | 29 -- frontend/src/app/app.component.ts | 7 +- frontend/src/app/app.config.ts | 35 +-- frontend/src/app/app.routes.ts | 28 +- .../app/calculator/calculator.component.html | 69 ----- .../app/calculator/calculator.component.scss | 0 .../calculator/calculator.component.spec.ts | 23 -- .../app/calculator/calculator.component.ts | 79 ----- .../common/stl-viewer/stl-viewer.component.ts | 218 -------------- .../src/app/contact/contact.component.html | 50 ---- .../src/app/contact/contact.component.scss | 97 ------- frontend/src/app/contact/contact.component.ts | 17 -- .../src/app/core/layout/footer.component.ts | 65 +++++ .../src/app/core/layout/layout.component.ts | 31 ++ .../src/app/core/layout/navbar.component.ts | 111 ++++++++ .../src/app/core/services/language.service.ts | 20 ++ .../features/about/about-page.component.ts | 54 ++++ .../src/app/features/about/about.routes.ts | 6 + .../contact-form/contact-form.component.ts | 66 +++++ .../calculator/calculator-page.component.ts | 139 +++++++++ .../features/calculator/calculator.routes.ts | 6 + .../quote-result/quote-result.component.ts | 63 ++++ .../upload-form/upload-form.component.ts | 120 ++++++++ .../services/quote-estimator.service.ts | 44 +++ .../product-card/product-card.component.ts | 48 ++++ .../features/shop/product-detail.component.ts | 80 ++++++ .../features/shop/services/shop.service.ts | 48 ++++ .../app/features/shop/shop-page.component.ts | 43 +++ frontend/src/app/features/shop/shop.routes.ts | 8 + frontend/src/app/home/home.component.html | 38 --- frontend/src/app/home/home.component.scss | 81 ------ frontend/src/app/home/home.component.ts | 12 - frontend/src/app/print.service.ts | 33 --- .../advanced-quote.component.html | 152 ---------- .../advanced-quote.component.scss | 269 ------------------ .../advanced-quote.component.ts | 151 ---------- .../basic-quote/basic-quote.component.html | 129 --------- .../basic-quote/basic-quote.component.scss | 254 ----------------- .../basic-quote/basic-quote.component.ts | 103 ------- .../app-alert/app-alert.component.ts | 36 +++ .../app-button/app-button.component.ts | 80 ++++++ .../components/app-card/app-card.component.ts | 26 ++ .../app-dropzone/app-dropzone.component.ts | 104 +++++++ .../app-input/app-input.component.ts | 72 +++++ .../app-select/app-select.component.ts | 72 +++++ .../components/app-tabs/app-tabs.component.ts | 52 ++++ frontend/src/assets/i18n/en.json | 43 +++ frontend/src/assets/i18n/it.json | 43 +++ frontend/src/styles.scss | 134 ++------- frontend/src/styles/theme.scss | 18 ++ frontend/src/styles/tokens.scss | 41 +++ 56 files changed, 1676 insertions(+), 1987 deletions(-) delete mode 100644 frontend/src/app/app.component.scss delete mode 100644 frontend/src/app/app.component.spec.ts delete mode 100644 frontend/src/app/calculator/calculator.component.html delete mode 100644 frontend/src/app/calculator/calculator.component.scss delete mode 100644 frontend/src/app/calculator/calculator.component.spec.ts delete mode 100644 frontend/src/app/calculator/calculator.component.ts delete mode 100644 frontend/src/app/common/stl-viewer/stl-viewer.component.ts delete mode 100644 frontend/src/app/contact/contact.component.html delete mode 100644 frontend/src/app/contact/contact.component.scss delete mode 100644 frontend/src/app/contact/contact.component.ts create mode 100644 frontend/src/app/core/layout/footer.component.ts create mode 100644 frontend/src/app/core/layout/layout.component.ts create mode 100644 frontend/src/app/core/layout/navbar.component.ts create mode 100644 frontend/src/app/core/services/language.service.ts create mode 100644 frontend/src/app/features/about/about-page.component.ts create mode 100644 frontend/src/app/features/about/about.routes.ts create mode 100644 frontend/src/app/features/about/components/contact-form/contact-form.component.ts create mode 100644 frontend/src/app/features/calculator/calculator-page.component.ts create mode 100644 frontend/src/app/features/calculator/calculator.routes.ts create mode 100644 frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts create mode 100644 frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts create mode 100644 frontend/src/app/features/calculator/services/quote-estimator.service.ts create mode 100644 frontend/src/app/features/shop/components/product-card/product-card.component.ts create mode 100644 frontend/src/app/features/shop/product-detail.component.ts create mode 100644 frontend/src/app/features/shop/services/shop.service.ts create mode 100644 frontend/src/app/features/shop/shop-page.component.ts create mode 100644 frontend/src/app/features/shop/shop.routes.ts delete mode 100644 frontend/src/app/home/home.component.html delete mode 100644 frontend/src/app/home/home.component.scss delete mode 100644 frontend/src/app/home/home.component.ts delete mode 100644 frontend/src/app/print.service.ts delete mode 100644 frontend/src/app/quote/advanced-quote/advanced-quote.component.html delete mode 100644 frontend/src/app/quote/advanced-quote/advanced-quote.component.scss delete mode 100644 frontend/src/app/quote/advanced-quote/advanced-quote.component.ts delete mode 100644 frontend/src/app/quote/basic-quote/basic-quote.component.html delete mode 100644 frontend/src/app/quote/basic-quote/basic-quote.component.scss delete mode 100644 frontend/src/app/quote/basic-quote/basic-quote.component.ts create mode 100644 frontend/src/app/shared/components/app-alert/app-alert.component.ts create mode 100644 frontend/src/app/shared/components/app-button/app-button.component.ts create mode 100644 frontend/src/app/shared/components/app-card/app-card.component.ts create mode 100644 frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts create mode 100644 frontend/src/app/shared/components/app-input/app-input.component.ts create mode 100644 frontend/src/app/shared/components/app-select/app-select.component.ts create mode 100644 frontend/src/app/shared/components/app-tabs/app-tabs.component.ts create mode 100644 frontend/src/assets/i18n/en.json create mode 100644 frontend/src/assets/i18n/it.json create mode 100644 frontend/src/styles/theme.scss create mode 100644 frontend/src/styles/tokens.scss diff --git a/frontend/README.md b/frontend/README.md index 2cc40f5..7e92d3a 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,59 +1,53 @@ -# Frontend +# Print Calculator Frontend -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.12. +This is a modern Angular application designed with a Clean Architecture approach (Core, Shared, Features) and Design Tokens for easy theming. -## Development server +## Project Structure -To start a local development server, run: +- **Core**: Singleton services, global layout components (Navbar, Footer), guards. +- **Shared**: Reusable dumb UI components (Buttons, Cards, Inputs). No business logic. +- **Features**: Lazy-loaded modules (Calculator, Shop, About). Each contains its own pages, components, and services. +- **Styles**: Design tokens and theming layer. -```bash -ng serve -``` +## Getting Started -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +1. **Install Dependencies**: + ```bash + npm install + ``` -## Code scaffolding +2. **Run Development Server**: + ```bash + ng serve + ``` + Navigate to `http://localhost:4200/`. -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: +## Theming -```bash -ng generate component component-name -``` +The application uses CSS Variables defined in `src/styles/tokens.scss` and mapped in `src/styles/theme.scss`. -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: +- **Change Colors**: Edit `src/styles/tokens.scss`. +- **Create New Theme**: + 1. Duplicate `src/styles/theme.scss` (e.g., `theme-dark.scss`). + 2. Override the semantic variables (e.g., `--color-bg`, `--color-text`). + 3. Load the new theme file or switch classes on the body tag. -```bash -ng generate --help -``` +## Adding a New Feature -## Building +1. **Create Directory**: `src/app/features/my-feature`. +2. **Create Routes**: Create `my-feature.routes.ts` exporting a `Routes` array. +3. **Register Route**: Add to `src/app/app.routes.ts` using lazy loading: + ```typescript + { + path: 'my-feature', + loadChildren: () => import('./features/my-feature/my-feature.routes').then(m => m.MY_FEATURE_ROUTES) + } + ``` -To build the project run: +## Internationalization (i18n) -```bash -ng build -``` +Translations are stored in `src/assets/i18n/`. +- `it.json` (Italian - Default) +- `en.json` (English) -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. - -## Running unit tests - -To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: - -```bash -ng test -``` - -## Running end-to-end tests - -For end-to-end (e2e) testing, run: - -```bash -ng e2e -``` - -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. - -## Additional Resources - -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +To add a language, create the JSON file and update `LanguageService` in `src/app/core/services/language.service.ts`. \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 05a10ef..e3aaedd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,8 @@ "@angular/platform-browser": "^19.2.18", "@angular/platform-browser-dynamic": "^19.2.18", "@angular/router": "^19.2.18", + "@ngx-translate/core": "^17.0.0", + "@ngx-translate/http-loader": "^17.0.0", "@types/three": "^0.182.0", "rxjs": "~7.8.0", "three": "^0.182.0", @@ -4305,6 +4307,32 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngx-translate/core": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-17.0.0.tgz", + "integrity": "sha512-Rft2D5ns2pq4orLZjEtx1uhNuEBerUdpFUG1IcqtGuipj6SavgB8SkxtNQALNDA+EVlvsNCCjC2ewZVtUeN6rg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-17.0.0.tgz", + "integrity": "sha512-hgS8sa0ARjH9ll3PhkLTufeVXNI2DNR2uFKDhBgq13siUXzzVr/a31M6zgecrtwbA34iaBV01hsTMbMS8V7iIw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index bf6f79e..bd5b43d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,8 @@ "@angular/platform-browser": "^19.2.18", "@angular/platform-browser-dynamic": "^19.2.18", "@angular/router": "^19.2.18", + "@ngx-translate/core": "^17.0.0", + "@ngx-translate/http-loader": "^17.0.0", "@types/three": "^0.182.0", "rxjs": "~7.8.0", "three": "^0.182.0", diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 0680b43..90c6b64 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1 +1 @@ - + \ No newline at end of file diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts deleted file mode 100644 index a6b0ab9..0000000 --- a/frontend/src/app/app.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppComponent], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); - - it(`should have the 'frontend' title`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('frontend'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend'); - }); -}); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 0555c0c..3c3d410 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -5,9 +5,6 @@ import { RouterOutlet } from '@angular/router'; selector: 'app-root', standalone: true, imports: [RouterOutlet], - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + template: `` }) -export class AppComponent { - title = 'frontend'; -} +export class AppComponent {} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 484db14..bb34c85 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,28 +1,23 @@ -import { ApplicationConfig, LOCALE_ID, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; - +import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core'; +import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; - -const resolveLocale = () => { - if (typeof navigator === 'undefined') { - return 'de-CH'; - } - const languages = navigator.languages ?? []; - if (navigator.language === 'it-CH' || languages.includes('it-CH')) { - return 'it-CH'; - } - if (navigator.language === 'de-CH' || languages.includes('de-CH')) { - return 'de-CH'; - } - return 'de-CH'; -}; +import { TranslateModule } from '@ngx-translate/core'; +import { provideTranslateHttpLoader } from '@ngx-translate/http-loader'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes), + provideRouter(routes, withComponentInputBinding(), withViewTransitions()), provideHttpClient(), - { provide: LOCALE_ID, useFactory: resolveLocale } + provideTranslateHttpLoader({ + prefix: './assets/i18n/', + suffix: '.json' + }), + importProvidersFrom( + TranslateModule.forRoot({ + defaultLanguage: 'it' + }) + ) ] -}; +}; \ No newline at end of file diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 402dc64..d206854 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,13 +1,23 @@ import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { BasicQuoteComponent } from './quote/basic-quote/basic-quote.component'; -import { AdvancedQuoteComponent } from './quote/advanced-quote/advanced-quote.component'; -import { ContactComponent } from './contact/contact.component'; export const routes: Routes = [ - { path: '', component: HomeComponent }, - { path: 'quote/basic', component: BasicQuoteComponent }, - { path: 'quote/advanced', component: AdvancedQuoteComponent }, - { path: 'contact', component: ContactComponent }, - { path: '**', redirectTo: '' } + { + path: '', + loadComponent: () => import('./core/layout/layout.component').then(m => m.LayoutComponent), + children: [ + { path: '', redirectTo: 'cal', pathMatch: 'full' }, + { + path: 'cal', + loadChildren: () => import('./features/calculator/calculator.routes').then(m => m.CALCULATOR_ROUTES) + }, + { + path: 'shop', + loadChildren: () => import('./features/shop/shop.routes').then(m => m.SHOP_ROUTES) + }, + { + path: 'about', + loadChildren: () => import('./features/about/about.routes').then(m => m.ABOUT_ROUTES) + } + ] + } ]; diff --git a/frontend/src/app/calculator/calculator.component.html b/frontend/src/app/calculator/calculator.component.html deleted file mode 100644 index 9f93d6c..0000000 --- a/frontend/src/app/calculator/calculator.component.html +++ /dev/null @@ -1,69 +0,0 @@ -
- - - 3D Print Quote Calculator - Bambu Lab A1 Estimation - - - -
- - - - -
- -
- -

Slicing model... this may take a minute...

-
- -
- {{ error }} -
- -
-
-

Total Estimate: {{ results?.cost?.total | currency:'CHF' }}

-
- -
-
- Print Time: - {{ results?.print_time_formatted }} -
-
- Material Used: - {{ results?.material_grams | number:'1.1-1' }} g -
-
- -

Cost Breakdown

-
    -
  • - Material - {{ results?.cost?.material | currency:'CHF' }} -
  • -
  • - Machine Time - {{ results?.cost?.machine | currency:'CHF' }} -
  • -
  • - Energy - {{ results?.cost?.energy | currency:'CHF' }} -
  • -
  • - Service/Markup - {{ results?.cost?.markup | currency:'CHF' }} -
  • -
-
-
-
-
diff --git a/frontend/src/app/calculator/calculator.component.scss b/frontend/src/app/calculator/calculator.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/calculator/calculator.component.spec.ts b/frontend/src/app/calculator/calculator.component.spec.ts deleted file mode 100644 index 6ad2d10..0000000 --- a/frontend/src/app/calculator/calculator.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CalculatorComponent } from './calculator.component'; - -describe('CalculatorComponent', () => { - let component: CalculatorComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CalculatorComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(CalculatorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/calculator/calculator.component.ts b/frontend/src/app/calculator/calculator.component.ts deleted file mode 100644 index fff56aa..0000000 --- a/frontend/src/app/calculator/calculator.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -// calculator.component.ts -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { HttpClient } from '@angular/common/http'; -import { MatCardModule } from '@angular/material/card'; -import { MatButtonModule } from '@angular/material/button'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; - -interface QuoteResponse { - printer: string; - print_time_formatted: string; - material_grams: number; - cost: { - material: number; - machine: number; - energy: number; - markup: number; - total: number; - }; -} - -import { environment } from '../../environments/environment'; - -@Component({ - selector: 'app-calculator', - standalone: true, - imports: [ - CommonModule, - FormsModule, - MatCardModule, - MatButtonModule, - MatProgressSpinnerModule - ], - templateUrl: './calculator.component.html', - styleUrls: ['./calculator.component.scss'] -}) -export class CalculatorComponent { - file: File | null = null; - results: QuoteResponse | null = null; - error = ''; - loading = false; - - constructor(private http: HttpClient) {} - - onFileSelected(event: Event): void { - const input = event.target as HTMLInputElement; - if (input.files && input.files.length > 0) { - this.file = input.files[0]; - this.results = null; - this.error = ''; - } - } - - uploadAndCalculate(): void { - if (!this.file) { - this.error = 'Please select a file first.'; - return; - } - const formData = new FormData(); - formData.append('file', this.file); - this.loading = true; - this.error = ''; - this.results = null; - - this.http.post(`${environment.apiUrl}/calculate/stl`, formData) - .subscribe({ - next: res => { - this.results = res; - this.loading = false; - }, - error: err => { - console.error(err); - this.error = err.error?.detail || "An error occurred during calculation."; - this.loading = false; - } - }); - } -} \ No newline at end of file diff --git a/frontend/src/app/common/stl-viewer/stl-viewer.component.ts b/frontend/src/app/common/stl-viewer/stl-viewer.component.ts deleted file mode 100644 index b498ebd..0000000 --- a/frontend/src/app/common/stl-viewer/stl-viewer.component.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, ViewChild, SimpleChanges } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import * as THREE from 'three'; -import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; - -@Component({ - selector: 'app-stl-viewer', - standalone: true, - imports: [CommonModule], - template: ` -
-
- autorenew -

Loading 3D Model...

-
-
-

Size: {{ dimensions.x | number:'1.1-1' }} x {{ dimensions.y | number:'1.1-1' }} x {{ dimensions.z | number:'1.1-1' }} mm

-
-
- `, - styles: [` - .viewer-container { - width: 100%; - height: 100%; - min-height: 300px; - position: relative; - background: #0f172a; /* Match app bg approx */ - overflow: hidden; - border-radius: inherit; - } - .loading-overlay { - position: absolute; - top: 0; left: 0; right: 0; bottom: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: rgba(15, 23, 42, 0.8); - color: white; - z-index: 10; - } - .spin { - animation: spin 1s linear infinite; - font-size: 2rem; - margin-bottom: 0.5rem; - } - @keyframes spin { 100% { transform: rotate(360deg); } } - - .dimensions-overlay { - position: absolute; - bottom: 10px; - right: 10px; - background: rgba(0,0,0,0.6); - color: white; - padding: 4px 8px; - border-radius: 4px; - font-size: 0.8rem; - pointer-events: none; - } - `] -}) -export class StlViewerComponent implements OnInit, OnDestroy, OnChanges { - @Input() file: File | null = null; - @ViewChild('rendererContainer', { static: true }) rendererContainer!: ElementRef; - - isLoading = false; - dimensions: { x: number, y: number, z: number } | null = null; - - private scene!: THREE.Scene; - private camera!: THREE.PerspectiveCamera; - private renderer!: THREE.WebGLRenderer; - private mesh!: THREE.Mesh; - private controls!: OrbitControls; - private animationId: number | null = null; - - ngOnInit() { - this.initThree(); - } - - ngOnChanges(changes: SimpleChanges) { - if (changes['file'] && this.file) { - this.loadSTL(this.file); - } - } - - ngOnDestroy() { - this.stopAnimation(); - if (this.renderer) { - this.renderer.dispose(); - } - if (this.mesh) { - this.mesh.geometry.dispose(); - (this.mesh.material as THREE.Material).dispose(); - } - } - - private initThree() { - const container = this.rendererContainer.nativeElement; - const width = container.clientWidth; - const height = container.clientHeight; - - // Scene - this.scene = new THREE.Scene(); - this.scene.background = new THREE.Color(0x1e293b); // Slate 800 - - // Camera - this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); - this.camera.position.set(100, 100, 100); - - // Renderer - this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); - this.renderer.setSize(width, height); - this.renderer.setPixelRatio(window.devicePixelRatio); - container.appendChild(this.renderer.domElement); - - // Controls - this.controls = new OrbitControls(this.camera, this.renderer.domElement); - this.controls.enableDamping = true; - this.controls.dampingFactor = 0.05; - this.controls.autoRotate = true; - this.controls.autoRotateSpeed = 2.0; - - // Lights - const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); - this.scene.add(ambientLight); - - const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); - dirLight.position.set(50, 50, 50); - this.scene.add(dirLight); - - const backLight = new THREE.DirectionalLight(0xffffff, 0.4); - backLight.position.set(-50, -50, -50); - this.scene.add(backLight); - - // Grid (Printer Bed attempt) - const gridHelper = new THREE.GridHelper(256, 20, 0x4f46e5, 0x334155); - this.scene.add(gridHelper); - - // Resize listener - const resizeObserver = new ResizeObserver(() => this.onWindowResize()); - resizeObserver.observe(container); - - this.animate(); - } - - private loadSTL(file: File) { - this.isLoading = true; - - // Remove previous mesh - if (this.mesh) { - this.scene.remove(this.mesh); - this.mesh.geometry.dispose(); - (this.mesh.material as THREE.Material).dispose(); - } - - const loader = new STLLoader(); - const reader = new FileReader(); - - reader.onload = (event) => { - const buffer = event.target?.result as ArrayBuffer; - const geometry = loader.parse(buffer); - - geometry.computeBoundingBox(); - const center = new THREE.Vector3(); - geometry.boundingBox?.getCenter(center); - geometry.center(); // Center geometry - - // Calculate dimensions - const size = new THREE.Vector3(); - geometry.boundingBox?.getSize(size); - this.dimensions = { x: size.x, y: size.y, z: size.z }; - - // Re-position camera based on size - const maxDim = Math.max(size.x, size.y, size.z); - this.camera.position.set(maxDim * 1.5, maxDim * 1.5, maxDim * 1.5); - this.camera.lookAt(0, 0, 0); - - // Material - const material = new THREE.MeshStandardMaterial({ - color: 0x6366f1, // Indigo 500 - roughness: 0.5, - metalness: 0.1 - }); - - this.mesh = new THREE.Mesh(geometry, material); - this.mesh.rotation.x = -Math.PI / 2; // STL usually needs this - this.scene.add(this.mesh); - - this.isLoading = false; - }; - - reader.readAsArrayBuffer(file); - } - - private animate() { - this.animationId = requestAnimationFrame(() => this.animate()); - this.controls.update(); - this.renderer.render(this.scene, this.camera); - } - - private stopAnimation() { - if (this.animationId !== null) { - cancelAnimationFrame(this.animationId); - } - } - - private onWindowResize() { - if (!this.rendererContainer) return; - const container = this.rendererContainer.nativeElement; - const width = container.clientWidth; - const height = container.clientHeight; - - this.camera.aspect = width / height; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(width, height); - } -} diff --git a/frontend/src/app/contact/contact.component.html b/frontend/src/app/contact/contact.component.html deleted file mode 100644 index 4c16996..0000000 --- a/frontend/src/app/contact/contact.component.html +++ /dev/null @@ -1,50 +0,0 @@ -
-
- - arrow_back Back - -

Contact Me

-

Have a special project? Let's talk.

-
- -
-
-
- email -
-

Email

-

joe@example.com

-
-
- -
- location_on -
-

Location

-

Milan, Italy

-
-
-
- -
- -
-
- - -
- -
- - -
- -
- - -
- - -
-
-
diff --git a/frontend/src/app/contact/contact.component.scss b/frontend/src/app/contact/contact.component.scss deleted file mode 100644 index b20bb64..0000000 --- a/frontend/src/app/contact/contact.component.scss +++ /dev/null @@ -1,97 +0,0 @@ -.section-header { - margin-bottom: 2rem; - text-align: center; - - .back-link { - position: absolute; - left: 2rem; - top: 2rem; - display: inline-flex; - align-items: center; - color: var(--text-muted); - - .material-icons { margin-right: 0.5rem; } - &:hover { color: var(--primary-color); } - } - - @media (max-width: 768px) { - .back-link { - position: static; - display: block; - margin-bottom: 1rem; - } - } -} - -.contact-card { - max-width: 600px; - margin: 0 auto; -} - -.contact-info { - display: flex; - gap: 2rem; - margin-bottom: 2rem; - - .info-item { - display: flex; - align-items: flex-start; - gap: 1rem; - - .material-icons { - color: var(--secondary-color); - background: rgba(236, 72, 153, 0.1); - padding: 0.5rem; - border-radius: 50%; - } - - h3 { margin: 0 0 0.25rem 0; font-size: 1rem; } - p { margin: 0; color: var(--text-muted); } - } -} - -.divider { - height: 1px; - background: var(--border-color); - margin: 2rem 0; -} - -.form-group { - margin-bottom: 1.5rem; - - label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-size: 0.9rem; - } - - input, textarea { - width: 100%; - padding: 0.75rem; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - color: var(--text-main); - font-family: inherit; - box-sizing: border-box; - - &:focus { - outline: none; - border-color: var(--secondary-color); - } - } -} - -.btn-block { - width: 100%; -} - -.fade-in { - animation: fadeIn 0.4s ease-out; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} diff --git a/frontend/src/app/contact/contact.component.ts b/frontend/src/app/contact/contact.component.ts deleted file mode 100644 index bac336c..0000000 --- a/frontend/src/app/contact/contact.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterLink } from '@angular/router'; - -@Component({ - selector: 'app-contact', - standalone: true, - imports: [CommonModule, RouterLink], - templateUrl: './contact.component.html', - styleUrls: ['./contact.component.scss'] -}) -export class ContactComponent { - onSubmit(event: Event) { - event.preventDefault(); - alert("Thanks for your message! This is a demo form."); - } -} diff --git a/frontend/src/app/core/layout/footer.component.ts b/frontend/src/app/core/layout/footer.component.ts new file mode 100644 index 0000000..16bbb2e --- /dev/null +++ b/frontend/src/app/core/layout/footer.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-footer', + standalone: true, + imports: [TranslateModule, RouterLink], + template: ` + + `, + styles: [` + .footer { + background-color: var(--color-neutral-900); + color: var(--color-neutral-300); + padding: var(--space-8) 0; + margin-top: auto; /* Push to bottom if content is short */ + } + .footer-inner { + display: flex; + justify-content: space-between; + align-items: center; + } + .brand { font-weight: 700; color: white; display: block; margin-bottom: var(--space-2); } + .copyright { font-size: 0.875rem; color: var(--color-secondary-500); margin: 0; } + + .links { + display: flex; + gap: var(--space-6); + a { + color: var(--color-neutral-300); + font-size: 0.875rem; + &:hover { color: white; text-decoration: underline; } + } + } + + .social { display: flex; gap: var(--space-3); } + .social-icon { + width: 24px; height: 24px; + background-color: var(--color-neutral-800); + border-radius: 50%; + } + `] +}) +export class FooterComponent {} diff --git a/frontend/src/app/core/layout/layout.component.ts b/frontend/src/app/core/layout/layout.component.ts new file mode 100644 index 0000000..ac27e33 --- /dev/null +++ b/frontend/src/app/core/layout/layout.component.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { NavbarComponent } from './navbar.component'; +import { FooterComponent } from './footer.component'; + +@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); + } + `] +}) +export class LayoutComponent {} diff --git a/frontend/src/app/core/layout/navbar.component.ts b/frontend/src/app/core/layout/navbar.component.ts new file mode 100644 index 0000000..6eec639 --- /dev/null +++ b/frontend/src/app/core/layout/navbar.component.ts @@ -0,0 +1,111 @@ +import { Component } from '@angular/core'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { LanguageService } from '../services/language.service'; +import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; + +@Component({ + selector: 'app-navbar', + standalone: true, + imports: [RouterLink, RouterLinkActive, TranslateModule], + template: ` + + `, + styles: [` + .navbar { + height: 64px; + border-bottom: 1px solid var(--color-border); + background-color: var(--color-bg-card); + position: sticky; + top: 0; + z-index: 100; + display: flex; + align-items: center; + } + .navbar-inner { + display: flex; + align-items: center; + justify-content: space-between; + } + .brand { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-text); + text-decoration: none; + } + .highlight { color: var(--color-brand); } + + .nav-links { + display: flex; + gap: var(--space-6); + + a { + color: var(--color-text-muted); + font-weight: 500; + text-decoration: none; + transition: color 0.2s; + + &:hover, &.active { + color: var(--color-brand); + } + } + } + + .actions { + display: flex; + align-items: center; + gap: var(--space-4); + } + + .lang-switch { + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 2px 6px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text-muted); + &:hover { color: var(--color-text); border-color: var(--color-text); } + } + + .icon-placeholder { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: var(--color-neutral-100); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-text-muted); + } + `] +}) +export class NavbarComponent { + constructor(public langService: LanguageService) {} + + toggleLang() { + const newLang = this.langService.currentLang() === 'it' ? 'en' : 'it'; + this.langService.switchLang(newLang); + } +} diff --git a/frontend/src/app/core/services/language.service.ts b/frontend/src/app/core/services/language.service.ts new file mode 100644 index 0000000..ead4dc0 --- /dev/null +++ b/frontend/src/app/core/services/language.service.ts @@ -0,0 +1,20 @@ +import { Injectable, signal } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LanguageService { + currentLang = signal('it'); + + constructor(private translate: TranslateService) { + this.translate.addLangs(['it', 'en']); + this.translate.setDefaultLang('it'); + this.translate.use('it'); + } + + switchLang(lang: string) { + this.translate.use(lang); + this.currentLang.set(lang); + } +} diff --git a/frontend/src/app/features/about/about-page.component.ts b/frontend/src/app/features/about/about-page.component.ts new file mode 100644 index 0000000..085acb2 --- /dev/null +++ b/frontend/src/app/features/about/about-page.component.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ContactFormComponent } from './components/contact-form/contact-form.component'; +import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; + +@Component({ + selector: 'app-about-page', + standalone: true, + imports: [TranslateModule, ContactFormComponent, AppCardComponent], + template: ` +
+

{{ 'ABOUT.TITLE' | translate }}

+
+ +
+
+

Our Mission

+

+ We make high-quality 3D printing accessible to everyone. + Whether you are a hobbyist or an industrial designer, + PrintCalc provides instant quotes and professional production. +

+ +

How it Works

+
    +
  1. Upload your STL file
  2. +
  3. Choose material & quality
  4. +
  5. Get instant quote
  6. +
  7. We print & ship
  8. +
+
+ +
+ +

{{ 'ABOUT.CONTACT_US' | translate }}

+ +
+
+
+ `, + styles: [` + .hero { padding: var(--space-8) 0; text-align: center; } + .content { + display: grid; + gap: var(--space-12); + @media(min-width: 768px) { grid-template-columns: 1fr 1fr; } + } + .steps { + padding-left: var(--space-4); + li { margin-bottom: var(--space-2); color: var(--color-text-muted); } + } + `] +}) +export class AboutPageComponent {} diff --git a/frontend/src/app/features/about/about.routes.ts b/frontend/src/app/features/about/about.routes.ts new file mode 100644 index 0000000..9ff57a6 --- /dev/null +++ b/frontend/src/app/features/about/about.routes.ts @@ -0,0 +1,6 @@ +import { Routes } from '@angular/router'; +import { AboutPageComponent } from './about-page.component'; + +export const ABOUT_ROUTES: Routes = [ + { path: '', component: AboutPageComponent } +]; diff --git a/frontend/src/app/features/about/components/contact-form/contact-form.component.ts b/frontend/src/app/features/about/components/contact-form/contact-form.component.ts new file mode 100644 index 0000000..942d393 --- /dev/null +++ b/frontend/src/app/features/about/components/contact-form/contact-form.component.ts @@ -0,0 +1,66 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component'; +import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component'; + +@Component({ + selector: 'app-contact-form', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppButtonComponent], + template: ` +
+ + + +
+ + +
+ +
+ + {{ sent() ? 'Sent!' : ('ABOUT.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); } + .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); } + } + `] +}) +export class ContactFormComponent { + form: FormGroup; + sent = signal(false); + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + name: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + message: ['', Validators.required] + }); + } + + onSubmit() { + if (this.form.valid) { + // Mock submit + this.sent.set(true); + setTimeout(() => { + this.sent.set(false); + this.form.reset(); + }, 3000); + } + } +} diff --git a/frontend/src/app/features/calculator/calculator-page.component.ts b/frontend/src/app/features/calculator/calculator-page.component.ts new file mode 100644 index 0000000..76629dd --- /dev/null +++ b/frontend/src/app/features/calculator/calculator-page.component.ts @@ -0,0 +1,139 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppTabsComponent } from '../../shared/components/app-tabs/app-tabs.component'; +import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; +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 { QuoteEstimatorService, QuoteRequest, QuoteResult } from './services/quote-estimator.service'; + +@Component({ + selector: 'app-calculator-page', + standalone: true, + imports: [CommonModule, TranslateModule, AppTabsComponent, AppCardComponent, AppAlertComponent, UploadFormComponent, QuoteResultComponent], + template: ` +
+

{{ 'CALC.TITLE' | translate }}

+

{{ 'CALC.SUBTITLE' | translate }}

+
+ +
+ +
+ +
+ + + +
+ + {{ 'CALC.MODE_EASY' | translate }} + + / + + {{ 'CALC.MODE_ADVANCED' | translate }} + +
+
+ + +
+
+ + +
+ @if (error()) { + An error occurred while calculating quote. + } + + @if (result()) { + + } @else { + +

Why choose PrintCalc?

+
    +
  • Instant AI-powered quotes
  • +
  • Industrial grade materials
  • +
  • Fast shipping worldwide
  • +
+
+ } +
+
+ `, + 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-8); + @media(min-width: 768px) { + grid-template-columns: 1.5fr 1fr; + } + } + + .tabs-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-6); + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-2); + } + + .sub-tabs { font-size: 0.875rem; color: var(--color-text-muted); } + .mode-switch { cursor: pointer; &:hover { color: var(--color-text); } } + .mode-switch.active { font-weight: 700; color: var(--color-brand); } + .divider { margin: 0 var(--space-2); } + + .benefits { padding-left: var(--space-4); color: var(--color-text-muted); line-height: 2; } + `] +}) +export class CalculatorPageComponent { + clientType = signal('private'); + mode = signal('easy'); + loading = signal(false); + result = signal(null); + error = signal(false); + + clientTabs = [ + { label: 'Private', value: 'private' }, + { label: 'Business', value: 'business' } + ]; + + constructor(private estimator: QuoteEstimatorService) {} + + onCalculate(req: QuoteRequest) { + this.loading.set(true); + this.error.set(false); + this.result.set(null); + + this.estimator.calculate(req).subscribe({ + next: (res) => { + this.result.set(res); + this.loading.set(false); + }, + error: () => { + this.error.set(true); + this.loading.set(false); + } + }); + } +} diff --git a/frontend/src/app/features/calculator/calculator.routes.ts b/frontend/src/app/features/calculator/calculator.routes.ts new file mode 100644 index 0000000..39a0c2e --- /dev/null +++ b/frontend/src/app/features/calculator/calculator.routes.ts @@ -0,0 +1,6 @@ +import { Routes } from '@angular/router'; +import { CalculatorPageComponent } from './calculator-page.component'; + +export const CALCULATOR_ROUTES: Routes = [ + { path: '', component: CalculatorPageComponent } +]; 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 new file mode 100644 index 0000000..ba620a1 --- /dev/null +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.ts @@ -0,0 +1,63 @@ +import { Component, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +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 { QuoteResult } from '../../services/quote-estimator.service'; + +@Component({ + selector: 'app-quote-result', + standalone: true, + imports: [CommonModule, TranslateModule, AppCardComponent, AppButtonComponent], + template: ` + +

{{ 'CALC.RESULT' | translate }}

+ +
+
+ {{ 'CALC.COST' | translate }} + {{ result().price | currency:result().currency }} +
+
+ {{ 'CALC.TIME' | translate }} + {{ result().printTimeHours }}h +
+
+ Material + {{ result().materialUsageGrams }}g +
+
+ +
+ {{ 'CALC.ORDER' | translate }} + {{ 'CALC.CONSULT' | translate }} +
+
+ `, + styles: [` + .title { margin-bottom: var(--space-6); text-align: center; } + .result-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-4); + margin-bottom: var(--space-6); + } + .item { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--space-3); + background: var(--color-neutral-50); + border-radius: var(--radius-md); + } + .item:first-child { grid-column: span 2; background: var(--color-neutral-100); } + .label { font-size: 0.875rem; color: var(--color-text-muted); } + .value { font-size: 1.25rem; font-weight: 700; } + .price { font-size: 2rem; color: var(--color-brand); } + + .actions { display: flex; flex-direction: column; gap: var(--space-3); } + `] +}) +export class QuoteResultComponent { + result = input.required(); +} 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 new file mode 100644 index 0000000..a6a0ee7 --- /dev/null +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -0,0 +1,120 @@ +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 { AppInputComponent } from '../../../../shared/components/app-input/app-input.component'; +import { AppSelectComponent } from '../../../../shared/components/app-select/app-select.component'; +import { AppDropzoneComponent } from '../../../../shared/components/app-dropzone/app-dropzone.component'; +import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component'; +import { QuoteRequest } from '../../services/quote-estimator.service'; + +@Component({ + selector: 'app-upload-form', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppSelectComponent, AppDropzoneComponent, AppButtonComponent], + template: ` +
+ +
+ + + @if (form.get('file')?.invalid && form.get('file')?.touched) { +
File required
+ } +
+ +
+ + + +
+ + + + @if (mode() === 'advanced') { + + } + +
+ + {{ loading() ? '...' : ('CALC.CALCULATE' | translate) }} + +
+
+ `, + styles: [` + .section { margin-bottom: var(--space-6); } + .grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } + .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; } + `] +}) +export class UploadFormComponent { + clientType = input<'business' | 'private'>('private'); + mode = input<'easy' | 'advanced'>('easy'); + loading = input(false); + submitRequest = output(); + + form: FormGroup; + + materials = [ + { label: 'PLA (Standard)', value: 'PLA' }, + { label: 'PETG (Durable)', value: 'PETG' }, + { label: 'TPU (Flexible)', value: 'TPU' } + ]; + + qualities = [ + { label: 'Draft (Fast)', value: 'Draft' }, + { label: 'Standard', value: 'Standard' }, + { label: 'High Detail', value: 'High' } + ]; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + file: [null, Validators.required], + material: ['PLA', Validators.required], + quality: ['Standard', Validators.required], + quantity: [1, [Validators.required, Validators.min(1)]], + notes: [''] + }); + } + + onFileDropped(file: File) { + this.form.patchValue({ file }); + this.form.get('file')?.markAsTouched(); + } + + onSubmit() { + if (this.form.valid) { + this.submitRequest.emit({ + ...this.form.value, + clientType: this.clientType(), + mode: this.mode() + }); + } else { + this.form.markAllAsTouched(); + } + } +} diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts new file mode 100644 index 0000000..410d554 --- /dev/null +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +export interface QuoteRequest { + file: File; + material: string; + quality: string; + quantity: number; + notes?: string; + clientType: 'business' | 'private'; + mode: 'easy' | 'advanced'; +} + +export interface QuoteResult { + price: number; + currency: string; + printTimeHours: number; + materialUsageGrams: number; + setupCost: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class QuoteEstimatorService { + + calculate(request: QuoteRequest): Observable { + // Mock logic + const basePrice = request.clientType === 'business' ? 50 : 20; + const materialCost = request.material === 'PETG' ? 1.5 : (request.material === 'TPU' ? 2 : 1); + const qualityMult = request.quality === 'High' ? 1.5 : (request.quality === 'Draft' ? 0.8 : 1); + + const estimatedPrice = (basePrice * materialCost * qualityMult * request.quantity) + 10; // +10 setup + + return of({ + price: Math.round(estimatedPrice * 100) / 100, + currency: 'EUR', + printTimeHours: Math.floor(Math.random() * 24) + 2, + materialUsageGrams: Math.floor(Math.random() * 500) + 50, + setupCost: 10 + }).pipe(delay(1500)); // Simulate network latency + } +} 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 new file mode 100644 index 0000000..0b5dc08 --- /dev/null +++ b/frontend/src/app/features/shop/components/product-card/product-card.component.ts @@ -0,0 +1,48 @@ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { Product } from '../../services/shop.service'; + +@Component({ + 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; } + `] +}) +export class ProductCardComponent { + product = input.required(); +} diff --git a/frontend/src/app/features/shop/product-detail.component.ts b/frontend/src/app/features/shop/product-detail.component.ts new file mode 100644 index 0000000..c2b212c --- /dev/null +++ b/frontend/src/app/features/shop/product-detail.component.ts @@ -0,0 +1,80 @@ +import { Component, input, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { ShopService, Product } from './services/shop.service'; +import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; + +@Component({ + 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 { +

Product not found.

+ } +
+ `, + 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); } + `] +}) +export class ProductDetailComponent { + // Input binding from router + id = input(); + + product = signal(undefined); + + constructor(private shopService: ShopService) {} + + ngOnInit() { + const productId = this.id(); + if (productId) { + this.shopService.getProductById(productId).subscribe(p => this.product.set(p)); + } + } + + addToCart() { + alert('Added to cart (Mock)'); + } +} diff --git a/frontend/src/app/features/shop/services/shop.service.ts b/frontend/src/app/features/shop/services/shop.service.ts new file mode 100644 index 0000000..644ec71 --- /dev/null +++ b/frontend/src/app/features/shop/services/shop.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +export interface Product { + id: string; + name: string; + description: string; + price: number; + category: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class ShopService { + // Dati statici per ora + private staticProducts: Product[] = [ + { + id: '1', + name: 'Filamento PLA Standard', + description: 'Il classico per ogni stampa, facile e affidabile.', + price: 24.90, + category: 'Filamenti' + }, + { + id: '2', + name: 'Filamento PETG Tough', + description: 'Resistente agli urti e alle temperature.', + price: 29.90, + category: 'Filamenti' + }, + { + id: '3', + name: 'Kit Ugelli (0.4mm)', + description: 'Set di ricambio per estrusore FDM.', + price: 15.00, + category: 'Accessori' + } + ]; + + getProducts(): Observable { + return of(this.staticProducts); + } + + getProductById(id: string): Observable { + return of(this.staticProducts.find(p => p.id === id)); + } +} \ No newline at end of file diff --git a/frontend/src/app/features/shop/shop-page.component.ts b/frontend/src/app/features/shop/shop-page.component.ts new file mode 100644 index 0000000..5da913b --- /dev/null +++ b/frontend/src/app/features/shop/shop-page.component.ts @@ -0,0 +1,43 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { ShopService, Product } from './services/shop.service'; +import { ProductCardComponent } from './components/product-card/product-card.component'; + +@Component({ + selector: 'app-shop-page', + standalone: true, + imports: [CommonModule, TranslateModule, ProductCardComponent], + template: ` +
+

{{ 'SHOP.TITLE' | translate }}

+

Componenti e materiali selezionati per la tua stampa 3D.

+
+ +
+
+ @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); + } + `] +}) +export class ShopPageComponent { + products = signal([]); + + constructor(private shopService: ShopService) { + this.shopService.getProducts().subscribe(data => { + this.products.set(data); + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/features/shop/shop.routes.ts b/frontend/src/app/features/shop/shop.routes.ts new file mode 100644 index 0000000..744d165 --- /dev/null +++ b/frontend/src/app/features/shop/shop.routes.ts @@ -0,0 +1,8 @@ +import { Routes } from '@angular/router'; +import { ShopPageComponent } from './shop-page.component'; +import { ProductDetailComponent } from './product-detail.component'; + +export const SHOP_ROUTES: Routes = [ + { path: '', component: ShopPageComponent }, + { path: ':id', component: ProductDetailComponent } +]; diff --git a/frontend/src/app/home/home.component.html b/frontend/src/app/home/home.component.html deleted file mode 100644 index ea9f0ea..0000000 --- a/frontend/src/app/home/home.component.html +++ /dev/null @@ -1,38 +0,0 @@ - diff --git a/frontend/src/app/home/home.component.scss b/frontend/src/app/home/home.component.scss deleted file mode 100644 index 3c51701..0000000 --- a/frontend/src/app/home/home.component.scss +++ /dev/null @@ -1,81 +0,0 @@ -.home-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 80vh; - text-align: center; -} - -.hero { - margin-bottom: 4rem; - - h1 { - font-size: 3.5rem; - margin-bottom: 1rem; - background: linear-gradient(to right, var(--primary-color), var(--secondary-color)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } - - .subtitle { - font-size: 1.25rem; - color: var(--text-muted); - max-width: 600px; - margin: 0 auto; - } -} - -.cards-wrapper { - width: 100%; - max-width: 900px; -} - -.action-card { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - text-decoration: none; - color: var(--text-main); - background: var(--surface-color); - - .icon-wrapper { - background: rgba(99, 102, 241, 0.1); - padding: 1rem; - border-radius: 50%; - margin-bottom: 1.5rem; - - .material-icons { - font-size: 2.5rem; - color: var(--primary-color); - } - } - - h2 { - font-size: 1.5rem; - margin-bottom: 0.5rem; - } - - p { - color: var(--text-muted); - margin-bottom: 2rem; - line-height: 1.6; - } - - .btn-secondary { - background-color: transparent; - border: 1px solid var(--border-color); - color: var(--text-main); - - &:hover { - background-color: var(--surface-hover); - } - } - - &:hover { - .icon-wrapper { - background: rgba(99, 102, 241, 0.2); - } - } -} diff --git a/frontend/src/app/home/home.component.ts b/frontend/src/app/home/home.component.ts deleted file mode 100644 index 7895791..0000000 --- a/frontend/src/app/home/home.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'app-home', - standalone: true, - imports: [RouterLink, CommonModule], - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'] -}) -export class HomeComponent {} diff --git a/frontend/src/app/print.service.ts b/frontend/src/app/print.service.ts deleted file mode 100644 index 36e11a0..0000000 --- a/frontend/src/app/print.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { environment } from '../environments/environment'; - -@Injectable({ - providedIn: 'root' -}) -export class PrintService { - private http = inject(HttpClient); - private apiUrl = environment.apiUrl; - - calculateQuote(file: File, params?: any): Observable { - const formData = new FormData(); - formData.append('file', file); - - // Append extra params if meant for backend - if (params) { - Object.keys(params).forEach(key => { - if (params[key] !== null && params[key] !== undefined) { - formData.append(key, params[key]); - } - }); - } - - return this.http.post(`${this.apiUrl}/api/quote`, formData); - } - - getProfiles(): Observable { - return this.http.get(`${this.apiUrl}/api/profiles/available`); - } -} - diff --git a/frontend/src/app/quote/advanced-quote/advanced-quote.component.html b/frontend/src/app/quote/advanced-quote/advanced-quote.component.html deleted file mode 100644 index ad42eb4..0000000 --- a/frontend/src/app/quote/advanced-quote/advanced-quote.component.html +++ /dev/null @@ -1,152 +0,0 @@ -
-
- - arrow_back Back - -

Advanced Quote

-

Configure detailed print parameters for your project.

-
- -
- -
- -
- - - - -
- cloud_upload -

Click or Drop STL here

-
- - -
-
- -
-
-
- description - {{ selectedFile.name }} -
- -
-
-
- - -
-

Print Settings

- -
- - -
- -
- - -
- -
- - -
- -
- -
- - {{ params.infill_density }}% -
-
- -
- - -
-
- -
- -
-
- - -
-
-

Estimated Cost

-
- {{ quoteResult?.cost?.total | currency:'CHF' }} -
- -
-
- Print Time - {{ quoteResult?.print_time_formatted }} -
-
- Material Weight - {{ quoteResult?.material_grams | number:'1.0-0' }}g -
-
- - -
-
Request Specs
-
- Infill - {{ params.infill_density }}% -
-
- Infill Pattern - {{ infillPatternLabel }} -
-
- Material - {{ materialLabel }} -
-
- Color - {{ params.material_color }} -
-
- -
-

Note: Color does not affect the estimate. Printer is fixed to Bambu Lab A1.

-
-
-
- - -
-
- science -

Advanced Quote

-

Configure settings and calculate.

-
-
-
-
diff --git a/frontend/src/app/quote/advanced-quote/advanced-quote.component.scss b/frontend/src/app/quote/advanced-quote/advanced-quote.component.scss deleted file mode 100644 index 6ab5ca5..0000000 --- a/frontend/src/app/quote/advanced-quote/advanced-quote.component.scss +++ /dev/null @@ -1,269 +0,0 @@ -.section-header { - margin-bottom: 2rem; - - .back-link { - display: inline-flex; - align-items: center; - color: var(--text-muted); - margin-bottom: 1rem; - font-size: 0.9rem; - cursor: pointer; - - .material-icons { - font-size: 1.1rem; - margin-right: 0.25rem; - } - - &:hover { - color: var(--primary-color); - } - } - - h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; - } - - p { - color: var(--text-muted); - } -} - -.upload-area { - border-bottom: 1px solid var(--border-color); - background: var(--surface-color); - transition: all 0.2s; - - &.drag-over { - background: rgba(99, 102, 241, 0.1); - } - - &:not(.has-file) { - cursor: pointer; - } - - .empty-state { - padding: 1.5rem; - text-align: center; - .material-icons { - font-size: 2rem; - color: var(--primary-color); - margin-bottom: 0.5rem; - } - p { margin: 0; color: var(--text-muted);} - } -} - -.viewer-wrapper { - height: 250px; - width: 100%; - background: #0f172a; -} - -.file-action-bar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - background: var(--surface-color); - - &.border-top { border-top: 1px solid var(--border-color); } - - .file-info { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--text-main); - - .material-icons { color: var(--primary-color); font-size: 1.2rem; } - .file-name { font-size: 0.9rem; } - } - - .btn-icon { - background: none; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0.25rem; - border-radius: 50%; - - .material-icons { - font-size: 1.2rem; - color: var(--text-muted); - } - - &.danger:hover { - background: rgba(239, 68, 68, 0.2); - .material-icons { color: #ef4444; } - } - } -} - -.params-form { - padding: 1.5rem; - - h3 { - font-size: 1.1rem; - margin-bottom: 1.5rem; - color: var(--text-main); - } -} - -.form-group { - margin-bottom: 1.25rem; - - label { - display: block; - color: var(--text-muted); - font-size: 0.9rem; - margin-bottom: 0.5rem; - } - - input[type="text"], - input[type="number"], - select { - width: 100%; - padding: 0.75rem; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - color: var(--text-main); - font-family: inherit; - font-size: 0.95rem; - - &:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); - } - } -} - - -.range-wrapper { - display: flex; - align-items: center; - gap: 1rem; - - input[type="range"] { - flex: 1; - accent-color: var(--primary-color); - } - - span { - width: 3rem; - text-align: right; - font-variant-numeric: tabular-nums; - } -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; -} - -.actions { - padding: 1.5rem; - border-top: 1px solid var(--border-color); - background: var(--surface-color); -} - -.btn-block { - width: 100%; - display: flex; -} - -/* Results - Reused mostly but tweaked */ -.result-card { - text-align: center; - background: linear-gradient(135deg, var(--surface-color) 0%, rgba(30, 41, 59, 0.8) 100%); - - h2 { - color: var(--text-muted); - font-size: 1.25rem; - margin-bottom: 1rem; - } - - .price-big { - font-size: 3.5rem; - font-weight: 700; - color: var(--primary-color); - margin-bottom: 2rem; - } -} - -.specs-list { - text-align: left; - margin-bottom: 1rem; - background: rgba(0,0,0,0.2); - border-radius: var(--radius-md); - padding: 1rem; - - .spec-header { - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - margin-bottom: 0.5rem; - opacity: 0.7; - } - - .spec-item { - display: flex; - justify-content: space-between; - padding: 0.75rem 0; - border-bottom: 1px solid rgba(255,255,255,0.05); - - &:last-child { border-bottom: none; } - - &.compact { - padding: 0.25rem 0; - font-size: 0.9rem; - } - - span:first-child { color: var(--text-muted); } - } - - &.secondary { - background: transparent; - border: 1px solid var(--border-color); - } -} - -.note-box { - background: rgba(99, 102, 241, 0.1); - color: var(--primary-color); // Blueish info for advanced note - padding: 0.75rem; - border-radius: var(--radius-sm); - font-size: 0.9rem; - margin-top: 1rem; -} - -.placeholder-card { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - color: var(--text-muted); - border-style: dashed; - - .material-icons { - font-size: 4rem; - margin-bottom: 1rem; - opacity: 0.5; - } -} - -/* Animation */ -.fade-in { - animation: fadeIn 0.4s ease-out; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} diff --git a/frontend/src/app/quote/advanced-quote/advanced-quote.component.ts b/frontend/src/app/quote/advanced-quote/advanced-quote.component.ts deleted file mode 100644 index 2c29f0e..0000000 --- a/frontend/src/app/quote/advanced-quote/advanced-quote.component.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterLink } from '@angular/router'; -import { FormsModule } from '@angular/forms'; -import { PrintService } from '../../print.service'; -import { StlViewerComponent } from '../../common/stl-viewer/stl-viewer.component'; - -@Component({ - selector: 'app-advanced-quote', - standalone: true, - imports: [CommonModule, RouterLink, FormsModule, StlViewerComponent], - templateUrl: './advanced-quote.component.html', - styleUrls: ['./advanced-quote.component.scss'] -}) -export class AdvancedQuoteComponent { - printService = inject(PrintService); - - selectedFile: File | null = null; - isDragOver = false; - isCalculating = false; - quoteResult: any = null; - - // Selectable options (mapped to backend profile ids where needed) - readonly materialOptions = [ - { value: 'pla_basic', label: 'PLA' }, - { value: 'petg_basic', label: 'PETG' }, - { value: 'abs_basic', label: 'ABS' }, - { value: 'tpu_95a', label: 'TPU 95A' } - ]; - readonly colorOptions = [ - 'Black', - 'White', - 'Gray', - 'Red', - 'Blue', - 'Green', - 'Yellow' - ]; - readonly qualityOptions = [ - { value: 'draft', label: 'Draft' }, - { value: 'standard', label: 'Standard' }, - { value: 'fine', label: 'Fine' } - ]; - readonly infillPatternOptions = [ - { value: 'grid', label: 'Grid' }, - { value: 'gyroid', label: 'Gyroid' }, - { value: 'cubic', label: 'Cubic' }, - { value: 'triangles', label: 'Triangles' }, - { value: 'rectilinear', label: 'Rectilinear' }, - { value: 'crosshatch', label: 'Crosshatch' }, - { value: 'zig-zag', label: 'Zig-zag' }, - { value: 'alignedrectilinear', label: 'Aligned Rectilinear' } - ]; - - // Parameters - params = { - filament: 'pla_basic', - material_color: 'Black', - quality: 'standard', - infill_density: 15, - infill_pattern: 'grid', - support_enabled: false - }; - - get materialLabel(): string { - const match = this.materialOptions.find(option => option.value === this.params.filament); - return match ? match.label : this.params.filament; - } - - get infillPatternLabel(): string { - const match = this.infillPatternOptions.find(option => option.value === this.params.infill_pattern); - return match ? match.label : this.params.infill_pattern; - } - - onDragOver(event: DragEvent) { - event.preventDefault(); - event.stopPropagation(); - this.isDragOver = true; - } - - onDragLeave(event: DragEvent) { - event.preventDefault(); - event.stopPropagation(); - this.isDragOver = false; - } - - onDrop(event: DragEvent) { - event.preventDefault(); - event.stopPropagation(); - this.isDragOver = false; - - const files = event.dataTransfer?.files; - if (files && files.length > 0) { - if (files[0].name.toLowerCase().endsWith('.stl')) { - this.selectedFile = files[0]; - this.quoteResult = null; - } else { - alert('Please upload an STL file.'); - } - } - } - - onFileSelected(event: any) { - const file = event.target.files[0]; - if (file) { - this.selectedFile = file; - this.quoteResult = null; - } - } - - removeFile(event: Event) { - event.stopPropagation(); - this.selectedFile = null; - this.quoteResult = null; - } - - calculate() { - if (!this.selectedFile) return; - - this.isCalculating = true; - - // Use PrintService - this.printService.calculateQuote(this.selectedFile, { - filament: this.params.filament, - quality: this.params.quality, - infill_density: this.params.infill_density, - infill_pattern: this.params.infill_pattern, - // Optional mappings if user selected overrides - // layer_height: this.params.layer_height, - // support_enabled: this.params.support_enabled - }) - .subscribe({ - next: (res) => { - console.log('API Response:', res); - if (res.success) { - this.quoteResult = res.data; - console.log('Quote Result set to:', this.quoteResult); - } else { - console.error('API succeeded but returned error flag:', res.error); - alert('Error: ' + res.error); - } - this.isCalculating = false; - }, - error: (err) => { - console.error(err); - alert('Calculation failed: ' + (err.error?.detail || err.message)); - this.isCalculating = false; - } - }); - } -} diff --git a/frontend/src/app/quote/basic-quote/basic-quote.component.html b/frontend/src/app/quote/basic-quote/basic-quote.component.html deleted file mode 100644 index cfd28ac..0000000 --- a/frontend/src/app/quote/basic-quote/basic-quote.component.html +++ /dev/null @@ -1,129 +0,0 @@ -
-
- - arrow_back Back - -

Quick Quote

-

Upload your 3D model and choose a strength level.

-
- -
- -
- -
- - - - -
- cloud_upload -

Drop your STL file here

-

or click to browse

-
- - -
-
- -
-
-
- description - {{ selectedFile.name }} -
-
- -
-
-
-
- - -
-

Select Strength

-
-
- 🥚 -
-

Standard

-

Balanced strength

-
-
- -
- 🧱 -
-

Strong

-

Higher infill

-
-
- -
- 🦾 -
-

Ultra

-

Max infill

-
-
-
-
- -
- -
-
- - -
-
-

Estimated Cost

-
- {{ quoteResult?.cost?.total | currency:'CHF' }} -
- -
-
- Print Time - {{ quoteResult?.print_time_formatted }} -
-
- Material - {{ quoteResult?.material_grams | number:'1.0-0' }}g -
-
- -
-

Note: This is an estimation. Final price may vary slightly.

-
-
-
- - -
-
- receipt_long -

Your Quote

-

Results will appear here after calculation.

-
-
-
-
diff --git a/frontend/src/app/quote/basic-quote/basic-quote.component.scss b/frontend/src/app/quote/basic-quote/basic-quote.component.scss deleted file mode 100644 index a75d9bf..0000000 --- a/frontend/src/app/quote/basic-quote/basic-quote.component.scss +++ /dev/null @@ -1,254 +0,0 @@ -.section-header { - margin-bottom: 2rem; - - .back-link { - display: inline-flex; - align-items: center; - color: var(--text-muted); - margin-bottom: 1rem; - font-size: 0.9rem; - - .material-icons { - font-size: 1.1rem; - margin-right: 0.25rem; - } - - &:hover { - color: var(--primary-color); - } - } - - h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; - } - - p { - color: var(--text-muted); - } -} - -.upload-area { - border-bottom: 1px solid var(--border-color); - background: var(--surface-color); - transition: all 0.2s; - - &.drag-over { - background: rgba(99, 102, 241, 0.1); - box-shadow: inset 0 0 0 2px var(--primary-color); - } - - /* Only show pointer on empty state or drag */ - &:not(.has-file) { - cursor: pointer; - } -} - -.empty-state { - padding: 3rem; - text-align: center; - - .upload-icon { - font-size: 4rem; - color: var(--text-muted); - margin-bottom: 1rem; - } - - h3 { margin-bottom: 0.5rem; } - p { color: var(--text-muted); margin: 0; } -} - -.viewer-wrapper { - height: 350px; - width: 100%; - background: #0f172a; - border-bottom: 1px solid var(--border-color); -} - -.file-action-bar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1.5rem; - background: var(--surface-color); - - .file-info { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--text-main); - - .material-icons { color: var(--primary-color); } - .file-name { font-weight: 500; } - } - - .btn-icon { - background: none; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0.5rem; - border-radius: 50%; - transition: background 0.2s; - - .material-icons { - font-size: 1.5rem; - color: var(--text-muted); - } - - &:hover { - background: var(--surface-hover); - .material-icons { color: var(--text-main); } - } - - &.danger { - &:hover { - background: rgba(239, 68, 68, 0.2); - .material-icons { color: #ef4444; } /* Red-500 */ - } - } - } -} - -.strength-selector { - padding: 1.5rem; - - h3 { - font-size: 1.1rem; - margin-bottom: 1rem; - } -} - -.strength-options { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.strength-card { - display: flex; - align-items: center; - padding: 1rem; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s; - - .emoji { - font-size: 2rem; - margin-right: 1rem; - } - - .info { - h4 { - margin: 0 0 0.25rem 0; - } - p { - margin: 0; - font-size: 0.9rem; - color: var(--text-muted); - } - } - - &:hover { - border-color: var(--primary-color); - background: var(--surface-hover); - } - - &.active { - border-color: var(--primary-color); - background: rgba(99, 102, 241, 0.1); - - .emoji { - transform: scale(1.1); - } - } -} - -.actions { - padding: 1.5rem; - border-top: 1px solid var(--border-color); -} - -.btn-block { - width: 100%; - display: flex; -} - -/* Results */ -.result-card { - text-align: center; - background: linear-gradient(135deg, var(--surface-color) 0%, rgba(30, 41, 59, 0.8) 100%); - - h2 { - color: var(--text-muted); - font-size: 1.25rem; - margin-bottom: 1rem; - } - - .price-big { - font-size: 3.5rem; - font-weight: 700; - color: var(--primary-color); - margin-bottom: 2rem; - } -} - -.specs-list { - text-align: left; - margin-bottom: 2rem; - background: rgba(0,0,0,0.2); - border-radius: var(--radius-md); - padding: 1rem; - - .spec-item { - display: flex; - justify-content: space-between; - padding: 0.75rem 0; - border-bottom: 1px solid rgba(255,255,255,0.05); - - &:last-child { - border-bottom: none; - } - - span { - color: var(--text-muted); - } - } -} - -.note-box { - background: rgba(236, 72, 153, 0.1); - color: var(--secondary-color); - padding: 0.75rem; - border-radius: var(--radius-sm); - font-size: 0.9rem; -} - -.placeholder-card { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - color: var(--text-muted); - border-style: dashed; - - .material-icons { - font-size: 4rem; - margin-bottom: 1rem; - opacity: 0.5; - } -} - -/* Animation */ -.fade-in { - animation: fadeIn 0.4s ease-out; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} diff --git a/frontend/src/app/quote/basic-quote/basic-quote.component.ts b/frontend/src/app/quote/basic-quote/basic-quote.component.ts deleted file mode 100644 index 8905da1..0000000 --- a/frontend/src/app/quote/basic-quote/basic-quote.component.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterLink } from '@angular/router'; -import { PrintService } from '../../print.service'; -import { StlViewerComponent } from '../../common/stl-viewer/stl-viewer.component'; - -@Component({ - selector: 'app-basic-quote', - standalone: true, - imports: [CommonModule, RouterLink, StlViewerComponent], - templateUrl: './basic-quote.component.html', - styleUrls: ['./basic-quote.component.scss'] -}) -export class BasicQuoteComponent { - printService = inject(PrintService); - - selectedFile: File | null = null; - selectedStrength: 'standard' | 'strong' | 'ultra' = 'standard'; - isDragOver = false; - isCalculating = false; - quoteResult: any = null; - private strengthToSettings: Record<'standard' | 'strong' | 'ultra', { infill_density: number; quality: 'draft' | 'standard' | 'fine' }> = { - standard: { infill_density: 15, quality: 'standard' }, - strong: { infill_density: 30, quality: 'standard' }, - ultra: { infill_density: 50, quality: 'standard' } - }; - - onDragOver(event: DragEvent) { - event.preventDefault(); - event.stopPropagation(); - this.isDragOver = true; - } - - onDragLeave(event: DragEvent) { - event.preventDefault(); - event.stopPropagation(); - this.isDragOver = false; - } - - onDrop(event: DragEvent) { - event.preventDefault(); - event.stopPropagation(); - this.isDragOver = false; - - const files = event.dataTransfer?.files; - if (files && files.length > 0) { - if (files[0].name.toLowerCase().endsWith('.stl')) { - this.selectedFile = files[0]; - this.quoteResult = null; - } else { - alert('Please upload an STL file.'); - } - } - } - - onFileSelected(event: any) { - const file = event.target.files[0]; - if (file) { - this.selectedFile = file; - this.quoteResult = null; - } - } - - removeFile(event: Event) { - event.stopPropagation(); - this.selectedFile = null; - this.quoteResult = null; - } - - selectStrength(strength: 'standard' | 'strong' | 'ultra') { - this.selectedStrength = strength; - } - - calculate() { - if (!this.selectedFile) return; - - this.isCalculating = true; - - const settings = this.strengthToSettings[this.selectedStrength]; - - this.printService.calculateQuote(this.selectedFile, { - quality: settings.quality, - infill_density: settings.infill_density - }) - .subscribe({ - next: (res) => { - if (res?.success) { - this.quoteResult = res.data; - } else { - console.error('Quote API returned error:', res?.error); - alert('Calculation failed: ' + (res?.error || 'Unknown error')); - this.quoteResult = null; - } - this.isCalculating = false; - }, - error: (err) => { - console.error(err); - alert('Calculation failed: ' + (err.error?.detail || err.message)); - this.isCalculating = false; - } - }); - } -} 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 new file mode 100644 index 0000000..c187d52 --- /dev/null +++ b/frontend/src/app/shared/components/app-alert/app-alert.component.ts @@ -0,0 +1,36 @@ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + 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; } + `] +}) +export class AppAlertComponent { + type = input<'info' | 'warning' | 'error' | 'success'>('info'); +} 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 new file mode 100644 index 0000000..9806066 --- /dev/null +++ b/frontend/src/app/shared/components/app-button/app-button.component.ts @@ -0,0 +1,80 @@ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + 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: 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: white; + &: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-brand); + } + } + + .btn-text { + background-color: transparent; + color: var(--color-text-muted); + padding: 0.5rem; + &:hover:not(:disabled) { color: var(--color-text); } + } + `] +}) +export class AppButtonComponent { + variant = input<'primary' | 'secondary' | 'outline' | 'text'>('primary'); + type = input<'button' | 'submit' | 'reset'>('button'); + disabled = input(false); + fullWidth = input(false); + + handleClick(event: Event) { + if (this.disabled()) { + event.preventDefault(); + event.stopPropagation(); + } + } +} 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 new file mode 100644 index 0000000..05dc74b --- /dev/null +++ b/frontend/src/app/shared/components/app-card/app-card.component.ts @@ -0,0 +1,26 @@ +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); + } + } + `] +}) +export class AppCardComponent {} 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 new file mode 100644 index 0000000..5cfe140 --- /dev/null +++ b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts @@ -0,0 +1,104 @@ +import { Component, input, output, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-dropzone', + standalone: true, + imports: [CommonModule], + template: ` +
+ + +
+
+ +
+

{{ label() }}

+

{{ subtext() }}

+ @if (fileName()) { +
+ {{ fileName() }} +
+ } +
+
+ `, + 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-badge { + margin-top: var(--space-4); + display: inline-block; + 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); + } + `] +}) +export class AppDropzoneComponent { + label = input('Drop file here or click to upload'); + subtext = input('Supports .stl, .obj'); + accept = input('.stl,.obj'); + + fileDropped = output(); + + isDragOver = signal(false); + fileName = signal(null); + + onDragOver(e: Event) { + e.preventDefault(); + e.stopPropagation(); + this.isDragOver.set(true); + } + + onDragLeave(e: Event) { + e.preventDefault(); + e.stopPropagation(); + this.isDragOver.set(false); + } + + onDrop(e: DragEvent) { + e.preventDefault(); + e.stopPropagation(); + this.isDragOver.set(false); + if (e.dataTransfer?.files.length) { + this.handleFile(e.dataTransfer.files[0]); + } + } + + onFileSelected(e: Event) { + const input = e.target as HTMLInputElement; + if (input.files?.length) { + this.handleFile(input.files[0]); + } + } + + handleFile(file: File) { + this.fileName.set(file.name); + this.fileDropped.emit(file); + } +} 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 new file mode 100644 index 0000000..974495b --- /dev/null +++ b/frontend/src/app/shared/components/app-input/app-input.component.ts @@ -0,0 +1,72 @@ +import { Component, input, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-input', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AppInputComponent), + 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(59, 130, 246, 0.2); } + &: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); } + `] +}) +export class AppInputComponent implements ControlValueAccessor { + label = input(''); + id = input('input-' + Math.random().toString(36).substr(2, 9)); + type = input('text'); + placeholder = input(''); + error = input(null); + + value: string = ''; + disabled = false; + + onChange: any = () => {}; + onTouched: any = () => {}; + + writeValue(obj: any): void { this.value = obj || ''; } + registerOnChange(fn: any): void { this.onChange = fn; } + registerOnTouched(fn: any): void { this.onTouched = fn; } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } + + onInput(event: Event) { + const val = (event.target as HTMLInputElement).value; + this.value = val; + this.onChange(val); + } +} 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 new file mode 100644 index 0000000..84f0ead --- /dev/null +++ b/frontend/src/app/shared/components/app-select/app-select.component.ts @@ -0,0 +1,72 @@ +import { Component, input, output, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-select', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AppSelectComponent), + 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); } + `] +}) +export class AppSelectComponent implements ControlValueAccessor { + label = input(''); + id = input('select-' + Math.random().toString(36).substr(2, 9)); + options = input<{label: string, value: any}[]>([]); + error = input(null); + + value: any = ''; + disabled = false; + + onChange: any = () => {}; + onTouched: any = () => {}; + + writeValue(obj: any): void { this.value = obj; } + registerOnChange(fn: any): void { this.onChange = fn; } + registerOnTouched(fn: any): void { this.onTouched = fn; } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } + + onSelect(event: Event) { + const val = (event.target as HTMLSelectElement).value; + this.value = val; + this.onChange(val); + } +} 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 new file mode 100644 index 0000000..9bf07ce --- /dev/null +++ b/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts @@ -0,0 +1,52 @@ +import { Component, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-tabs', + standalone: true, + imports: [CommonModule], + 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); + } + } + `] +}) +export class AppTabsComponent { + tabs = input<{label: string, value: string}[]>([]); + activeTab = input(''); + tabChange = output(); + + selectTab(val: string) { + this.tabChange.emit(val); + } +} diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json new file mode 100644 index 0000000..c62326e --- /dev/null +++ b/frontend/src/assets/i18n/en.json @@ -0,0 +1,43 @@ +{ + "NAV": { + "CALCULATOR": "Calculator", + "SHOP": "Shop", + "ABOUT": "About" + }, + "FOOTER": { + "PRIVACY": "Privacy", + "TERMS": "Terms & Conditions", + "CONTACT": "Contact Us" + }, + "CALC": { + "TITLE": "3D Print Calculator", + "SUBTITLE": "Upload your STL file and get an instant estimate of printing costs and time.", + "CTA_START": "Start Now", + "BUSINESS": "Business", + "PRIVATE": "Private", + "MODE_EASY": "Easy", + "MODE_ADVANCED": "Advanced", + "UPLOAD_LABEL": "Drag your STL file here", + "UPLOAD_SUB": "Supports STL, OBJ up to 50MB", + "MATERIAL": "Material", + "QUALITY": "Quality", + "QUANTITY": "Quantity", + "NOTES": "Additional Notes", + "CALCULATE": "Calculate Quote", + "RESULT": "Estimated Quote", + "TIME": "Print Time", + "COST": "Total Cost", + "ORDER": "Order Now", + "CONSULT": "Request Consultation" + }, + "SHOP": { + "TITLE": "Filaments & Accessories", + "ADD_CART": "Add to Cart", + "BACK": "Back to Shop" + }, + "ABOUT": { + "TITLE": "About Us", + "CONTACT_US": "Contact Us", + "SEND": "Send Message" + } +} diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json new file mode 100644 index 0000000..e6a7201 --- /dev/null +++ b/frontend/src/assets/i18n/it.json @@ -0,0 +1,43 @@ +{ + "NAV": { + "CALCULATOR": "Calcolatore", + "SHOP": "Shop", + "ABOUT": "Chi Siamo" + }, + "FOOTER": { + "PRIVACY": "Privacy", + "TERMS": "Termini & Condizioni", + "CONTACT": "Contattaci" + }, + "CALC": { + "TITLE": "Calcola Preventivo 3D", + "SUBTITLE": "Carica il tuo file STL e ricevi una stima immediata dei costi e tempi di stampa.", + "CTA_START": "Inizia Ora", + "BUSINESS": "Aziende", + "PRIVATE": "Privati", + "MODE_EASY": "Easy", + "MODE_ADVANCED": "Advanced", + "UPLOAD_LABEL": "Trascina il tuo file STL qui", + "UPLOAD_SUB": "Supportiamo STL, OBJ fino a 50MB", + "MATERIAL": "Materiale", + "QUALITY": "Qualità", + "QUANTITY": "Quantità", + "NOTES": "Note aggiuntive", + "CALCULATE": "Calcola Preventivo", + "RESULT": "Preventivo Stimato", + "TIME": "Tempo Stampa", + "COST": "Costo Totale", + "ORDER": "Ordina Ora", + "CONSULT": "Richiedi Consulenza" + }, + "SHOP": { + "TITLE": "Filamenti & Accessori", + "ADD_CART": "Aggiungi al Carrello", + "BACK": "Torna allo Shop" + }, + "ABOUT": { + "TITLE": "Chi Siamo", + "CONTACT_US": "Contattaci", + "SEND": "Invia Messaggio" + } +} diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index cac4953..63b43b8 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,119 +1,47 @@ -/* Global Styles & Variables */ -:root { - /* Color Palette - Modern Dark Theme */ - --primary-color: #6366f1; /* Indigo 500 */ - --primary-hover: #4f46e5; /* Indigo 600 */ - --secondary-color: #ec4899; /* Pink 500 */ - - --bg-color: #0f172a; /* Slate 900 */ - --surface-color: #1e293b; /* Slate 800 */ - --surface-hover: #334155; /* Slate 700 */ - - --text-main: #f8fafc; /* Slate 50 */ - --text-muted: #94a3b8; /* Slate 400 */ - - --border-color: #334155; - - /* Spacing & Radius */ - --radius-sm: 0.375rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; - - /* Shadows */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - - /* Transitions */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); -} +/* src/styles.scss */ +@use './styles/theme'; -html, body { - height: 100%; +/* Reset / Base */ +*, *::before, *::after { + box-sizing: border-box; } body { margin: 0; - font-family: 'Roboto', 'Helvetica Neue', sans-serif; - background-color: var(--bg-color); - color: var(--text-main); + font-family: var(--font-family-sans); + background-color: var(--color-bg); + color: var(--color-text); + line-height: 1.5; -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } -/* Common Layout Utilities */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; +h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: 600; + line-height: 1.2; } -.card { - background-color: var(--surface-color); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: 2rem; - box-shadow: var(--shadow-md); - transition: transform var(--transition-fast), box-shadow var(--transition-fast); -} - -.card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.75rem 1.5rem; - border-radius: var(--radius-md); - font-weight: 500; - cursor: pointer; - transition: background-color var(--transition-fast); - border: none; - font-size: 1rem; - text-decoration: none; -} - -.btn-primary { - background-color: var(--primary-color); - color: white; - - &:hover { - background-color: var(--primary-hover); - } -} - -.btn-secondary { - background-color: transparent; - border: 1px solid var(--border-color); - color: var(--text-main); - - &:hover { - background-color: var(--surface-hover); - } -} - -.grid-2 { - display: grid; - grid-template-columns: 1fr; - gap: 2rem; - - @media (min-width: 768px) { - grid-template-columns: repeat(2, 1fr); - } -} - -h1, h2, h3 { +p { margin-top: 0; - font-weight: 700; - letter-spacing: -0.025em; + margin-bottom: var(--space-4); } a { - color: var(--primary-color); + color: var(--color-brand); text-decoration: none; - cursor: pointer; + &:hover { + text-decoration: underline; + } } + +/* Utilities */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--space-4); +} + +.text-center { text-align: center; } +.mb-4 { margin-bottom: var(--space-4); } +.mt-4 { margin-top: var(--space-4); } \ No newline at end of file diff --git a/frontend/src/styles/theme.scss b/frontend/src/styles/theme.scss new file mode 100644 index 0000000..9f67b3c --- /dev/null +++ b/frontend/src/styles/theme.scss @@ -0,0 +1,18 @@ +/* src/styles/theme.scss */ +@use 'tokens'; + +:root { + /* Semantic Colors - Theming Layer */ + --color-bg: var(--color-neutral-50); + --color-bg-card: #ffffff; + --color-text: var(--color-neutral-900); + --color-text-muted: var(--color-secondary-500); + + --color-brand: var(--color-primary-600); + --color-brand-hover: var(--color-primary-700); + + --color-border: var(--color-neutral-200); + + /* Font */ + --font-family-sans: 'Inter', system-ui, -apple-system, sans-serif; +} diff --git a/frontend/src/styles/tokens.scss b/frontend/src/styles/tokens.scss new file mode 100644 index 0000000..1e9dbc6 --- /dev/null +++ b/frontend/src/styles/tokens.scss @@ -0,0 +1,41 @@ +/* src/styles/tokens.scss */ +:root { + /* Colors - Palette */ + --color-primary-500: #3b82f6; + --color-primary-600: #2563eb; + --color-primary-700: #1d4ed8; + + --color-secondary-500: #64748b; + --color-secondary-600: #475569; + + --color-success-500: #22c55e; + --color-warning-500: #eab308; + --color-danger-500: #ef4444; + + --color-neutral-50: #f8fafc; + --color-neutral-100: #f1f5f9; + --color-neutral-200: #e2e8f0; + --color-neutral-300: #cbd5e1; + --color-neutral-800: #1e293b; + --color-neutral-900: #0f172a; + + /* Spacing */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-12: 3rem; + + /* Radius */ + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 1rem; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); +} -- 2.49.1 From 0a538b0d88cad2fd3d0e5b1c13780a03adf0d298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 2 Feb 2026 17:41:20 +0100 Subject: [PATCH 002/135] feat(web): home component --- frontend/src/app/app.routes.ts | 5 +- .../src/app/features/home/home.component.ts | 180 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/features/home/home.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index d206854..bcca1e0 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -5,7 +5,10 @@ export const routes: Routes = [ path: '', loadComponent: () => import('./core/layout/layout.component').then(m => m.LayoutComponent), children: [ - { path: '', redirectTo: 'cal', pathMatch: 'full' }, + { + path: '', + loadComponent: () => import('./features/home/home.component').then(m => m.HomeComponent) + }, { path: 'cal', loadChildren: () => import('./features/calculator/calculator.routes').then(m => m.CALCULATOR_ROUTES) diff --git a/frontend/src/app/features/home/home.component.ts b/frontend/src/app/features/home/home.component.ts new file mode 100644 index 0000000..092dd73 --- /dev/null +++ b/frontend/src/app/features/home/home.component.ts @@ -0,0 +1,180 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; +import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; + +@Component({ + selector: 'app-home-page', + standalone: true, + imports: [CommonModule, RouterLink, TranslateModule, AppButtonComponent, AppCardComponent], + template: ` + +
+
+

+ La Stampa 3D
+ Professionale e Veloce +

+

+ Carica il tuo file STL, ottieni un preventivo istantaneo e ricevi i tuoi pezzi in pochi giorni. + Qualità industriale per prototipi e produzione. +

+
+ + Inizia Preventivo + + + Come Funziona + +
+
+
+ + +
+
+

Dal File all'Oggetto in 3 Step

+ +
+ +
1
+

Carica STL

+

Trascina il tuo modello 3D nel nostro calcolatore intelligente.

+
+ + +
2
+

Configura

+

Scegli materiale (PLA, PETG, TPU) e qualità di stampa.

+
+ + +
3
+

Ricevi

+

Produciamo e spediamo in 24/48 ore con corriere espresso.

+
+
+
+
+ + +
+
+
+

Non solo Servizio Stampa

+

+ Hai già una stampante? Forniamo i migliori filamenti e accessori testati quotidianamente nella nostra farm. + Qualità garantita dai nostri esperti. +

+ + Visita lo Shop + +
+
+ +
🛒
+
+
+
+ + +
+
+

Perché Sceglierci?

+
+
+
5000+
+
Ore di Stampa
+
+
+
100%
+
Qualità Garantita
+
+
+
24h
+
Spedizione Rapida
+
+
+
+ Chi Siamo +
+
+
+ `, + styles: [` + .hero-section { + padding: 8rem 0 6rem; + background: linear-gradient(to bottom right, var(--color-neutral-50), var(--color-neutral-100)); + text-align: center; + } + .hero-content { max-width: 800px; margin: 0 auto; } + .hero-title { + font-size: 3rem; + font-weight: 800; + margin-bottom: var(--space-4); + line-height: 1.1; + letter-spacing: -0.02em; + } + .highlight { color: var(--color-brand); } + .hero-subtitle { + font-size: 1.25rem; + color: var(--color-text-muted); + margin-bottom: var(--space-8); + max-width: 600px; + margin-left: auto; + margin-right: auto; + } + .hero-actions { + display: flex; + gap: var(--space-4); + justify-content: center; + } + + .section { padding: 6rem 0; } + .bg-neutral { background-color: var(--color-neutral-50); } + .bg-dark { background-color: var(--color-neutral-900); } + .text-white { color: white !important; } + + .section-title { font-size: 2rem; margin-bottom: var(--space-8); font-weight: 700; } + .text-center { text-align: center; } + .text-muted { color: var(--color-text-muted); } + + .grid-3 { + display: grid; + gap: var(--space-8); + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + } + + .step-number { + font-size: 3rem; + font-weight: 900; + color: var(--color-neutral-200); + margin-bottom: var(--space-2); + line-height: 1; + } + + .split-layout { + display: grid; + gap: var(--space-12); + align-items: center; + @media(min-width: 768px) { grid-template-columns: 1fr 1fr; } + } + + .mock-img { + width: 100%; + height: 300px; + background-color: var(--color-neutral-100); + border-radius: var(--radius-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 5rem; + } + + .stat-value { font-size: 2.5rem; font-weight: 700; color: var(--color-brand); } + .stat-label { color: var(--color-neutral-300); } + `] +}) +export class HomeComponent {} -- 2.49.1 From 32b9b2ef8dd9639890409d5b3c5c86b074adc9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 2 Feb 2026 18:38:25 +0100 Subject: [PATCH 003/135] feat(web): new style and calculator revisited --- frontend/angular.json | 3 +- .../src/app/core/layout/footer.component.ts | 4 +- .../src/app/core/layout/navbar.component.ts | 2 +- .../features/about/about-page.component.ts | 76 ++- .../contact-form/contact-form.component.ts | 8 +- .../calculator/calculator-page.component.ts | 23 +- .../quote-result/quote-result.component.ts | 43 +- .../upload-form/upload-form.component.ts | 114 +++- .../services/quote-estimator.service.ts | 108 +++- .../src/app/features/home/home.component.ts | 585 ++++++++++++++---- .../product-card/product-card.component.ts | 2 +- .../features/shop/product-detail.component.ts | 4 +- .../app/features/shop/shop-page.component.ts | 4 +- .../app-button/app-button.component.ts | 7 +- .../app-dropzone/app-dropzone.component.ts | 41 +- .../app-input/app-input.component.ts | 2 +- .../components/app-tabs/app-tabs.component.ts | 5 +- .../stl-viewer/stl-viewer.component.ts | 186 ++++++ .../summary-card/summary-card.component.ts | 52 ++ frontend/src/assets/i18n/en.json | 33 +- frontend/src/assets/i18n/it.json | 35 +- frontend/src/index.html | 6 +- frontend/src/styles.scss | 3 +- frontend/src/styles/theme.scss | 13 +- frontend/src/styles/tokens.scss | 24 +- 25 files changed, 1084 insertions(+), 299 deletions(-) create mode 100644 frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts create mode 100644 frontend/src/app/shared/components/summary-card/summary-card.component.ts diff --git a/frontend/angular.json b/frontend/angular.json index 4f1cbbd..03d92e6 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -29,7 +29,8 @@ { "glob": "**/*", "input": "public" - } + }, + "src/assets" ], "styles": [ "@angular/material/prebuilt-themes/azure-blue.css", diff --git a/frontend/src/app/core/layout/footer.component.ts b/frontend/src/app/core/layout/footer.component.ts index 16bbb2e..d0cb4ae 100644 --- a/frontend/src/app/core/layout/footer.component.ts +++ b/frontend/src/app/core/layout/footer.component.ts @@ -10,8 +10,8 @@ import { RouterLink } from '@angular/router';