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 }}

+
+
+
+

Member 1

+
+
+
+

Member 2

+
+
+
+

Member 3

+
+
-
- -

{{ 'ABOUT.CONTACT_US' | 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: ` -
- - - -
- - -
- -
- - {{ sent() ? 'Inviato!' : ('ABOUT.SEND' | translate) }} - -
-
- `, - styles: [` - .form-group { display: flex; flex-direction: column; margin-bottom: var(--space-4); } - label { font-size: 0.875rem; font-weight: 500; margin-bottom: var(--space-2); color: var(--color-text); } - .form-control { - padding: 0.5rem 0.75rem; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - width: 100%; - background: var(--color-bg-card); - color: var(--color-text); - font-family: inherit; - &:focus { outline: none; border-color: var(--color-brand); } - } - `] -}) -export class ContactFormComponent { - form: FormGroup; - sent = signal(false); - - constructor(private fb: FormBuilder) { - this.form = this.fb.group({ - name: ['', Validators.required], - email: ['', [Validators.required, Validators.email]], - message: ['', Validators.required] - }); - } - - onSubmit() { - if (this.form.valid) { - // Mock submit - this.sent.set(true); - setTimeout(() => { - this.sent.set(false); - this.form.reset(); - }, 3000); - } - } -} diff --git a/frontend/src/app/features/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: ` +
+ +
+ + +
+ +
+ + + + +
+ + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +

{{ 'CONTACT.UPLOAD_HINT' | translate }}

+ +
+ +

{{ 'CONTACT.DROP_FILES' | translate }}

+
+ +
+
+ + +
+ PDF + 3D +
+
{{ file.file.name }}
+
+
+
+ +
+ + {{ sent() ? 'Inviato!' : ('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; } + } + } + + /* 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: ` +
+
+

{{ 'CONTACT.TITLE' | translate }}

+

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

+
+
+ +
+ + + +
+ `, + 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" } }