Fixing Angular 21 by migrating all values by signals. (#11)
Some checks failed
Build and Deploy Java Gradle Application / build-and-deploy (push) Failing after 53s

This commit was merged in pull request #11.
This commit is contained in:
2026-02-03 15:07:55 +01:00
parent 1ca2f872f7
commit 0cce8b2982
102 changed files with 4102 additions and 4852 deletions

View File

@@ -1,6 +1,6 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

View File

@@ -1,13 +1,13 @@
import { Component, OnInit, inject } from '@angular/core';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import { AuthenticationService } from '../../core/service/authentication.service';
import { Router } from '@angular/router';
import {Component, inject, OnInit} from '@angular/core';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {AuthenticationService} from '../../core/service/authentication.service';
import {Router} from '@angular/router';
@Component({
selector: 'app-disconnection',
imports: [MatProgressSpinnerModule],
templateUrl: './disconnection.component.html',
styleUrl: './disconnection.component.scss'
selector: 'app-disconnection',
imports: [MatProgressSpinnerModule],
templateUrl: './disconnection.component.html',
styleUrl: './disconnection.component.scss'
})
export class DisconnectionComponent implements OnInit {
private authenticationService = inject(AuthenticationService);

View File

@@ -1,11 +1,11 @@
<h1 i18n>Last publications</h1>
@if ((isLoading$ | async) === true) {
<h2 i18n>Publications loading...</h2>
<mat-spinner></mat-spinner>
@if ((isLoading())) {
<h2 i18n>Publications loading...</h2>
<mat-spinner/>
} @else {
@if ((publications$ | async) != []) {
<app-publication-list [publications$]="publications$"></app-publication-list>
} @else {
<h2 i18n>No any publication.</h2>
}
@if (publications(); as publications) {
<app-publication-list [publications]="publications"/>
} @else {
<h2 i18n>No any publication.</h2>
}
}

View File

@@ -1,6 +1,6 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

View File

@@ -1,34 +1,26 @@
import { Component, OnInit, inject } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Observable } from 'rxjs';
import { PublicationListComponent } from '../../components/publication-list/publication-list.component';
import { Publication } from '../../core/rest-services/publications/model/publication';
import { HomeService } from './home.service';
import { CommonModule } from '@angular/common';
import {Component, inject, OnInit} from '@angular/core';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {PublicationListComponent} from '../../components/publication-list/publication-list.component';
import {HomeService} from './home.service';
import {CommonModule} from '@angular/common';
@Component({
selector: 'app-home',
imports: [
CommonModule,
MatProgressSpinnerModule,
PublicationListComponent
],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
providers: [HomeService]
selector: 'app-home',
imports: [
CommonModule,
MatProgressSpinnerModule,
PublicationListComponent
],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
providers: [HomeService]
})
export class HomeComponent implements OnInit {
private homeService = inject(HomeService);
get isLoading$(): Observable<boolean> {
return this.homeService.isLoading$;
}
get publications$(): Observable<Publication[]> {
return this.homeService.publications$;
}
readonly #homeService = inject(HomeService);
isLoading = this.#homeService.isLoading;
publications = this.#homeService.publications;
ngOnInit(): void {
this.homeService.startLatestPublicationsRetrieving();
this.#homeService.startLatestPublicationsRetrieving();
}
}

View File

@@ -1,35 +1,34 @@
import { Injectable, inject } from "@angular/core";
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
import { BehaviorSubject, Observable } from "rxjs";
import { MatSnackBar } from "@angular/material/snack-bar"
import { Publication } from "../../core/rest-services/publications/model/publication";
import {inject, Injectable, Signal, signal} from "@angular/core";
import {PublicationRestService} from "../../core/rest-services/publications/publication.rest-service";
import {MatSnackBar} from "@angular/material/snack-bar"
import {Publication} from "../../core/rest-services/publications/model/publication";
@Injectable()
export class HomeService {
private publicationRestService = inject(PublicationRestService);
private snackBar = inject(MatSnackBar);
private publicationRestService = inject(PublicationRestService);
private snackBar = inject(MatSnackBar);
private publicationsSubject = new BehaviorSubject<Publication[]>([]);
private isLoadingSubject = new BehaviorSubject<boolean>(false);
#publications = signal<Publication[]>([]);
#isLoadingSubject = signal<boolean>(false);
get isLoading$(): Observable<boolean> {
return this.isLoadingSubject.asObservable();
}
get isLoading(): Signal<boolean> {
return this.#isLoadingSubject.asReadonly();
}
get publications$(): Observable<Publication[]> {
return this.publicationsSubject.asObservable();
}
get publications(): Signal<Publication[]> {
return this.#publications.asReadonly();
}
startLatestPublicationsRetrieving(): void {
this.isLoadingSubject.next(true);
startLatestPublicationsRetrieving(): void {
this.#isLoadingSubject.set(true);
this.publicationRestService.getLatest()
.then(publications => this.publicationsSubject.next(publications))
.catch(error => {
const errorMessage = $localize`An error occurred while retrieving latest publications...`;
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
console.error(errorMessage, error);
})
.finally(() => this.isLoadingSubject.next(false));
}
}
this.publicationRestService.getLatest()
.then(publications => this.#publications.set(publications))
.catch(error => {
const errorMessage = $localize`An error occurred while retrieving latest publications...`;
this.snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
console.error(errorMessage, error);
})
.finally(() => this.#isLoadingSubject.set(false));
}
}

View File

@@ -1,23 +1,23 @@
<form [formGroup]="loginForm" (submit)="performLogin()" class="cod-form card" ngNativeValidate>
<h1 i18n>Login</h1>
<div class="form-field">
<mat-icon>mail</mat-icon>
<label for="email" i18n>
Email address
<span class="required">*</span>
</label>
<input type="email" id="email" formControlName="email" autocomplete="email" required />
</div>
<div class="form-field">
<mat-icon>lock</mat-icon>
<label for="password" i18n>
Password
<span class="required">*</span>
</label>
<input type="password" id="password" formControlName="password" required />
</div>
<div class="actions reversed">
<app-submit-button [requestPending]="false" [disabled]="false" i18n>Send</app-submit-button>
<a [routerLink]="['/signin']" class="cod-button secondary" matRipple i18n>Create an account</a>
</div>
</form>
<h1 i18n>Login</h1>
<div class="form-field">
<mat-icon>mail</mat-icon>
<label for="email" i18n>
Email address
<span class="required">*</span>
</label>
<input type="email" id="email" formControlName="email" autocomplete="email" required/>
</div>
<div class="form-field">
<mat-icon>lock</mat-icon>
<label for="password" i18n>
Password
<span class="required">*</span>
</label>
<input type="password" id="password" formControlName="password" required/>
</div>
<div class="actions reversed">
<app-submit-button [requestPending]="false" [disabled]="false" i18n>Send</app-submit-button>
<a [routerLink]="['/signin']" class="cod-button secondary" matRipple i18n>Create an account</a>
</div>
</form>

View File

@@ -1,7 +1,7 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1em;
}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1em;
}

View File

@@ -1,25 +1,25 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { Subscription, debounceTime, map } from 'rxjs';
import { LoginService } from './login.service';
import { MatIconModule } from '@angular/material/icon';
import { RouterModule } from '@angular/router';
import { SubmitButtonComponent } from "../../components/submit-button/submit-button.component";
import { MatRippleModule } from '@angular/material/core';
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {debounceTime, map, Subscription} from 'rxjs';
import {LoginService} from './login.service';
import {MatIconModule} from '@angular/material/icon';
import {RouterModule} from '@angular/router';
import {SubmitButtonComponent} from "../../components/submit-button/submit-button.component";
import {MatRippleModule} from '@angular/material/core';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrl: './login.component.scss',
imports: [
MatIconModule,
MatRippleModule,
ReactiveFormsModule,
RouterModule,
SubmitButtonComponent
],
providers: [LoginService, MatSnackBarModule]
selector: 'app-login',
templateUrl: './login.component.html',
styleUrl: './login.component.scss',
imports: [
MatIconModule,
MatRippleModule,
ReactiveFormsModule,
RouterModule,
SubmitButtonComponent
],
providers: [LoginService, MatSnackBarModule]
})
export class LoginComponent implements OnInit, OnDestroy {
private loginService = inject(LoginService);
@@ -52,10 +52,10 @@ export class LoginComponent implements OnInit, OnDestroy {
this.subscriptions.push(passwordSubscription)
const stateSubscription = this.loginService.state$
.subscribe(state => {
this.loginForm.controls['email'].setValue(state.request.email, { emitEvent: false });
this.loginForm.controls['password'].setValue(state.request.password, { emitEvent: false });
});
.subscribe(state => {
this.loginForm.controls['email'].setValue(state.request.email, {emitEvent: false});
this.loginForm.controls['password'].setValue(state.request.password, {emitEvent: false});
});
this.subscriptions.push(stateSubscription);
}

View File

@@ -1,12 +1,12 @@
import { Injectable, inject } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { copy } from '../../core/utils/ObjectUtils';
import { FormError } from '../../core/model/FormError';
import { UserRestService } from '../../core/rest-services/user/user.rest-service';
import { LoginRequest } from '../../core/rest-services/user/model/login.model';
import { AuthenticationService } from '../../core/service/authentication.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import {inject, Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {copy} from '../../core/utils/ObjectUtils';
import {FormError} from '../../core/model/FormError';
import {UserRestService} from '../../core/rest-services/user/user.rest-service';
import {LoginRequest} from '../../core/rest-services/user/model/login.model';
import {AuthenticationService} from '../../core/service/authentication.service';
import {MatSnackBar} from '@angular/material/snack-bar';
import {Router} from '@angular/router';
export interface LoginState {
request: LoginRequest;
@@ -65,15 +65,15 @@ export class LoginService {
.login(state.request)
.then((response) => {
this.authenticationService.authenticate(response.accessToken, response.refreshToken);
this.snackBar.open($localize`Authentication succeeded!`, $localize`Close`, { duration: 5000 });
this.snackBar.open($localize`Authentication succeeded!`, $localize`Close`, {duration: 5000});
this.router.navigate(['/home']);
})
.catch((error) => {
console.error(error);
this.snackBar.open($localize`Authentication failed.`, $localize`Close`, { duration: 5000 });
this.snackBar.open($localize`Authentication failed.`, $localize`Close`, {duration: 5000});
});
} else {
this.snackBar.open($localize`Please, fill the inputs before send.`, $localize`Close`, { duration: 5000 });
this.snackBar.open($localize`Please, fill the inputs before send.`, $localize`Close`, {duration: 5000});
}
}

View File

@@ -1,21 +1,21 @@
<h1 i18n>Your publications</h1>
<a [routerLink]="['/publications/new']"
class="new-publication"
matTooltip="Add a new publication"
matTooltipPosition="left"
matRipple
i18n-matTooltip>
+
class="new-publication"
matTooltip="Add a new publication"
matTooltipPosition="left"
matRipple
i18n-matTooltip>
+
</a>
@if ((isLoading$ | async) === true) {
<h2 i18n>Publication loading...</h2>
<mat-spinner></mat-spinner>
@if (isLoading()) {
<h2 i18n>Publication loading...</h2>
<mat-spinner></mat-spinner>
} @else {
@if ((isLoaded$ | async) === true) {
<app-publication-list [publications$]="publications$"></app-publication-list>
} @else {
<h2 i18n>There is no any publication...</h2>
}
}
@if (isLoaded()) {
<app-publication-list [publications]="publications()"></app-publication-list>
} @else {
<h2 i18n>There is no any publication...</h2>
}
}

View File

@@ -1,29 +1,29 @@
$newPublicationButtonSize: 1.7em;
:host {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
.new-publication {
position: fixed;
border-radius: 10em;
background-color: #14A44D;
color: white;
display: flex;
justify-content: center;
align-items: center;
bottom: $newPublicationButtonSize;
right: $newPublicationButtonSize;
width: $newPublicationButtonSize;
height: $newPublicationButtonSize;
text-decoration: none;
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
transition: background-color .2s ease-in-out;
font-size: 2.2em;
&:hover {
background-color: #0e7a3a;
}
.new-publication {
position: fixed;
border-radius: 10em;
background-color: #14A44D;
color: white;
display: flex;
justify-content: center;
align-items: center;
bottom: $newPublicationButtonSize;
right: $newPublicationButtonSize;
width: $newPublicationButtonSize;
height: $newPublicationButtonSize;
text-decoration: none;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12);
transition: background-color .2s ease-in-out;
font-size: 2.2em;
&:hover {
background-color: #0e7a3a;
}
}
}
}

View File

@@ -1,45 +1,35 @@
import { Component, inject, OnInit } from "@angular/core";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { MyPublicationsService } from "./my-publications.service";
import { Observable } from "rxjs";
import { PublicationListComponent } from "../../components/publication-list/publication-list.component";
import { Publication } from "../../core/rest-services/publications/model/publication";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatRippleModule } from "@angular/material/core";
import {Component, inject, OnInit} from "@angular/core";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MyPublicationsService} from "./my-publications.service";
import {PublicationListComponent} from "../../components/publication-list/publication-list.component";
import {CommonModule} from "@angular/common";
import {RouterModule} from "@angular/router";
import {MatTooltipModule} from "@angular/material/tooltip";
import {MatRippleModule} from "@angular/material/core";
@Component({
selector: 'app-my-component',
templateUrl: './my-publications.component.html',
styleUrl: './my-publications.component.scss',
imports: [
CommonModule,
MatProgressSpinnerModule,
MatRippleModule,
MatTooltipModule,
PublicationListComponent,
RouterModule
],
providers: [MyPublicationsService]
selector: 'app-my-component',
templateUrl: './my-publications.component.html',
styleUrl: './my-publications.component.scss',
imports: [
CommonModule,
MatProgressSpinnerModule,
MatRippleModule,
MatTooltipModule,
PublicationListComponent,
RouterModule
],
providers: [MyPublicationsService]
})
export class MyPublicationsComponent implements OnInit {
private readonly myPublicationsService = inject(MyPublicationsService);
private readonly myPublicationsService = inject(MyPublicationsService);
get publications$(): Observable<Publication[]> {
return this.myPublicationsService.publications$;
}
publications = this.myPublicationsService.publications;
isLoading = this.myPublicationsService.isLoading;
isLoaded = this.myPublicationsService.isLoaded;
get isLoading$(): Observable<boolean> {
return this.myPublicationsService.isLoading$;
}
get isLoaded$(): Observable<boolean> {
return this.myPublicationsService.isLoaded$;
}
ngOnInit(): void {
this.myPublicationsService.loadAuthenticatedUserPublications();
}
}
ngOnInit(): void {
this.myPublicationsService.loadAuthenticatedUserPublications();
}
}

View File

@@ -1,7 +1,7 @@
import { Route } from "@angular/router";
import { authenticationGuard } from "../../core/guard/authentication.guard";
import { MyPublicationsComponent } from "./my-publications.component";
import {Route} from "@angular/router";
import {authenticationGuard} from "../../core/guard/authentication.guard";
import {MyPublicationsComponent} from "./my-publications.component";
export const ROUTES: Route[] = [
{ path: '', component: MyPublicationsComponent, canActivate: [authenticationGuard] }
]
{path: '', component: MyPublicationsComponent, canActivate: [authenticationGuard]}
]

View File

@@ -1,57 +1,56 @@
import { inject, Injectable } from "@angular/core";
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
import { AuthenticationService } from "../../core/service/authentication.service";
import { BehaviorSubject, Observable } from "rxjs";
import { Publication } from "../../core/rest-services/publications/model/publication";
import { Router } from "@angular/router";
import { MatSnackBar } from "@angular/material/snack-bar";
import {inject, Injectable, Signal, signal} from "@angular/core";
import {PublicationRestService} from "../../core/rest-services/publications/publication.rest-service";
import {AuthenticationService} from "../../core/service/authentication.service";
import {Publication} from "../../core/rest-services/publications/model/publication";
import {Router} from "@angular/router";
import {MatSnackBar} from "@angular/material/snack-bar";
@Injectable()
export class MyPublicationsService {
private readonly authenticationService = inject(AuthenticationService);
private readonly publicationRestService = inject(PublicationRestService);
private readonly snackBar = inject(MatSnackBar);
private readonly router = inject(Router);
private publicationsSubject = new BehaviorSubject<Publication[]>([]);
private isLoadingSubject = new BehaviorSubject<boolean>(false);
private isLoadedSubject = new BehaviorSubject<boolean>(false);
readonly #authenticationService = inject(AuthenticationService);
readonly #publicationRestService = inject(PublicationRestService);
readonly #snackBar = inject(MatSnackBar);
readonly #router = inject(Router);
#publications = signal<Publication[]>([]);
#isLoading = signal(false);
#isLoaded = signal(false);
get publications$(): Observable<Publication[]> {
return this.publicationsSubject.asObservable();
get publications(): Signal<Publication[]> {
return this.#publications.asReadonly();
}
get isLoading(): Signal<boolean> {
return this.#isLoading.asReadonly();
}
get isLoaded(): Signal<boolean> {
return this.#isLoaded.asReadonly();
}
loadAuthenticatedUserPublications(): void {
const authenticatedUser = this.#authenticationService.getAuthenticatedUser();
if (authenticatedUser) {
this.#isLoading.set(true);
this.#isLoaded.set(false);
const query = `author_id=${authenticatedUser.id}`;
this.#publicationRestService.search(query)
.then(publications => {
this.#publications.set(publications);
})
.catch(error => {
const errorMessage = $localize`An error occurred while retrieving your publications...`;
this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
console.error(errorMessage, error);
})
.finally(() => {
this.#isLoading.set(false);
this.#isLoaded.set(true);
});
} else {
this.#authenticationService.unauthenticate();
this.#snackBar.open($localize`You are unauthenticated. Please, log-in first.`, $localize`Close`, {duration: 5000});
this.#router.navigate(['/login']);
}
get isLoading$(): Observable<boolean> {
return this.isLoadingSubject.asObservable();
}
get isLoaded$(): Observable<boolean> {
return this.isLoadedSubject.asObservable();
}
loadAuthenticatedUserPublications(): void {
const authenticatedUser = this.authenticationService.getAuthenticatedUser();
if (authenticatedUser) {
this.isLoadingSubject.next(true);
this.isLoadedSubject.next(false);
const query = `author_id=${authenticatedUser.id}`;
this.publicationRestService.search(query)
.then(publications => {
this.publicationsSubject.next(publications);
})
.catch(error => {
const errorMessage = $localize`An error occurred while retrieving your publications...`;
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
console.error(errorMessage, error);
})
.finally(() => {
this.isLoadingSubject.next(false);
this.isLoadedSubject.next(true);
});
} else {
this.authenticationService.unauthenticate();
this.snackBar.open($localize`You are unauthenticated. Please, log-in first.`, $localize`Close`, { duration: 5000 });
this.router.navigate(['/login']);
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
<app-publication-edition
title="Creation of a new publication"
[publication]="publication"
[isSaving$]="isSaving$"
(publicationSave)="onPublicationSave($event)"
i18n-title>
</app-publication-edition>
title="Creation of a new publication"
[publication]="publication()"
[isSaving]="isSaving()"
(publicationSave)="onPublicationSave($event)"
i18n-title
/>

View File

@@ -1,74 +1,53 @@
import { Component, inject, OnInit } from "@angular/core";
import { PublicationEditionComponent } from "../../components/publication-edition/publication-edition.component";
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
import { ActivatedRoute, Router } from "@angular/router";
import { MatSnackBar } from "@angular/material/snack-bar";
import { BehaviorSubject, Observable, Subscription } from "rxjs";
import { Publication } from "../../core/rest-services/publications/model/publication";
import { AuthenticationService } from "../../core/service/authentication.service";
import { Author } from "../../core/rest-services/publications/model/author";
import {Component, inject, OnInit, signal} from "@angular/core";
import {PublicationEditionComponent} from "../../components/publication-edition/publication-edition.component";
import {PublicationRestService} from "../../core/rest-services/publications/publication.rest-service";
import {Router} from "@angular/router";
import {MatSnackBar} from "@angular/material/snack-bar";
import {DEFAULT_PUBLICATION, Publication} from "../../core/rest-services/publications/model/publication";
import {AuthenticationService} from "../../core/service/authentication.service";
import {Author} from "../../core/rest-services/publications/model/author";
@Component({
selector: 'app-publication-creation',
templateUrl: './publication-creation.component.html',
styleUrl: './publication-creation.component.scss',
imports: [
selector: 'app-publication-creation',
templateUrl: './publication-creation.component.html',
styleUrl: './publication-creation.component.scss',
imports: [
PublicationEditionComponent
]
]
})
export class PublicationCreationComponent implements OnInit {
private readonly authenticationService = inject(AuthenticationService);
private readonly publicationRestService = inject(PublicationRestService);
private readonly router = inject(Router);
private readonly snackBar = inject(MatSnackBar);
private isSavingSubject = new BehaviorSubject<boolean>(false);
private subscriptions: Subscription[] = [];
publication!: Publication;
readonly #authenticationService = inject(AuthenticationService);
readonly #publicationRestService = inject(PublicationRestService);
readonly #router = inject(Router);
readonly #snackBar = inject(MatSnackBar);
isSaving = signal(false);
publication = signal<Publication>(DEFAULT_PUBLICATION);
get isSaving$(): Observable<boolean> {
return this.isSavingSubject.asObservable();
ngOnInit(): void {
const authenticatedUser = this.#authenticationService.getAuthenticatedUser();
if (authenticatedUser) {
const author: Author = {
id: authenticatedUser.id,
name: authenticatedUser.pseudo,
image: authenticatedUser.photoId ?? ''
};
const newPublication: Publication = {...DEFAULT_PUBLICATION, author};
this.publication.set(newPublication);
}
}
ngOnInit(): void {
const authenticatedUser = this.authenticationService.getAuthenticatedUser();
if (authenticatedUser) {
const author: Author = {
id: authenticatedUser.id,
name: authenticatedUser.pseudo,
image: authenticatedUser.photoId ?? ''
};
this.publication = {
id: '',
key: '',
title: '',
text: '',
parsedText: '',
description: '',
creationDate: new Date(),
illustrationId: '',
categoryId: '',
author
};
}
}
ngOnDestroy(): void {
this.subscriptions.forEach(subscription => subscription?.unsubscribe());
}
onPublicationSave(publication: Publication): void {
this.isSavingSubject.next(true);
this.publicationRestService.create(publication)
.then(() => {
this.snackBar.open($localize`Publication created succesfully!`, $localize`Close`, { duration: 5000 });
this.router.navigate(['/my-publications']);
})
.catch(error => {
const errorMessage = $localize`An error occured while saving new publication.`;
console.error(errorMessage, error);
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
})
.finally(() => this.isSavingSubject.next(false));
}
}
onPublicationSave(publication: Publication): void {
this.isSaving.set(true);
this.#publicationRestService.create(publication)
.then(() => {
this.#snackBar.open($localize`Publication created succesfully!`, $localize`Close`, {duration: 5000});
this.#router.navigate(['/my-publications']);
})
.catch(error => {
const errorMessage = $localize`An error occured while saving new publication.`;
console.error(errorMessage, error);
this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
})
.finally(() => this.isSaving.set(false));
}
}

View File

@@ -1,7 +1,7 @@
import { Route } from "@angular/router";
import { authenticationGuard } from "../../core/guard/authentication.guard";
import { PublicationCreationComponent } from "./publication-creation.component";
import {Route} from "@angular/router";
import {authenticationGuard} from "../../core/guard/authentication.guard";
import {PublicationCreationComponent} from "./publication-creation.component";
export const ROUTES: Route[] = [
{ path: '', component: PublicationCreationComponent, canActivate: [authenticationGuard] }
]
{path: '', component: PublicationCreationComponent, canActivate: [authenticationGuard]}
]

View File

@@ -1,20 +1,18 @@
@if ((isLoading$ | async) == true) {
<h2 i18n>Loading publication to edit...</h2>
<mat-spinner></mat-spinner>
}
@else {
@if (publication) {
<app-publication-edition
title="Update publication {{ publication.title }}"
[publication]="publication"
[isSaving$]="isSaving$"
(publicationSave)="onPublicationSave($event)"
i18n-title>
</app-publication-edition>
}
@else {
<div class="loading-failed">
<h1 i18n>Publication failed to load...</h1>
</div>
}
}
@if (isLoading()) {
<h2 i18n>Loading publication to edit...</h2>
<mat-spinner/>
} @else {
@if (publication(); as publication) {
<app-publication-edition
title="Update publication {{ publication.title }}"
[publication]="publication"
[isSaving]="isSaving()"
(publicationSave)="onPublicationSave($event)"
i18n-title
/>
} @else {
<div class="loading-failed">
<h1 i18n>Publication failed to load...</h1>
</div>
}
}

View File

@@ -1,7 +1,7 @@
:host {
display: flex;
display: flex;
app-publication-edition {
flex: 1;
}
}
app-publication-edition {
flex: 1;
}
}

View File

@@ -1,93 +1,93 @@
import { CommonModule, Location } from '@angular/common';
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { PublicationEditionComponent } from '../../components/publication-edition/publication-edition.component';
import { Publication } from '../../core/rest-services/publications/model/publication';
import { PublicationRestService } from '../../core/rest-services/publications/publication.rest-service';
import {CommonModule, Location} from '@angular/common';
import {Component, effect, inject, Signal, signal} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {MatDialogModule} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatSnackBar} from '@angular/material/snack-bar';
import {MatTabsModule} from '@angular/material/tabs';
import {MatTooltipModule} from '@angular/material/tooltip';
import {ActivatedRoute, Router} from '@angular/router';
import {PublicationEditionComponent} from '../../components/publication-edition/publication-edition.component';
import {Publication} from '../../core/rest-services/publications/model/publication';
import {PublicationRestService} from '../../core/rest-services/publications/publication.rest-service';
import {toSignal} from "@angular/core/rxjs-interop";
import {map} from "rxjs";
@Component({
selector: 'app-publication-update',
imports: [
CommonModule,
MatDialogModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatTabsModule,
MatTooltipModule,
ReactiveFormsModule,
PublicationEditionComponent
],
templateUrl: './publication-update.component.html',
styleUrl: './publication-update.component.scss'
selector: 'app-publication-update',
imports: [
CommonModule,
MatDialogModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatTabsModule,
MatTooltipModule,
ReactiveFormsModule,
PublicationEditionComponent
],
templateUrl: './publication-update.component.html',
styleUrl: './publication-update.component.scss'
})
export class PublicationUpdateComponent implements OnInit, OnDestroy {
private readonly publicationRestService = inject(PublicationRestService);
private readonly activatedRoute = inject(ActivatedRoute);
private readonly location = inject(Location);
private readonly router = inject(Router);
private readonly snackBar = inject(MatSnackBar);
private isLoadingSubject = new BehaviorSubject<boolean>(false);
private isSavingSubject = new BehaviorSubject<boolean>(false);
private subscriptions: Subscription[] = [];
publication: Publication | undefined;
export class PublicationUpdateComponent {
readonly #activatedRoute = inject(ActivatedRoute);
readonly #location = inject(Location);
readonly #publicationRestService = inject(PublicationRestService);
readonly #router = inject(Router);
readonly #snackBar = inject(MatSnackBar);
#isLoading = signal(false);
#isSavingSubject = signal(false);
publication = signal<Publication | undefined>(undefined);
get isLoading$(): Observable<boolean> {
return this.isLoadingSubject.asObservable();
#publicationId$ = this.#activatedRoute.params
.pipe(map(queryParams => queryParams['publicationId']));
#publicationId = toSignal(this.#publicationId$, {initialValue: undefined});
get isLoading(): Signal<boolean> {
return this.#isLoading.asReadonly();
}
get isSaving$(): Observable<boolean> {
return this.isSavingSubject.asObservable();
get isSaving(): Signal<boolean> {
return this.#isSavingSubject.asReadonly();
}
ngOnInit(): void {
this.isLoadingSubject.next(true);
const activatedRouteSubscription = this.activatedRoute.paramMap.subscribe(params => {
const publicationId = params.get('publicationId');
if (publicationId == undefined) {
this.snackBar.open($localize`A technical error occurred while loading publication data.`, $localize`Close`, { duration: 5000 });
this.location.back();
} else {
this.publicationRestService.getById(publicationId)
.then(publication => {
this.publication = publication;
})
.catch(error => {
const errorMessage = $localize`A technical error occurred while loading publication data.`;
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
console.error(errorMessage, error)
})
.finally(() => this.isLoadingSubject.next(false));
}
});
this.subscriptions.push(activatedRouteSubscription);
}
constructor() {
effect(() => {
this.#isLoading.set(true);
ngOnDestroy(): void {
this.subscriptions.forEach(subscription => subscription?.unsubscribe());
const publicationId = this.#publicationId();
if (publicationId) {
this.#publicationRestService.getById(publicationId)
.then(publication => {
this.publication.set(publication);
})
.catch(error => {
const errorMessage = $localize`A technical error occurred while loading publication data.`;
this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
console.error(errorMessage, error)
})
.finally(() => this.#isLoading.set(false));
} else {
this.#snackBar.open($localize`A technical error occurred while loading publication data.`, $localize`Close`, {duration: 5000});
this.#location.back();
}
});
}
onPublicationSave(publication: Publication): void {
this.isSavingSubject.next(true);
this.publicationRestService.update(publication)
.then(() => {
this.snackBar.open($localize`Publication updated succesfully!`, $localize`Close`, { duration: 5000 });
this.router.navigate(['/home']);
})
.catch(error => {
const errorMessage = $localize`An error occured while saving publication modifications.`;
console.error(errorMessage, error);
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
})
.finally(() => this.isSavingSubject.next(false));
this.#isSavingSubject.set(true);
this.#publicationRestService.update(publication)
.then(() => {
this.#snackBar.open($localize`Publication updated succesfully!`, $localize`Close`, {duration: 5000});
this.#router.navigate(['/home']);
})
.catch(error => {
const errorMessage = $localize`An error occured while saving publication modifications.`;
console.error(errorMessage, error);
this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
})
.finally(() => this.#isSavingSubject.set(false));
}
}

View File

@@ -1,7 +1,7 @@
import { Route } from "@angular/router";
import { PublicationUpdateComponent } from "./publication-update.component";
import { authenticationGuard } from "../../core/guard/authentication.guard";
import {Route} from "@angular/router";
import {PublicationUpdateComponent} from "./publication-update.component";
import {authenticationGuard} from "../../core/guard/authentication.guard";
export const ROUTES: Route[] = [
{ path: '', component: PublicationUpdateComponent, canActivate: [authenticationGuard] }
]
{path: '', component: PublicationUpdateComponent, canActivate: [authenticationGuard]}
]

View File

@@ -1,50 +1,50 @@
@if (isLoading) {
<h2 i18n>Publication content loading...</h2>
<mat-spinner></mat-spinner>
@if (isLoading()) {
<h2 i18n>Publication content loading...</h2>
<mat-spinner></mat-spinner>
} @else {
@if (publication) {
<div class="card">
<img src="/api/pictures/{{ publication.illustrationId }}" />
<header>
<h1>{{ publication.title }}</h1>
<h2>{{ publication.description }}</h2>
@if (isAuthorAndUserEquals) {
<a [routerLink]="['edit']"
class="button action"
matTooltip="Click to edit the publication"
matRipple
i18n-matTooltip>
<mat-icon>edit</mat-icon>
</a>
}
</header>
<main [innerHTML]="publication.parsedText"></main>
<footer>
<div class="metadata">
<img src="/api/pictures/{{ publication.author.image }}" [matTooltip]="publication.author.name" />
<div class="posting-data">
<span i18n>Publication posted by {{ publication.author.name }}</span>
<span class="publication-date">
@if (publication(); as publication) {
<div class="card">
<img src="/api/pictures/{{ publication.illustrationId }}"/>
<header>
<h1>{{ publication.title }}</h1>
<h2>{{ publication.description }}</h2>
@if (isAuthorAndUserEquals()) {
<a [routerLink]="['edit']"
class="button action"
matTooltip="Click to edit the publication"
matRipple
i18n-matTooltip>
<mat-icon>edit</mat-icon>
</a>
}
</header>
<main [innerHTML]="publication.parsedText"></main>
<footer>
<div class="metadata">
<img src="/api/pictures/{{ publication.author.image }}" [matTooltip]="publication.author.name"/>
<div class="posting-data">
<span i18n>Publication posted by {{ publication.author.name }}</span>
<span class="publication-date">
({{ publication.creationDate | date: 'short' }})
</span>
</div>
</div>
@if (isAuthorAndUserEquals) {
<button type="button"
(click)="deletePublication()"
matTooltip="Click to delete the publication"
matTooltipPosition="left"
matRipple
i18n-matTooltip>
<mat-icon>delete</mat-icon>
Delete
</button>
}
</footer>
</div>
</div>
} @else {
<div class="loading-failed">
<h1 i18n>Publication failed to load...</h1>
</div>
}
}
@if (isAuthorAndUserEquals()) {
<button type="button"
(click)="deletePublication()"
matTooltip="Click to delete the publication"
matTooltipPosition="left"
matRipple
i18n-matTooltip>
<mat-icon>delete</mat-icon>
Delete
</button>
}
</footer>
</div>
} @else {
<div class="loading-failed">
<h1 i18n>Publication failed to load...</h1>
</div>
}
}

View File

@@ -1,140 +1,140 @@
$cardBorderRadius: .5em;
:host {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
.card {
.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;
position: relative;
h1 {
font-size: 2em;
margin-bottom: .5em;
}
h2 {
font-size: 1.2em;
line-height: 1.6em;
margin: 0;
font-weight: 400;
}
a {
padding: .8em .8em;
border-radius: 10em;
border: none;
background-color: #3f51b5;
color: white;
transition: background-color .2s ease-in-out;
position: absolute;
top: -1.5em;
right: 3em;
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);
justify-content: center;
align-items: center;
&:hover {
background-color: #5b6ed8;
cursor: pointer;
}
}
}
main {
border-top: 1px solid #dddddd;
margin: 0 2em 2em 2em;
padding-top: 2em;
text-align: justify;
}
footer {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background-color: #f0f0f0;
border-radius: 0 0 $cardBorderRadius $cardBorderRadius;
padding: 1em 2em;
gap: 1em;
@media screen and (min-width: 500px) {
flex-direction: row;
}
.metadata {
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
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;
}
$imageSize: 4em;
border-radius: 10em;
width: $imageSize;
height: $imageSize;
object-fit: cover;
}
header {
padding: 2em;
position: relative;
.posting-data {
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
h1 {
font-size: 2em;
margin-bottom: .5em;
}
h2 {
font-size: 1.2em;
line-height: 1.6em;
margin: 0;
font-weight: 400;
}
a {
padding: .8em .8em;
border-radius: 10em;
border: none;
background-color: #3f51b5;
color: white;
transition: background-color .2s ease-in-out;
position: absolute;
top: -1.5em;
right: 3em;
display: flex;
justify-content: center;
align-items: center;
&:hover {
background-color: #5b6ed8;
cursor: pointer;
}
}
.publication-date {
font-style: italic;
color: #bdbdbd;
}
}
}
main {
border-top: 1px solid #dddddd;
margin: 0 2em 2em 2em;
padding-top: 2em;
text-align: justify;
}
footer {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background-color: #f0f0f0;
border-radius: 0 0 $cardBorderRadius $cardBorderRadius;
padding: 1em 2em;
gap: 1em;
@media screen and (min-width: 500px) {
flex-direction: row;
}
.metadata {
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
img {
$imageSize: 4em;
border-radius: 10em;
width: $imageSize;
height: $imageSize;
object-fit: cover;
}
.posting-data {
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
.publication-date {
font-style: italic;
color: #bdbdbd;
}
}
}
button {
padding: .8em 1.2em;
border-radius: 10em;
border: none;
background-color: #D50000;
color: white;
transition: background-color .2s ease-in-out;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: .5em;
&:hover {
background-color: #E53935;
cursor: pointer;
}
}
button {
padding: .8em 1.2em;
border-radius: 10em;
border: none;
background-color: #D50000;
color: white;
transition: background-color .2s ease-in-out;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: .5em;
&:hover {
background-color: #E53935;
cursor: pointer;
}
}
}
}
}
}

View File

@@ -1,32 +1,32 @@
import { CommonModule, Location } from '@angular/common';
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { Subscription } from 'rxjs';
import { ConfirmationDialog } from '../../components/confirmation-dialog/confirmation-dialog.component';
import { Publication } from '../../core/rest-services/publications/model/publication';
import { PublicationRestService } from '../../core/rest-services/publications/publication.rest-service';
import { AuthenticationService } from '../../core/service/authentication.service';
import {CommonModule, Location} from '@angular/common';
import {Component, inject, OnDestroy, OnInit, signal} from '@angular/core';
import {MatRippleModule} from '@angular/material/core';
import {MatDialog} from '@angular/material/dialog';
import {MatIcon} from '@angular/material/icon';
import {MatProgressSpinner} from '@angular/material/progress-spinner';
import {MatSnackBar} from '@angular/material/snack-bar';
import {MatTooltipModule} from '@angular/material/tooltip';
import {ActivatedRoute, RouterModule} from '@angular/router';
import {Subscription} from 'rxjs';
import {ConfirmationDialog} from '../../components/confirmation-dialog/confirmation-dialog.component';
import {Publication} from '../../core/rest-services/publications/model/publication';
import {PublicationRestService} from '../../core/rest-services/publications/publication.rest-service';
import {AuthenticationService} from '../../core/service/authentication.service';
declare let Prism: any;
@Component({
selector: 'app-publication',
templateUrl: './publication.component.html',
styleUrl: './publication.component.scss',
imports: [
CommonModule,
MatIcon,
MatRippleModule,
MatProgressSpinner,
MatTooltipModule,
RouterModule
]
selector: 'app-publication',
templateUrl: './publication.component.html',
styleUrl: './publication.component.scss',
imports: [
CommonModule,
MatIcon,
MatRippleModule,
MatProgressSpinner,
MatTooltipModule,
RouterModule
]
})
export class PublicationComponent implements OnInit, OnDestroy {
private readonly activatedRoute = inject(ActivatedRoute);
@@ -37,35 +37,35 @@ export class PublicationComponent implements OnInit, OnDestroy {
private readonly snackBar = inject(MatSnackBar);
private paramMapSubscription?: Subscription;
private afterDialogCloseSubscription?: Subscription;
isLoading: boolean = false;
isAuthorAndUserEquals: boolean = false;
publication?: Publication;
isLoading = signal(false);
isAuthorAndUserEquals = signal(false);
publication = signal<Publication | null>(null);
ngOnInit(): void {
this.paramMapSubscription = this.activatedRoute
.paramMap
.subscribe(params => {
const publicationId = params.get('publicationId');
.paramMap
.subscribe(params => {
const publicationId = params.get('publicationId');
if (publicationId) {
this.isLoading = true;
if (publicationId) {
this.isLoading.set(true);
this.publicationRestService.getById(publicationId)
.then(publication => {
this.publication = publication;
this.isAuthorAndUserEquals = this.authenticationService.getAuthenticatedUser()?.id === this.publication.author.id;
setTimeout(() => Prism.highlightAll(), 100);
})
.catch(error => {
const errorMessage = $localize`An error occurred while loading publication...`;
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
console.error(errorMessage, error);
})
.finally(() => {
this.isLoading = false;
});
}
});
this.publicationRestService.getById(publicationId)
.then(publication => {
this.publication.set(publication);
this.isAuthorAndUserEquals.set(this.authenticationService.getAuthenticatedUser()?.id === this.publication()?.author.id);
setTimeout(() => Prism.highlightAll(), 100);
})
.catch(error => {
const errorMessage = $localize`An error occurred while loading publication...`;
this.snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
console.error(errorMessage, error);
})
.finally(() => {
this.isLoading.set(false);
});
}
});
}
ngOnDestroy(): void {
@@ -86,9 +86,10 @@ export class PublicationComponent implements OnInit, OnDestroy {
this.afterDialogCloseSubscription = dialogRef.afterClosed()
.subscribe(response => {
if (response && this.publication?.id) {
this.publicationRestService.delete(this.publication.id);
this.snackBar.open($localize`Publication deleted`, $localize`Close`, { duration: 5000 });
const publication = this.publication();
if (response && publication?.id) {
this.publicationRestService.delete(publication.id);
this.snackBar.open($localize`Publication deleted`, $localize`Close`, {duration: 5000});
this.location.back();
}
});

View File

@@ -1,13 +1,13 @@
<h1 i18n>Search results</h1>
@if((isLoading$ | async) === true) {
<h2 i18n>Search in progress...</h2>
<mat-spinner></mat-spinner>
} @else if((isLoaded$ | async) === true) {
@if((publications$ | async)?.length) {
<app-publication-list [publications$]="publications$"></app-publication-list>
} @else {
<span i18n>No any result.</span>
}
} @else {
@if (isLoading()) {
<h2 i18n>Search in progress...</h2>
<mat-spinner/>
} @else if (isLoaded()) {
@if (publications().length) {
<app-publication-list [publications]="publications()"/>
} @else {
<span i18n>No any result.</span>
}
} @else {
<span i18n>No any result.</span>
}

View File

@@ -1,6 +1,6 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

View File

@@ -1,51 +1,39 @@
import { CommonModule } from "@angular/common";
import { Component, inject, OnDestroy, OnInit } from "@angular/core";
import { MatProgressSpinner } from "@angular/material/progress-spinner";
import { ActivatedRoute } from "@angular/router";
import { Observable, Subscription } from "rxjs";
import { PublicationListComponent } from "../../components/publication-list/publication-list.component";
import { Publication } from "../../core/rest-services/publications/model/publication";
import { SearchPublicationsService } from "./search-publications.service";
import {CommonModule} from "@angular/common";
import {Component, effect, inject} from "@angular/core";
import {MatProgressSpinner} from "@angular/material/progress-spinner";
import {ActivatedRoute} from "@angular/router";
import {PublicationListComponent} from "../../components/publication-list/publication-list.component";
import {SearchPublicationsService} from "./search-publications.service";
import {toSignal} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-search-publications',
templateUrl: './search-publications.component.html',
styleUrl: './search-publications.component.scss',
imports: [CommonModule, MatProgressSpinner, PublicationListComponent],
providers: [SearchPublicationsService]
selector: 'app-search-publications',
templateUrl: './search-publications.component.html',
styleUrl: './search-publications.component.scss',
imports: [CommonModule, MatProgressSpinner, PublicationListComponent],
providers: [SearchPublicationsService]
})
export class SearchPublicationsComponent implements OnInit, OnDestroy {
private searchPublicationsService = inject(SearchPublicationsService);
private activatedRoute = inject(ActivatedRoute);
private queryParamsSubscription?: Subscription;
export class SearchPublicationsComponent {
private searchPublicationsService = inject(SearchPublicationsService);
private activatedRoute = inject(ActivatedRoute);
publications = this.searchPublicationsService.publications;
isLoading = this.searchPublicationsService.isLoading;
isLoaded = this.searchPublicationsService.isLoaded;
ngOnInit(): void {
this.queryParamsSubscription = this.activatedRoute.queryParamMap
.subscribe(params => {
const categoryId = params.get('category-id');
if (categoryId) {
this.searchPublicationsService.loadPublications(`category_id=${categoryId}`);
}
const query = params.get('query')
if (query) {
this.searchPublicationsService.loadPublications(query);
}
});
}
ngOnDestroy(): void {
this.queryParamsSubscription?.unsubscribe();
}
get publications$(): Observable<Publication[]> {
return this.searchPublicationsService.publications$;
}
get isLoading$(): Observable<boolean> {
return this.searchPublicationsService.isLoading$;
}
get isLoaded$(): Observable<boolean> {
return this.searchPublicationsService.isLoaded$;
}
}
constructor() {
const queryParams = toSignal(this.activatedRoute.queryParamMap);
effect(() => {
let params = queryParams();
if (params) {
const categoryId = params.get('category-id');
if (categoryId) {
this.searchPublicationsService.loadPublications(`category_id=${categoryId}`);
}
const query = params.get('query')
if (query) {
this.searchPublicationsService.loadPublications(query);
}
}
});
}
}

View File

@@ -1,49 +1,47 @@
import { inject, Injectable } from "@angular/core";
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
import { BehaviorSubject, Observable } from "rxjs";
import { Publication } from "../../core/rest-services/publications/model/publication";
import { MatSnackBar } from "@angular/material/snack-bar";
import {inject, Injectable, Signal, signal} from "@angular/core";
import {PublicationRestService} from "../../core/rest-services/publications/publication.rest-service";
import {Publication} from "../../core/rest-services/publications/model/publication";
import {MatSnackBar} from "@angular/material/snack-bar";
@Injectable()
export class SearchPublicationsService {
private publicationRestService = inject(PublicationRestService);
private publicationsSubject = new BehaviorSubject<Publication[]>([]);
private isLoadingSubject = new BehaviorSubject<boolean>(false);
private isLoadedSubject = new BehaviorSubject<boolean>(false);
private snackBar = inject(MatSnackBar);
#snackBar = inject(MatSnackBar);
#publicationRestService = inject(PublicationRestService);
#publications = signal<Publication[]>([]);
#isLoading = signal(false);
#isLoaded = signal(false);
get publications$(): Observable<Publication[]> {
return this.publicationsSubject.asObservable();
}
get isLoading$(): Observable<boolean> {
return this.isLoadingSubject.asObservable();
}
get publications(): Signal<Publication[]> {
return this.#publications.asReadonly();
}
get isLoaded$(): Observable<boolean> {
return this.isLoadedSubject.asObservable();
}
get isLoading(): Signal<boolean> {
return this.#isLoading.asReadonly();
}
loadPublications(searchCriteria: string): void {
this.isLoadingSubject.next(true);
this.isLoadedSubject.next(false);
this.publicationsSubject.next([]);
get isLoaded(): Signal<boolean> {
return this.#isLoaded.asReadonly();
}
this.publicationRestService.search(searchCriteria)
.then(publications => {
this.publicationsSubject.next(publications);
})
.catch(error => {
if (error.status !== 404) {
const errorMessage = $localize`An error occured while retrieving publications.`;
console.error(errorMessage, error);
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
}
})
.finally(() => {
this.isLoadingSubject.next(false);
this.isLoadedSubject.next(true);
});
}
}
loadPublications(searchCriteria: string): void {
this.#isLoading.set(true);
this.#isLoaded.set(false);
this.#publications.set([]);
this.#publicationRestService.search(searchCriteria)
.then(publications => {
this.#publications.set(publications);
})
.catch(error => {
if (error.status !== 404) {
const errorMessage = $localize`An error occured while retrieving publications.`;
console.error(errorMessage, error);
this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
}
})
.finally(() => {
this.#isLoading.set(false);
this.#isLoaded.set(true);
});
}
}

View File

@@ -1,39 +1,39 @@
<form [formGroup]="signinForm" (submit)="performSignin()" class="cod-form card" ngNativeValidate>
<h1 i18n>Signin</h1>
<div class="form-field">
<mat-icon>person</mat-icon>
<label for="pseudo" i18n>
Pseudo
<span class="required">*</span>
</label>
<input type="text" id="pseudo" formControlName="pseudo" autocomplete="pseudo" required />
</div>
<div class="form-field">
<mat-icon>mail</mat-icon>
<label for="email" i18n>
Email address
<span class="required">*</span>
</label>
<input type="email" id="email" formControlName="email" autocomplete="email" required />
</div>
<div class="form-field">
<mat-icon>lock</mat-icon>
<label for="password" i18n>
Password
<span class="required">*</span>
</label>
<input type="password" id="password" formControlName="password" required />
</div>
<div class="form-field">
<mat-icon>lock</mat-icon>
<label for="confirm-password" i18n>
Confirm password
<span class="required">*</span>
</label>
<input type="password" id="confirm-password" formControlName="confirmPassword" required />
</div>
<div class="actions reversed">
<app-submit-button [requestPending]="false" [disabled]="false" i18n>Send</app-submit-button>
<a [routerLink]="['/login']" class="cod-button secondary" matRipple i18n>I already have an account</a>
</div>
</form>
<h1 i18n>Signin</h1>
<div class="form-field">
<mat-icon>person</mat-icon>
<label for="pseudo" i18n>
Pseudo
<span class="required">*</span>
</label>
<input type="text" id="pseudo" formControlName="pseudo" autocomplete="pseudo" required/>
</div>
<div class="form-field">
<mat-icon>mail</mat-icon>
<label for="email" i18n>
Email address
<span class="required">*</span>
</label>
<input type="email" id="email" formControlName="email" autocomplete="email" required/>
</div>
<div class="form-field">
<mat-icon>lock</mat-icon>
<label for="password" i18n>
Password
<span class="required">*</span>
</label>
<input type="password" id="password" formControlName="password" required/>
</div>
<div class="form-field">
<mat-icon>lock</mat-icon>
<label for="confirm-password" i18n>
Confirm password
<span class="required">*</span>
</label>
<input type="password" id="confirm-password" formControlName="confirmPassword" required/>
</div>
<div class="actions reversed">
<app-submit-button [requestPending]="false" [disabled]="false" i18n>Send</app-submit-button>
<a [routerLink]="['/login']" class="cod-button secondary" matRipple i18n>I already have an account</a>
</div>
</form>

View File

@@ -1,7 +1,7 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1em;
}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1em;
}

View File

@@ -1,26 +1,26 @@
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';
import { SubmitButtonComponent } from "../../components/submit-button/submit-button.component";
import { MatRippleModule } from '@angular/material/core';
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatIconModule} from '@angular/material/icon';
import {RouterModule} from '@angular/router';
import {debounceTime, distinctUntilChanged, map, Subscription} from 'rxjs';
import {SigninService} from './signin.service';
import {LoginService} from '../login/login.service';
import {FormError} from '../../core/model/FormError';
import {SubmitButtonComponent} from "../../components/submit-button/submit-button.component";
import {MatRippleModule} from '@angular/material/core';
@Component({
selector: 'app-signin',
templateUrl: './signin.component.html',
styleUrl: './signin.component.scss',
imports: [
MatIconModule,
MatRippleModule,
ReactiveFormsModule,
RouterModule,
SubmitButtonComponent
],
providers: [SigninService, LoginService]
selector: 'app-signin',
templateUrl: './signin.component.html',
styleUrl: './signin.component.scss',
imports: [
MatIconModule,
MatRippleModule,
ReactiveFormsModule,
RouterModule,
SubmitButtonComponent
],
providers: [SigninService, LoginService]
})
export class SigninComponent implements OnInit, OnDestroy {
private signinService = inject(SigninService);
@@ -38,63 +38,63 @@ export class SigninComponent implements OnInit, OnDestroy {
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);
});
.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);
});
.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);
});
.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);
});
.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 });
});
.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();

View File

@@ -1,12 +1,12 @@
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';
import {inject, Injectable} 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;
@@ -74,12 +74,12 @@ export class SigninService {
state.confirmPassword = newConfirmPassword;
if (state.request.password !== state.confirmPassword) {
const confirmPasswordError: FormError = {
fieldName: 'confirmPassword',
errorMessage: $localize`Typed passwords are different.`
}
state.errors.filter(error => error.fieldName !== 'confirmPassword');
state.errors.push(confirmPasswordError)
const confirmPasswordError: FormError = {
fieldName: 'confirmPassword',
errorMessage: $localize`Typed passwords are different.`
}
state.errors.filter(error => error.fieldName !== 'confirmPassword');
state.errors.push(confirmPasswordError)
}
this.save(state);
@@ -91,12 +91,12 @@ export class SigninService {
// Check state is valid
this.userRestService
.signin(state.request)
.then(() => {
this.loginService.editEmail(state.request.email!!);
this.loginService.editPassword(state.request.password!!);
.signin(state.request)
.then(() => {
this.loginService.editEmail(state.request.email!!);
this.loginService.editPassword(state.request.password!!);
this.loginService.performLogin();
})
this.loginService.performLogin();
})
}
}