Parcourir la source

ajout de la page d'inscription et globalisation du css entre login et register

master
trauchessec il y a 2 semaines
Parent
révision
a1250d7600
9 fichiers modifiés avec 909 ajouts et 430 suppressions
  1. +0
    -430
      src/app/pages/login/login.scss
  2. +197
    -0
      src/app/pages/register/register.component.html
  3. +123
    -0
      src/app/pages/register/register.component.spec.ts
  4. +81
    -0
      src/app/pages/register/register.component.ts
  5. +8
    -0
      src/app/pages/register/register.model.ts
  6. +9
    -0
      src/app/pages/register/register.route.ts
  7. +49
    -0
      src/app/pages/register/register.service.spec.ts
  8. +16
    -0
      src/app/pages/register/register.service.ts
  9. +426
    -0
      src/styles.scss

+ 0
- 430
src/app/pages/login/login.scss Voir le fichier

@@ -1,430 +0,0 @@
// auth-page.scss - Fichier partagé pour login, register et forgot-password

.auth-page-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #243b5d 0%, #2984a1 100%);
padding: 20px;
position: relative;
overflow: hidden;



.auth-card {
position: relative;
z-index: 1;
width: 100%;
max-width: 440px;
background: #ffffff;
border-radius: 24px;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.3),
0 0 100px rgba(102, 126, 234, 0.2);
overflow: hidden;
animation: fadeInUp 0.6s ease;

.auth-header {
padding: 48px 40px 32px;
text-align: center;
background: linear-gradient(180deg, #f9fafb 0%, #ffffff 100%);

.logo {
display: inline-block;
font-size: 32px;
font-weight: 800;
color: #2984a1;
margin-bottom: 24px;
letter-spacing: -1px;
}

.auth-title {
font-size: 28px;
font-weight: 700;
color: #1f2937;
margin: 0 0 12px 0;
line-height: 1.2;
}

.auth-subtitle {
font-size: 15px;
color: #6b7280;
margin: 0;
font-weight: 400;
line-height: 1.5;
}
}

.auth-content {
padding: 32px 40px 40px;

.auth-form {
display: flex;
flex-direction: column;
gap: 24px;

.form-field {
display: flex;
flex-direction: column;
gap: 8px;

.form-label {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0;
}

.input-wrapper {
position: relative;
display: flex;
align-items: center;

.form-input {
width: 100%;
padding: 14px 16px;
padding-right: 48px;
font-size: 15px;
color: #1f2937;
background: #f9fafb;
border: 2px solid #e5e7eb;
border-radius: 12px;
transition: all 0.2s ease;
outline: none;

&::placeholder {
color: #9ca3af;
}

&:focus {
background: #ffffff;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

&.error {
border-color: #ef4444;
background: #fef2f2;

&:focus {
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
}
}

.input-icon {
position: absolute;
right: 16px;
color: #9ca3af;
pointer-events: none;
transition: color 0.2s ease;

&.clickable {
pointer-events: all;
cursor: pointer;

&:hover {
color: #667eea;
}
}
}

.form-input:focus ~ .input-icon {
color: #667eea;
}
}

.error-message {
font-size: 13px;
color: #ef4444;
margin: 0;
padding-left: 4px;
}

.success-message {
font-size: 13px;
color: #10b981;
margin: 0;
padding-left: 4px;
}

.info-message {
font-size: 13px;
color: #6b7280;
margin: 0;
padding-left: 4px;
}
}

.form-options {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;

.checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;

input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}

.checkbox-label {
font-size: 14px;
color: #6b7280;
user-select: none;
}
}

.forgot-password-link {
font-size: 14px;
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;

&:hover {
color: #5568d3;
text-decoration: underline;
}
}
}

.auth-btn {
width: 100%;
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff;
background: linear-gradient(-45deg, #243b5d 0%, #2984a1 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
margin-top: 8px;

&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}

&:active:not(:disabled) {
transform: translateY(0);
}

&:disabled {
opacity: 0.6;
cursor: not-allowed;
}

span {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
}

.divider {
display: flex;
align-items: center;
text-align: center;
margin: 8px 0;

&::before,
&::after {
content: '';
flex: 1;
border-bottom: 1px solid #e5e7eb;
}

span {
padding: 0 16px;
font-size: 14px;
color: #9ca3af;
font-weight: 500;
}
}

.social-buttons {
display: flex;
gap: 12px;

.social-btn {
flex: 1;
padding: 12px;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #374151;

&:hover {
border-color: #667eea;
background: #f9fafb;
}

img, svg {
width: 20px;
height: 20px;
}
}
}
}
}

.auth-footer {
padding: 24px 40px;
text-align: center;
background: #f9fafb;
border-top: 1px solid #e5e7eb;

p {
margin: 0;
font-size: 14px;
color: #6b7280;
}

.register-link,
.login-link,
.back-to-login-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
transition: color 0.2s ease;

&:hover {
color: #5568d3;
text-decoration: underline;
}
}
}
}
}

// Animations
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes backgroundMove {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
}
33% {
transform: translate(5%, -5%) rotate(5deg);
}
66% {
transform: translate(-5%, 5%) rotate(-5deg);
}
}

// Responsive
@media (max-width: 640px) {
.auth-page-container {
padding: 16px;

.auth-card {
max-width: 100%;
border-radius: 20px;

.auth-header {
padding: 32px 24px 24px;

.logo {
font-size: 28px;
margin-bottom: 20px;
}

.auth-title {
font-size: 24px;
}

.auth-subtitle {
font-size: 14px;
}
}

.auth-content {
padding: 24px;

.auth-form {
gap: 20px;

.form-field {
.input-wrapper {
.form-input {
padding: 12px 14px;
padding-right: 44px;
font-size: 14px;
}
}
}

.form-options {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}

.auth-btn {
padding: 12px 20px;
font-size: 15px;
}

.social-buttons {
flex-direction: column;

.social-btn {
width: 100%;
}
}
}
}

.auth-footer {
padding: 20px 24px;
}
}
}
}

// Variantes spécifiques pour chaque page
.auth-page-container {
// Page Register - carte plus haute
&.register-page {
.auth-card {
max-width: 480px;
}
}

// Page Forgot Password - carte plus petite
&.forgot-password-page {
.auth-card {
max-width: 400px;
}
}
}

+ 197
- 0
src/app/pages/register/register.component.html Voir le fichier

@@ -0,0 +1,197 @@
<div class="auth-page-container register-page">
<div class="auth-card">
<div class="auth-header">
<div class="logo">AIT</div>
<h1 class="auth-title">Créer un compte</h1>
<p class="auth-subtitle">Rejoignez-nous dès maintenant et commencez votre aventure.</p>
</div>

<div class="auth-content">
<!-- Success Message -->
<div class="alert alert-success" *ngIf="success()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<div>
<strong>Inscription réussie !</strong>
<p>Un email de confirmation vous a été envoyé. Veuillez vérifier votre boîte de réception.</p>
</div>
</div>

<!-- Error Messages -->
<div class="alert alert-danger" *ngIf="error()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<p>Une erreur s'est produite lors de l'inscription. Veuillez réessayer.</p>
</div>

<div class="alert alert-danger" *ngIf="errorUserExists()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<p>Ce nom d'utilisateur est déjà utilisé. Veuillez en choisir un autre.</p>
</div>

<div class="alert alert-danger" *ngIf="errorEmailExists()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<p>Cette adresse email est déjà utilisée. Veuillez vous connecter ou utiliser une autre adresse.</p>
</div>

<div class="alert alert-danger" *ngIf="doNotMatch()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<p>Les mots de passe ne correspondent pas.</p>
</div>

<form [formGroup]="registerForm" (ngSubmit)="register()" class="auth-form" *ngIf="!success()">

<!-- Login/Username Field -->
<div class="form-field">
<label for="login" class="form-label">Nom d'utilisateur</label>
<div class="input-wrapper">
<input
#login
id="login"
type="text"
formControlName="login"
class="form-input"
placeholder="Votre nom d'utilisateur"
[class.error]="registerForm.get('login')?.invalid && registerForm.get('login')?.touched"
>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<span class="error-message" *ngIf="registerForm.get('login')?.hasError('required') && registerForm.get('login')?.touched">
Le nom d'utilisateur est requis.
</span>
<span class="error-message" *ngIf="registerForm.get('login')?.hasError('minlength') && registerForm.get('login')?.touched">
Le nom d'utilisateur doit contenir au moins 1 caractère.
</span>
<span class="error-message" *ngIf="registerForm.get('login')?.hasError('maxlength') && registerForm.get('login')?.touched">
Le nom d'utilisateur ne peut pas dépasser 50 caractères.
</span>
<span class="error-message" *ngIf="registerForm.get('login')?.hasError('pattern') && registerForm.get('login')?.touched">
Le nom d'utilisateur contient des caractères invalides.
</span>
</div>

<!-- Email Field -->
<div class="form-field">
<label for="email" class="form-label">Email</label>
<div class="input-wrapper">
<input
id="email"
type="email"
formControlName="email"
class="form-input"
placeholder="votre.email@exemple.com"
[class.error]="registerForm.get('email')?.invalid && registerForm.get('email')?.touched"
>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</div>
<span class="error-message" *ngIf="registerForm.get('email')?.hasError('required') && registerForm.get('email')?.touched">
L'adresse email est requise.
</span>
<span class="error-message" *ngIf="registerForm.get('email')?.hasError('email') && registerForm.get('email')?.touched">
Veuillez entrer une adresse email valide.
</span>
<span class="error-message" *ngIf="registerForm.get('email')?.hasError('minlength') && registerForm.get('email')?.touched">
L'email doit contenir au moins 5 caractères.
</span>
<span class="error-message" *ngIf="registerForm.get('email')?.hasError('maxlength') && registerForm.get('email')?.touched">
L'email ne peut pas dépasser 254 caractères.
</span>
</div>

<!-- Password Field -->
<div class="form-field">
<label for="password" class="form-label">Mot de passe</label>
<div class="input-wrapper">
<input
id="password"
type="password"
formControlName="password"
class="form-input"
placeholder="••••••••"
[class.error]="registerForm.get('password')?.invalid && registerForm.get('password')?.touched"
>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<span class="error-message" *ngIf="registerForm.get('password')?.hasError('required') && registerForm.get('password')?.touched">
Le mot de passe est requis.
</span>
<span class="error-message" *ngIf="registerForm.get('password')?.hasError('minlength') && registerForm.get('password')?.touched">
Le mot de passe doit contenir au moins 4 caractères.
</span>
<span class="error-message" *ngIf="registerForm.get('password')?.hasError('maxlength') && registerForm.get('password')?.touched">
Le mot de passe ne peut pas dépasser 50 caractères.
</span>

</div>

<!-- Confirm Password Field -->
<div class="form-field">
<label for="confirmPassword" class="form-label">Confirmer le mot de passe</label>
<div class="input-wrapper">
<input
id="confirmPassword"
type="password"
formControlName="confirmPassword"
class="form-input"
placeholder="••••••••"
[class.error]="registerForm.get('confirmPassword')?.invalid && registerForm.get('confirmPassword')?.touched"
>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<span class="error-message" *ngIf="registerForm.get('confirmPassword')?.hasError('required') && registerForm.get('confirmPassword')?.touched">
La confirmation du mot de passe est requise.
</span>
<span class="error-message" *ngIf="registerForm.get('confirmPassword')?.hasError('minlength') && registerForm.get('confirmPassword')?.touched">
La confirmation doit contenir au moins 4 caractères.
</span>
</div>

<!-- Submit Button -->
<button
type="submit"
[disabled]="registerForm.invalid"
class="auth-btn"
>
<span>Créer mon compte</span>
</button>


</form>

</div>

<!-- Footer -->
<div class="auth-footer">
<p>Vous avez déjà un compte ? <a routerLink="/login" class="login-link">Se connecter</a></p>
</div>
</div>
</div>

+ 123
- 0
src/app/pages/register/register.component.spec.ts Voir le fichier

@@ -0,0 +1,123 @@
import { ComponentFixture, TestBed, fakeAsync, inject, tick, waitForAsync } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { FormBuilder } from '@angular/forms';
import { of, throwError } from 'rxjs';
import { TranslateModule, TranslateService } from '@ngx-translate/core';

import { EMAIL_ALREADY_USED_TYPE, LOGIN_ALREADY_USED_TYPE } from 'app/config/error.constants';

import { RegisterService } from './register.service';
import RegisterComponent from './register.component';

describe('RegisterComponent', () => {
let fixture: ComponentFixture<RegisterComponent>;
let comp: RegisterComponent;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RegisterComponent],
providers: [provideHttpClient(), FormBuilder],
})
.overrideTemplate(RegisterComponent, '')
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(RegisterComponent);
comp = fixture.componentInstance;
});

it('should ensure the two passwords entered match', () => {
comp.registerForm.patchValue({
password: 'password',
confirmPassword: 'non-matching',
});

comp.register();

expect(comp.doNotMatch()).toBe(true);
});

it('should update success to true after creating an account', inject(
[RegisterService, TranslateService],
fakeAsync((service: RegisterService, mockTranslateService: TranslateService) => {
jest.spyOn(service, 'save').mockReturnValue(of({}));
mockTranslateService.currentLang = 'fr';
comp.registerForm.patchValue({
password: 'password',
confirmPassword: 'password',
});

comp.register();
tick();

expect(service.save).toHaveBeenCalledWith({
email: '',
password: 'password',
login: '',
langKey: 'fr',
});
expect(comp.success()).toBe(true);
expect(comp.errorUserExists()).toBe(false);
expect(comp.errorEmailExists()).toBe(false);
expect(comp.error()).toBe(false);
}),
));

it('should notify of user existence upon 400/login already in use', inject(
[RegisterService],
fakeAsync((service: RegisterService) => {
const err = { status: 400, error: { type: LOGIN_ALREADY_USED_TYPE } };
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
comp.registerForm.patchValue({
password: 'password',
confirmPassword: 'password',
});

comp.register();
tick();

expect(comp.errorUserExists()).toBe(true);
expect(comp.errorEmailExists()).toBe(false);
expect(comp.error()).toBe(false);
}),
));

it('should notify of email existence upon 400/email address already in use', inject(
[RegisterService],
fakeAsync((service: RegisterService) => {
const err = { status: 400, error: { type: EMAIL_ALREADY_USED_TYPE } };
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
comp.registerForm.patchValue({
password: 'password',
confirmPassword: 'password',
});

comp.register();
tick();

expect(comp.errorEmailExists()).toBe(true);
expect(comp.errorUserExists()).toBe(false);
expect(comp.error()).toBe(false);
}),
));

it('should notify of generic error', inject(
[RegisterService],
fakeAsync((service: RegisterService) => {
const err = { status: 503 };
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
comp.registerForm.patchValue({
password: 'password',
confirmPassword: 'password',
});

comp.register();
tick();

expect(comp.errorUserExists()).toBe(false);
expect(comp.errorEmailExists()).toBe(false);
expect(comp.error()).toBe(true);
}),
));
});

+ 81
- 0
src/app/pages/register/register.component.ts Voir le fichier

@@ -0,0 +1,81 @@
import { AfterViewInit, Component, ElementRef, inject, signal, viewChild } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';


import { RegisterService } from './register.service';
import SharedModule from '../../shared/shared.module';
import {EMAIL_ALREADY_USED_TYPE, LOGIN_ALREADY_USED_TYPE} from '../../config/error.constants';

@Component({
selector: 'jhi-register',
imports: [SharedModule, RouterModule, FormsModule, ReactiveFormsModule],
templateUrl: './register.component.html',
})
export default class RegisterComponent implements AfterViewInit {
login = viewChild.required<ElementRef>('login');

doNotMatch = signal(false);
error = signal(false);
errorEmailExists = signal(false);
errorUserExists = signal(false);
success = signal(false);

registerForm = new FormGroup({
login: new FormControl('', {
nonNullable: true,
validators: [
Validators.required,
Validators.minLength(1),
Validators.maxLength(50),
Validators.pattern('^[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$|^[_.@A-Za-z0-9-]+$'),
],
}),
email: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email],
}),
password: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
}),
confirmPassword: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
}),
});

private readonly registerService = inject(RegisterService);

ngAfterViewInit(): void {
this.login().nativeElement.focus();
}

register(): void {
this.doNotMatch.set(false);
this.error.set(false);
this.errorEmailExists.set(false);
this.errorUserExists.set(false);

const { password, confirmPassword } = this.registerForm.getRawValue();
if (password !== confirmPassword) {
this.doNotMatch.set(true);
} else {
const { login, email } = this.registerForm.getRawValue();
this.registerService
.save({ login, email, password, langKey: 'fr' })
.subscribe({ next: () => this.success.set(true), error: response => this.processError(response) });
}
}

private processError(response: HttpErrorResponse): void {
if (response.status === 400 && response.error.type === LOGIN_ALREADY_USED_TYPE) {
this.errorUserExists.set(true);
} else if (response.status === 400 && response.error.type === EMAIL_ALREADY_USED_TYPE) {
this.errorEmailExists.set(true);
} else {
this.error.set(true);
}
}
}

+ 8
- 0
src/app/pages/register/register.model.ts Voir le fichier

@@ -0,0 +1,8 @@
export class Registration {
constructor(
public login: string,
public email: string,
public password: string,
public langKey: string,
) {}
}

+ 9
- 0
src/app/pages/register/register.route.ts Voir le fichier

@@ -0,0 +1,9 @@
import { Route } from '@angular/router';

import RegisterComponent from './register.component';

const registerRoute: Route = {

};

export default registerRoute;

+ 49
- 0
src/app/pages/register/register.service.spec.ts Voir le fichier

@@ -0,0 +1,49 @@
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';

import { RegisterService } from './register.service';
import { Registration } from './register.model';
import {ApplicationConfigService} from '../../core/config/application-config.service';

describe('RegisterService Service', () => {
let service: RegisterService;
let httpMock: HttpTestingController;
let applicationConfigService: ApplicationConfigService;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});

service = TestBed.inject(RegisterService);
applicationConfigService = TestBed.inject(ApplicationConfigService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

describe('Service methods', () => {
it('should call register endpoint with correct values', () => {
// GIVEN
const login = 'abc';
const email = 'test@test.com';
const password = 'pass';
const langKey = 'FR';
const registration = new Registration(login, email, password, langKey);

// WHEN
service.save(registration).subscribe();

const testRequest = httpMock.expectOne({
method: 'POST',
url: applicationConfigService.getEndpointFor('api/register'),
});

// THEN
expect(testRequest.request.body).toEqual({ email, langKey, login, password });
});
});
});

+ 16
- 0
src/app/pages/register/register.service.ts Voir le fichier

@@ -0,0 +1,16 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

import { Registration } from './register.model';
import {ApplicationConfigService} from '../../core/config/application-config.service';

@Injectable({ providedIn: 'root' })
export class RegisterService {
private readonly http = inject(HttpClient);
private readonly applicationConfigService = inject(ApplicationConfigService);

save(registration: Registration): Observable<{}> {
return this.http.post(this.applicationConfigService.getEndpointFor('api/register'), registration);
}
}

+ 426
- 0
src/styles.scss Voir le fichier

@@ -68,3 +68,429 @@ body {
.p-2 { padding: 1rem; }
.p-3 { padding: 1.5rem; }
.p-4 { padding: 2rem; }


.auth-page-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #243b5d 0%, #2984a1 100%);
padding: 20px;
position: relative;
overflow: hidden;



.auth-card {
position: relative;
z-index: 1;
width: 100%;
max-width: 440px;
background: #ffffff;
border-radius: 24px;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.3),
0 0 100px rgba(102, 126, 234, 0.2);
overflow: hidden;
animation: fadeInUp 0.6s ease;

.auth-header {
padding: 48px 40px 32px;
text-align: center;
background: linear-gradient(180deg, #f9fafb 0%, #ffffff 100%);

.logo {
display: inline-block;
font-size: 32px;
font-weight: 800;
color: #2984a1;
margin-bottom: 24px;
letter-spacing: -1px;
}

.auth-title {
font-size: 28px;
font-weight: 700;
color: #1f2937;
margin: 0 0 12px 0;
line-height: 1.2;
}

.auth-subtitle {
font-size: 15px;
color: #6b7280;
margin: 0;
font-weight: 400;
line-height: 1.5;
}
}

.auth-content {
padding: 32px 40px 40px;

.auth-form {
display: flex;
flex-direction: column;
gap: 24px;

.form-field {
display: flex;
flex-direction: column;
gap: 8px;

.form-label {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0;
}

.input-wrapper {
position: relative;
display: flex;
align-items: center;

.form-input {
width: 100%;
padding: 14px 16px;
padding-right: 48px;
font-size: 15px;
color: #1f2937;
background: #f9fafb;
border: 2px solid #e5e7eb;
border-radius: 12px;
transition: all 0.2s ease;
outline: none;

&::placeholder {
color: #9ca3af;
}

&:focus {
background: #ffffff;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

&.error {
border-color: #ef4444;
background: #fef2f2;

&:focus {
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
}
}

.input-icon {
position: absolute;
right: 16px;
color: #9ca3af;
pointer-events: none;
transition: color 0.2s ease;

&.clickable {
pointer-events: all;
cursor: pointer;

&:hover {
color: #667eea;
}
}
}

.form-input:focus ~ .input-icon {
color: #667eea;
}
}

.error-message {
font-size: 13px;
color: #ef4444;
margin: 0;
padding-left: 4px;
}

.success-message {
font-size: 13px;
color: #10b981;
margin: 0;
padding-left: 4px;
}

.info-message {
font-size: 13px;
color: #6b7280;
margin: 0;
padding-left: 4px;
}
}

.form-options {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;

.checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;

input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}

.checkbox-label {
font-size: 14px;
color: #6b7280;
user-select: none;
}
}

.forgot-password-link {
font-size: 14px;
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;

&:hover {
color: #5568d3;
text-decoration: underline;
}
}
}

.auth-btn {
width: 100%;
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
color: #ffffff;
background: linear-gradient(-45deg, #243b5d 0%, #2984a1 100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
margin-top: 8px;

&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}

&:active:not(:disabled) {
transform: translateY(0);
}

&:disabled {
opacity: 0.6;
cursor: not-allowed;
}

span {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
}

.divider {
display: flex;
align-items: center;
text-align: center;
margin: 8px 0;

&::before,
&::after {
content: '';
flex: 1;
border-bottom: 1px solid #e5e7eb;
}

span {
padding: 0 16px;
font-size: 14px;
color: #9ca3af;
font-weight: 500;
}
}

.social-buttons {
display: flex;
gap: 12px;

.social-btn {
flex: 1;
padding: 12px;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #374151;

&:hover {
border-color: #667eea;
background: #f9fafb;
}

img, svg {
width: 20px;
height: 20px;
}
}
}
}
}

.auth-footer {
padding: 24px 40px;
text-align: center;
background: #f9fafb;
border-top: 1px solid #e5e7eb;

p {
margin: 0;
font-size: 14px;
color: #6b7280;
}

.register-link,
.login-link,
.back-to-login-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
transition: color 0.2s ease;

&:hover {
color: #5568d3;
text-decoration: underline;
}
}
}
}
}

// Animations
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes backgroundMove {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
}
33% {
transform: translate(5%, -5%) rotate(5deg);
}
66% {
transform: translate(-5%, 5%) rotate(-5deg);
}
}

@media (max-width: 640px) {
.auth-page-container {
padding: 16px;

.auth-card {
max-width: 100%;
border-radius: 20px;

.auth-header {
padding: 32px 24px 24px;

.logo {
font-size: 28px;
margin-bottom: 20px;
}

.auth-title {
font-size: 24px;
}

.auth-subtitle {
font-size: 14px;
}
}

.auth-content {
padding: 24px;

.auth-form {
gap: 20px;

.form-field {
.input-wrapper {
.form-input {
padding: 12px 14px;
padding-right: 44px;
font-size: 14px;
}
}
}

.form-options {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}

.auth-btn {
padding: 12px 20px;
font-size: 15px;
}

.social-buttons {
flex-direction: column;

.social-btn {
width: 100%;
}
}
}
}

.auth-footer {
padding: 20px 24px;
}
}
}
}

.auth-page-container {
&.register-page {
.auth-card {
max-width: 480px;
}
}

&.forgot-password-page {
.auth-card {
max-width: 400px;
}
}
}

Chargement…
Annuler
Enregistrer