diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 21fd4658..23fe93b8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -32,7 +32,6 @@ import { VerificationComponent } from './pages/admin/verification/verification.c import { CatalogsComponent } from "./pages/catalogs/catalogs.component"; import { BillingAddressComponent } from "./pages/checkout/billing-address/billing-address.component"; import { CheckoutComponent } from "./pages/checkout/checkout.component"; -import { ContactUsFormComponent } from './pages/contact-us/contact-us-form.component'; import { OrganizationDetailsComponent } from './pages/organization-details/organization-details.component'; import { ProductDetailsComponent } from "./pages/product-details/product-details.component"; import { InventoryProductsComponent } from './pages/product-inventory/inventory-items/inventory-products/inventory-products.component'; @@ -148,7 +147,6 @@ import { RequestValidationModalComponent } from './pages/seller-offerings/offeri UpdateCategoryComponent, CategoriesRecursionListComponent, ContactUsComponent, - ContactUsFormComponent, VerificationComponent, EmailComponent, InventoryResourcesComponent, diff --git a/src/app/pages/contact-us/contact-us-form.component.css b/src/app/pages/contact-us/contact-us-form.component.css index e69de29b..eefb9e36 100644 --- a/src/app/pages/contact-us/contact-us-form.component.css +++ b/src/app/pages/contact-us/contact-us-form.component.css @@ -0,0 +1,348 @@ +.contact-section { + width: 100%; + background: #dde6f6; + box-shadow: 0 16px 24px rgba(0, 0, 0, 0.15); + position: relative; +} + +.contact-copy { + position: relative; + z-index: 4; +} + +.contact-card { + position: relative; + z-index: 5; + min-height: 739px; + border-radius: 16px; +} + +.contact-card-success { + min-height: 340px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 16px; +} + +.contact-glow { + position: absolute; + border-radius: 9999px; + filter: blur(50px); + pointer-events: none; +} + +.contact-glow-1 { + width: 440px; + height: 440px; + left: min(61.875vw, 891px); + top: -17.5px; + background: #3d71cc; + opacity: 0.2; + z-index: 0; +} + +.contact-glow-2 { + width: 440px; + height: 440px; + left: min(39.653vw, 571px); + top: 349.5px; + background: #668ed6; + opacity: 0.2; + z-index: 1; +} + +.contact-glow-3 { + width: 440px; + height: 440px; + left: min(67.083vw, 966px); + top: 349.5px; + background: #8eace1; + opacity: 0.2; + z-index: 2; +} + +.contact-glow-4 { + width: 566px; + height: 566px; + left: min(34.653vw, 499px); + top: -33.5px; + background: #9eeeff; + opacity: 0.2; + z-index: 3; +} + +.contact-field { + display: flex; + flex-direction: column; + gap: 8px; +} + +.contact-label { + font-size: 14px; + line-height: 16px; + font-weight: 600; + letter-spacing: 0.5px; + color: #4c5a6b; +} + +.contact-input, +.contact-textarea { + width: 100%; + border: 1px solid #bcbfc7; + background: #ffffff; + color: #131b25; + outline: none; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease, + background-color 0.2s ease; +} + +.contact-input { + height: 40px; + border-radius: 6px; + padding: 8px 12px; + font-size: clamp(0.95rem, 0.92rem + 0.1vw, 1rem); + line-height: 24px; +} + +.contact-textarea { + min-height: 126px; + resize: vertical; + border-radius: 6px; + padding: 8px 12px; + font-size: clamp(0.95rem, 0.92rem + 0.1vw, 1rem); + line-height: 24px; +} + +.contact-input::placeholder, +.contact-textarea::placeholder { + color: #858b99; +} + +.contact-input:hover, +.contact-textarea:hover { + border-color: #9da6b2; +} + +.contact-input:focus, +.contact-textarea:focus { + border-color: #3d71cc; + box-shadow: 0 0 0 3px rgba(61, 113, 204, 0.14); +} + +.contact-input-error { + border-color: #d14343; + box-shadow: 0 0 0 3px rgba(209, 67, 67, 0.08); +} + +.contact-error { + font-size: 12px; + line-height: 14px; + color: #d14343; +} + +.contact-checkbox { + display: flex; + align-items: center; + gap: 12px; + font-size: clamp(0.95rem, 0.92rem + 0.08vw, 1rem); + line-height: 24px; + color: #4c5a6b; +} + +.contact-checkbox-top { + align-items: flex-start; +} + +.contact-checkbox input[type="checkbox"] { + margin: 0; + width: 20px; + height: 20px; + min-width: 20px; + border: 1px solid #bcbfc7; + border-radius: 4px; + accent-color: #2d58a7; + cursor: pointer; +} + +.contact-link { + font-weight: 600; + color: #2d58a7; + text-decoration: none; +} + +.contact-link:hover { + text-decoration: underline; +} + +.contact-submit { + display: inline-flex; + width: 100%; + min-height: 56px; + align-items: center; + justify-content: center; + border: 0; + border-radius: 8px; + background: #2d58a7; + padding: 16px 24px; + color: #ffffff; + font-size: clamp(1rem, 0.95rem + 0.18vw, 1.125rem); + font-weight: 600; + line-height: 22px; + transition: + transform 0.2s ease, + background-color 0.2s ease, + box-shadow 0.2s ease, + opacity 0.2s ease; +} + +.contact-submit:hover { + background: #244a8d; + box-shadow: 0 10px 24px rgba(45, 88, 167, 0.22); +} + +.contact-submit:active { + transform: translateY(1px); +} + +.contact-submit:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.contact-success-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + width: 100%; +} + +.contact-success-icon-wrap { + display: flex; + justify-content: center; + width: 100%; +} + +.contact-success-icon { + width: 72px; + height: 72px; + border-radius: 9999px; + background: #dde6f6; + display: flex; + align-items: center; + justify-content: center; + color: #2d58a7; + font-size: 34px; + line-height: 1; +} + +.contact-success-title { + width: 100%; + margin: 0; + text-align: center; + color: #111827; + font-size: clamp(2rem, 1.85rem + 0.35vw, 2.125rem); + line-height: 1.2; + font-weight: 700; +} + +.contact-success-description { + width: 100%; + max-width: 508px; + margin: 0; + text-align: center; + color: #4c5a6b; + font-size: clamp(1.0625rem, 0.98rem + 0.2vw, 1.25rem); + line-height: 1.6; +} + +.contact-success-cta { + width: 100%; + padding-top: 16px; +} + +@media (max-width: 1279px) { + .contact-card { + min-height: auto; + } + + .contact-glow-1 { + left: auto; + right: -60px; + top: -40px; + } + + .contact-glow-2 { + left: 42%; + top: 38%; + } + + .contact-glow-3 { + right: -90px; + left: auto; + top: 48%; + } + + .contact-glow-4 { + left: 28%; + top: -60px; + } +} + +@media (max-width: 1023px) { + .contact-section { + box-shadow: none; + } + + .contact-glow { + filter: blur(65px); + transform: scale(0.8); + } +} + +@media (max-width: 767px) { + .contact-glow-1 { + width: 280px; + height: 280px; + right: -60px; + top: -20px; + } + + .contact-glow-2 { + width: 260px; + height: 260px; + left: -40px; + top: 44%; + } + + .contact-glow-3 { + width: 240px; + height: 240px; + right: -50px; + top: 72%; + } + + .contact-glow-4 { + width: 320px; + height: 320px; + left: 18%; + top: -40px; + } + + .contact-checkbox { + align-items: flex-start; + } + + .contact-success-title { + font-size: 2rem; + line-height: 1.2; + } + + .contact-success-description { + font-size: 1.125rem; + line-height: 1.55; + } +} diff --git a/src/app/pages/contact-us/contact-us-form.component.html b/src/app/pages/contact-us/contact-us-form.component.html index b8b93d6e..dbec228e 100644 --- a/src/app/pages/contact-us/contact-us-form.component.html +++ b/src/app/pages/contact-us/contact-us-form.component.html @@ -1,151 +1,202 @@ +
+
+
+
+
-
- -
-
-
-
-

{{ 'CONTACTUS._title' | translate }}

-

{{ 'CONTACTUS._subtitle' | translate }}

- -
-
-
-
- -
-
-
-

{{ 'CONTACTUS._fill' | translate }}

-
-
- - - @if(contactForm.get('email')?.invalid && contactForm.get('email')?.errors?.['maxlength']){ -

{{ 'CONTACTUS._emaillong' | translate }}

- } - @if(contactForm.get('email')?.invalid && contactForm.get('email')?.errors?.['pattern']){ -

{{ 'CONTACTUS._emailpattern' | translate }}

- } - @if(contactForm.get('email')?.touched && contactForm.get('email')?.hasError('required')){ -

{{ 'CONTACTUS._mandatory' | translate }}

- } -
-
- - - @if(contactForm.get('name')?.invalid && contactForm.get('name')?.errors?.['maxlength']){ -

{{ 'CONTACTUS._fieldlong' | translate }}

- } - @if(contactForm.get('name')?.touched && contactForm.get('name')?.hasError('required')){ -

{{ 'CONTACTUS._mandatory' | translate }}

- } -
-
- - - @if(contactForm.get('lastname')?.invalid && contactForm.get('lastname')?.errors?.['maxlength']){ -

{{ 'CONTACTUS._fieldlong' | translate }}

- } - @if(contactForm.get('lastname')?.touched && contactForm.get('lastname')?.hasError('required')){ -

{{ 'CONTACTUS._mandatory' | translate }}

- } -
+
+
+

+ {{ "ContactUs.hero.titleStart" | translate }} + + {{ "ContactUs.hero.titleAccent" | translate }} + +

+ +

+ {{ "ContactUs.hero.description" | translate }} +

+
+ + @if (!submittedSuccessfully) { +
+
+

+ {{ "ContactUs.form.title" | translate }} +

+ +

+ {{ "ContactUs.form.subtitle" | translate }} +

+
+ + +
+
+ + + + + @if (hasError('firstName')) { + + {{ "ContactUs.validation.required" | translate }} + + } +
+ +
+ + + + + @if (hasError('lastName')) { + + {{ "ContactUs.validation.required" | translate }} + + } +
-
-
- - - @if(contactForm.get('organization')?.invalid && contactForm.get('organization')?.errors?.['maxlength']){ -

{{ 'CONTACTUS._mandatory' | translate }}

- } - @if(contactForm.get('organization')?.touched && contactForm.get('organization')?.hasError('required')){ -

{{ 'CONTACTUS._fieldlong' | translate }}

- } -
-
- - - @if(contactForm.get('role')?.invalid && contactForm.get('role')?.errors?.['maxlength']){ -

{{ 'CONTACTUS._fieldlong' | translate }}

- } - @if(contactForm.get('role')?.touched && contactForm.get('role')?.hasError('required')){ -

{{ 'CONTACTUS._mandatory' | translate }}

- } -
+ +
+ + + + + @if (hasError('email')) { + + {{ + f.email.errors?.["email"] + ? ("ContactUs.validation.email" | translate) + : ("ContactUs.validation.required" | translate) + }} + + }
-
- - - @if(contactForm.get('message')?.touched && contactForm.get('message')?.hasError('required')){ -

{{ 'CONTACTUS._mandatory' | translate }}

+ +
+
+ + + + + @if (hasError('organization')) { + + {{ "ContactUs.validation.required" | translate }} + } -
-

- - {{ 'CONTACTUS._info' | translate }} - {{ 'CONTACTUS._controller' | translate }}. - {{ 'CONTACTUS._purpose' | translate }} - privacy.helpdesk@dome-project.eu, - {{ 'CONTACTUS._indicating' | translate }} - {{ 'CONTACTUS._here' | translate }}. +

+ +
+ + + + + @if (hasError('roleInOrganization')) { + + {{ "ContactUs.validation.required" | translate }} -

- - - - -
-@if(showThanksMessage){ -
- + } @else { +
+
+
+
+ +
+
+ +

+ {{ "ContactUs.success.title" | translate }} +

+ +

+ {{ "ContactUs.success.description" | translate }} +

+
+ +
+
-
-} \ No newline at end of file + } +
+
diff --git a/src/app/pages/contact-us/contact-us-form.component.spec.ts b/src/app/pages/contact-us/contact-us-form.component.spec.ts index 82649dc8..3197ecee 100644 --- a/src/app/pages/contact-us/contact-us-form.component.spec.ts +++ b/src/app/pages/contact-us/contact-us-form.component.spec.ts @@ -1,124 +1,232 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { RouterTestingModule } from '@angular/router/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Observable, Subject } from 'rxjs'; -import { ThemeService } from 'src/app/services/theme.service'; -import { ThemeConfig } from 'src/app/themes'; - -import { ContactUsFormComponent } from './contact-us-form.component'; - -describe('ContactUsFormComponent', () => { +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideRouter, Router } from "@angular/router"; +import { of, Subject } from "rxjs"; +import { ContactUsService } from "../../services/contactUs.service"; +import { ContactUsFormComponent, IContactUs } from "./contact-us-form.component"; + +interface ISendEmail { + form: IContactUs; +} + +describe("ContactUsFormComponent", () => { let component: ContactUsFormComponent; let fixture: ComponentFixture; - let themeSubject: Subject; + let contactUsServiceSpy: jasmine.SpyObj; + let router: Router; + + const validFormValue: IContactUs = { + firstName: "Luigi", + lastName: "Borriello", + email: "luigi@test.com", + organization: "OpenAI", + roleInOrganization: "Senior Angular Developer", + message: "Test message", + privacyAccepted: true, + marketingAccepted: false + }; - const themeServiceStub: { currentTheme$: Observable } = { - currentTheme$: new Subject().asObservable(), + const sendEmailResponse: ISendEmail = { + form: validFormValue }; beforeEach(async () => { - themeSubject = new Subject(); - themeServiceStub.currentTheme$ = themeSubject.asObservable(); + contactUsServiceSpy = jasmine.createSpyObj("ContactUsService", ["sendEmail"]); await TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [HttpClientTestingModule, RouterTestingModule, TranslateModule.forRoot()], - declarations: [ContactUsFormComponent], - providers: [{ provide: ThemeService, useValue: themeServiceStub }], + imports: [ContactUsFormComponent], + providers: [ + provideRouter([]), + { provide: ContactUsService, useValue: contactUsServiceSpy } + ] }) - .overrideComponent(ContactUsFormComponent, { - set: { template: '' }, - }) - .compileComponents(); - + .overrideComponent(ContactUsFormComponent, { + set: { + template: "" + } + }) + .compileComponents(); + fixture = TestBed.createComponent(ContactUsFormComponent); component = fixture.componentInstance; + router = TestBed.inject(Router); + + spyOn(router, "navigate").and.resolveTo(true); + + fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + function fillValidForm(): void { + component.form.setValue(validFormValue); + } + + it("should create", () => { + expect(component).toBeDefined(); }); - it('should initialize with an invalid form', () => { - expect(component.contactForm.valid).toBeFalse(); - expect(component.contactForm.get('email')?.hasError('required')).toBeTrue(); - expect(component.contactForm.get('message')?.hasError('required')).toBeTrue(); + it("should initialize form with default values", () => { + expect(component.form.getRawValue()).toEqual({ + firstName: "", + lastName: "", + email: "", + organization: "", + roleInOrganization: "", + message: "", + privacyAccepted: false, + marketingAccepted: false + }); }); - it('should validate email format', () => { - component.contactForm.patchValue({ email: 'invalid-email' }); - expect(component.contactForm.get('email')?.hasError('email')).toBeTrue(); + it("should be invalid on init", () => { + expect(component.form.invalid).toBeTrue(); }); - it('ngOnInit should update currentTheme when theme service emits', () => { - const theme = { name: 'default', displayName: 'Default', assets: { jumboBgUrl: '/bg.png' } } as any; + it("should expose controls through getter f", () => { + expect(component.f.firstName).toBeDefined(); + expect(component.f.lastName).toBeDefined(); + expect(component.f.email).toBeDefined(); + expect(component.f.organization).toBeDefined(); + expect(component.f.roleInOrganization).toBeDefined(); + expect(component.f.message).toBeDefined(); + expect(component.f.privacyAccepted).toBeDefined(); + expect(component.f.marketingAccepted).toBeDefined(); + }); - component.ngOnInit(); - themeSubject.next(theme); + it("should return false from hasError if control is invalid but untouched and not submitted", () => { + expect(component.hasError("firstName")).toBeFalse(); + }); - expect(component.currentTheme).toEqual(theme); + it("should return true from hasError if control is touched and invalid", () => { + component.f.firstName.markAsTouched(); + + expect(component.hasError("firstName")).toBeTrue(); }); - it('ngOnDestroy should stop updating theme after destroy', () => { - const firstTheme = { name: 'first', displayName: 'First', assets: {} } as any; - const secondTheme = { name: 'second', displayName: 'Second', assets: {} } as any; + it("should return true from hasError if form was submitted and control is invalid", () => { + component.submitted = true; + + expect(component.hasError("firstName")).toBeTrue(); + }); - component.ngOnInit(); - themeSubject.next(firstTheme); - component.ngOnDestroy(); - themeSubject.next(secondTheme); + it("should validate invalid email", () => { + component.f.email.setValue("abc"); + component.f.email.markAsTouched(); + component.f.email.updateValueAndValidity(); - expect(component.currentTheme).toEqual(firstTheme); + expect(component.f.email.invalid).toBeTrue(); + expect(component.f.email.hasError("email")).toBeTrue(); + expect(component.hasError("email")).toBeTrue(); }); - it('hide should set showThanksMessage to false', () => { - component.showThanksMessage = true; + it("should validate privacyAccepted with requiredTrue", () => { + component.f.privacyAccepted.setValue(false); + component.f.privacyAccepted.markAsTouched(); + component.f.privacyAccepted.updateValueAndValidity(); + + expect(component.f.privacyAccepted.invalid).toBeTrue(); + expect(component.f.privacyAccepted.hasError("required")).toBeTrue(); + }); - component.hide(); + it("should accept privacyAccepted when true", () => { + component.f.privacyAccepted.setValue(true); + component.f.privacyAccepted.updateValueAndValidity(); - expect(component.showThanksMessage).toBeFalse(); + expect(component.f.privacyAccepted.valid).toBeTrue(); + expect(component.f.privacyAccepted.hasError("required")).toBeFalse(); }); - it('resetContactForm should clear values and control state', () => { - component.contactForm.patchValue({ - email: 'john@example.com', - name: 'John', - lastname: 'Doe', - organization: 'Acme', - role: 'Engineer', - message: 'Hello', - }); - component.contactForm.markAllAsTouched(); + it("should be valid when form is correctly filled", () => { + fillValidForm(); - component.resetContactForm(); + expect(component.form.valid).toBeTrue(); + }); - expect(component.contactForm.value).toEqual({ - email: null, - name: null, - lastname: null, - organization: null, - role: null, - message: null, - }); - Object.values(component.contactForm.controls).forEach((control) => { - expect(control.pristine).toBeTrue(); - expect(control.untouched).toBeTrue(); - expect(control.value).toBeNull(); + it("should set submitted to true and not call service if form is invalid", () => { + component.onSubmit(); + + expect(component.submitted).toBeTrue(); + expect(contactUsServiceSpy.sendEmail).not.toHaveBeenCalled(); + expect(component.submittedSuccessfully).toBeFalse(); + }); + + it("should mark all controls as touched on invalid submit", () => { + component.onSubmit(); + + expect(component.f.firstName.touched).toBeTrue(); + expect(component.f.lastName.touched).toBeTrue(); + expect(component.f.email.touched).toBeTrue(); + expect(component.f.organization.touched).toBeTrue(); + expect(component.f.roleInOrganization.touched).toBeTrue(); + expect(component.f.message.touched).toBeTrue(); + expect(component.f.privacyAccepted.touched).toBeTrue(); + }); + + it("should call sendEmail with raw form value when form is valid", () => { + fillValidForm(); + contactUsServiceSpy.sendEmail.and.returnValue(of(sendEmailResponse) as any); + + component.onSubmit(); + + expect(contactUsServiceSpy.sendEmail).toHaveBeenCalledOnceWith(validFormValue); + }); + + it("should set submittedSuccessfully to true when sendEmail succeeds", () => { + fillValidForm(); + contactUsServiceSpy.sendEmail.and.returnValue(of(sendEmailResponse) as any); + + component.onSubmit(); + + expect(component.submittedSuccessfully).toBeTrue(); + }); + + it("should reset form and flags and navigate to dashboard on continue browsing", () => { + fillValidForm(); + component.submitted = true; + component.submittedSuccessfully = true; + + component.onContinueBrowsing(); + + expect(component.submitted).toBeFalse(); + expect(component.submittedSuccessfully).toBeFalse(); + expect(component.form.getRawValue()).toEqual({ + firstName: "", + lastName: "", + email: "", + organization: "", + roleInOrganization: "", + message: "", + privacyAccepted: false, + marketingAccepted: false }); + expect(router.navigate).toHaveBeenCalledWith(["/dashboard"]); }); - it('should make form valid when all required fields are provided with valid values', () => { - component.contactForm.patchValue({ - email: 'john@example.com', - name: 'John', - lastname: 'Doe', - organization: 'Acme', - role: 'Engineer', - message: 'Hello', + it("should emit and complete unsub subject on destroy", () => { + const nextSpy = jasmine.createSpy("next"); + const completeSpy = jasmine.createSpy("complete"); + + component.unsub.subscribe({ + next: nextSpy, + complete: completeSpy }); - expect(component.contactForm.valid).toBeTrue(); + fixture.destroy(); + + expect(nextSpy).toHaveBeenCalled(); + expect(completeSpy).toHaveBeenCalled(); + }); + + it("should unsubscribe active request on destroy", () => { + const response$ = new Subject(); + contactUsServiceSpy.sendEmail.and.returnValue(response$.asObservable() as any); + + fillValidForm(); + component.onSubmit(); + + fixture.destroy(); + + response$.next(sendEmailResponse); + response$.complete(); + + expect(component.submittedSuccessfully).toBeFalse(); }); }); diff --git a/src/app/pages/contact-us/contact-us-form.component.ts b/src/app/pages/contact-us/contact-us-form.component.ts index fb0c6fde..3e9853b5 100644 --- a/src/app/pages/contact-us/contact-us-form.component.ts +++ b/src/app/pages/contact-us/contact-us-form.component.ts @@ -1,83 +1,113 @@ -import {Component, HostListener, OnInit, ChangeDetectorRef, OnDestroy} from '@angular/core'; -import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms'; -import {faHandsHoldingHeart} from "@fortawesome/pro-solid-svg-icons"; -import { environment } from 'src/environments/environment'; -import {ThemeService} from "../../services/theme.service"; -import {ThemeConfig} from "../../themes"; -import { Subscription, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { CommonModule } from "@angular/common"; +import { Component, inject, OnDestroy } from "@angular/core"; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators +} from "@angular/forms"; +import { Router } from '@angular/router'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faThumbsUp } from '@fortawesome/pro-regular-svg-icons'; +import { TranslateModule } from "@ngx-translate/core"; +import { Subject, takeUntil } from 'rxjs'; +import { ContactUsService } from '../../services/contactUs.service'; + +export interface IContactUs { + firstName: string; + lastName: string; + email: string; + organization: string; + roleInOrganization: string; + message: string; + privacyAccepted: boolean; + marketingAccepted: boolean; +} + +export type IContactUsForm = { + [K in keyof IContactUs]: FormControl; +}; @Component({ - selector: 'app-contact-us-form', - templateUrl: './contact-us-form.component.html', - styleUrl: './contact-us-form.component.css' + selector: "app-contact-us", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + TranslateModule, + FontAwesomeModule + ], + templateUrl: "./contact-us-form.component.html", + styleUrl: "./contact-us-form.component.css" }) +export class ContactUsFormComponent implements OnDestroy { + private readonly fb = inject(FormBuilder); + private readonly router = inject(Router); + private readonly contactUsService = inject(ContactUsService) + + faThumbsUp = faThumbsUp; -export class ContactUsFormComponent implements OnInit, OnDestroy { - contactForm = new FormGroup({ - email: new FormControl('', [Validators.required, Validators.email, Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'), Validators.maxLength(320)]), - name: new FormControl('', [Validators.required, Validators.maxLength(100)]), - lastname: new FormControl('', [Validators.required, Validators.maxLength(100)]), - organization: new FormControl('', [Validators.required, Validators.maxLength(100)]), - role: new FormControl('', [Validators.required, Validators.maxLength(100)]), - message: new FormControl('', [Validators.required]), + unsub = new Subject(); + + submitted = false; + submittedSuccessfully = false; + + form: FormGroup = this.fb.nonNullable.group({ + firstName: ["", [Validators.required]], + lastName: ["", [Validators.required]], + email: ["", [Validators.required, Validators.email]], + organization: ["", [Validators.required]], + roleInOrganization: ["", [Validators.required]], + message: ["", [Validators.required]], + privacyAccepted: [false, [Validators.requiredTrue]], + marketingAccepted: [false] }); - constructor( - private cdr: ChangeDetectorRef, - private themeService: ThemeService - ) { } - - protected readonly faHandsHoldingHeart = faHandsHoldingHeart; - - dataControllerDome:any='https://dome-project.eu/about/#partners'; - DPA:any='https://dome-marketplace-sbx.org/assets/documents/privacy.pdf'; - showThanksMessage:boolean=false; - currentTheme: ThemeConfig | null = null; - private themeSubscription: Subscription = new Subscription(); - private destroy$ = new Subject(); - - ngOnInit() { - this.themeSubscription = this.themeService.currentTheme$ - .pipe(takeUntil(this.destroy$)) - .subscribe(theme => { - this.currentTheme = theme; - }); + get f() { + return this.form.controls; } - ngOnDestroy(){ - this.destroy$.next(); - this.destroy$.complete(); + hasError(controlName: keyof typeof this.form.controls): boolean { + const control = this.form.controls[controlName]; + return !!control && control.invalid && (control.touched || this.submitted); } - sendMail() { - let message = 'First name: '+this.contactForm.value.name+ '%0A' + - 'Last name: '+this.contactForm.value.lastname+ '%0A' + - 'Email: '+this.contactForm.value.email+ '%0A' + - 'Organization: '+this.contactForm.value.organization+ '%0A' + - 'Role: '+this.contactForm.value.role+ '%0A'+ - this.contactForm.value.message; - - const mailtoLink = `mailto:info@dome-marketplace.eu?body=${message}`; - - this.showThanksMessage=true; - // Open the mailto link - window.location.href = mailtoLink; - this.resetContactForm(); - } + onSubmit(): void { + this.submitted = true; + this.form.markAllAsTouched(); - hide(){ - this.showThanksMessage=false; + if (this.form.invalid) { + return; + } + + + this.contactUsService.sendEmail(this.form.getRawValue()).pipe(takeUntil(this.unsub)).subscribe({ + next: () => { + this.submittedSuccessfully = true; + }, + }); } - resetContactForm(): void { - this.contactForm.reset(); - - Object.values(this.contactForm.controls).forEach(control => { - control.setErrors(null); // clear errors - control.markAsPristine(); - control.markAsUntouched(); - control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + + onContinueBrowsing(): void { + this.submittedSuccessfully = false; + this.submitted = false; + this.form.reset({ + firstName: "", + lastName: "", + email: "", + organization: "", + roleInOrganization: "", + message: "", + privacyAccepted: false, + marketingAccepted: false }); + + this.router.navigate(['/dashboard']) + } + + ngOnDestroy(): void { + this.unsub.next(); + this.unsub.complete(); } - } diff --git a/src/app/services/contactUs.service.ts b/src/app/services/contactUs.service.ts new file mode 100644 index 00000000..727dd952 --- /dev/null +++ b/src/app/services/contactUs.service.ts @@ -0,0 +1,39 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { delay, Observable, of } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { IContactUs } from '../pages/contact-us/contact-us-form.component'; + +export interface ISendEmail { + form: IContactUs +} + + +@Injectable({ + providedIn: 'root' +}) +export class ContactUsService { + protected BASE_URL = environment.BASE_URL; + private debug = true; + + constructor(private http: HttpClient) { } + + sendEmail(form: IContactUs): Observable { + if (this.debug) { + return of({ + form: { + firstName: "", + lastName: "", + email: "", + organization: "", + roleInOrganization: "", + message: "", + privacyAccepted: false, + marketingAccepted: false + } + }).pipe(delay(3000)); + } + + return this.http.post(`${this.BASE_URL}/to-do`, form); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index f73eb4e6..c52d2840 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -234,6 +234,66 @@ "_more": "Read more" } }, + "ContactUs": { + "hero": { + "titleStart": "Let’s get", + "titleAccent": "in touch", + "description": "We are glad you are interested in the DOME Marketplace! Please share your questions or thoughts with us and we'll get back to you shortly." + }, + "form": { + "title": "Send us a message", + "subtitle": "Fill out the form and our team will be in touch as soon as possible." + }, + "fields": { + "firstName": { + "label": "First name", + "placeholder": "Enter your first name" + }, + "lastName": { + "label": "Last name", + "placeholder": "Enter your last name" + }, + "email": { + "label": "Email", + "placeholder": "Enter your email" + }, + "organization": { + "label": "Organization", + "placeholder": "Enter your organization" + }, + "roleInOrganization": { + "label": "Role in organization", + "placeholder": "Enter your role" + }, + "message": { + "label": "Message", + "placeholder": "Add product description" + } + }, + "checkboxes": { + "privacy": { + "prefix": "I agree to the", + "privacyPolicy": "Privacy Policy", + "middle": "and", + "termsOfService": "Terms of Services", + "suffix": "*" + }, + "marketing": "I agree to receive marketing and commercial communications from DOME Marketplace." + }, + "cta": { + "submit": "Submit message" + }, + "success": { + "title": "Thank you for contacting us.", + "description": "Our team has received your message and will get back to you as soon as possible.", + "cta": "Continue browsing" + }, + "validation": { + "required": "This field is required.", + "email": "Please enter a valid email address.", + "privacyRequired": "You must accept the Privacy Policy and Terms of Services." + } + }, "LANDINGPAGE": { "providers": { "badge": "For providers", diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index f73eb4e6..c52d2840 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -234,6 +234,66 @@ "_more": "Read more" } }, + "ContactUs": { + "hero": { + "titleStart": "Let’s get", + "titleAccent": "in touch", + "description": "We are glad you are interested in the DOME Marketplace! Please share your questions or thoughts with us and we'll get back to you shortly." + }, + "form": { + "title": "Send us a message", + "subtitle": "Fill out the form and our team will be in touch as soon as possible." + }, + "fields": { + "firstName": { + "label": "First name", + "placeholder": "Enter your first name" + }, + "lastName": { + "label": "Last name", + "placeholder": "Enter your last name" + }, + "email": { + "label": "Email", + "placeholder": "Enter your email" + }, + "organization": { + "label": "Organization", + "placeholder": "Enter your organization" + }, + "roleInOrganization": { + "label": "Role in organization", + "placeholder": "Enter your role" + }, + "message": { + "label": "Message", + "placeholder": "Add product description" + } + }, + "checkboxes": { + "privacy": { + "prefix": "I agree to the", + "privacyPolicy": "Privacy Policy", + "middle": "and", + "termsOfService": "Terms of Services", + "suffix": "*" + }, + "marketing": "I agree to receive marketing and commercial communications from DOME Marketplace." + }, + "cta": { + "submit": "Submit message" + }, + "success": { + "title": "Thank you for contacting us.", + "description": "Our team has received your message and will get back to you as soon as possible.", + "cta": "Continue browsing" + }, + "validation": { + "required": "This field is required.", + "email": "Please enter a valid email address.", + "privacyRequired": "You must accept the Privacy Policy and Terms of Services." + } + }, "LANDINGPAGE": { "providers": { "badge": "For providers",