diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index bcca1e0..19d0cdf 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -20,6 +20,10 @@ export const routes: Routes = [
{
path: 'about',
loadChildren: () => import('./features/about/about.routes').then(m => m.ABOUT_ROUTES)
+ },
+ {
+ path: 'contact',
+ loadChildren: () => import('./features/contact/contact.routes').then(m => m.CONTACT_ROUTES)
}
]
}
diff --git a/frontend/src/app/core/layout/navbar.component.html b/frontend/src/app/core/layout/navbar.component.html
index cb0e293..ec9383a 100644
--- a/frontend/src/app/core/layout/navbar.component.html
+++ b/frontend/src/app/core/layout/navbar.component.html
@@ -3,9 +3,11 @@
3D fab
diff --git a/frontend/src/app/features/about/about-page.component.ts b/frontend/src/app/features/about/about-page.component.ts
index 6c07431..0a58711 100644
--- a/frontend/src/app/features/about/about-page.component.ts
+++ b/frontend/src/app/features/about/about-page.component.ts
@@ -1,12 +1,11 @@
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],
+ imports: [TranslateModule],
template: `
@@ -37,14 +36,25 @@ import { AppCardComponent } from '../../shared/components/app-card/app-card.comp
{{ 'ABOUT.TARGET_TITLE' | translate }}
{{ 'ABOUT.TARGET_TEXT' | translate }}
+
+
{{ 'ABOUT.TEAM_TITLE' | translate }}
+
-
+
`,
styles: [`
@@ -91,6 +101,22 @@ import { AppCardComponent } from '../../shared/components/app-card/app-card.comp
font-weight: 600;
}
.text-muted { color: var(--color-text-muted); }
+ .team-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: var(--space-4);
+ margin-top: var(--space-4);
+ }
+ .team-member {
+ text-align: center;
+ }
+ .placeholder-img {
+ width: 100%;
+ aspect-ratio: 1;
+ background: var(--color-neutral-100);
+ border-radius: var(--radius-md);
+ margin-bottom: var(--space-2);
+ }
`]
})
export class 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
deleted file mode 100644
index eb6cc2e..0000000
--- a/frontend/src/app/features/about/components/contact-form/contact-form.component.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-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: `
-
- `,
- 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/contact/components/contact-form/contact-form.component.ts b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts
new file mode 100644
index 0000000..9500fc9
--- /dev/null
+++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.ts
@@ -0,0 +1,306 @@
+import { Component, signal, effect } 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';
+
+interface FilePreview {
+ file: File;
+ url?: string;
+ type: 'image' | 'pdf' | '3d' | 'other';
+}
+
+@Component({
+ selector: 'app-contact-form',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, TranslateModule, AppInputComponent, AppButtonComponent],
+ template: `
+
+ `,
+ 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; }
+ }
+ }
+
+ /* Modify direct app-input child of row if possible or target host */
+ app-input.col {
+ width: 100%;
+ }
+
+ .checkbox-group {
+ flex-direction: row;
+ align-items: center;
+ gap: var(--space-2);
+ input[type="checkbox"] { width: auto; margin: 0; }
+ label { margin: 0; }
+ }
+
+ .company-fields {
+ padding-left: var(--space-4);
+ border-left: 2px solid var(--color-border);
+ margin-bottom: var(--space-4);
+ }
+
+ .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; }
+ }
+ `]
+})
+export class ContactFormComponent {
+ form: FormGroup;
+ sent = signal(false);
+ files = signal([]);
+
+ requestTypes = [
+ { value: 'custom', label: 'CONTACT.REQ_TYPE_CUSTOM' },
+ { value: 'series', label: 'CONTACT.REQ_TYPE_SERIES' },
+ { value: 'consult', label: 'CONTACT.REQ_TYPE_CONSULT' },
+ { value: 'question', label: 'CONTACT.REQ_TYPE_QUESTION' }
+ ];
+
+ constructor(private fb: FormBuilder) {
+ this.form = this.fb.group({
+ requestType: ['custom', Validators.required],
+ name: ['', Validators.required],
+ email: ['', [Validators.required, Validators.email]],
+ phone: [''],
+ message: ['', Validators.required],
+ isCompany: [false],
+ companyName: [''],
+ referencePerson: ['']
+ });
+
+ // Handle conditional validation for Company fields
+ this.form.get('isCompany')?.valueChanges.subscribe(isCompany => {
+ const companyNameControl = this.form.get('companyName');
+ const refPersonControl = this.form.get('referencePerson');
+
+ if (isCompany) {
+ companyNameControl?.setValidators([Validators.required]);
+ refPersonControl?.setValidators([Validators.required]);
+ } else {
+ companyNameControl?.clearValidators();
+ refPersonControl?.clearValidators();
+ }
+ companyNameControl?.updateValueAndValidity();
+ refPersonControl?.updateValueAndValidity();
+ });
+ }
+
+ onFileSelected(event: Event) {
+ const input = event.target as HTMLInputElement;
+ if (input.files) this.handleFiles(Array.from(input.files));
+ }
+
+ onDragOver(event: DragEvent) {
+ event.preventDefault(); event.stopPropagation();
+ }
+
+ onDrop(event: DragEvent) {
+ event.preventDefault(); event.stopPropagation();
+ if (event.dataTransfer?.files) this.handleFiles(Array.from(event.dataTransfer.files));
+ }
+
+ handleFiles(newFiles: File[]) {
+ const currentFiles = this.files();
+ if (currentFiles.length + newFiles.length > 15) {
+ alert("Max 15 files limit reached.");
+ return;
+ }
+
+ newFiles.forEach(file => {
+ const type = this.getFileType(file);
+ const preview: FilePreview = { file, type };
+
+ if (type === 'image') {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ preview.url = e.target?.result as string;
+ this.files.update(files => [...files]);
+ };
+ reader.readAsDataURL(file);
+ }
+ this.files.update(files => [...files, preview]);
+ });
+ }
+
+ removeFile(index: number) {
+ this.files.update(files => files.filter((_, i) => i !== index));
+ }
+
+ getFileType(file: File): 'image' | 'pdf' | '3d' | 'other' {
+ if (file.type.startsWith('image/')) return 'image';
+ if (file.type === 'application/pdf') return 'pdf';
+ const ext = file.name.split('.').pop()?.toLowerCase();
+ if (['stl', 'step', 'stp', '3mf', 'obj'].includes(ext || '')) return '3d';
+ return 'other';
+ }
+
+ onSubmit() {
+ if (this.form.valid) {
+ const formData = {
+ ...this.form.value,
+ files: this.files().map(f => f.file)
+ };
+ console.log('Form Submit:', formData);
+
+ this.sent.set(true);
+ setTimeout(() => {
+ this.sent.set(false);
+ this.form.reset({ requestType: 'custom', isCompany: false });
+ this.files.set([]);
+ }, 3000);
+ } else {
+ this.form.markAllAsTouched();
+ }
+ }
+}
diff --git a/frontend/src/app/features/contact/contact-page.component.ts b/frontend/src/app/features/contact/contact-page.component.ts
new file mode 100644
index 0000000..280b744
--- /dev/null
+++ b/frontend/src/app/features/contact/contact-page.component.ts
@@ -0,0 +1,42 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+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-contact-page',
+ standalone: true,
+ imports: [CommonModule, TranslateModule, ContactFormComponent, AppCardComponent],
+ template: `
+
+
+
+ `,
+ styles: [`
+ .contact-hero {
+ padding: 5rem 0 3.5rem;
+ background: var(--color-bg);
+ text-align: center;
+ }
+ .subtitle {
+ color: var(--color-text-muted);
+ max-width: 640px;
+ margin: var(--space-3) auto 0;
+ }
+ .content {
+ padding: 3rem 0 5rem;
+ max-width: 800px;
+ }
+ `]
+})
+export class ContactPageComponent {}
diff --git a/frontend/src/app/features/contact/contact.routes.ts b/frontend/src/app/features/contact/contact.routes.ts
new file mode 100644
index 0000000..64a1096
--- /dev/null
+++ b/frontend/src/app/features/contact/contact.routes.ts
@@ -0,0 +1,8 @@
+import { Routes } from '@angular/router';
+
+export const CONTACT_ROUTES: Routes = [
+ {
+ path: '',
+ loadComponent: () => import('./contact-page.component').then(m => m.ContactPageComponent)
+ }
+];
diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json
index b34fb34..93de138 100644
--- a/frontend/src/assets/i18n/en.json
+++ b/frontend/src/assets/i18n/en.json
@@ -1,8 +1,10 @@
{
"NAV": {
+ "HOME": "Home",
"CALCULATOR": "Calculator",
"SHOP": "Shop",
- "ABOUT": "About"
+ "ABOUT": "About",
+ "CONTACT": "Contact Us"
},
"FOOTER": {
"PRIVACY": "Privacy",
@@ -58,7 +60,22 @@
"SERVICE_4": "File verification and optimization for printing",
"TARGET_TITLE": "Who is it for",
"TARGET_TEXT": "Small businesses, freelancers, makers and customers looking for a ready-made product from the shop.",
- "CONTACT_US": "Contact Us",
- "SEND": "Send Message"
+ "TEAM_TITLE": "Our Team"
+ },
+ "CONTACT": {
+ "TITLE": "Contact Us",
+ "SEND": "Send Message",
+ "REQ_TYPE_LABEL": "Type of Request",
+ "REQ_TYPE_CUSTOM": "Custom Quote",
+ "REQ_TYPE_SERIES": "Series Production",
+ "REQ_TYPE_CONSULT": "Consultation",
+ "REQ_TYPE_QUESTION": "General Questions",
+ "PHONE": "Phone",
+ "IS_COMPANY": "Are you a company?",
+ "COMPANY_NAME": "Company Name",
+ "REF_PERSON": "Reference Person",
+ "UPLOAD_LABEL": "Attachments",
+ "UPLOAD_HINT": "Max 15 files. Supported: Images, PDF, STL, STEP, 3MF, OBJ",
+ "DROP_FILES": "Drop files here or click to upload"
}
}
diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json
index 209a435..a11d2ee 100644
--- a/frontend/src/assets/i18n/it.json
+++ b/frontend/src/assets/i18n/it.json
@@ -1,8 +1,10 @@
{
"NAV": {
+ "HOME": "Home",
"CALCULATOR": "Calcolatore",
"SHOP": "Shop",
- "ABOUT": "Chi Siamo"
+ "ABOUT": "Chi Siamo",
+ "CONTACT": "Contattaci"
},
"FOOTER": {
"PRIVACY": "Privacy",
@@ -63,7 +65,22 @@
"SERVICE_4": "Verifica file e ottimizzazione per la stampa",
"TARGET_TITLE": "Per chi è",
"TARGET_TEXT": "Piccole aziende, freelance, smanettoni e clienti che cercano un prodotto già pronto dallo shop.",
- "CONTACT_US": "Contattaci",
- "SEND": "Invia Messaggio"
+ "TEAM_TITLE": "Il Nostro Team"
+ },
+ "CONTACT": {
+ "TITLE": "Contattaci",
+ "SEND": "Invia Messaggio",
+ "REQ_TYPE_LABEL": "Tipo di Richiesta",
+ "REQ_TYPE_CUSTOM": "Preventivo Personalizzato",
+ "REQ_TYPE_SERIES": "Stampa in Serie",
+ "REQ_TYPE_CONSULT": "Consulenza",
+ "REQ_TYPE_QUESTION": "Domande Generali",
+ "PHONE": "Telefono",
+ "IS_COMPANY": "Sei un'azienda?",
+ "COMPANY_NAME": "Ragione Sociale",
+ "REF_PERSON": "Persona di Riferimento",
+ "UPLOAD_LABEL": "Allegati",
+ "UPLOAD_HINT": "Max 15 file. Supportati: Immagini, PDF, STL, STEP, 3MF, OBJ",
+ "DROP_FILES": "Trascina qui i file o clicca per caricare"
}
}