diff --git a/angular.json b/angular.json index 6ff6ea82bb729a7d47c0e13aad7011ce42e08e2d..5483fa1cd096e150f7febcaae44cb71c0e944afb 100644 --- a/angular.json +++ b/angular.json @@ -47,6 +47,10 @@ { "replace": "src/i18n/geosource/geosource.ts", "with": "src/i18n/geosource/geosource.fr.ts" + }, + { + "replace": "src/i18n/contact/contact.ts", + "with": "src/i18n/contact/contact.fr.ts" } ] }, @@ -99,6 +103,10 @@ { "replace": "src/i18n/geosource/geosource.ts", "with": "src/i18n/geosource/geosource.fr.ts" + }, + { + "replace": "src/i18n/contact/contact.ts", + "with": "src/i18n/contact/contact.fr.ts" } ], "outputPath": "dist/fr", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 06ff11ae21af544d9826869eaf1a5b177e2b1ad6..2c744415a07e4f794f0f01504d281c71ec595631 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,10 +3,10 @@ import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; import { AppRoutes } from './routes'; export const routes: Routes = [ - { - path: AppRoutes.research.uri, - loadChildren: './geosource/geosource.module#GeosourceModule', - }, + // { + // path: AppRoutes.research.uri, + // loadChildren: './geosource/geosource.module#GeosourceModule', + // }, ]; @NgModule({ diff --git a/src/app/core/components/contact/contact.component.html b/src/app/core/components/contact/contact.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c0077ff24fe75fe9e5554692dcce92cfc0662435 --- /dev/null +++ b/src/app/core/components/contact/contact.component.html @@ -0,0 +1,188 @@ +<h1 class="has-text-centered" i18n="@@contact.contactUs">Contact us</h1> + +<div class="contact-form-container"> + <h2 i18n="@@contact.contactForm">Form contact</h2> + + <form [formGroup]="form" (ngSubmit)="send()"> + <div class="columns is-multiline"> + <div class="column is-12-mobile is-2-tablet is-3-desktop"> + <h3 i18n="@@contact.identity">Identity</h3> + </div> + + <div class="column is-12-mobile is-10-tablet is-9-desktop"> + <div class="fields-container"> + + <div class="field"> + <label class="label" for="lastname" i18n="@@contact.lastname">Lastname</label> + <p class="control has-icons-right"> + <input id="lastname" class="input" type="text" formControlName="lastname" (keyup)="toUppercase('lastname')" + [ngClass]="{'is-danger': fieldIsInvalid('lastname'), 'is-success': fieldIsValid('lastname')}"> + <span class="icon is-small is-right has-text-success" *ngIf="fieldIsValid('lastname')"> + <i class="fas fa-check-circle"></i> + </span> + <span class="icon is-small is-right has-text-danger" *ngIf="fieldIsInvalid('lastname')"> + <i class="fas fa-exclamation-circle"></i> + </span> + </p> + + <div class="incorrect-field-message" *ngIf="fieldIsInvalid('lastname')"> + <div *ngIf="form.controls['lastname'].errors.required" i18n="@@contact.errors.missingLastname"> + You must indicate a lastname. + </div> + <div *ngIf="form.controls['lastname'].errors.pattern" i18n="@@contact.errors.forbiddenCharacters"> + Special characters are forbidden. + </div> + </div> + </div> + + <div class="field"> + <label class="label" for="firstname" i18n="@@contact.firstname">Firstname</label> + <p class="control has-icons-right"> + <input id="firstname" class="input" type="text" (keyup)="toUppercase('firstname')" formControlName="firstname" + [ngClass]="{'is-danger': fieldIsInvalid('firstname'), 'is-success': fieldIsValid('firstname')}"> + <span class="icon is-small is-right has-text-success" *ngIf="fieldIsValid('firstname')"> + <i class="fas fa-check-circle"></i> + </span> + <span class="icon is-small is-right has-text-danger" *ngIf="fieldIsInvalid('firstname')"> + <i class="fas fa-exclamation-circle"></i> + </span> + </p> + + <div class="incorrect-field-message" *ngIf="fieldIsInvalid('firstname')"> + <div *ngIf="form.controls['firstname'].errors.required" i18n="@@contact.errors.missingFirstname"> + You must indicate a firstname. + </div> + <div *ngIf="form.controls['lastname'].errors.pattern" i18n="@@contact.errors.forbiddenCharacters"> + Special characters are forbidden. + </div> + </div> + </div> + + <div class="field"> + <label class="label" for="email" i18n="@@contact.email">Email</label> + <p class="control has-icons-right"> + <input id="email" class="input" type="email" formControlName="email" [ngClass]="{'is-danger': fieldIsInvalid('email'), 'is-success': fieldIsValid('email')}"> + <span class="icon is-small is-right has-text-success" *ngIf="fieldIsValid('email')"> + <i class="fas fa-check-circle"></i> + </span> + <span class="icon is-small is-right has-text-danger" *ngIf="fieldIsInvalid('email')"> + <i class="fas fa-exclamation-circle"></i> + </span> + </p> + + <div class="incorrect-field-message" *ngIf="fieldIsInvalid('email')"> + <div *ngIf="form.controls['email'].errors.required" i18n="@@contact.errors.missingEmail"> + You must enter an email address. + </div> + + <div *ngIf="form.controls['email'].errors.email" i18n="@@contact.errors.invalidEmail"> + You must enter a valid email address. + </div> + </div> + </div> + + <div class="field"> + <label class="label" for="emailConfirmation" i18n="@@contact.emailConfirmation">Confirm your email + address</label> + <p class="control has-icons-right"> + <input blockCopyPaste id="emailConfirmation" class="input" type="email" formControlName="emailConfirmation" + [ngClass]="{'is-danger': emailConfirmationError || fieldIsInvalid('emailConfirmation'), 'is-success': !emailConfirmationError && fieldIsValid('emailConfirmation')}"> + <span class="icon is-small is-right has-text-success" *ngIf="!emailConfirmationError && fieldIsValid('emailConfirmation')"> + <i class="fas fa-check-circle"></i> + </span> + <span class="icon is-small is-right has-text-danger" *ngIf="emailConfirmationError || fieldIsInvalid('emailConfirmation')"> + <i class="fas fa-exclamation-circle"></i> + </span> + </p> + + <div class="incorrect-field-message" *ngIf="fieldIsInvalid('emailConfirmation')"> + <div *ngIf="form.controls['emailConfirmation'].errors.required" i18n="@@contact.errors.missingConfirmationEmail"> + You must confirm your email address. + </div> + + <div *ngIf="form.controls['emailConfirmation'].errors.email" i18n="@@contact.errors.invalidEmail"> + You must enter a valid email address. + </div> + </div> + <div class="incorrect-field-message" *ngIf="emailConfirmationError"> + <div *ngIf="emailConfirmationError" i18n="@@contact.errors.confirmationEmailNotCorresponding"> + You must enter the same email address. + </div> + </div> + </div> + + </div> + </div> + + <div class="column is-12-mobile is-2-tablet is-3-desktop"> + <h3 i18n="@@contact.message">Message</h3> + </div> + + <div class="column is-12-mobile is-10-tablet is-9-desktop"> + <div class="field"> + <label class="label" [for]="subjectLabelFor" i18n="@@contact.subject">Subject</label> + <div class="dropdown" [ngClass]="{'is-active': subjectDropdownState}" (clickOutside)="closeSubjectDropdown()"> + <div class="dropdown-trigger" (click)="toggleSubject()"> + <button id="subjectDropdown" type="button" class="button" aria-haspopup="true" aria-controls="dropdown-menu" + [disabled]="formDisabled"> + <span>{{ selectedSubject !== null && selectedSubject.value !== null ? selectedSubject.value : '---'}}</span> + <span class="icon is-small"> + <i class="fas fa-angle-down" aria-hidden="true"></i> + </span> + </button> + </div> + <div class="dropdown-menu" id="dropdown-menu" role="menu"> + <ul class="dropdown-content"> + <li class="dropdown-item" *ngFor="let sub of subjects" (keydown.enter)="setSubject(sub)" (click)="setSubject(sub)" + tabindex=0> + {{ sub.value }} + </li> + </ul> + </div> + </div> + <p class="control has-icons-right subject-input-control" *ngIf="displaySubjectInput === true"> + <input id="subjectInput" class="input" type="text" formControlName="subject" [ngClass]="{'is-danger': fieldIsInvalid('subject'), 'is-success': fieldIsValid('subject')}"> + <span class="icon is-small is-right has-text-success" *ngIf="fieldIsValid('subject')"> + <i class="fas fa-check-circle"></i> + </span> + <span class="icon is-small is-right has-text-danger" *ngIf="fieldIsInvalid('subject')"> + <i class="fas fa-exclamation-circle"></i> + </span> + </p> + + <div class="incorrect-field-message" *ngIf="fieldIsInvalid('subject')"> + <div *ngIf="form.controls['subject'].errors.required" i18n="@@contact.errors.missingSubject"> + You must enter a subject. + </div> + </div> + </div> + + <div class="field"> + <label class="label" for="text" i18n="@@contact.messageField">Message</label> + <p class="control has-icons-right"> + <textarea id="text" class="textarea has-fixed-size" type="textarea" formControlName="text" [ngClass]="{'is-danger': fieldIsInvalid('text'), 'is-success': fieldIsValid('text')}" + rows=6></textarea> + <span class="icon is-small is-right has-text-success" *ngIf="fieldIsValid('text')"> + <i class="fas fa-check-circle"></i> + </span> + <span class="icon is-small is-right has-text-danger" *ngIf="fieldIsInvalid('text')"> + <i class="fas fa-exclamation-circle"></i> + </span> + </p> + + <div class="incorrect-field-message" *ngIf="fieldIsInvalid('text')"> + <div *ngIf="form.controls['text'].errors.required" i18n="@@contact.errors.invalidMessage"> + You must enter a message. + </div> + </div> + </div> + + <div class="has-text-right button-wrapper"> + <button class="button button-gl is-outlined" type="button" [disabled]="formDisabled" (click)="cancel()" i18n="@@contact.cancel">Cancel</button> + <button type="submit" class="button button-gl" [ngClass]="{'is-loading': formDisabled}" [disabled]="formIsInvalid || formDisabled" + i18n="@@contact.send">Send</button> + </div> + </div> + </div> + </form> +</div> \ No newline at end of file diff --git a/src/app/core/components/contact/contact.component.scss b/src/app/core/components/contact/contact.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5b76c9a460dd64dedc83164f35c6b107013d95be --- /dev/null +++ b/src/app/core/components/contact/contact.component.scss @@ -0,0 +1,74 @@ +@import '../../../../scss/variables.scss'; +@import "../../../../../node_modules/bulma/sass/utilities/_all"; + +h1 { + font-size: 2rem; + margin: 0; + padding: 1.25rem; + background-color: white; +} + +h2 { + font-size: 1.6rem; + margin-bottom: 2rem; +} + +h3 { + margin-top: 0; + margin-bottom: 0; + margin-right: 0; +} + +.contact-form-container { + background-color: white; + padding: 0 1rem 0 1rem; + margin-left: 1.5rem; + margin-right: 1.5rem; + margin-bottom: 1.25rem; + + + @media(min-width: $tablet) { + .fields-container { + width: 66%; + } + } + + label { + font-size: 1rem; + font-weight: normal; + font-style: normal; + &:hover { + cursor: pointer; + } + } + + .incorrect-field-message { + color: #333745; + font-style: italic; + font-size: $size-6; + } + + .subject-input-control { + margin-top: 0.5rem; + } + + .dropdown-item:hover { + cursor: pointer; + background-color: lightgrey; + } + + .button-gl { + width:8rem; + + &:first-of-type { + margin-right: 1.25rem; + } + } +} + +.title-label.is-danger { + &:hover, &:focus { + background-color:white; + color: $red; + } +} \ No newline at end of file diff --git a/src/app/core/components/contact/contact.component.spec.ts b/src/app/core/components/contact/contact.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c96cc896011afa63744b689c3eba64c88c058b0f --- /dev/null +++ b/src/app/core/components/contact/contact.component.spec.ts @@ -0,0 +1,400 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContactComponent } from './contact.component'; +import { ReactiveFormsModule, FormControl, AbstractControl } from '@angular/forms'; +import { EmailService, NotificationService, NavigationHistoryService } from '../../services'; +import { BehaviorSubject, of } from 'rxjs'; +import { Email } from '../../models'; +import { RouterTestingModule } from '@angular/router/testing'; + +export class NotificationServiceMock { + + constructor() { } + + send(email: Email) { + return of(null); + } + +} + +export class EmailServiceMock { + + _notification: BehaviorSubject<Notification> = new BehaviorSubject(null); + + constructor() { } + + notify(notification: Notification) { + this._notification.next(notification); + } + + get notification$() { + return this._notification; + } + +} + +describe('ContactComponent', () => { + let component: ContactComponent; + let fixture: ComponentFixture<ContactComponent>; + let firstname: AbstractControl; + let lastname: AbstractControl; + let email: AbstractControl; + let emailConfirmation: AbstractControl; + let subject: AbstractControl; + let text: AbstractControl; + + describe('Template', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + ], + providers: [ + { + provide: EmailService, + useClass: EmailServiceMock, + }, + { + provide: NotificationService, + useClass: NotificationServiceMock, + }, + NavigationHistoryService, + ], + declarations: [ContactComponent], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ContactComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + firstname = component.form.controls.firstname; + lastname = component.form.controls.lastname; + email = component.form.controls.email; + emailConfirmation = component.form.controls.emailConfirmation; + subject = component.form.controls.subject; + text = component.form.controls.text; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('Form validation', () => { + + it('form invalid when empty', () => { + expect(component.form.valid).toBeFalsy(); + }); + + describe('Firstname control validation', () => { + beforeEach(() => { + firstname.reset(); + }); + + it('firstname control is valid with lowercase and uppercase letter', () => { + // Given + const value = 'John Doe'; + // When + firstname.setValue(value); + // Then + expect(firstname.valid).toBeTruthy(); + }); + + it('firstname control is valid with letter and space', () => { + // Given + const value = 'John Doe'; + // When + firstname.setValue(value); + // Then + expect(firstname.valid).toBeTruthy(); + }); + + it('firstname control is valid with letter and dash', () => { + // Given + const value = 'Jean-jacques'; + // When + firstname.setValue(value); + // Then + expect(firstname.valid).toBeTruthy(); + }); + + it('firstname control is valid with letter and apostrophe', () => { + // Given + const value = 'De\'Andre'; + // When + firstname.setValue(value); + // Then + expect(firstname.valid).toBeTruthy(); + }); + + it('firstname control accept any of those characters', () => { + // Given + let specialChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + specialChars += 'abcdefghijklmnopqrstuvwxyz'; + specialChars += 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝ'; + specialChars += 'àáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿ'; + // When + firstname.setValue(specialChars); + // Then + expect(firstname.valid).toBeTruthy(); + }); + + it('firstname control is invalid if first letter is lowercased, has error "pattern"', () => { + // Given + const value = 'david'; + // When + firstname.setValue(value); + const errors = firstname.errors || {}; + // Then + expect(firstname.valid).toBeFalsy(); + expect(errors['pattern']).toBeTruthy(); + }); + + it('firstname control is invalid if contains numbers', () => { + // Given + const value = '0123456789'; + // When + firstname.setValue(value); + const errors = firstname.errors || {}; + // Then + expect(firstname.valid).toBeFalsy(); + expect(errors['pattern']).toBeTruthy(); + }); + + it('firstname control is invalid if contains special characters', () => { + // Given + const value = '"*$^!qjsdk+°)'; + // When + firstname.setValue(value); + const errors = firstname.errors || {}; + // Then + expect(firstname.valid).toBeFalsy(); + expect(errors['pattern']).toBeTruthy(); + }); + + it('firstname control is invalid if the value is empty', () => { + // Given + const value = ''; + // When + firstname.setValue(value); + const errors = firstname.errors || {}; + // Then + expect(firstname.valid).toBeFalsy(); + expect(errors['required']).toBeTruthy(); + }); + }); + + describe('Lastname control validation', () => { + beforeEach(() => { + lastname.reset(); + }); + + it('lastname control is valid with lowercase and uppercase letter', () => { + // Given + const value = 'John Doe'; + // When + lastname.setValue(value); + // Then + expect(lastname.valid).toBeTruthy(); + }); + + it('lastname control is valid with letter and space', () => { + // Given + const value = 'John Doe'; + // When + lastname.setValue(value); + // Then + expect(lastname.valid).toBeTruthy(); + }); + + it('lastname control is valid with letter and dash', () => { + // Given + const value = 'Jean-jacques'; + // When + lastname.setValue(value); + // Then + expect(lastname.valid).toBeTruthy(); + }); + + it('lastname control is valid with letter and apostrophe', () => { + // Given + const value = 'De\'Andre'; + // When + lastname.setValue(value); + // Then + expect(lastname.valid).toBeTruthy(); + }); + + it('lastname control accept any of those characters', () => { + // Given + let specialChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + specialChars += 'abcdefghijklmnopqrstuvwxyz'; + specialChars += 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝ'; + specialChars += 'àáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿ'; + // When + lastname.setValue(specialChars); + // Then + expect(lastname.valid).toBeTruthy(); + }); + + it('lastname control is invalid if first letter is lowercased, has error "pattern"', () => { + // Given + const value = 'david'; + // When + lastname.setValue(value); + const errors = lastname.errors || {}; + // Then + expect(lastname.valid).toBeFalsy(); + expect(errors['pattern']).toBeTruthy(); + }); + + it('lastname control is invalid if contains numbers', () => { + // Given + const value = '0123456789'; + // When + lastname.setValue(value); + const errors = lastname.errors || {}; + // Then + expect(lastname.valid).toBeFalsy(); + expect(errors['pattern']).toBeTruthy(); + }); + + it('lastname control is invalid if contains special characters', () => { + // Given + const value = '"*$^!qjsdk+°)'; + // When + lastname.setValue(value); + const errors = lastname.errors || {}; + // Then + expect(lastname.valid).toBeFalsy(); + expect(errors['pattern']).toBeTruthy(); + }); + + it('lastname control is invalid if the value is empty', () => { + // Given + const value = ''; + // When + lastname.setValue(value); + const errors = lastname.errors || {}; + // Then + expect(lastname.valid).toBeFalsy(); + expect(errors['required']).toBeTruthy(); + }); + }); + + describe('Email control validation', () => { + beforeEach(() => { + email.reset(); + }); + + it('Email control is invalid if the value is empty', () => { + // Given + const value = ''; + // When + email.setValue(value); + const errors = email.errors || {}; + // Then + expect(email.valid).toBeFalsy(); + expect(errors['required']).toBeTruthy(); + }); + + it('Email control is invalid if the value is not a valid email', () => { + // Given + const value = 'toto.grandlyon.com'; + // When + email.setValue(value); + const errors = email.errors || {}; + // Then + expect(email.valid).toBeFalsy(); + expect(errors['email']).toBeTruthy(); + }); + + it('Email control is valid if the value is a valid email', () => { + // Given + const value = 'toto@grandlyon.com'; + // When + email.setValue(value); + const errors = email.errors || {}; + // Then + expect(email.valid).toBeTruthy(); + expect(errors).toEqual({}); + }); + }); + + describe('emailConfirmation control validation', () => { + beforeEach(() => { + emailConfirmation.reset(); + }); + + it('emailConfirmation control is invalid if the value is empty', () => { + // Given + const value = ''; + // When + emailConfirmation.setValue(value); + const errors = emailConfirmation.errors || {}; + // Then + expect(emailConfirmation.valid).toBeFalsy(); + expect(errors['required']).toBeTruthy(); + }); + + it('emailConfirmation control is invalid if the value is not a valid emailConfirmation', () => { + // Given + const value = 'toto.grandlyon.com'; + // When + emailConfirmation.setValue(value); + const errors = emailConfirmation.errors || {}; + // Then + expect(emailConfirmation.valid).toBeFalsy(); + expect(errors['email']).toBeTruthy(); + }); + + it('emailConfirmation control is valid if the value is a valid emailConfirmation', () => { + // Given + const value = 'toto@grandlyon.com'; + // When + emailConfirmation.setValue(value); + const errors = emailConfirmation.errors || {}; + // Then + expect(emailConfirmation.valid).toBeTruthy(); + expect(errors).toEqual({}); + }); + }); + + describe('subject control validation', () => { + beforeEach(() => { + subject.reset(); + }); + + it('subject control is invalid if the value is empty', () => { + // Given + const value = ''; + // When + subject.setValue(value); + const errors = subject.errors || {}; + // Then + expect(subject.valid).toBeFalsy(); + expect(errors['required']).toBeTruthy(); + }); + }); + + describe('text control validation', () => { + beforeEach(() => { + text.reset(); + }); + + it('text control is invalid if the value is empty', () => { + // Given + const value = ''; + // When + text.setValue(value); + const errors = text.errors || {}; + // Then + expect(text.valid).toBeFalsy(); + expect(errors['required']).toBeTruthy(); + }); + }); + }); + }); +}); diff --git a/src/app/core/components/contact/contact.component.ts b/src/app/core/components/contact/contact.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e06099f82cabc4b6c1b0e82bd188d7004f70664e --- /dev/null +++ b/src/app/core/components/contact/contact.component.ts @@ -0,0 +1,166 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; +import { EmailService, NotificationService, NavigationHistoryService } from '../../services'; +import { Email } from '../../models'; +import { subjects as Subjects, feedbackMessages } from '../../../../i18n/contact/contact'; +import { Router, ActivatedRoute } from '@angular/router'; +import { AppRoutes } from '../../../routes'; + +@Component({ + selector: 'app-contact', + templateUrl: './contact.component.html', + styleUrls: ['./contact.component.scss'], +}) +export class ContactComponent implements OnInit { + + form: FormGroup; + subjectDropdownState = false; + subjects: Subject[] = Subjects; // Keep this line in order to access the subjects in the html template + selectedSubject: Subject = null; + + constructor( + private _fb: FormBuilder, + private _emailService: EmailService, + private _notificationService: NotificationService, + private _navigationHistoryService: NavigationHistoryService, + private _router: Router, + private _route: ActivatedRoute, + ) { + this.form = this._fb.group( + { + firstname: ['', [ + Validators.required, + Validators.pattern('([A-ZÀ-ÖØ-Ý][a-zà-öø-ýÿA-ZÀ-ÖØ-Ý]*)([ \'-][a-zA-ZÀ-ÖØ-öø-ýÿ]*)*'), + ]], + lastname: ['', [Validators.required, + Validators.pattern('([A-ZÀ-ÖØ-Ý][a-zà-öø-ýÿA-ZÀ-ÖØ-Ý]*)([ \'-][a-zA-ZÀ-ÖØ-öø-ýÿ]*)*'), + ]], + email: ['', [Validators.required, Validators.email]], + emailConfirmation: ['', [Validators.required, Validators.email]], + subject: ['', Validators.required], + text: ['', Validators.required], + }); + } + + ngOnInit() { + this._route.queryParams.subscribe((params) => { + if (params.subject) { + this.selectedSubject = this.subjects.find(sub => sub.key === 'other'); + this.form.controls.subject.patchValue(params.subject); + } else { + this.selectedSubject = null; + } + }); + } + + send() { + if (!this.formIsInvalid) { + const email = new Email(this.form.value); + this.form.disable(); + this._emailService.send(email).subscribe( + (res) => { + this.form.enable(); + this._notificationService.notify( + { + type: 'success', + message: feedbackMessages.success, + }, + ); + this.form.reset(); + }, + (err) => { + this.form.enable(); + this._notificationService.notify( + { + type: 'error', + message: feedbackMessages.error, + }, + ); + }, + ); + } + } + + get emailConfirmationIsCorrect(): boolean { + const value = this.form.controls.email.value === this.form.controls.emailConfirmation.value ? true : false; + return value; + } + + get emailConfirmationError() { + // Display the message if + // Input values have been modified + // Input values have been modified but the email values are not similar + return this.form.controls['email'].touched && + this.form.controls['emailConfirmation'].touched && + !this.emailConfirmationIsCorrect; + } + + // Return true if one of the fields at least doesn't respect its validators + // or if the confirmation email is different from the email + get formIsInvalid() { + const controls = this.form.controls; + const value = this.form.invalid || this.emailConfirmationError; + return value; + } + + fieldIsInvalid(field: string) { + return (this.form.controls[field].touched) && this.form.controls[field].invalid; + } + + fieldIsValid(field: string) { + return (this.form.controls[field].touched) && this.form.controls[field].valid; + } + + toggleSubject() { + if (!this.formDisabled) { + this.subjectDropdownState = !this.subjectDropdownState; + } + } + + closeSubjectDropdown() { + this.subjectDropdownState = false; + } + + setSubject(subject: Subject) { + this.toggleSubject(); + this.selectedSubject = subject; + if (this.selectedSubject.key === 'other') { + this.form.controls.subject.patchValue(''); + this.form.controls.subject.reset(); + } else { + this.form.controls.subject.patchValue(this.selectedSubject.value); + } + } + + cancel() { + const previous = this._navigationHistoryService.getFromLast(1); + if (previous !== null) { + this._router.navigateByUrl(previous); + } else { + this._router.navigateByUrl(AppRoutes.home.uri); + } + } + + get subjectLabelFor(): string { + return this.displaySubjectInput ? 'subjectInput' : 'subjectDropdown'; + } + + get formDisabled(): boolean { + return this.form.disabled; + } + + get displaySubjectInput(): boolean { + return (this.selectedSubject && this.selectedSubject.key) === 'other' ? true : false; + } + + toUppercase(controlName: string) { + const input = this.form.controls[controlName].value; + const uppercased = input.substring(0, 1).toUpperCase() + input.substring(1); + this.form.controls[controlName].patchValue(uppercased); + } +} + +interface Subject { + key: string; + value: string; +} diff --git a/src/app/core/components/footer/footer.component.html b/src/app/core/components/footer/footer.component.html index a19ff64f6f047d33ec4ec4e6573559bcd66dbc5a..a86ecc5c4c290cd113084eb5c6de29751d9e4cbf 100644 --- a/src/app/core/components/footer/footer.component.html +++ b/src/app/core/components/footer/footer.component.html @@ -34,7 +34,7 @@ <a class="legal-mentions-link" [routerLink]="['/', AppRoutes.legalNotices.uri]" i18n="@@footer.notices">Legal Notices</a> </li> <li class="left-border"> - <a routerLink="/" i18n="@@footer.contactus">Contact Us</a> + <a [routerLink]="['/', AppRoutes.contact.uri]" i18n="@@footer.contactus">Contact Us</a> </li> </ul> </div> diff --git a/src/app/core/components/index.ts b/src/app/core/components/index.ts index 82c8d51d679ad7dbfaa2a3ab26af313913aafd4c..84499c9c375311044cafe141314ed1f4dfd6555e 100644 --- a/src/app/core/components/index.ts +++ b/src/app/core/components/index.ts @@ -3,8 +3,9 @@ import { MainComponent } from './main/main.component'; import { FooterComponent } from './footer/footer.component'; import { NotificationsComponent } from './notifications/notifications.component'; import { ErrorComponent } from './error/error.component'; +import { ContactComponent } from './contact/contact.component'; -export { HeaderComponent, MainComponent, FooterComponent, NotificationsComponent, ErrorComponent }; +export { HeaderComponent, MainComponent, FooterComponent, NotificationsComponent, ErrorComponent, ContactComponent }; // tslint:disable-next-line:variable-name export const CoreComponents = [ @@ -13,4 +14,5 @@ export const CoreComponents = [ FooterComponent, NotificationsComponent, ErrorComponent, + ContactComponent, ]; diff --git a/src/app/core/core-routing.module.ts b/src/app/core/core-routing.module.ts index 5f7c40380de53928ca3c06c5c803c518ac7fb50a..8b73098a4c181c67f2d96e2304ea3bda276835c7 100644 --- a/src/app/core/core-routing.module.ts +++ b/src/app/core/core-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AppRoutes } from '../routes'; -import { ErrorComponent } from './components'; +import { ErrorComponent, ContactComponent } from './components'; export const routes: Routes = [ { @@ -16,6 +16,13 @@ export const routes: Routes = [ title: AppRoutes.error.title, }, }, + { + path: AppRoutes.contact.uri, + component: ContactComponent, + data: { + title: AppRoutes.contact.title, + }, + }, ]; @NgModule({ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 4a5b5aa6c31e21248953c02735e6b3a45742a71b..058450e9b4b9cf934c204c381d8c6a216fae33e7 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -9,12 +9,14 @@ import { HttpErrorResponseInterceptor } from './interceptors/http-error-response import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { CoreServices } from './services'; import { RouterModule } from '../../../node_modules/@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, CoreRoutingModule, SharedModule, + ReactiveFormsModule, ], declarations: [CoreComponents], providers: [ diff --git a/src/app/core/interceptors/http-error-response-interceptor.ts b/src/app/core/interceptors/http-error-response-interceptor.ts index c8bae8b6e53fd88682086548d8078b317ea6006d..a54205a989c1bc62bc6b0ed5342259bb4d5a93a3 100644 --- a/src/app/core/interceptors/http-error-response-interceptor.ts +++ b/src/app/core/interceptors/http-error-response-interceptor.ts @@ -18,7 +18,6 @@ export class HttpErrorResponseInterceptor implements HttpInterceptor { } }, (err) => { - console.log(err); if (err instanceof HttpErrorResponse) { switch (err.status) { case 401: diff --git a/src/app/core/models/email.model.ts b/src/app/core/models/email.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9a21ce1c9dbff20190a705efaa824be6e3086d9 --- /dev/null +++ b/src/app/core/models/email.model.ts @@ -0,0 +1,26 @@ +import { environment } from '../../../environments/environment'; + +export interface IContactForm { + email: string; + emailConfirmation: string; + subject: string; + text: string; + firstname: string; + lastname: string; +} + +export class Email { + from: string; + subject: string; + firstname: string; + lastname: string; + text: string; + + constructor(contactForm: IContactForm) { + this.subject = contactForm.subject; + this.text = contactForm.text; + this.from = contactForm.email; + this.firstname = contactForm.firstname; + this.lastname = contactForm.lastname; + } +} diff --git a/src/app/core/models/index.ts b/src/app/core/models/index.ts index 804ec59ded0ef6ad842ec93fe8c958a85456e135..0d3c38d322c55b79a68bf7e0e53594f3ac8a21c1 100644 --- a/src/app/core/models/index.ts +++ b/src/app/core/models/index.ts @@ -1,5 +1,7 @@ import { Notification, INotification } from './notification.model'; import { IMatomoResponse } from './matomo.model'; +import { IContactForm, Email } from './email.model'; export { Notification, INotification }; export { IMatomoResponse }; +export { IContactForm, Email }; diff --git a/src/app/core/services/email.service.ts b/src/app/core/services/email.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ad680edb69015df6f16c1a6780ce253a1c61149 --- /dev/null +++ b/src/app/core/services/email.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../../environments/environment'; +import { Email } from '../models'; + +@Injectable() +export class EmailService { + + constructor( + private _httpClient: HttpClient, + ) {} + + send(email: Email) { + return this._httpClient.post(environment.emailService.url + '/contact', email); + } +} diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts index ed90907ea1e9a8b600f61fe1ae841317c93fb80e..15ae6b2712fa5254fde435de2ef7bf1e5028a5ac 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -3,8 +3,9 @@ import { NotificationService } from './notification.service'; import { MatomoService } from './matomo.service'; import { NavigationHistoryService } from './navigation-history.service'; import { StorageService } from './storage.service'; +import { EmailService } from './email.service'; -export { ErrorService, NotificationService, MatomoService, NavigationHistoryService }; +export { ErrorService, NotificationService, MatomoService, NavigationHistoryService, EmailService }; // tslint:disable-next-line:variable-name export const CoreServices = [ @@ -13,4 +14,5 @@ export const CoreServices = [ MatomoService, NavigationHistoryService, StorageService, + EmailService, ]; diff --git a/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.html b/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.html index 3b9525d52c7d4973b42f82ba901db2d33ce20c6c..3214aa437eb9790fc53cdb1ecc90c3ef3910bcab 100644 --- a/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.html +++ b/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.html @@ -47,7 +47,8 @@ {{ generalInfo['parent'].keyTitle }} </div> <div class="column is-10 is-size-7 even"> - <a [routerLink]="['/', AppRoutes.research.uri, AppRoutes.datasets.uri, generalInfo['parent'].uuid]">{{ generalInfo['parent'].title }}</a> + <a [routerLink]="['/', AppRoutes.research.uri, AppRoutes.datasets.uri, generalInfo['parent'].uuid]">{{ + generalInfo['parent'].title }}</a> </div> </ng-container> </div> @@ -85,8 +86,15 @@ </ng-container> </div> </div> - + <div class="contact"> + <div class="columns is-marginless"> + <div class="column is-12 title is-size-7"> + <span i18n="@@dataset.info.questions">You have questions on this dataset, </span> + <a i18n="@@dataset.info.contactus" class="link-red" [routerLink]="['/', AppRoutes.contact.uri]" [queryParams]="{ subject: metadata.title }">contact us.</a> + </div> + </div> + </div> </div> </div> </div> -</ng-container> +</ng-container> \ No newline at end of file diff --git a/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.scss b/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.scss index cec3249eef9955a73ba4713a68e442dbca4ff45d..bec1e28d4f117c5a09afd69f1b86e5a0e9c80a9c 100644 --- a/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.scss +++ b/src/app/geosource/components/dataset-detail/dataset-info/dataset-info.component.scss @@ -36,7 +36,7 @@ div.unitary-description:not(:first-of-type) { } -.children , .general-info, .articles { +.children , .general-info, .articles, .contact { .column { padding-top: 0.3125rem; padding-bottom: 0.3125rem; diff --git a/src/app/geosource/geosource-routing.module.ts b/src/app/geosource/geosource-routing.module.ts index e1ccd324ab818c99b91e3c33ef6bb5cff72a129f..d17b00236bc3147b151903bedc4a4d67e74e0fea 100644 --- a/src/app/geosource/geosource-routing.module.ts +++ b/src/app/geosource/geosource-routing.module.ts @@ -6,7 +6,7 @@ import { AppRoutes } from '../routes'; export const routes: Routes = [ { - path: '', + path: AppRoutes.research.uri, component: ResearchComponent, data: { title: AppRoutes.research.title, diff --git a/src/app/geosource/services/elasticsearch.service.ts b/src/app/geosource/services/elasticsearch.service.ts index 2b2ff4d273e7d7573dbd5a4a93c7a7c60a9d19a6..cab37974091ddcc1ae1eb47787111178990cc0b3 100644 --- a/src/app/geosource/services/elasticsearch.service.ts +++ b/src/app/geosource/services/elasticsearch.service.ts @@ -283,8 +283,8 @@ export class ElasticsearchService { } /* - * This request will get one suggestion from the query text - * The suggest is based on metadata-fr.title property + * This request will get one phrase suggestion out of the query text + * cf. https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-phrase.html */ getSuggestion(query: string): Observable<SearchSuggestion> { return this._http.request<IElasticsearchResponse>('POST', this.url, { @@ -293,16 +293,20 @@ export class ElasticsearchService { text: query, suggestion: { phrase: { - field: 'data_and_metadata', + field: 'data_and_metadata.suggest', + // as only the very first suggestion will be used, let's limit the size of the results to 1: + size: 1, + max_errors: query.split(' ').length, highlight: { pre_tag: '<b><i>', post_tag: '</i></b>', }, + analyzer: 'my_search_analyzer', direct_generator: [{ - field: 'data_and_metadata', - suggest_mode: 'popular', - // prefix_length: 4, - min_word_length: 1, + field: 'data_and_metadata.suggest', + suggest_mode: 'missing', + prefix_length: 1, + min_word_length: 4, }], }, }, diff --git a/src/app/routes.ts b/src/app/routes.ts index 18c900f31150a428a6b70fbec525cad9fd9ab9a8..27db92a085df95479d39970f1394e216f8f7ed9b 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -21,6 +21,13 @@ export const AppRoutes = { en: 'Accessibility', }, }, + contact: { + uri: 'contact', + title: { + fr: 'Contact', + en: 'Contact', + }, + }, siteMap: { uri: 'plan-du-site', title: { diff --git a/src/app/shared/directives/block-copypaste.directive.ts b/src/app/shared/directives/block-copypaste.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..7cb989da64336c3b1758850a9df2f4609dfffdcc --- /dev/null +++ b/src/app/shared/directives/block-copypaste.directive.ts @@ -0,0 +1,20 @@ +import { Directive, HostListener } from '@angular/core'; + +@Directive({ + selector: '[blockCopyPaste]', +}) +export class BlockCopyPasteDirective { + constructor() { } + + @HostListener('paste', ['$event']) blockPaste(e: KeyboardEvent) { + e.preventDefault(); + } + + @HostListener('copy', ['$event']) blockCopy(e: KeyboardEvent) { + e.preventDefault(); + } + + @HostListener('cut', ['$event']) blockCut(e: KeyboardEvent) { + e.preventDefault(); + } +} diff --git a/src/app/shared/directives/click-outside.directive.ts b/src/app/shared/directives/click-outside.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..d89778c8bacbaeb2b1e0d6bee03c4193ac0c7cae --- /dev/null +++ b/src/app/shared/directives/click-outside.directive.ts @@ -0,0 +1,20 @@ +import { Directive, ElementRef, Output, EventEmitter, HostListener } from '@angular/core'; + +@Directive({ + selector: '[clickOutside]', +}) +export class ClickOutsideDirective { + constructor(private _elementRef: ElementRef) { + } + + @Output() + public clickOutside = new EventEmitter(); + + @HostListener('document:click', ['$event.target']) + public onClick(targetElement) { + const clickedInside = this._elementRef.nativeElement.contains(targetElement); + if (!clickedInside) { + this.clickOutside.emit(null); + } + } +} diff --git a/src/app/shared/directives/index.ts b/src/app/shared/directives/index.ts index 08d1c25f0ae96ac403d347efe562839a3278f1fb..79989eb79dd7c268f6ed1e4d29230a25cd8e8b87 100644 --- a/src/app/shared/directives/index.ts +++ b/src/app/shared/directives/index.ts @@ -1,10 +1,14 @@ import { PreventDefaultDirective } from './prevent-default.directive'; import { DynamicLinks } from './dynamic-links'; +import { ClickOutsideDirective } from './click-outside.directive'; +import { BlockCopyPasteDirective } from './block-copypaste.directive'; -export { PreventDefaultDirective, DynamicLinks }; +export { PreventDefaultDirective, DynamicLinks, ClickOutsideDirective, BlockCopyPasteDirective }; // tslint:disable-next-line:variable-name export const SharedDirectives = [ PreventDefaultDirective, DynamicLinks, + ClickOutsideDirective, + BlockCopyPasteDirective, ]; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 7534c324adaf40ea2ba484d91de1cc31d6ef667a..40f7139cbc7993534c5baa6c2402fe61de090034 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -23,6 +23,10 @@ export const environment = { url: servicesProxyUrl + '/backend', }, + emailService: { + url: 'https://kong.alpha.grandlyon.com/email', + }, + // Path to the built app in a particular language angularAppHost: { fr: '/fr', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index f32fb6a47b50e6cd4d6aa32e2f22449193910f1b..a5259414d400596e255d2fb252b08dd87513f173 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -11,7 +11,7 @@ export const environment = { // ElasticSearch elasticsearchUrl: { - full: servicesProxyUrl + '/elasticsearch2/test-all-in-one-index.full.v8.quadtree', + full: servicesProxyUrl + '/elasticsearch2/test-all-in-one-index.full.v9.quadtree', meta: servicesProxyUrl + '/elasticsearch/*.meta', }, @@ -23,6 +23,10 @@ export const environment = { url: 'http://localhost:3000', }, + emailService: { + url: 'https://kong.alpha.grandlyon.com/email', + }, + // Path to the built app in a particular language angularAppHost: { fr: '/fr', diff --git a/src/i18n/contact/contact.fr.ts b/src/i18n/contact/contact.fr.ts new file mode 100644 index 0000000000000000000000000000000000000000..8136b61273e65e81410a83fdf365546a93134fbd --- /dev/null +++ b/src/i18n/contact/contact.fr.ts @@ -0,0 +1,35 @@ +export const subjects = [ + { + key: 'licenseQuestion', + value: 'question sur les licences', + }, + { + key: 'technicalQuestion', + value: 'question technique', + }, + { + key: 'anomalyReport', + value: 'signalement d\'anomalie', + }, + { + key: 'contactRequest', + value: 'demande de contact', + }, + { + key: 'infoRequest', + value: 'demande d\'information', + }, + { + key: 'newDataRequest', + value: 'demande de nouvelle donnée', + }, + { + key: 'other', + value: 'autre', + }, +]; + +export const feedbackMessages = { + error: 'Désolé, nous n\'avons pas pu envoyer votre demande, veuillez réessayer ultérieurement', + success: 'Votre message a bien été envoyé', +}; diff --git a/src/i18n/contact/contact.ts b/src/i18n/contact/contact.ts new file mode 100644 index 0000000000000000000000000000000000000000..c340e7d73203c4ca57c922d316ae6041d33dbcfc --- /dev/null +++ b/src/i18n/contact/contact.ts @@ -0,0 +1,35 @@ +export const subjects = [ + { + key: 'licenseQuestion', + value: 'question about licences', + }, + { + key: 'technicalQuestion', + value: 'technical question', + }, + { + key: 'anomalyReport', + value: 'anomaly report', + }, + { + key: 'contactRequest', + value: 'request contact', + }, + { + key: 'infoRequest', + value: 'information request', + }, + { + key: 'newDataRequest', + value: 'request for new data', + }, + { + key: 'other', + value: 'other', + }, +]; + +export const feedbackMessages = { + error: 'Sorry, we couldn\'t send your message, please try again later', + success: 'Your message has been sent successfully.', +}; diff --git a/src/i18n/messages.en.xlf b/src/i18n/messages.en.xlf index ab6eba3dd56c666ec50c6a76f2091babc57e0a6b..69beaa12491b596941485abca3eb6a33f0072497 100644 --- a/src/i18n/messages.en.xlf +++ b/src/i18n/messages.en.xlf @@ -259,6 +259,14 @@ <source>Number of views (last 30 days)</source> <target>Number of views (last 30 days)</target> </trans-unit> + <trans-unit id="dataset.info.questions" datatype="html"> + <source>You have questions on this dataset, </source> + <target>You have questions on this dataset, </target> + </trans-unit> + <trans-unit id="dataset.info.contactus" datatype="html"> + <source>contact us.</source> + <target>contact us.</target> + </trans-unit> <trans-unit id="dataset.data.unavailableInfo" datatype="html"> <source>Unavailable information for this data</source> <target>Unavailable information for this data</target> @@ -287,6 +295,90 @@ <source>RESEARCH</source> <target>RESEARCH</target> </trans-unit> + <trans-unit id="contact.contactUs" datatype="html"> + <source>Contact us</source> + <target>Contact us</target> + </trans-unit> + <trans-unit id="contact.contactForm" datatype="html"> + <source>Form contact</source> + <target>Form contact</target> + </trans-unit> + <trans-unit id="contact.identity" datatype="html"> + <source>Identity</source> + <target>Identity</target> + </trans-unit> + <trans-unit id="contact.lastname" datatype="html"> + <source>Lastname</source> + <target>Lastname</target> + </trans-unit> + <trans-unit id="contact.errors.missingLastname" datatype="html"> + <source>You must indicate your lastname.</source> + <target>You must indicate your lastname.</target> + </trans-unit> + <trans-unit id="contact.errors.forbiddenCharacters" datatype="html"> + <source>Special characters are forbidden.</source> + <target>Special characters are forbidden.</target> + </trans-unit> + <trans-unit id="contact.firstname" datatype="html"> + <source>Firstname</source> + <target>Firstname</target> + </trans-unit> + <trans-unit id="contact.errors.missingFirstname" datatype="html"> + <source>You must indicate a firstname.</source> + <target>You must indicate a firstname.</target> + </trans-unit> + <trans-unit id="contact.email" datatype="html"> + <source>Email</source> + <target>Email</target> + </trans-unit> + <trans-unit id="contact.errors.missingEmail" datatype="html"> + <source>You must enter an email address.</source> + <target>You must enter an email address.</target> + </trans-unit> + <trans-unit id="contact.errors.invalidEmail" datatype="html"> + <source>You must enter a valid email address.</source> + <target>You must enter a valid email address.</target> + </trans-unit> + <trans-unit id="contact.emailConfirmation" datatype="html"> + <source>Confirm your email address</source> + <target>Confirm your email address</target> + </trans-unit> + <trans-unit id="contact.errors.missingConfirmationEmail" datatype="html"> + <source>You must confirm your email address.</source> + <target>You must confirm your email address.</target> + </trans-unit> + <trans-unit id="contact.errors.confirmationEmailNotCorresponding" datatype="html"> + <source>You must enter the same email address.</source> + <target>You must enter the same email address.</target> + </trans-unit> + <trans-unit id="contact.message" datatype="html"> + <source>Message</source> + <target>Message</target> + </trans-unit> + <trans-unit id="contact.subject" datatype="html"> + <source>Subject</source> + <target>Subject</target> + </trans-unit> + <trans-unit id="contact.errors.missingSubject" datatype="html"> + <source>You must enter a subject.</source> + <target>You must enter a subject.</target> + </trans-unit> + <trans-unit id="contact.messageField" datatype="html"> + <source>Message</source> + <target>Message</target> + </trans-unit> + <trans-unit id="contact.errors.invalidMessage" datatype="html"> + <source>You must enter a message.</source> + <target>You must enter a message.</target> + </trans-unit> + <trans-unit id="contact.send" datatype="html"> + <source>Send</source> + <target>Send</target> + </trans-unit> + <trans-unit id="contact.cancel" datatype="html"> + <source>Cancel</source> + <target>Cancel</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/i18n/messages.fr.xlf b/src/i18n/messages.fr.xlf index ace293798ac696b708bfa1c5bae25e643bbddfad..3aa38827300b53531b25d547d26f728350070947 100644 --- a/src/i18n/messages.fr.xlf +++ b/src/i18n/messages.fr.xlf @@ -267,6 +267,14 @@ <source>Number of views (last 30 days)</source> <target>Nombre de vues (30 derniers jours)</target> </trans-unit> + <trans-unit id="dataset.info.questions" datatype="html"> + <source>You have questions on this dataset, </source> + <target>Vous avez des questions sur ce jeu de données, </target> + </trans-unit> + <trans-unit id="dataset.info.contactus" datatype="html"> + <source>contact us.</source> + <target>contactez-nous.</target> + </trans-unit> <trans-unit id="dataset.data.unavailableInfo" datatype="html"> <source>Unavailable information for this data</source> <target>Information non disponible pour cette donnée</target> @@ -295,6 +303,90 @@ <source>RESEARCH</source> <target>RECHERCHER</target> </trans-unit> + <trans-unit id="contact.contactUs" datatype="html"> + <source>Contact us</source> + <target>Contactez nous</target> + </trans-unit> + <trans-unit id="contact.contactForm" datatype="html"> + <source>Form contact</source> + <target>Formulaire de contact</target> + </trans-unit> + <trans-unit id="contact.identity" datatype="html"> + <source>Identity</source> + <target>Identité</target> + </trans-unit> + <trans-unit id="contact.lastname" datatype="html"> + <source>Lastname</source> + <target>Nom de famille</target> + </trans-unit> + <trans-unit id="contact.errors.missingLastname" datatype="html"> + <source>You must indicate your lastname.</source> + <target>Veuillez indiquer votre nom de famille.</target> + </trans-unit> + <trans-unit id="contact.errors.forbiddenCharacters" datatype="html"> + <source>Special characters are forbidden.</source> + <target>Les caractères spéciaux sont interdits.</target> + </trans-unit> + <trans-unit id="contact.firstname" datatype="html"> + <source>Firstname</source> + <target>Prénom</target> + </trans-unit> + <trans-unit id="contact.errors.missingFirstname" datatype="html"> + <source>You must indicate a firstname.</source> + <target>Veuillez indiquer votre prénom.</target> + </trans-unit> + <trans-unit id="contact.email" datatype="html"> + <source>Email</source> + <target>Email</target> + </trans-unit> + <trans-unit id="contact.errors.missingEmail" datatype="html"> + <source>You must enter an email address.</source> + <target>Veuillez saisir votre adresse email.</target> + </trans-unit> + <trans-unit id="contact.errors.invalidEmail" datatype="html"> + <source>You must enter a valid email address.</source> + <target>Veuillez saisir une adresse email valide.</target> + </trans-unit> + <trans-unit id="contact.emailConfirmation" datatype="html"> + <source>Confirm your email address</source> + <target>Confirmez votre adresse email.</target> + </trans-unit> + <trans-unit id="contact.errors.missingConfirmationEmail" datatype="html"> + <source>You must confirm your email address.</source> + <target>Veuillez confirmer votre adresse email.</target> + </trans-unit> + <trans-unit id="contact.errors.confirmationEmailNotCorresponding" datatype="html"> + <source>You must enter the same email address.</source> + <target>La confirmation de l'email ne correspond pas à l'adresse email saisie.</target> + </trans-unit> + <trans-unit id="contact.message" datatype="html"> + <source>Message</source> + <target>Message</target> + </trans-unit> + <trans-unit id="contact.subject" datatype="html"> + <source>Subject</source> + <target>Sujet</target> + </trans-unit> + <trans-unit id="contact.errors.missingSubject" datatype="html"> + <source>You must enter a subject.</source> + <target>Veuillez saisir un sujet.</target> + </trans-unit> + <trans-unit id="contact.messageField" datatype="html"> + <source>Message</source> + <target>Message</target> + </trans-unit> + <trans-unit id="contact.errors.invalidMessage" datatype="html"> + <source>You must enter a message.</source> + <target>Veuillez saisir votre message.</target> + </trans-unit> + <trans-unit id="contact.send" datatype="html"> + <source>Send</source> + <target>Envoyer</target> + </trans-unit> + <trans-unit id="contact.cancel" datatype="html"> + <source>Cancel</source> + <target>Annuler</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/scss/variables.scss b/src/scss/variables.scss index 2521a1cbfe00bb2b96dff82a3b5bf0aa5fcbfc10..43592a0686a262a0ca66325723bbbccf2058bd87 100644 --- a/src/scss/variables.scss +++ b/src/scss/variables.scss @@ -3,7 +3,8 @@ // 2. Set your own initial variables $main: #17252b; $red: #d5232a; -$green: #04BA5B ; +$green: #04BA5B; +$dark-blue: #333745; $app-background-color: rgb(252, 252, 252); diff --git a/src/styles.scss b/src/styles.scss index 8b2428c13aa347b7750472db023c22e8b2450e01..96eed87e5d9cf812640d63328e850eedecee8700 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -154,4 +154,32 @@ a:hover { } } +.button-gl { + background: linear-gradient(to bottom, #ff6459, $red); + border-radius: 2px; + border-width: 0; + font-size: $size-6; + color: white; + text-transform: uppercase; + &:hover, &:focus { + color:white; + background: $red; + } + &.is-outlined { + border: 2px solid $dark-blue; + font-weight: bold; + color: $dark-blue; + background: transparent; + + &:hover, &:focus { + border-color: $red; + } + } +} + +.link-red { + color: $red; + text-decoration: underline; +} + @import "./scss/wordpress-style"; \ No newline at end of file