From bcdeafe1194cf1ad9d78010585e40c6dcc30d907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 6 Feb 2026 11:33:25 +0100 Subject: [PATCH] chore(web): refractor --- GEMINI.md | 4 + frontend/src/app/app.component.scss | 0 frontend/src/app/app.component.ts | 3 +- .../src/app/core/layout/layout.component.html | 7 + .../src/app/core/layout/layout.component.scss | 9 + .../src/app/core/layout/layout.component.ts | 22 +- .../features/about/about-page.component.html | 42 +++ .../features/about/about-page.component.scss | 157 +++++++++ .../features/about/about-page.component.ts | 205 +----------- .../calculator/calculator-page.component.html | 64 ++++ .../calculator/calculator-page.component.scss | 99 ++++++ .../calculator/calculator-page.component.ts | 169 +--------- .../quote-result/quote-result.component.html | 62 ++++ .../quote-result/quote-result.component.scss | 85 +++++ .../quote-result/quote-result.component.ts | 153 +-------- .../upload-form/upload-form.component.html | 134 ++++++++ .../upload-form/upload-form.component.scss | 174 ++++++++++ .../upload-form/upload-form.component.ts | 314 +----------------- .../contact-form/contact-form.component.html | 73 ++++ .../contact-form/contact-form.component.scss | 133 ++++++++ .../contact-form/contact-form.component.ts | 212 +----------- .../contact/contact-page.component.html | 12 + .../contact/contact-page.component.scss | 14 + .../contact/contact-page.component.ts | 32 +- .../product-card/product-card.component.html | 13 + .../product-card/product-card.component.scss | 18 + .../product-card/product-card.component.ts | 37 +-- .../shop/product-detail.component.html | 25 ++ .../shop/product-detail.component.scss | 20 ++ .../features/shop/product-detail.component.ts | 51 +-- .../features/shop/shop-page.component.html | 12 + .../features/shop/shop-page.component.scss | 7 + .../app/features/shop/shop-page.component.ts | 25 +- .../app-alert/app-alert.component.html | 9 + .../app-alert/app-alert.component.scss | 12 + .../app-alert/app-alert.component.ts | 27 +- .../app-button/app-button.component.html | 7 + .../app-button/app-button.component.scss | 49 +++ .../app-button/app-button.component.ts | 62 +--- .../app-card/app-card.component.html | 3 + .../app-card/app-card.component.scss | 12 + .../components/app-card/app-card.component.ts | 21 +- .../app-dropzone/app-dropzone.component.html | 26 ++ .../app-dropzone/app-dropzone.component.scss | 32 ++ .../app-dropzone/app-dropzone.component.ts | 64 +--- .../app-input/app-input.component.html | 14 + .../app-input/app-input.component.scss | 14 + .../app-input/app-input.component.ts | 34 +- .../app-select/app-select.component.html | 16 + .../app-select/app-select.component.scss | 13 + .../app-select/app-select.component.ts | 35 +- .../app-tabs/app-tabs.component.html | 10 + .../app-tabs/app-tabs.component.scss | 21 ++ .../components/app-tabs/app-tabs.component.ts | 37 +-- .../stl-viewer/stl-viewer.component.html | 13 + .../stl-viewer/stl-viewer.component.scss | 44 +++ .../stl-viewer/stl-viewer.component.ts | 63 +--- .../summary-card/summary-card.component.html | 6 + .../summary-card/summary-card.component.scss | 29 ++ .../summary-card/summary-card.component.ts | 41 +-- 60 files changed, 1534 insertions(+), 1567 deletions(-) create mode 100644 frontend/src/app/app.component.scss create mode 100644 frontend/src/app/core/layout/layout.component.html create mode 100644 frontend/src/app/core/layout/layout.component.scss create mode 100644 frontend/src/app/features/about/about-page.component.html create mode 100644 frontend/src/app/features/about/about-page.component.scss create mode 100644 frontend/src/app/features/calculator/calculator-page.component.html create mode 100644 frontend/src/app/features/calculator/calculator-page.component.scss create mode 100644 frontend/src/app/features/calculator/components/quote-result/quote-result.component.html create mode 100644 frontend/src/app/features/calculator/components/quote-result/quote-result.component.scss create mode 100644 frontend/src/app/features/calculator/components/upload-form/upload-form.component.html create mode 100644 frontend/src/app/features/calculator/components/upload-form/upload-form.component.scss create mode 100644 frontend/src/app/features/contact/components/contact-form/contact-form.component.html create mode 100644 frontend/src/app/features/contact/components/contact-form/contact-form.component.scss create mode 100644 frontend/src/app/features/contact/contact-page.component.html create mode 100644 frontend/src/app/features/contact/contact-page.component.scss create mode 100644 frontend/src/app/features/shop/components/product-card/product-card.component.html create mode 100644 frontend/src/app/features/shop/components/product-card/product-card.component.scss create mode 100644 frontend/src/app/features/shop/product-detail.component.html create mode 100644 frontend/src/app/features/shop/product-detail.component.scss create mode 100644 frontend/src/app/features/shop/shop-page.component.html create mode 100644 frontend/src/app/features/shop/shop-page.component.scss create mode 100644 frontend/src/app/shared/components/app-alert/app-alert.component.html create mode 100644 frontend/src/app/shared/components/app-alert/app-alert.component.scss create mode 100644 frontend/src/app/shared/components/app-button/app-button.component.html create mode 100644 frontend/src/app/shared/components/app-button/app-button.component.scss create mode 100644 frontend/src/app/shared/components/app-card/app-card.component.html create mode 100644 frontend/src/app/shared/components/app-card/app-card.component.scss create mode 100644 frontend/src/app/shared/components/app-dropzone/app-dropzone.component.html create mode 100644 frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss create mode 100644 frontend/src/app/shared/components/app-input/app-input.component.html create mode 100644 frontend/src/app/shared/components/app-input/app-input.component.scss create mode 100644 frontend/src/app/shared/components/app-select/app-select.component.html create mode 100644 frontend/src/app/shared/components/app-select/app-select.component.scss create mode 100644 frontend/src/app/shared/components/app-tabs/app-tabs.component.html create mode 100644 frontend/src/app/shared/components/app-tabs/app-tabs.component.scss create mode 100644 frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html create mode 100644 frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss create mode 100644 frontend/src/app/shared/components/summary-card/summary-card.component.html create mode 100644 frontend/src/app/shared/components/summary-card/summary-card.component.scss diff --git a/GEMINI.md b/GEMINI.md index 043579f..997d781 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -36,3 +36,7 @@ Questo file serve a dare contesto all'AI (Antigravity/Gemini) sulla struttura e - Per eseguire il backend serve `uvicorn`. - Il frontend richiede `npm install` al primo avvio. - Le configurazioni di stampa (layer height, wall thickness, infill) sono attualmente hardcoded o con valori di default nel backend, ma potrebbero essere esposte come parametri API in futuro. + +## AI Agent Rules +- **No Inline Code**: Tutti i componenti Angular DEVONO usare file separati per HTML (`templateUrl`) e SCSS (`styleUrl`). È vietato usare `template` o `styles` inline nel decoratore `@Component`. + diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 3c3d410..966ecc2 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -5,6 +5,7 @@ import { RouterOutlet } from '@angular/router'; selector: 'app-root', standalone: true, imports: [RouterOutlet], - template: `` + templateUrl: './app.component.html', + styleUrl: './app.component.scss' }) export class AppComponent {} diff --git a/frontend/src/app/core/layout/layout.component.html b/frontend/src/app/core/layout/layout.component.html new file mode 100644 index 0000000..a1775b8 --- /dev/null +++ b/frontend/src/app/core/layout/layout.component.html @@ -0,0 +1,7 @@ +
+ +
+ +
+ +
diff --git a/frontend/src/app/core/layout/layout.component.scss b/frontend/src/app/core/layout/layout.component.scss new file mode 100644 index 0000000..dbb9226 --- /dev/null +++ b/frontend/src/app/core/layout/layout.component.scss @@ -0,0 +1,9 @@ +.layout-wrapper { + display: flex; + flex-direction: column; + min-height: 100vh; +} +.main-content { + flex: 1; + padding-bottom: var(--space-12); +} diff --git a/frontend/src/app/core/layout/layout.component.ts b/frontend/src/app/core/layout/layout.component.ts index ac27e33..7e90e14 100644 --- a/frontend/src/app/core/layout/layout.component.ts +++ b/frontend/src/app/core/layout/layout.component.ts @@ -7,25 +7,7 @@ import { FooterComponent } from './footer.component'; selector: 'app-layout', standalone: true, imports: [RouterOutlet, NavbarComponent, FooterComponent], - template: ` -
- -
- -
- -
- `, - styles: [` - .layout-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; - } - .main-content { - flex: 1; - padding-bottom: var(--space-12); - } - `] + templateUrl: './layout.component.html', + styleUrl: './layout.component.scss' }) export class LayoutComponent {} diff --git a/frontend/src/app/features/about/about-page.component.html b/frontend/src/app/features/about/about-page.component.html new file mode 100644 index 0000000..51321c3 --- /dev/null +++ b/frontend/src/app/features/about/about-page.component.html @@ -0,0 +1,42 @@ +
+
+ + +
+

{{ 'ABOUT.EYEBROW' | translate }}

+

{{ 'ABOUT.TITLE' | translate }}

+

{{ 'ABOUT.SUBTITLE' | translate }}

+ +
+ +

{{ 'ABOUT.HOW_TEXT' | translate }}

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

{{ 'ABOUT.EYEBROW' | translate }}

-

{{ 'ABOUT.TITLE' | translate }}

-

{{ 'ABOUT.SUBTITLE' | translate }}

- -
- -

{{ 'ABOUT.HOW_TEXT' | translate }}

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

{{ 'CALC.TITLE' | translate }}

+

{{ 'CALC.SUBTITLE' | translate }}

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

Analisi in corso...

+

Stiamo analizzando la geometria e calcolando il percorso utensile.

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

{{ 'CALC.BENEFITS_TITLE' | translate }}

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

{{ 'CALC.TITLE' | translate }}

-

{{ 'CALC.SUBTITLE' | translate }}

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

Analisi in corso...

-

Stiamo analizzando la geometria e calcolando il percorso utensile.

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

{{ 'CALC.BENEFITS_TITLE' | translate }}

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

{{ 'CALC.RESULT' | translate }}

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

{{ 'CALC.RESULT' | translate }}

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

{{ 'CONTACT.UPLOAD_HINT' | translate }}

+ +
+ +

{{ 'CONTACT.DROP_FILES' | translate }}

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

{{ 'CONTACT.UPLOAD_HINT' | translate }}

- -
- -

{{ 'CONTACT.DROP_FILES' | translate }}

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

{{ 'CONTACT.TITLE' | translate }}

+

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

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

{{ 'CONTACT.TITLE' | translate }}

-

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

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

+ {{ product().name }} +

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

- {{ product().name }} -

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

{{ p.name }}

+

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

+ +

{{ p.description }}

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

Prodotto non trovato.

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

{{ p.name }}

-

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

- -

{{ p.description }}

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

Prodotto non trovato.

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

{{ 'SHOP.TITLE' | translate }}

+

{{ 'SHOP.SUBTITLE' | translate }}

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

{{ 'SHOP.TITLE' | translate }}

-

{{ 'SHOP.SUBTITLE' | translate }}

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

{{ label() }}

+

{{ subtext() }}

+ + @if (fileNames().length > 0) { +
+ @for (name of fileNames(); track name) { +
{{ name }}
+ } +
+ } +
+
diff --git a/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss new file mode 100644 index 0000000..42c3b1c --- /dev/null +++ b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.scss @@ -0,0 +1,32 @@ +.dropzone { + border: 2px dashed var(--color-border); + border-radius: var(--radius-lg); + padding: var(--space-8); + text-align: center; + cursor: pointer; + transition: all 0.2s; + background-color: var(--color-neutral-50); + + &:hover, &.dragover { + border-color: var(--color-brand); + background-color: var(--color-neutral-100); + } +} +.icon { color: var(--color-brand); margin-bottom: var(--space-4); } +.text { font-weight: 600; margin-bottom: var(--space-2); } +.subtext { font-size: 0.875rem; color: var(--color-text-muted); } +.file-badges { + margin-top: var(--space-4); + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + justify-content: center; +} +.file-badge { + padding: var(--space-2) var(--space-4); + background: var(--color-neutral-200); + border-radius: var(--radius-md); + font-weight: 600; + color: var(--color-primary-700); + font-size: 0.85rem; +} diff --git a/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts index fd02157..fd1e563 100644 --- a/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts +++ b/frontend/src/app/shared/components/app-dropzone/app-dropzone.component.ts @@ -5,68 +5,8 @@ import { CommonModule } from '@angular/common'; selector: 'app-dropzone', standalone: true, imports: [CommonModule], - template: ` -
- - -
-
- -
-

{{ label() }}

-

{{ subtext() }}

- - @if (fileNames().length > 0) { -
- @for (name of fileNames(); track name) { -
{{ name }}
- } -
- } -
-
- `, - styles: [` - .dropzone { - border: 2px dashed var(--color-border); - border-radius: var(--radius-lg); - padding: var(--space-8); - text-align: center; - cursor: pointer; - transition: all 0.2s; - background-color: var(--color-neutral-50); - - &:hover, &.dragover { - border-color: var(--color-brand); - background-color: var(--color-neutral-100); - } - } - .icon { color: var(--color-brand); margin-bottom: var(--space-4); } - .text { font-weight: 600; margin-bottom: var(--space-2); } - .subtext { font-size: 0.875rem; color: var(--color-text-muted); } - .file-badges { - margin-top: var(--space-4); - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - justify-content: center; - } - .file-badge { - padding: var(--space-2) var(--space-4); - background: var(--color-neutral-200); - border-radius: var(--radius-md); - font-weight: 600; - color: var(--color-primary-700); - font-size: 0.85rem; - } - `] + templateUrl: './app-dropzone.component.html', + styleUrl: './app-dropzone.component.scss' }) export class AppDropzoneComponent { label = input('Drop files here or click to upload'); diff --git a/frontend/src/app/shared/components/app-input/app-input.component.html b/frontend/src/app/shared/components/app-input/app-input.component.html new file mode 100644 index 0000000..7e30cf2 --- /dev/null +++ b/frontend/src/app/shared/components/app-input/app-input.component.html @@ -0,0 +1,14 @@ +
+ @if (label()) { } + + @if (error()) { {{ error() }} } +
diff --git a/frontend/src/app/shared/components/app-input/app-input.component.scss b/frontend/src/app/shared/components/app-input/app-input.component.scss new file mode 100644 index 0000000..8919bfb --- /dev/null +++ b/frontend/src/app/shared/components/app-input/app-input.component.scss @@ -0,0 +1,14 @@ +.form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); } +label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); } +.form-control { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-size: 1rem; + width: 100%; + background: var(--color-bg-card); + color: var(--color-text); + &:focus { outline: none; border-color: var(--color-brand); box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25); } + &:disabled { background: var(--color-neutral-100); cursor: not-allowed; } +} +.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } diff --git a/frontend/src/app/shared/components/app-input/app-input.component.ts b/frontend/src/app/shared/components/app-input/app-input.component.ts index 929fe11..6723696 100644 --- a/frontend/src/app/shared/components/app-input/app-input.component.ts +++ b/frontend/src/app/shared/components/app-input/app-input.component.ts @@ -13,38 +13,8 @@ import { CommonModule } from '@angular/common'; multi: true } ], - template: ` -
- @if (label()) { } - - @if (error()) { {{ error() }} } -
- `, - styles: [` - .form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); } - label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); } - .form-control { - padding: 0.5rem 0.75rem; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - font-size: 1rem; - width: 100%; - background: var(--color-bg-card); - color: var(--color-text); - &:focus { outline: none; border-color: var(--color-brand); box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25); } - &:disabled { background: var(--color-neutral-100); cursor: not-allowed; } - } - .error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } - `] + templateUrl: './app-input.component.html', + styleUrl: './app-input.component.scss' }) export class AppInputComponent implements ControlValueAccessor { label = input(''); diff --git a/frontend/src/app/shared/components/app-select/app-select.component.html b/frontend/src/app/shared/components/app-select/app-select.component.html new file mode 100644 index 0000000..ddb857a --- /dev/null +++ b/frontend/src/app/shared/components/app-select/app-select.component.html @@ -0,0 +1,16 @@ +
+ @if (label()) { } + + @if (error()) { {{ error() }} } +
diff --git a/frontend/src/app/shared/components/app-select/app-select.component.scss b/frontend/src/app/shared/components/app-select/app-select.component.scss new file mode 100644 index 0000000..bcf488a --- /dev/null +++ b/frontend/src/app/shared/components/app-select/app-select.component.scss @@ -0,0 +1,13 @@ +.form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); } +label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); } +.form-control { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-size: 1rem; + width: 100%; + background: var(--color-bg-card); + color: var(--color-text); + &:focus { outline: none; border-color: var(--color-brand); } +} +.error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } diff --git a/frontend/src/app/shared/components/app-select/app-select.component.ts b/frontend/src/app/shared/components/app-select/app-select.component.ts index 84f0ead..e4e5e0d 100644 --- a/frontend/src/app/shared/components/app-select/app-select.component.ts +++ b/frontend/src/app/shared/components/app-select/app-select.component.ts @@ -13,39 +13,8 @@ import { CommonModule } from '@angular/common'; multi: true } ], - template: ` -
- @if (label()) { } - - @if (error()) { {{ error() }} } -
- `, - styles: [` - .form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); } - label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); } - .form-control { - padding: 0.5rem 0.75rem; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - font-size: 1rem; - width: 100%; - background: var(--color-bg-card); - color: var(--color-text); - &:focus { outline: none; border-color: var(--color-brand); } - } - .error-text { color: var(--color-danger-500); font-size: 0.75rem; margin-top: var(--space-1); } - `] + templateUrl: './app-select.component.html', + styleUrl: './app-select.component.scss' }) export class AppSelectComponent implements ControlValueAccessor { label = input(''); diff --git a/frontend/src/app/shared/components/app-tabs/app-tabs.component.html b/frontend/src/app/shared/components/app-tabs/app-tabs.component.html new file mode 100644 index 0000000..d85c910 --- /dev/null +++ b/frontend/src/app/shared/components/app-tabs/app-tabs.component.html @@ -0,0 +1,10 @@ +
+ @for (tab of tabs(); track tab.value) { + + } +
diff --git a/frontend/src/app/shared/components/app-tabs/app-tabs.component.scss b/frontend/src/app/shared/components/app-tabs/app-tabs.component.scss new file mode 100644 index 0000000..2825a0e --- /dev/null +++ b/frontend/src/app/shared/components/app-tabs/app-tabs.component.scss @@ -0,0 +1,21 @@ +.tabs { + display: flex; + border-bottom: 1px solid var(--color-border); + gap: var(--space-4); +} +.tab { + background: none; + border: none; + padding: var(--space-3) var(--space-4); + cursor: pointer; + font-weight: 500; + color: var(--color-text-muted); + border-bottom: 2px solid transparent; + transition: all 0.2s; + + &:hover { color: var(--color-text); } + &.active { + color: var(--color-brand); + border-bottom-color: var(--color-brand); + } +} diff --git a/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts b/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts index 27d093b..28ed4ec 100644 --- a/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts +++ b/frontend/src/app/shared/components/app-tabs/app-tabs.component.ts @@ -6,41 +6,8 @@ import { TranslateModule } from '@ngx-translate/core'; selector: 'app-tabs', standalone: true, imports: [CommonModule, TranslateModule], - template: ` -
- @for (tab of tabs(); track tab.value) { - - } -
- `, - styles: [` - .tabs { - display: flex; - border-bottom: 1px solid var(--color-border); - gap: var(--space-4); - } - .tab { - background: none; - border: none; - padding: var(--space-3) var(--space-4); - cursor: pointer; - font-weight: 500; - color: var(--color-text-muted); - border-bottom: 2px solid transparent; - transition: all 0.2s; - - &:hover { color: var(--color-text); } - &.active { - color: var(--color-brand); - border-bottom-color: var(--color-brand); - } - } - `] + templateUrl: './app-tabs.component.html', + styleUrl: './app-tabs.component.scss' }) export class AppTabsComponent { tabs = input<{label: string, value: string}[]>([]); diff --git a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html new file mode 100644 index 0000000..afeb836 --- /dev/null +++ b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.html @@ -0,0 +1,13 @@ +
+ @if (loading) { +
+
+ Loading 3D Model... +
+ } + @if (file && !loading) { +
+ {{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm +
+ } +
diff --git a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss new file mode 100644 index 0000000..32c4c4a --- /dev/null +++ b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.scss @@ -0,0 +1,44 @@ +.viewer-container { + width: 100%; + height: 300px; + background: var(--color-neutral-50); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + overflow: hidden; + position: relative; +} +.loading-overlay { + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + z-index: 10; + color: var(--color-text-muted); +} +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--color-neutral-200); + border-top-color: var(--color-brand); + border-radius: 50%; + animation: spin 1s linear infinite; +} +@keyframes spin { + to { transform: rotate(360deg); } +} +.dims-overlay { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(0,0,0,0.6); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-family: monospace; + pointer-events: none; +} diff --git a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts index 45a9ee5..038c6b8 100644 --- a/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts +++ b/frontend/src/app/shared/components/stl-viewer/stl-viewer.component.ts @@ -10,67 +10,8 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; selector: 'app-stl-viewer', standalone: true, imports: [CommonModule], - template: ` -
- @if (loading) { -
-
- Loading 3D Model... -
- } - @if (file && !loading) { -
- {{ dimensions.x }} x {{ dimensions.y }} x {{ dimensions.z }} mm -
- } -
- `, - styles: [` - .viewer-container { - width: 100%; - height: 300px; - background: var(--color-neutral-50); - border-radius: var(--radius-lg); - border: 1px solid var(--color-border); - overflow: hidden; - position: relative; - } - .loading-overlay { - position: absolute; - inset: 0; - background: rgba(255, 255, 255, 0.8); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - z-index: 10; - color: var(--color-text-muted); - } - .spinner { - width: 32px; - height: 32px; - border: 3px solid var(--color-neutral-200); - border-top-color: var(--color-brand); - border-radius: 50%; - animation: spin 1s linear infinite; - } - @keyframes spin { - to { transform: rotate(360deg); } - } - .dims-overlay { - position: absolute; - bottom: 8px; - right: 8px; - background: rgba(0,0,0,0.6); - color: white; - padding: 4px 8px; - border-radius: 4px; - font-size: 0.75rem; - font-family: monospace; - pointer-events: none; - } - `] + templateUrl: './stl-viewer.component.html', + styleUrl: './stl-viewer.component.scss' }) export class StlViewerComponent implements OnInit, OnDestroy, OnChanges { @Input() file: File | null = null; diff --git a/frontend/src/app/shared/components/summary-card/summary-card.component.html b/frontend/src/app/shared/components/summary-card/summary-card.component.html new file mode 100644 index 0000000..adb2df4 --- /dev/null +++ b/frontend/src/app/shared/components/summary-card/summary-card.component.html @@ -0,0 +1,6 @@ +
+ {{ label() }} + + + +
diff --git a/frontend/src/app/shared/components/summary-card/summary-card.component.scss b/frontend/src/app/shared/components/summary-card/summary-card.component.scss new file mode 100644 index 0000000..3c8c0f9 --- /dev/null +++ b/frontend/src/app/shared/components/summary-card/summary-card.component.scss @@ -0,0 +1,29 @@ +.summary-card { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--space-3); + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + height: 100%; + justify-content: center; +} +.highlight { + background: var(--color-neutral-100); + border-color: var(--color-border); +} +.label { + font-size: 0.875rem; + color: var(--color-text-muted); + margin-bottom: var(--space-1); +} +.value { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-text); +} +.large { + font-size: 2rem; + color: var(--color-brand); +} diff --git a/frontend/src/app/shared/components/summary-card/summary-card.component.ts b/frontend/src/app/shared/components/summary-card/summary-card.component.ts index a15faa4..68a8f3f 100644 --- a/frontend/src/app/shared/components/summary-card/summary-card.component.ts +++ b/frontend/src/app/shared/components/summary-card/summary-card.component.ts @@ -5,45 +5,8 @@ import { CommonModule } from '@angular/common'; selector: 'app-summary-card', standalone: true, imports: [CommonModule], - template: ` -
- {{ label() }} - - - -
- `, - styles: [` - .summary-card { - display: flex; - flex-direction: column; - align-items: center; - padding: var(--space-3); - background: var(--color-bg-card); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - height: 100%; - justify-content: center; - } - .highlight { - background: var(--color-neutral-100); - border-color: var(--color-border); - } - .label { - font-size: 0.875rem; - color: var(--color-text-muted); - margin-bottom: var(--space-1); - } - .value { - font-size: 1.25rem; - font-weight: 700; - color: var(--color-text); - } - .large { - font-size: 2rem; - color: var(--color-brand); - } - `] + templateUrl: './summary-card.component.html', + styleUrl: './summary-card.component.scss' }) export class SummaryCardComponent { label = input.required();