feat(frontend): design système - tokens, composants ui et restylage des pages

Centralise les couleurs/rayons/espacements dispersés en dur dans chaque page
(login, change-password, dashboard) en tokens CSS partagés, ajoute un petit
set de composants standalone réutilisables (ev-button, ev-card, ev-alert,
ev-badge) et intègre le logo EnerVision en en-tête des pages ainsi que dans
Swagger/ReDoc côté backend.

Refs #91
This commit is contained in:
Johan LEROY
2026-09-17 11:14:02 +02:00
parent 3692d486c6
commit 9c78c6dc38
35 changed files with 724 additions and 288 deletions
@@ -1,31 +1,36 @@
<div class="auth-page">
<form class="auth-card" [formGroup]="form" (ngSubmit)="onSubmit()">
<h1>Nouveau mot de passe</h1>
<p class="auth-subtitle">Votre mot de passe est provisoire, vous devez le modifier avant de continuer</p>
<form class="auth-card-wrapper" [formGroup]="form" (ngSubmit)="onSubmit()">
<ev-card>
<img src="logo.png" alt="EnerVision" class="auth-logo" />
<h1>Nouveau mot de passe</h1>
<p class="auth-subtitle">Votre mot de passe est provisoire, vous devez le modifier avant de continuer</p>
<label for="current_password">Mot de passe actuel</label>
<input
id="current_password"
type="password"
formControlName="current_password"
autocomplete="current-password"
/>
<label class="form-label" for="current_password">Mot de passe actuel</label>
<input
id="current_password"
class="form-input"
type="password"
formControlName="current_password"
autocomplete="current-password"
/>
<label for="new_password">Nouveau mot de passe</label>
<input
id="new_password"
type="password"
formControlName="new_password"
autocomplete="new-password"
/>
<span class="auth-hint">12 à 128 caractères</span>
<label class="form-label" for="new_password">Nouveau mot de passe</label>
<input
id="new_password"
class="form-input"
type="password"
formControlName="new_password"
autocomplete="new-password"
/>
<span class="form-hint">12 à 128 caractères</span>
@if (errorMessage()) {
<p class="auth-error">{{ errorMessage() }}</p>
}
@if (errorMessage()) {
<ev-alert severity="danger">{{ errorMessage() }}</ev-alert>
}
<button type="submit" [disabled]="form.invalid || isLoading()">
{{ isLoading() ? 'Modification...' : 'Valider' }}
</button>
<ev-button type="submit" [disabled]="form.invalid || isLoading()">
{{ isLoading() ? 'Modification...' : 'Valider' }}
</ev-button>
</ev-card>
</form>
</div>
@@ -3,86 +3,42 @@
align-items: center;
justify-content: center;
min-height: 100vh;
background: #f3f4f6;
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--color-bg);
}
.auth-card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 2.5rem;
.auth-card-wrapper {
width: 100%;
max-width: 360px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
}
.auth-subtitle {
margin: 0.25rem 0 1.5rem;
color: #6b7280;
font-size: 0.9rem;
line-height: 1.4;
}
label {
font-size: 0.85rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.35rem;
margin-top: 1rem;
}
input {
padding: 0.6rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 0.95rem;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
}
button {
margin-top: 1.5rem;
padding: 0.7rem;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
&:disabled {
background: #9ca3af;
cursor: not-allowed;
}
&:not(:disabled):hover {
background: #2563eb;
}
}
}
.auth-hint {
font-size: 0.75rem;
color: #9ca3af;
margin-top: 0.25rem;
.auth-logo {
display: block;
height: 48px;
margin: 0 auto 1rem;
}
.auth-error {
margin: 0.75rem 0 0;
color: #dc2626;
font-size: 0.85rem;
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text);
text-align: center;
}
.auth-subtitle {
margin: 0.25rem 0 1.5rem;
color: var(--color-text-muted);
font-size: 0.9rem;
line-height: 1.4;
text-align: center;
}
ev-alert {
display: block;
margin-top: 0.75rem;
}
ev-button {
display: block;
margin-top: 1.5rem;
}
@@ -54,7 +54,7 @@ describe('ChangePassword', () => {
fixture.detectChanges(); // rend le bloc @if (errorMessage())
expect(component.errorMessage()).toContain('incorrect');
const errorEl = fixture.nativeElement.querySelector('.auth-error');
const errorEl = fixture.nativeElement.querySelector('.ev-alert');
expect(errorEl?.textContent).toContain('incorrect');
});
@@ -64,7 +64,7 @@ describe('ChangePassword', () => {
const button = fixture.nativeElement.querySelector('button[type="submit"]');
expect(button.disabled).toBe(true);
expect(fixture.nativeElement.querySelector('.auth-error')).toBeNull();
expect(fixture.nativeElement.querySelector('.ev-alert')).toBeNull();
});
it('déclenche onSubmit via la soumission réelle du formulaire (ngSubmit)', () => {
@@ -2,11 +2,14 @@ import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../../../core/services/auth.service';
import { Button } from '../../../shared/components/ui/button/button';
import { Card } from '../../../shared/components/ui/card/card';
import { Alert } from '../../../shared/components/ui/alert/alert';
@Component({
selector: 'app-change-password',
standalone: true,
imports: [ReactiveFormsModule],
imports: [ReactiveFormsModule, Button, Card, Alert],
templateUrl: './change-password.html',
styleUrl: './change-password.scss',
})
@@ -1,36 +1,41 @@
<div class="auth-page">
<form class="auth-card" [formGroup]="form" (ngSubmit)="onSubmit()">
<h1>Connexion</h1>
<p class="auth-subtitle">Accédez à votre espace EnerVision</p>
<form class="auth-card-wrapper" [formGroup]="form" (ngSubmit)="onSubmit()">
<ev-card>
<img src="logo.png" alt="EnerVision" class="auth-logo" />
<h1>Connexion</h1>
<p class="auth-subtitle">Accédez à votre espace EnerVision</p>
<label for="email">Email</label>
<input
id="email"
type="email"
formControlName="email"
autocomplete="username"
placeholder="vous@enervision.fr"
/>
<label class="form-label" for="email">Email</label>
<input
id="email"
class="form-input"
type="email"
formControlName="email"
autocomplete="username"
placeholder="vous@enervision.fr"
/>
<label for="password">Mot de passe</label>
<input
id="password"
type="password"
formControlName="password"
autocomplete="current-password"
/>
<label class="form-label" for="password">Mot de passe</label>
<input
id="password"
class="form-input"
type="password"
formControlName="password"
autocomplete="current-password"
/>
@if (errorMessage()) {
<p class="auth-error">
{{ errorMessage() }}
@if (retryAfterSeconds(); as seconds) {
(réessayez dans {{ seconds }}s)
}
</p>
}
@if (errorMessage()) {
<ev-alert severity="danger">
{{ errorMessage() }}
@if (retryAfterSeconds(); as seconds) {
(réessayez dans {{ seconds }}s)
}
</ev-alert>
}
<button type="submit" [disabled]="form.invalid || isLoading()">
{{ isLoading() ? 'Connexion...' : 'Se connecter' }}
</button>
<ev-button type="submit" [disabled]="form.invalid || isLoading()">
{{ isLoading() ? 'Connexion...' : 'Se connecter' }}
</ev-button>
</ev-card>
</form>
</div>
@@ -3,79 +3,41 @@
align-items: center;
justify-content: center;
min-height: 100vh;
background: #f3f4f6;
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--color-bg);
}
.auth-card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 2.5rem;
.auth-card-wrapper {
width: 100%;
max-width: 360px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
}
.auth-subtitle {
margin: 0.25rem 0 1.5rem;
color: #6b7280;
font-size: 0.9rem;
}
label {
font-size: 0.85rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.35rem;
margin-top: 1rem;
}
input {
padding: 0.6rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 0.95rem;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
}
button {
margin-top: 1.5rem;
padding: 0.7rem;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
&:disabled {
background: #9ca3af;
cursor: not-allowed;
}
&:not(:disabled):hover {
background: #2563eb;
}
}
}
.auth-error {
margin: 0.75rem 0 0;
color: #dc2626;
font-size: 0.85rem;
.auth-logo {
display: block;
height: 48px;
margin: 0 auto 1rem;
}
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text);
text-align: center;
}
.auth-subtitle {
margin: 0.25rem 0 1.5rem;
color: var(--color-text-muted);
font-size: 0.9rem;
text-align: center;
}
ev-alert {
display: block;
margin-top: 0.75rem;
}
ev-button {
display: block;
margin-top: 1.5rem;
}
@@ -63,7 +63,7 @@ describe('Login', () => {
fixture.detectChanges(); // rend le bloc @if (errorMessage()) du template
expect(component.errorMessage()).toBe('Email ou mot de passe incorrect.');
const errorEl = fixture.nativeElement.querySelector('.auth-error');
const errorEl = fixture.nativeElement.querySelector('.ev-alert');
expect(errorEl?.textContent).toContain('Email ou mot de passe incorrect.');
});
@@ -80,7 +80,7 @@ describe('Login', () => {
fixture.detectChanges(); // rend aussi le sous-bloc @if (retryAfterSeconds(); as seconds)
expect(component.retryAfterSeconds()).toBe(30);
const errorEl = fixture.nativeElement.querySelector('.auth-error');
const errorEl = fixture.nativeElement.querySelector('.ev-alert');
expect(errorEl?.textContent).toContain('30s');
});
@@ -90,7 +90,7 @@ describe('Login', () => {
const button = fixture.nativeElement.querySelector('button[type="submit"]');
expect(button.disabled).toBe(true);
expect(fixture.nativeElement.querySelector('.auth-error')).toBeNull();
expect(fixture.nativeElement.querySelector('.ev-alert')).toBeNull();
});
it('déclenche onSubmit via la soumission réelle du formulaire (ngSubmit)', () => {
@@ -3,11 +3,14 @@ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '../../../core/services/auth.service';
import { Button } from '../../../shared/components/ui/button/button';
import { Card } from '../../../shared/components/ui/card/card';
import { Alert } from '../../../shared/components/ui/alert/alert';
@Component({
selector: 'app-login',
standalone: true,
imports: [ReactiveFormsModule],
imports: [ReactiveFormsModule, Button, Card, Alert],
templateUrl: './login.html',
styleUrl: './login.scss',
})