Compare commits
9 Commits
e5076f0c64
...
42c4f76c0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42c4f76c0d | ||
|
|
4cc2a15231 | ||
|
|
54fbc7d609 | ||
|
|
8e9440a104 | ||
|
|
00d49d5fa4 | ||
|
|
78325c8729 | ||
|
|
1e18e3bc52 | ||
|
|
8ada2a15ef | ||
|
|
95d5308934 |
@@ -54,7 +54,8 @@ public class SecurityConfiguration {
|
|||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
POST,
|
POST,
|
||||||
"/api/users/login",
|
"/api/users/login",
|
||||||
"/api/users/refresh-token"
|
"/api/users/refresh-token",
|
||||||
|
"/api/users"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
POST,
|
POST,
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
|
<main>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
</main>
|
||||||
|
<app-footer></app-footer>
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
app-header {
|
app-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 1em;
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { HeaderComponent } from './components/header/header.component';
|
import { HeaderComponent } from './components/header/header.component';
|
||||||
|
import { FooterComponent } from './components/footer/footer.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -9,7 +10,8 @@ import { HeaderComponent } from './components/header/header.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterOutlet,
|
RouterOutlet,
|
||||||
HeaderComponent
|
HeaderComponent,
|
||||||
|
FooterComponent
|
||||||
],
|
],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss'
|
styleUrl: './app.component.scss'
|
||||||
|
|||||||
@@ -5,10 +5,18 @@ export const routes: Routes = [
|
|||||||
path: 'login',
|
path: 'login',
|
||||||
loadComponent: () => import('./pages/login/login.component').then(module => module.LoginComponent)
|
loadComponent: () => import('./pages/login/login.component').then(module => module.LoginComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'signin',
|
||||||
|
loadComponent: () => import('./pages/signin/signin.component').then(module => module.SigninComponent)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'disconnect',
|
path: 'disconnect',
|
||||||
loadComponent: () => import('./pages/disconnection/disconnection.component').then(module => module.DisconnectionComponent)
|
loadComponent: () => import('./pages/disconnection/disconnection.component').then(module => module.DisconnectionComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'publications/:publicationId',
|
||||||
|
loadComponent: () => import('./pages/publication/publication.component').then(module => module.PublicationComponent)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
loadComponent: () => import('./pages/home/home.component').then(module => module.HomeComponent)
|
loadComponent: () => import('./pages/home/home.component').then(module => module.HomeComponent)
|
||||||
|
|||||||
15
frontend/src/app/components/footer/footer.component.html
Normal file
15
frontend/src/app/components/footer/footer.component.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<div>
|
||||||
|
<span class="copy-left">
|
||||||
|
©
|
||||||
|
</span>
|
||||||
|
2027 - 2024 Tous droits réservés
|
||||||
|
<version>
|
||||||
|
<a [routerLink]="['healthCheck']">
|
||||||
|
<mat-icon>favorite</mat-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-icon>menu_book</mat-icon>
|
||||||
|
-
|
||||||
|
Développements réalisés par Florian THIERRY
|
||||||
|
</div>
|
||||||
28
frontend/src/app/components/footer/footer.component.scss
Normal file
28
frontend/src/app/components/footer/footer.component.scss
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
:host {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: rgba(255,255,255,.6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5em;
|
||||||
|
font-size: 1.1em;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(255,255,255,.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 1em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
frontend/src/app/components/footer/footer.component.spec.ts
Normal file
23
frontend/src/app/components/footer/footer.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FooterComponent } from './footer.component';
|
||||||
|
|
||||||
|
describe('FooterComponent', () => {
|
||||||
|
let component: FooterComponent;
|
||||||
|
let fixture: ComponentFixture<FooterComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FooterComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FooterComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
14
frontend/src/app/components/footer/footer.component.ts
Normal file
14
frontend/src/app/components/footer/footer.component.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-footer',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatIconModule, RouterModule],
|
||||||
|
templateUrl: './footer.component.html',
|
||||||
|
styleUrl: './footer.component.scss'
|
||||||
|
})
|
||||||
|
export class FooterComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ng-container *ngIf="isAuthenticated; else anonymousRightMenu">
|
<ng-container *ngIf="isAuthenticated; else anonymousRightMenu">
|
||||||
<a [routerLink]="['/disconnect']">Disconnect</a>
|
<a [routerLink]="['/disconnect']" matRipple>Disconnect</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #anonymousRightMenu>
|
<ng-template #anonymousRightMenu>
|
||||||
<a [routerLink]="['/login']">Login</a>
|
<a [routerLink]="['/login']" matRipple>Login</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<app-side-menu #sideMenu></app-side-menu>
|
<app-side-menu #sideMenu></app-side-menu>
|
||||||
@@ -60,6 +60,12 @@ $headerHeight: 3.5em;
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .2s ease-in-out;
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,9 +75,38 @@ $headerHeight: 3.5em;
|
|||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 50em;
|
|
||||||
$borderRadiusValue: 10em;
|
$borderRadiusValue: 10em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
max-width: 12em;
|
||||||
|
transition: max-width .2s ease-in-out;
|
||||||
|
|
||||||
|
@media screen and (min-width: 435px) {
|
||||||
|
max-width: 16em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 500px) {
|
||||||
|
max-width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 700px) {
|
||||||
|
max-width: 24em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 800px) {
|
||||||
|
max-width: 32em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 900px) {
|
||||||
|
max-width: 38em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
max-width: 45em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1100px) {
|
||||||
|
max-width: 50em;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -114,7 +149,16 @@ $headerHeight: 3.5em;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
color: white;
|
color: white;
|
||||||
margin: 0 .5em;
|
margin: 0.5em 0.5em;
|
||||||
|
border-radius: 10em;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0 .8em;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
transition: background-color .2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #5c6bc0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { AuthenticationService } from '../../core/service/authentication.service';
|
import { AuthenticationService } from '../../core/service/authentication.service';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { SideMenuComponent } from '../side-menu/side-menu.component';
|
import { SideMenuComponent } from '../side-menu/side-menu.component';
|
||||||
|
import { MatRippleModule } from '@angular/material/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MatButtonModule, MatIconModule, RouterModule, SideMenuComponent],
|
imports: [CommonModule, MatButtonModule, MatIconModule, RouterModule, SideMenuComponent, MatRippleModule],
|
||||||
templateUrl: './header.component.html',
|
templateUrl: './header.component.html',
|
||||||
styleUrl: './header.component.scss',
|
styleUrl: './header.component.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,4 +12,8 @@ export class PublicationRestService {
|
|||||||
getLatest(): Promise<Publication[]> {
|
getLatest(): Promise<Publication[]> {
|
||||||
return lastValueFrom(this.httpClient.get<Publication[]>('/api/publications/latest'));
|
return lastValueFrom(this.httpClient.get<Publication[]>('/api/publications/latest'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getById(publicationId: string): Promise<Publication> {
|
||||||
|
return lastValueFrom(this.httpClient.get<Publication>(`/api/publications/${publicationId}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SigninRequest {
|
||||||
|
pseudo?: string;
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { HttpClient } from "@angular/common/http";
|
|||||||
import { Injectable, inject } from "@angular/core";
|
import { Injectable, inject } from "@angular/core";
|
||||||
import { LoginRequest, LoginResponse } from "./model/login.model";
|
import { LoginRequest, LoginResponse } from "./model/login.model";
|
||||||
import { lastValueFrom } from "rxjs";
|
import { lastValueFrom } from "rxjs";
|
||||||
|
import { SigninRequest } from "./model/signin.model";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -12,4 +13,8 @@ export class UserRestService {
|
|||||||
login(request: LoginRequest): Promise<LoginResponse> {
|
login(request: LoginRequest): Promise<LoginResponse> {
|
||||||
return lastValueFrom(this.httpClient.post<LoginResponse>('/api/users/login', request));
|
return lastValueFrom(this.httpClient.post<LoginResponse>('/api/users/login', request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signin(request: SigninRequest): Promise<void> {
|
||||||
|
return lastValueFrom(this.httpClient.post<void>('/api/users', request));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ $cardBorderRadius: .5em;
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2em;
|
gap: 2em;
|
||||||
max-width: 50em;
|
max-width: 50em;
|
||||||
|
width: 90%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
.publication {
|
.publication {
|
||||||
@@ -21,6 +22,7 @@ $cardBorderRadius: .5em;
|
|||||||
transition: box-shadow .2s ease-in-out;
|
transition: box-shadow .2s ease-in-out;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: black;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,.24),0 4px 14px 0 rgba(0,0,0,.16);
|
box-shadow: 0 4px 8px 0 rgba(0,0,0,.24),0 4px 14px 0 rgba(0,0,0,.16);
|
||||||
@@ -28,8 +30,21 @@ $cardBorderRadius: .5em;
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
height: 32em;
|
height: 15em;
|
||||||
border-radius: $cardBorderRadius $cardBorderRadius 0 0;
|
border-radius: $cardBorderRadius $cardBorderRadius 0 0;
|
||||||
|
transition: height .2s ease-in-out;
|
||||||
|
|
||||||
|
@media screen and (min-width: 450px) {
|
||||||
|
height: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
height: 25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 750px) {
|
||||||
|
height: 32em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
<form [formGroup]="loginForm" (submit)="performLogin()" ngNativeValidate>
|
<form [formGroup]="loginForm" (submit)="performLogin()" ngNativeValidate>
|
||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
<div>
|
<div>
|
||||||
|
<mat-icon>mail</mat-icon>
|
||||||
<label for="email">
|
<label for="email">
|
||||||
Email address
|
Email address
|
||||||
|
<span class="required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="email" formControlName="email" autocomplete="email" required />
|
<input type="email" id="email" formControlName="email" autocomplete="email" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<mat-icon>lock</mat-icon>
|
||||||
<label for="password">
|
<label for="password">
|
||||||
Password
|
Password
|
||||||
|
<span class="required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="password" formControlName="password" required />
|
<input type="password" id="password" formControlName="password" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit">Send</button>
|
<button type="submit">Send</button>
|
||||||
|
<a [routerLink]="['/signin']">Create an account</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -3,34 +3,79 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
form {
|
form {
|
||||||
min-width: 40em;
|
width: 80%;
|
||||||
max-width: 90%;
|
max-width: 20em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: .5em;
|
gap: 1em;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||||
|
border-radius: .5em;
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5em;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
gap: .1em;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.3em;
|
||||||
|
left: .5em;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
&.actions {
|
&.actions {
|
||||||
justify-content: end;
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: 10em;
|
padding: .8em 1.2em;
|
||||||
height: 2em;
|
border-radius: 10em;
|
||||||
|
border: none;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
transition: background-color .2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #5b6ed8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3f51b5;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
flex: 1 30%;
|
flex: 1;
|
||||||
|
font-style: italic;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: #777;
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
flex: 1 70%;
|
flex: 1;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10em;
|
||||||
|
padding: 1em 1em 1em 3em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,21 @@ import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators }
|
|||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { Subscription, debounceTime, map } from 'rxjs';
|
import { Subscription, debounceTime, map } from 'rxjs';
|
||||||
import { LoginService } from './login.service';
|
import { LoginService } from './login.service';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrl: './login.component.scss',
|
styleUrl: './login.component.scss',
|
||||||
imports: [ ReactiveFormsModule ],
|
imports: [ReactiveFormsModule, MatIconModule, RouterModule],
|
||||||
providers: [LoginService, MatSnackBarModule]
|
providers: [LoginService, MatSnackBarModule]
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent implements OnInit, OnDestroy {
|
||||||
private loginService = inject(LoginService);
|
private loginService = inject(LoginService);
|
||||||
private formBuilder = inject(FormBuilder);
|
private formBuilder = inject(FormBuilder);
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
emailValue: string | undefined;
|
|
||||||
loginForm: FormGroup = this.formBuilder.group({
|
loginForm: FormGroup = this.formBuilder.group({
|
||||||
email: new FormControl<string | undefined>('', [Validators.required, Validators.email]),
|
email: new FormControl<string | undefined>('', [Validators.required, Validators.email]),
|
||||||
password: new FormControl<string | undefined>('', [Validators.required])
|
password: new FormControl<string | undefined>('', [Validators.required])
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<ng-container *ngIf="isLoading; else afterLoadingPart">
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #afterLoadingPart>
|
||||||
|
<ng-container *ngIf="publication; else loadingFailedMessage">
|
||||||
|
<div class="card">
|
||||||
|
<img src="/pictures/{{ publication.illustrationId }}" />
|
||||||
|
<header>
|
||||||
|
<h1>{{ publication.title }}</h1>
|
||||||
|
<h2>{{ publication.description }}</h2>
|
||||||
|
</header>
|
||||||
|
<main [innerHTML]="publication.parsedText"></main>
|
||||||
|
<footer>
|
||||||
|
<img src="/pictures/{{ publication.author.image }}" [matTooltip]="publication.author.name" />
|
||||||
|
Publication posted by {{ publication.author.name }}
|
||||||
|
<span class="publication-date">
|
||||||
|
({{ publication.creationDate | date: 'short' : 'fr-FR' }})
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #loadingFailedMessage>
|
||||||
|
<div class="loading-failed">
|
||||||
|
<h1>Publication failed to load...</h1>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
$cardBorderRadius: .5em;
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 1em;
|
||||||
|
max-width: 80em;
|
||||||
|
width: 90%;
|
||||||
|
border-radius: .5em;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 12em;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: $cardBorderRadius $cardBorderRadius 0 0;
|
||||||
|
transition: height .2s ease-in-out;
|
||||||
|
|
||||||
|
@media screen and (min-width: 450px) {
|
||||||
|
height: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
height: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 750px) {
|
||||||
|
height: 25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 2em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1.6em;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
margin: 0 2em 2em 2em;
|
||||||
|
padding-top: 2em;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 0 0 $cardBorderRadius $cardBorderRadius;
|
||||||
|
padding: 1em 2em;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
$imageSize: 4em;
|
||||||
|
border-radius: 10em;
|
||||||
|
width: $imageSize;
|
||||||
|
height: $imageSize;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publication-date {
|
||||||
|
font-style: italic;
|
||||||
|
color: #bdbdbd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
frontend/src/app/pages/publication/publication.component.ts
Normal file
55
frontend/src/app/pages/publication/publication.component.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||||
|
import { PublicationRestService } from '../../core/rest-services/publications/publication.rest-service';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { Publication } from '../../core/rest-services/publications/model/publication';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||||
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-publication',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatProgressSpinner, MatTooltip],
|
||||||
|
templateUrl: './publication.component.html',
|
||||||
|
styleUrl: './publication.component.scss'
|
||||||
|
})
|
||||||
|
export class PublicationComponent implements OnInit, OnDestroy {
|
||||||
|
private activatedRoute = inject(ActivatedRoute);
|
||||||
|
private publicationRestService = inject(PublicationRestService);
|
||||||
|
private paramMapSubscription?: Subscription;
|
||||||
|
private snackBar = inject(MatSnackBar);
|
||||||
|
isLoading: boolean = false;
|
||||||
|
publication?: Publication;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.paramMapSubscription = this.activatedRoute
|
||||||
|
.paramMap
|
||||||
|
.subscribe(params => {
|
||||||
|
const publicationId = params.get('publicationId');
|
||||||
|
|
||||||
|
if (publicationId) {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.publicationRestService.getById(publicationId)
|
||||||
|
.then(publication => {
|
||||||
|
this.publication = publication;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.snackBar.open('An error occurred while loading publication...', 'Close', { duration: 5000 });
|
||||||
|
console.error('An error occurred while loading publication...', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// this.publicationRestService.getById()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.paramMapSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
frontend/src/app/pages/signin/signin.component.html
Normal file
39
frontend/src/app/pages/signin/signin.component.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<form [formGroup]="signinForm" (submit)="performSignin()" ngNativeValidate>
|
||||||
|
<h1>Signin</h1>
|
||||||
|
<div>
|
||||||
|
<mat-icon>person</mat-icon>
|
||||||
|
<label for="pseudo">
|
||||||
|
Pseudo
|
||||||
|
<span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="pseudo" formControlName="pseudo" autocomplete="pseudo" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-icon>mail</mat-icon>
|
||||||
|
<label for="email">
|
||||||
|
Email address
|
||||||
|
<span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="email" id="email" formControlName="email" autocomplete="email" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-icon>lock</mat-icon>
|
||||||
|
<label for="password">
|
||||||
|
Password
|
||||||
|
<span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="password" id="password" formControlName="password" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-icon>lock</mat-icon>
|
||||||
|
<label for="confirm-password">
|
||||||
|
Confirm password
|
||||||
|
<span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="password" id="confirm-password" formControlName="confirmPassword" required />
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
<a [routerLink]="['/login']">I already have an account</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
82
frontend/src/app/pages/signin/signin.component.scss
Normal file
82
frontend/src/app/pages/signin/signin.component.scss
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 20em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||||
|
border-radius: .5em;
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
gap: .1em;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.3em;
|
||||||
|
left: .5em;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: .8em 1.2em;
|
||||||
|
border-radius: 10em;
|
||||||
|
border: none;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
transition: background-color .2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #5b6ed8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3f51b5;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 1;
|
||||||
|
font-style: italic;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: #777;
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10em;
|
||||||
|
padding: 1em 1em 1em 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
frontend/src/app/pages/signin/signin.component.spec.ts
Normal file
23
frontend/src/app/pages/signin/signin.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SigninComponent } from './signin.component';
|
||||||
|
|
||||||
|
describe('SigninComponent', () => {
|
||||||
|
let component: SigninComponent;
|
||||||
|
let fixture: ComponentFixture<SigninComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SigninComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SigninComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
96
frontend/src/app/pages/signin/signin.component.ts
Normal file
96
frontend/src/app/pages/signin/signin.component.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||||
|
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { Subscription, debounceTime, distinctUntilChanged, map } from 'rxjs';
|
||||||
|
import { SigninService } from './signin.service';
|
||||||
|
import { LoginService } from '../login/login.service';
|
||||||
|
import { FormError } from '../../core/model/FormError';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-signin',
|
||||||
|
standalone: true,
|
||||||
|
imports: [ReactiveFormsModule, MatIconModule, RouterModule],
|
||||||
|
templateUrl: './signin.component.html',
|
||||||
|
styleUrl: './signin.component.scss',
|
||||||
|
providers: [SigninService, LoginService]
|
||||||
|
})
|
||||||
|
export class SigninComponent implements OnInit, OnDestroy {
|
||||||
|
private signinService = inject(SigninService);
|
||||||
|
private formBuilder = inject(FormBuilder);
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
signinForm: FormGroup = this.formBuilder.group({
|
||||||
|
pseudo: new FormControl<string | undefined>('', Validators.required),
|
||||||
|
email: new FormControl<string | undefined>('', [Validators.required, Validators.email]),
|
||||||
|
password: new FormControl<string | undefined>('', [Validators.required]),
|
||||||
|
confirmPassword: new FormControl<string | undefined>('', [Validators.required])
|
||||||
|
});
|
||||||
|
|
||||||
|
formErrors: FormError[] = []
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const pseudoSubscription = this.signinForm.controls['pseudo'].valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map(value => value?.length ? value as string : '')
|
||||||
|
)
|
||||||
|
.subscribe(pseudo => {
|
||||||
|
this.signinService.editPseudo(pseudo);
|
||||||
|
});
|
||||||
|
this.subscriptions.push(pseudoSubscription);
|
||||||
|
|
||||||
|
const emailSubscription = this.signinForm.controls['email'].valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map(value => value?.length ? value as string : '')
|
||||||
|
)
|
||||||
|
.subscribe(email => {
|
||||||
|
this.signinService.editEmail(email);
|
||||||
|
});
|
||||||
|
this.subscriptions.push(emailSubscription);
|
||||||
|
|
||||||
|
const passwordSubscription = this.signinForm.controls['password'].valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map(value => value?.length ? value as string : '')
|
||||||
|
)
|
||||||
|
.subscribe(password => {
|
||||||
|
this.signinService.editPassword(password);
|
||||||
|
});
|
||||||
|
this.subscriptions.push(passwordSubscription);
|
||||||
|
|
||||||
|
const confirmPasswordSubscription = this.signinForm.controls['confirmPassword'].valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map(value => value?.length ? value as string : '')
|
||||||
|
)
|
||||||
|
.subscribe(confirmPassword => {
|
||||||
|
this.signinService.editConfirmPassword(confirmPassword);
|
||||||
|
});
|
||||||
|
this.subscriptions.push(confirmPasswordSubscription);
|
||||||
|
|
||||||
|
const stateSubscription = this.signinService.state$
|
||||||
|
.subscribe(state => {
|
||||||
|
this.signinForm.controls['pseudo'].setValue(state.request.pseudo, { emitEvent: false });
|
||||||
|
this.signinForm.controls['email'].setValue(state.request.email, { emitEvent: false });
|
||||||
|
this.signinForm.controls['password'].setValue(state.request.password, { emitEvent: false });
|
||||||
|
this.signinForm.controls['confirmPassword'].setValue(state.confirmPassword, { emitEvent: false });
|
||||||
|
});
|
||||||
|
this.subscriptions.push(stateSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
performSignin(): void {
|
||||||
|
if (this.signinForm.valid) {
|
||||||
|
this.signinService.performSignin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
frontend/src/app/pages/signin/signin.service.ts
Normal file
102
frontend/src/app/pages/signin/signin.service.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { SigninRequest } from '../../core/rest-services/user/model/signin.model';
|
||||||
|
import { FormError } from '../../core/model/FormError';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { copy } from '../../core/utils/ObjectUtils';
|
||||||
|
import { UserRestService } from '../../core/rest-services/user/user.rest-service';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { LoginService } from '../login/login.service';
|
||||||
|
|
||||||
|
export interface SigninState {
|
||||||
|
request: SigninRequest;
|
||||||
|
confirmPassword?: string;
|
||||||
|
errors: FormError[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_STATE: SigninState = {
|
||||||
|
request: {
|
||||||
|
pseudo: undefined,
|
||||||
|
email: undefined,
|
||||||
|
password: undefined,
|
||||||
|
},
|
||||||
|
confirmPassword: undefined,
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SigninService {
|
||||||
|
private stateSubject = new BehaviorSubject<SigninState>(copy(DEFAULT_STATE));
|
||||||
|
private userRestService = inject(UserRestService);
|
||||||
|
private snackBar = inject(MatSnackBar);
|
||||||
|
private router = inject(Router);
|
||||||
|
private loginService = inject(LoginService);
|
||||||
|
|
||||||
|
get state$(): Observable<SigninState> {
|
||||||
|
return this.stateSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private get state(): SigninState {
|
||||||
|
return this.stateSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private save(newState: SigninState): void {
|
||||||
|
this.stateSubject.next(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
editPseudo(newPseudo: string): void {
|
||||||
|
const state = this.state;
|
||||||
|
|
||||||
|
state.request.pseudo = newPseudo;
|
||||||
|
|
||||||
|
this.save(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
editEmail(newEmail: string): void {
|
||||||
|
const state = this.state;
|
||||||
|
|
||||||
|
state.request.email = newEmail;
|
||||||
|
|
||||||
|
this.save(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
editPassword(newPassword: string): void {
|
||||||
|
const state = this.state;
|
||||||
|
|
||||||
|
state.request.password = newPassword;
|
||||||
|
|
||||||
|
this.save(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
editConfirmPassword(newConfirmPassword: string): void {
|
||||||
|
const state = this.state;
|
||||||
|
|
||||||
|
state.confirmPassword = newConfirmPassword;
|
||||||
|
|
||||||
|
if (state.request.password !== state.confirmPassword) {
|
||||||
|
const confirmPasswordError: FormError = {
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
errorMessage: 'Typed password are different.'
|
||||||
|
}
|
||||||
|
state.errors.filter(error => error.fieldName !== 'confirmPassword');
|
||||||
|
state.errors.push(confirmPasswordError)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
performSignin(): void {
|
||||||
|
const state = this.state;
|
||||||
|
|
||||||
|
// Check state is valid
|
||||||
|
|
||||||
|
this.userRestService
|
||||||
|
.signin(state.request)
|
||||||
|
.then(() => {
|
||||||
|
this.loginService.editEmail(state.request.email!!);
|
||||||
|
this.loginService.editPassword(state.request.password!!);
|
||||||
|
|
||||||
|
this.loginService.performLogin();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
|
||||||
html, body { height: 100%; }
|
html, body { height: 100%; }
|
||||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||||
|
background-color: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user