add initial Angular components, services, and routing setup

This commit is contained in:
Vincent Guillet
2025-09-24 11:31:28 +02:00
parent dfb4ac302a
commit 18f0364e26
64 changed files with 15879 additions and 0 deletions

View File

@@ -0,0 +1 @@
<p>admin works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminComponent } from './admin.component';
describe('AdminComponent', () => {
let component: AdminComponent;
let fixture: ComponentFixture<AdminComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AdminComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-admin',
standalone: true,
imports: [],
templateUrl: './admin.component.html',
styleUrl: './admin.component.css'
})
export class AdminComponent {
}

View File

@@ -0,0 +1,13 @@
.home-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
text-align: center;
}
.home-actions {
margin-top: 2rem;
display: flex;
justify-content: center;
gap: 1rem;
}

View File

@@ -0,0 +1,13 @@
<div class="home-container">
@if (getUser(); as user) {
<h1>Welcome, {{ user.firstName }}!</h1>
<p>What would you like to do today?</p>
} @else {
<h1>Welcome to the demo</h1>
<p>Create an account or sign in to get started.</p>
<div class="home-actions">
<button mat-flat-button [routerLink]="'/login'">Login</button>
<button mat-raised-button [routerLink]="'/register'">Sign Up</button>
</div>
}
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
import {Component, inject} from '@angular/core';
import {MatButton} from '@angular/material/button';
import {AuthService} from '../../services/auth/auth.service';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-home',
standalone: true,
imports: [
MatButton,
RouterLink
],
templateUrl: './home.component.html',
styleUrl: './home.component.css'
})
export class HomeComponent {
protected readonly authService: AuthService = inject(AuthService);
getUser() {
return this.authService.user();
}
}

View File

@@ -0,0 +1,20 @@
#container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
form {
background-color: white;
padding: 20px;
border-radius: 10px;
display: flex;
flex-direction: column;
width: 400px;
}
mat-error {
text-align: center;
}

View File

@@ -0,0 +1,17 @@
<div id="container">
<form (submit)="login()" [formGroup]="loginFormGroup">
<h3>Sign in</h3>
<mat-form-field>
<mat-label>Username</mat-label>
<input matInput formControlName="username">
</mat-form-field>
<mat-form-field>
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password">
</mat-form-field>
<button mat-flat-button [disabled]="loginFormGroup.invalid">Login</button>
@if (invalidCredentials) {
<mat-error>Invalid credentials</mat-error>
}
</form>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,65 @@
import {Component, inject, OnDestroy} from '@angular/core';
import {MatError, MatFormField, MatLabel} from '@angular/material/form-field';
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
import {AuthService} from '../../services/auth/auth.service';
import {Router} from '@angular/router';
import {Subscription} from 'rxjs';
import {Credentials} from '../../interfaces/credentials/credentials';
import {User} from '../../models/user/user.model';
import {MatInput} from '@angular/material/input';
import {MatButton} from '@angular/material/button';
@Component({
selector: 'app-login',
standalone: true,
imports: [
MatFormField,
MatLabel,
MatError,
ReactiveFormsModule,
MatInput,
MatButton
],
templateUrl: './login.component.html',
styleUrl: './login.component.css'
})
export class LoginComponent implements OnDestroy {
private readonly formBuilder: FormBuilder = inject(FormBuilder);
private readonly authService: AuthService = inject(AuthService);
private readonly router: Router = inject(Router);
private loginSubscription: Subscription | null = null;
loginFormGroup = this.formBuilder.group({
username: ['', [
Validators.required
]],
password: ['', [
Validators.required
]]
});
invalidCredentials = false;
ngOnDestroy() {
this.loginSubscription?.unsubscribe();
}
login() {
this.loginSubscription = this.authService.login(
this.loginFormGroup.value as Credentials).subscribe({
next: (result: User | null | undefined) => {
this.navigateHome();
},
error: (error) => {
console.log(error);
this.invalidCredentials = true;
}
});
}
navigateHome() {
this.router.navigate(['/home']).then();
}
}

View File

@@ -0,0 +1 @@
<p>not-found works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundComponent } from './not-found.component';
describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NotFoundComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NotFoundComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found',
standalone: true,
imports: [],
templateUrl: './not-found.component.html',
styleUrl: './not-found.component.css'
})
export class NotFoundComponent {
}

View File

@@ -0,0 +1,72 @@
:host {
display: flex;
justify-content: center;
align-items: center;
min-height: 50vh;
}
.profile-card {
max-width: 380px;
width: 100%;
margin: 0 auto;
padding: 2rem 1.5rem 1.5rem 1.5rem;
border-radius: 18px;
box-shadow: 0 4px 24px rgba(25, 118, 210, 0.08);
background: #fff;
}
.profile-avatar {
background: #1976d2;
color: #fff;
width: 64px;
height: 64px;
font-size: 48px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.15);
}
.profile-actions {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
mat-card-header {
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: 1rem;
}
mat-card-title {
font-size: 1.3rem;
font-weight: 600;
margin-top: 0.5rem;
}
mat-card-subtitle {
color: #888;
font-size: 1rem;
margin-bottom: 0.5rem;
}
mat-card-content p {
display: flex;
align-items: center;
gap: 10px;
margin: 1rem 0 0 0;
font-size: 1.05rem;
color: #444;
justify-content: center;
}
mat-card-actions {
display: flex;
justify-content: center;
margin-top: 1.5rem;
}

View File

@@ -0,0 +1,27 @@
@if (getUser(); as user) {
<mat-card class="profile-card">
<mat-card-header>
<div mat-card-avatar class="profile-avatar">
<mat-icon>account_circle</mat-icon>
</div>
<mat-card-title>{{ user.firstName }} {{ user.lastName }}</mat-card-title>
<mat-card-subtitle>{{ user.username }} ({{ user.role }})</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
<mat-icon>email</mat-icon>
{{ user.email }}
</p>
</mat-card-content>
<mat-card-actions class="profile-actions">
<button mat-raised-button color="primary">
<mat-icon>edit</mat-icon>
Edit profile
</button>
<button mat-raised-button color="warn" (click)="logout()">
<mat-icon>logout</mat-icon>
Logout
</button>
</mat-card-actions>
</mat-card>
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileComponent } from './profile.component';
describe('ProfileComponent', () => {
let component: ProfileComponent;
let fixture: ComponentFixture<ProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,51 @@
import {Component, inject} from '@angular/core';
import {
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatCardSubtitle,
MatCardTitle
} from '@angular/material/card';
import {MatIcon} from '@angular/material/icon';
import {MatButton} from '@angular/material/button';
import {AuthService} from '../../services/auth/auth.service';
import {User} from '../../models/user/user.model';
import {Router} from '@angular/router';
@Component({
selector: 'app-profile',
standalone: true,
imports: [
MatCard,
MatCardHeader,
MatIcon,
MatCardTitle,
MatCardSubtitle,
MatCardContent,
MatCardActions,
MatButton
],
templateUrl: './profile.component.html',
styleUrl: './profile.component.css'
})
export class ProfileComponent {
private readonly authService: AuthService = inject(AuthService);
private readonly router: Router = inject(Router);
logout() {
this.authService.logout().subscribe({
next: () => {
this.router.navigate(['/login'], {queryParams: {redirect: '/profile'}}).then();
},
error: (err) => {
console.error('Logout failed:', err);
}
});
}
getUser(): User | null {
return this.authService.user();
}
}

View File

@@ -0,0 +1,32 @@
.auth-wrap {
min-height: 100vh;
display: grid;
place-items: center;
padding: 16px;
}
.auth-card {
width: 100%;
max-width: 520px;
}
.form-grid {
display: grid;
gap: 16px;
margin-top: 16px;
}
.actions {
display: flex;
margin: 8px;
button {
display: inline-flex;
align-items: center;
gap: 8px;
}
}
.ml-8 {
margin-left: 8px;
}

View File

@@ -0,0 +1,137 @@
<section class="auth-wrap">
<mat-card class="auth-card">
<mat-card-header>
<mat-card-title>Registration</mat-card-title>
<mat-card-subtitle>Create a new account</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form [formGroup]="registerForm" (ngSubmit)="onRegister()" class="form-grid">
<!-- First Name -->
<mat-form-field appearance="outline">
<mat-label>First Name</mat-label>
<input matInput
id="firstName"
name="firstName"
formControlName="firstName"
type="text"
autocomplete="given-name"
required>
@if (isFieldInvalid('firstName')) {
<mat-error>{{ getFieldError('firstName') }}</mat-error>
}
</mat-form-field>
<!-- Last Name -->
<mat-form-field appearance="outline">
<mat-label>Last Name</mat-label>
<input matInput
id="lastName"
name="lastName"
formControlName="lastName"
type="text"
autocomplete="family-name"
required>
@if (isFieldInvalid('lastName')) {
<mat-error>{{ getFieldError('lastName') }}</mat-error>
}
</mat-form-field>
<!-- Username -->
<mat-form-field appearance="outline">
<mat-label>Username</mat-label>
<input matInput
id="username"
name="username"
formControlName="username"
type="text"
autocomplete="username"
required>
@if (isFieldInvalid('username')) {
<mat-error>{{ getFieldError('username') }}</mat-error>
}
</mat-form-field>
<!-- Email -->
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput
id="email"
name="email"
formControlName="email"
type="email"
autocomplete="email"
required>
@if (isFieldInvalid('email')) {
<mat-error>{{ getFieldError('email') }}</mat-error>
}
</mat-form-field>
<!-- Password -->
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input matInput
id="password"
name="password"
formControlName="password"
type="password"
autocomplete="new-password"
required>
@if (isFieldInvalid('password')) {
<mat-error>{{ getFieldError('password') }}</mat-error>
}
</mat-form-field>
<!-- Password confirmation -->
<mat-form-field appearance="outline">
<mat-label>Confirm Password</mat-label>
<input matInput
id="confirmPassword"
name="confirmPassword"
formControlName="confirmPassword"
type="password"
autocomplete="new-password"
required>
@if (isFieldInvalid('confirmPassword')) {
<mat-error>{{ getFieldError('confirmPassword') }}</mat-error>
}
</mat-form-field>
@if (registerForm.hasError('passwordMismatch') && (registerForm.dirty || registerForm.touched || isSubmitted)) {
<mat-error>Les mots de passe ne correspondent pas</mat-error>
}
<!-- Terms and Conditions -->
<mat-checkbox formControlName="termsAndConditions" id="iAgree">
I agree to the <a href="#" target="_blank" rel="noopener">terms and conditions</a>
</mat-checkbox>
@if (isFieldInvalid('termsAndConditions')) {
<div class="mat-caption mat-error">{{ getFieldError('termsAndConditions') }}</div>
}
<!-- Submit Button -->
<div class="actions">
<button mat-raised-button color="primary"
type="submit"
[disabled]="isLoading || registerForm.invalid">
@if (isLoading) {
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
<span class="ml-8">Signing up…</span>
} @else {
Sign up
}
</button>
</div>
</form>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions align="end">
<span class="mat-body-small">
Already have an account?
<a [routerLink]="'/login'">Sign in</a>
</span>
</mat-card-actions>
</mat-card>
</section>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegisterComponent } from './register.component';
describe('RegisterComponent', () => {
let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RegisterComponent]
})
.compileComponents();
fixture = TestBed.createComponent(RegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,157 @@
import {Component, inject, OnDestroy} from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormGroup,
ReactiveFormsModule,
ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
import {Router, RouterLink} from '@angular/router';
import {MatError, MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatCardSubtitle,
MatCardTitle
} from '@angular/material/card';
import {MatProgressSpinner} from '@angular/material/progress-spinner';
import {MatDivider} from '@angular/material/divider';
import {AuthService} from '../../services/auth/auth.service';
import {MatCheckbox} from '@angular/material/checkbox';
import {MatButton} from '@angular/material/button';
import {Subscription} from 'rxjs';
@Component({
selector: 'app-register',
standalone: true,
imports: [
MatError,
MatFormField,
MatLabel,
MatInput,
MatCard,
MatCardHeader,
MatCardTitle,
MatCardSubtitle,
MatCardContent,
ReactiveFormsModule,
MatProgressSpinner,
MatDivider,
MatCardActions,
RouterLink,
MatCheckbox,
MatButton
],
templateUrl: './register.component.html',
styleUrl: './register.component.css'
})
export class RegisterComponent implements OnDestroy {
registerForm: FormGroup;
isSubmitted = false;
isLoading = false;
private readonly router: Router = inject(Router);
private readonly authService: AuthService = inject(AuthService);
private registerSubscription: Subscription | null = null;
constructor(private readonly formBuilder: FormBuilder) {
this.registerForm = this.formBuilder.group({
firstName: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(50),
Validators.pattern('^[a-zA-Z]+$')
]],
lastName: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(50),
Validators.pattern('^[a-zA-Z]+$')
]],
username: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(20),
Validators.pattern('^[a-zA-Z0-9_]+$')
]],
email: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(120),
Validators.email
]],
password: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$')
]],
confirmPassword: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$')
]],
termsAndConditions: [false, Validators.requiredTrue]
}, {validators: this.passwordMatchValidator});
}
ngOnDestroy(): void {
this.registerSubscription?.unsubscribe();
}
private readonly passwordMatchValidator: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return password === confirmPassword ? null : {passwordMismatch: true};
};
onRegister() {
this.isSubmitted = true;
if (this.registerForm.valid) {
this.isLoading = true;
const registrationData = this.registerForm.value;
delete registrationData.confirmPassword;
this.registerSubscription = this.authService.register(registrationData).subscribe({
next: (response) => {
this.isLoading = false;
this.registerForm.reset();
this.isSubmitted = false;
alert("Registration successful!");
this.router.navigate(['/']).then();
},
error: (error) => {
console.error('Erreur HTTP:', error);
this.isLoading = false;
alert("An error occurred during registration. Please try again.");
}
});
}
}
isFieldInvalid(fieldName: string): boolean {
const field = this.registerForm.get(fieldName);
return Boolean(field && field.invalid && (field.dirty || field.touched || this.isSubmitted));
}
getFieldError(fieldName: string): string {
const field = this.registerForm.get(fieldName);
if (field && field.errors) {
if (field.errors['required']) return `Ce champ est obligatoire`;
if (field.errors['email']) return `Format d'email invalide`;
if (field.errors['minlength']) return `Minimum ${field.errors['minlength'].requiredLength} caractères`;
if (field.errors['maxlength']) return `Maximum ${field.errors['maxlength'].requiredLength} caractères`;
}
return '';
}
}