diff --git a/package-lock.json b/package-lock.json index f08d173097762be94ceb8a517a5cc4e2e4e84b35..081023ce893e265db11bf6fb3edf24b34de8e5ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12046,6 +12046,14 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "ngx-toastr": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-13.2.1.tgz", + "integrity": "sha512-UAzp7/xWK9IXA2LsOmhpaaIGCqscvJokoQpBNpAMrjEkDeSlFf8PWQAuQY795KW0mJb3qF9UG/s23nsXfMYKmg==", + "requires": { + "tslib": "^2.0.0" + } + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/package.json b/package.json index afd4b144520f656fc19f1c26c1d40f07424e89c5..647ecf9016492e037f7cb8346b338a7d34266b9f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "leaflet.locatecontrol": "^0.72.0", "lodash": "^4.17.20", "luxon": "^1.25.0", + "ngx-toastr": "^13.2.1", "npx": "^10.2.2", "rxjs": "~6.6.0", "tslib": "^2.0.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index d4fdc3edb27a07a4a389b134adedfa2c4afcedb0..4aa08599e8d1d2c34747436da1b3a08be9cf767e 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AboutComponent } from './about/about.component'; +import { ContactComponent } from './contact/contact.component'; import { FormComponent } from './form/structure-form/form.component'; import { AdminGuard } from './guards/admin.guard'; import { AuthGuard } from './guards/auth.guard'; @@ -48,6 +49,10 @@ const routes: Routes = [ path: 'about', component: AboutComponent, }, + { + path: 'contact', + component: ContactComponent, + }, { path: 'users/verify/:id', component: UserVerificationComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4111e2b27575bf8c920a36f80997213fcf1d0f24..0a62423acc10b1a702befd3065f8658b579ae010 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,8 @@ import { LOCALE_ID, NgModule } from '@angular/core'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ToastrModule } from 'ngx-toastr'; import { AppRoutingModule } from './app-routing.module'; @@ -19,6 +21,7 @@ import { StructureOpeningStatusComponent } from './structure-list/components/str import { ModalFilterComponent } from './structure-list/components/modal-filter/modal-filter.component'; import { LegalNoticeComponent } from './legal-notice/legal-notice.component'; import { AboutComponent } from './about/about.component'; +import { ContactComponent } from './contact/contact.component'; import { FormComponent } from './form/structure-form/form.component'; import { UserVerificationComponent } from './user-verification/user-verification.component'; import { AuthGuard } from './guards/auth.guard'; @@ -53,6 +56,7 @@ import { RoleGuard } from './guards/role.guard'; StructureOpeningStatusComponent, LegalNoticeComponent, AboutComponent, + ContactComponent, UserVerificationComponent, ResetEmailComponent, ResetPasswordComponent, @@ -65,7 +69,7 @@ import { RoleGuard } from './guards/role.guard'; StructureListPrintComponent, StructurePrintHeaderComponent, ], - imports: [BrowserModule, HttpClientModule, AppRoutingModule, SharedModule, MapModule], + imports: [BrowserModule, HttpClientModule, AppRoutingModule, SharedModule, MapModule, BrowserAnimationsModule, ToastrModule.forRoot()], providers: [ { provide: LOCALE_ID, useValue: 'fr' }, { provide: HTTP_INTERCEPTORS, useClass: CustomHttpInterceptor, multi: true }, diff --git a/src/app/contact/contact.component.html b/src/app/contact/contact.component.html new file mode 100644 index 0000000000000000000000000000000000000000..500329f5afd2cb1b0050debabc01d483408263cb --- /dev/null +++ b/src/app/contact/contact.component.html @@ -0,0 +1,125 @@ +<div fxLayout="column" class="form content-container full-screen"> + <div class="section-container"> + <div class="contactForm"> + <form [formGroup]="contactForm" (ngSubmit)="onSubmit()"> + <div class="form-fields"> + <h2>Nous contacter</h2> + <div class="form-group"> + <label for="name">Prénom et Nom</label> + <div fxLayout="row" fxLayoutGap="15px"> + <input type="text" autocomplete="on" formControlName="name" class="form-input" /> + <app-svg-icon + *ngIf="contactForm.get('name').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'validate'" + ></app-svg-icon> + <app-svg-icon + *ngIf="contactForm.get('name').value && !contactForm.get('name').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'notValidate'" + ></app-svg-icon> + </div> + </div> + + <div class="form-group"> + <label for="email">Adresse mail</label> + <div fxLayout="row" fxLayoutGap="15px"> + <input type="text" autocomplete="on" formControlName="email" class="form-input" /> + <app-svg-icon + *ngIf="contactForm.get('email').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'validate'" + ></app-svg-icon> + <app-svg-icon + *ngIf="contactForm.get('email').value && !contactForm.get('email').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'notValidate'" + ></app-svg-icon> + </div> + </div> + + <div class="form-group"> + <label for="phone">N° de téléphone</label> + <p class="notRequired">facultatif</p> + <div fxLayout="row" fxLayoutGap="15px"> + <input + type="text" + autocomplete="on" + formControlName="phone" + class="form-input phone" + (input)="utils.modifyPhoneInput(contactForm, 'phone', $event.target.value)" + /> + <app-svg-icon + *ngIf="contactForm.get('phone').value && contactForm.get('phone').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'validate'" + ></app-svg-icon> + <app-svg-icon + *ngIf="contactForm.get('phone').value && !contactForm.get('phone').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'notValidate'" + ></app-svg-icon> + </div> + </div> + + <div class="form-group"> + <label for="subject">Objet du message</label> + <div fxLayout="row" fxLayoutGap="15px"> + <input type="text" maxlength="100" formControlName="subject" class="form-input subject" /> + <app-svg-icon + *ngIf="contactForm.get('subject').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'validate'" + ></app-svg-icon> + <app-svg-icon + *ngIf="contactForm.get('subject').value && !contactForm.get('subject').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'notValidate'" + ></app-svg-icon> + </div> + </div> + + <div class="form-group" fx-layout="column"> + <label for="message">Message</label> + <div class="textareaBlock" fxLayout="row" fxLayoutGap="15px"> + <textarea + rows="8" + placeholder="Exemple : J'aimerais avoir de l'aide sur Rés'IN." + maxlength="500" + formControlName="message" + ></textarea> + <app-svg-icon + *ngIf="contactForm.get('message').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'validate'" + ></app-svg-icon> + <app-svg-icon + *ngIf="contactForm.get('message').value && !contactForm.get('message').valid" + [iconClass]="'validation'" + [type]="'form'" + [icon]="'notValidate'" + ></app-svg-icon> + </div> + <p>{{ contactForm.get('message').value ? contactForm.get('message').value.length : 0 }}/500</p> + </div> + </div> + <div class="button" fxLayout="row" fxLayoutAlign="center center"> + <a routerLink="../home" class="btn btn-link">Annuler</a> + <button type="submit" class="btn btn-primary" [ngClass]="{ invalid: !contactForm.valid || loading }"> + <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span> + Envoyer + </button> + </div> + </form> + </div> + </div> +</div> diff --git a/src/app/contact/contact.component.scss b/src/app/contact/contact.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..625934b44f3ea86b2675f956d31d213a13debc12 --- /dev/null +++ b/src/app/contact/contact.component.scss @@ -0,0 +1,45 @@ +@import '../../assets/scss/color'; +@import '../../assets/scss/layout'; +@import '../../assets/scss/breakpoint'; +@import '../../assets/scss/typography'; +@import '../../assets/scss/shapes'; +@import '../../assets/scss/z-index'; + +.form-fields { + max-width: 960px; + padding: 20px; + background: $white; +} +.phone { + width: 200px; +} +.subject { + width: 600px; +} +.textareaBlock { + flex-direction: column; + box-sizing: border-box; + display: flex; + textarea { + font-family: $text-font; + width: 94%; + margin-top: 4px; + &:focus { + border: 1px solid $blue; + outline: none !important; + } + } +} +.button { + max-width: 960px; + padding: 35px; + gap: 8px; +} +p { + text-align: right; + width: 96%; + &.notRequired { + text-align: left; + margin: 0; + } +} diff --git a/src/app/contact/contact.component.spec.ts b/src/app/contact/contact.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c74f0165bb293c6c567af9fc77732e8664923499 --- /dev/null +++ b/src/app/contact/contact.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContactComponent } from './contact.component'; + +describe('ContactComponent', () => { + let component: ContactComponent; + let fixture: ComponentFixture<ContactComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ContactComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContactComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/contact/contact.component.ts b/src/app/contact/contact.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..367ec8eca317d810079db2f3aed8218312e7aff3 --- /dev/null +++ b/src/app/contact/contact.component.ts @@ -0,0 +1,74 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { ContactMessage } from '../models/contact-message.model'; +import { AuthService } from '../services/auth.service'; +import { ContactService } from '../services/contact.service'; +import { NotificationService } from '../services/notification.service'; +import { CustomRegExp } from '../utils/CustomRegExp'; +import { Utils } from '../utils/utils'; + +@Component({ + selector: 'app-contact', + templateUrl: './contact.component.html', + styleUrls: ['./contact.component.scss'], +}) +export class ContactComponent implements OnInit { + public contactForm: FormGroup; + public loading = false; + + constructor( + private formBuilder: FormBuilder, + private contactService: ContactService, + private router: Router, + private authService: AuthService, + private notificationService: NotificationService, + public utils: Utils + ) {} + + ngOnInit(): void { + this.contactForm = this.formBuilder.group({ + name: [ + this.isLoggedIn ? this.displayFullname : '', + [Validators.required, Validators.pattern(CustomRegExp.TEXT_WITHOUT_NUMBER)], + ], + phone: ['', [Validators.pattern(CustomRegExp.PHONE)]], + email: [this.isLoggedIn ? this.displayEmail : '', [Validators.required, Validators.pattern(CustomRegExp.EMAIL)]], + subject: ['', Validators.required], + message: ['', Validators.required], + }); + } + + public get isLoggedIn(): boolean { + return this.authService.isLoggedIn(); + } + public get displayFullname(): string { + return this.authService.getUsernameDisplay() + ' ' + this.authService.getUsersurnameDisplay(); + } + public get displayEmail(): string { + return this.authService.getUserEmailDisplay(); + } + + public onSubmit(): void { + if (!this.contactForm.valid) { + return; + } + this.loading = true; + + let contactMessage: ContactMessage = this.contactForm.value; + this.contactService.sendMessage(contactMessage).subscribe( + () => { + this.loading = false; + this.notificationService.showSuccess('Votre message a bien été envoyé', 'Demande de contact'); + this.router.navigate(['']); + }, + () => { + this.loading = false; + this.notificationService.showError( + 'Merci de réessayer plus tard', + "Votre demande de contact n'a pas pu être envoyée" + ); + } + ); + } +} diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index b6de162784a7e1fde622c48d205e021c4cb1f83e..e778892c8768eb0db1451f9a3f0183d766360ae9 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -3,7 +3,7 @@ <a class="clickable text-align-center" routerLink="/legal-notice" i18n>Mentions légales</a> <a class="clickable text-align-center" routerLink="/newsletter" i18n>Newsletter</a> <!-- <a class="clickable text-align-center" routerLink="/sitemap" i18n>Plan du site</a> --> - <a class="clickable text-align-center" href="mailto:inclusionnumerique@grandlyon.com">Contact</a> + <a class="clickable text-align-center" routerLink="/contact" i18n>Contact</a> </div> <a class="metro-link" diff --git a/src/app/form/structure-form/form.component.html b/src/app/form/structure-form/form.component.html index 827a2522eb9ace02ee8be6f17ab4e6ae8684356d..a8ff1816b45803e7a6668fae7fc710c9e7466d6d 100644 --- a/src/app/form/structure-form/form.component.html +++ b/src/app/form/structure-form/form.component.html @@ -179,7 +179,7 @@ type="text" formControlName="phone" class="form-input phone" - (input)="modifyPhoneInput(accountForm, 'phone', $event.target.value)" + (input)="utils.modifyPhoneInput(accountForm, 'phone', $event.target.value)" /> <app-svg-icon *ngIf="accountForm.get('phone').valid" @@ -483,7 +483,7 @@ type="text" formControlName="contactPhone" class="form-input" - (input)="modifyPhoneInput(structureForm, 'contactPhone', $event.target.value)" + (input)="utils.modifyPhoneInput(structureForm, 'contactPhone', $event.target.value)" /> <app-svg-icon *ngIf="getStructureControl('contactPhone').valid" diff --git a/src/app/form/structure-form/form.component.scss b/src/app/form/structure-form/form.component.scss index c21116ce490e1ec6b4c2a0eb6025a769abcac45f..79d9304e1114bb5358e5b5f0a359acc905a4f8b4 100644 --- a/src/app/form/structure-form/form.component.scss +++ b/src/app/form/structure-form/form.component.scss @@ -191,11 +191,6 @@ h4 { color: $orange-warning; } } - &.notRequired { - margin-top: 0px; - font-style: italic; - color: $secondary-color; - } &.informationEndForm { margin-top: 18px; color: $grey-2; @@ -206,14 +201,6 @@ h4 { @media #{$tablet} { max-width: 90%; } - textarea { - padding: 13px 8px; - background: $grey-6; - border: 1px solid $grey-4; - border-radius: 1px; - resize: none; - @include cn-regular-16; - } p { text-align: right; } @@ -275,10 +262,6 @@ h4 { } } .form-group { - margin-bottom: 26px; - label { - color: $grey-2; - } &.facebook, &.twitter, &.instagram, @@ -321,8 +304,8 @@ h4 { } } } + input { - margin-top: 4px; &.email-placeholder::placeholder { color: #cacccb; font-style: italic; diff --git a/src/app/form/structure-form/form.component.ts b/src/app/form/structure-form/form.component.ts index 939f1f52eb59bcf0e0be4ab5d324d764bbacc0b1..c454231a2b91d92d21b946726565a9f0cdd0bb84 100644 --- a/src/app/form/structure-form/form.component.ts +++ b/src/app/form/structure-form/form.component.ts @@ -21,6 +21,8 @@ import { CustomRegExp } from '../../utils/CustomRegExp'; import { StructureWithOwners } from '../../models/structureWithOwners.model'; import { RouterListenerService } from '../../services/routerListener.service'; import { NewsletterService } from '../../services/newsletter.service'; +import { Utils } from '../../utils/utils'; + @Component({ selector: 'app-structure-form', templateUrl: './form.component.html', @@ -90,7 +92,8 @@ export class FormComponent implements OnInit { private router: Router, private route: ActivatedRoute, private routerListener: RouterListenerService, - private newsletterService: NewsletterService + private newsletterService: NewsletterService, + public utils: Utils ) {} async ngOnInit(): Promise<void> { @@ -409,17 +412,6 @@ export class FormComponent implements OnInit { return this.structureForm.get('address').get(nameControl); } - public modifyPhoneInput(form: FormGroup, controlName: string, phoneNumber: string): void { - // Take length of phone number without spaces. - const phoneNoSpace = phoneNumber.replace(/\s/g, ''); - // Check to refresh every 2 number. - if (phoneNoSpace.length % 2 === 0) { - // Add space every 2 number - form.get(controlName).setValue(phoneNoSpace.replace(/(?!^)(?=(?:\d{2})+$)/g, ' ')); //NOSONAR - } - this.setValidationsForm(); - } - private createDay(day: Day): FormGroup { return new FormGroup({ open: new FormControl(day.open, Validators.required), diff --git a/src/app/models/contact-message.model.ts b/src/app/models/contact-message.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2b4ea2bec98d096bbcb82f7ef6c4f07e1ccc424 --- /dev/null +++ b/src/app/models/contact-message.model.ts @@ -0,0 +1,9 @@ +export class ContactMessage { + public _id: string = null; + public name: string = null; + public surname: string = null; + public email: string = null; + public phone: string = null; + public subject: string = null; + public message: string = null; +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index ba2da5e7537e6731cc2fa12201f839bb605d4d40..3b19a0dbdc8521e89b625bf0ace2bdef3695589d 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -46,6 +46,14 @@ export class AuthService { return `${this.userValue.name}`; } + public getUsersurnameDisplay(): string { + return `${this.userValue.surname}`; + } + + public getUserEmailDisplay(): string { + return `${this.userValue.username}`; + } + private getExpiration(): DateTime { return DateTime.fromISO(this.userValue.expiresAt, { zone: 'Europe/Paris' }); } diff --git a/src/app/services/contact.service.ts b/src/app/services/contact.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..8fb8cd52356b225cd3e7f564951281890600f50a --- /dev/null +++ b/src/app/services/contact.service.ts @@ -0,0 +1,15 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { ContactMessage } from '../models/contact-message.model'; + +@Injectable({ + providedIn: 'root', +}) +export class ContactService { + constructor(private http: HttpClient) {} + + public sendMessage(contactMessage: ContactMessage): Observable<any> { + return this.http.post('/api/contact/message', { contactMessage }); + } +} diff --git a/src/app/services/notification.service.spec.ts b/src/app/services/notification.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4f2cd67eeb877c523ad095e23448e51603286ce --- /dev/null +++ b/src/app/services/notification.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { NotificationService } from './notification.service'; + +describe('NotificationService', () => { + let service: NotificationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(NotificationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/notification.service.ts b/src/app/services/notification.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..998656c9c7cb6385906eb6b8ca743e9068086469 --- /dev/null +++ b/src/app/services/notification.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { ToastrService } from 'ngx-toastr'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationService { + constructor(private toastr: ToastrService) {} + + showSuccess(message: string, title: string, timespan: number = 10000): void { + this.toastr.success(message, title, { + timeOut: timespan, + }); + } + + // Par defaut, l'erreur reste affichée jusqu'à ce qu'on clique dessus + showError(message: string, title: string, timespan: number = 0): void { + this.toastr.error(message, title, { + timeOut: timespan, + disableTimeOut: timespan ? false : true, + }); + } +} diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..e37ab99dd002bdcbe9cc54dd725e2f88e14349c3 --- /dev/null +++ b/src/app/utils/utils.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Injectable({ + providedIn: 'root', +}) +export class Utils { + public modifyPhoneInput(form: FormGroup, controlName: string, phoneNumber: string): void { + // Take length of phone number without spaces. + const phoneNoSpace = phoneNumber.replace(/\s/g, ''); + // Check to refresh every 2 number. + if (phoneNoSpace.length % 2 === 0) { + // Add space every 2 number + form.get(controlName).setValue(phoneNoSpace.replace(/(?!^)(?=(?:\d{2})+$)/g, ' ')); //NOSONAR + } + } +} diff --git a/src/styles.scss b/src/styles.scss index ed277ef13c5f961ff8ba4316e9e4713f5d450520..114a5e4e2ab09e6bb9000664ac5e33c02d1b1e1a 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -10,6 +10,7 @@ @import 'assets/scss/layout'; @import 'assets/scss/buttons'; @import '../node_modules/leaflet.locatecontrol/dist/L.Control.Locate.css'; +@import '~ngx-toastr/toastr'; html { height: -webkit-fill-available; @@ -90,6 +91,36 @@ a { } } +// Forms +.form-group { + margin-bottom: 26px; + label { + color: $grey-2; + } +} +form p.notRequired { + margin-top: 0px; + font-style: italic; + color: $secondary-color; +} + +/** Inputs **/ +input { + margin-top: 4px; +} + +/** Textarea **/ +.textareaBlock { + textarea { + padding: 13px 8px; + background: $grey-6; + border: 1px solid $grey-4; + border-radius: $input-radius; + resize: none; + @include cn-regular-16; + } +} + /** Buttons **/ button { &:focus { @@ -98,7 +129,6 @@ button { } /** Checkboxes **/ - .checkbox { list-style-type: none; width: 100%;