diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index ab227fa..58985bc 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,5 +1,5 @@ import { ApplicationConfig } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { provideRouter, withRouterConfig } from '@angular/router'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; @@ -8,7 +8,13 @@ import { JwtInterceptor } from './core/interceptor/jwt.interceptor'; export const appConfig: ApplicationConfig = { providers: [ - provideRouter(routes), + provideRouter( + routes, + withRouterConfig({ + paramsInheritanceStrategy: 'always', + onSameUrlNavigation: 'reload' + }) + ), provideAnimationsAsync(), provideHttpClient(withInterceptorsFromDi()), { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 70e78e0..1507ec2 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -21,6 +21,10 @@ export const routes: Routes = [ path: 'publications/:publicationId/edit', loadComponent: () => import('./pages/publication-edition/publication-edition.component').then(module => module.PublicationEditionComponent) }, + { + path: 'publications', + loadComponent: () => import('./pages/search-publications/search-publications.component').then(module => module.SearchPublicationsComponent) + }, { path: '**', loadComponent: () => import('./pages/home/home.component').then(module => module.HomeComponent) diff --git a/frontend/src/app/components/publication-list/publication-list.component.html b/frontend/src/app/components/publication-list/publication-list.component.html new file mode 100644 index 0000000..7cb99ea --- /dev/null +++ b/frontend/src/app/components/publication-list/publication-list.component.html @@ -0,0 +1,16 @@ +@for(publication of publications$ | async; track publication) { + + +
+

{{publication.title}}

+

{{publication.description}}

+
+ +
+} \ No newline at end of file diff --git a/frontend/src/app/components/publication-list/publication-list.component.scss b/frontend/src/app/components/publication-list/publication-list.component.scss new file mode 100644 index 0000000..a43901b --- /dev/null +++ b/frontend/src/app/components/publication-list/publication-list.component.scss @@ -0,0 +1,87 @@ +$cardBorderRadius: .5em; + +:host { + display: flex; + flex-direction: column; + gap: 2em; + max-width: 50em; + width: 90%; + margin: auto; + + .publication { + display: flex; + flex-direction: column; + border-radius: $cardBorderRadius; + box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); + transition: box-shadow .2s ease-in-out; + text-decoration: none; + color: black; + background-color: #ffffff; + + &:hover { + box-shadow: 0 4px 8px 0 rgba(0,0,0,.24),0 4px 14px 0 rgba(0,0,0,.16); + } + + img { + object-fit: cover; + height: 15em; + 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 { + display: flex; + flex-direction: column; + padding: 1.5em 2em; + + h1 { + font-size: 1.8em; + margin-bottom: .5em; + } + + h2 { + font-size: 1em; + line-height: 1.4em; + margin: 0; + color: #747373; + font-weight: 400; + } + } + + .footer { + display: flex; + flex-direction: row; + align-items: center; + background-color: #f0f0f0; + border-radius: 0 0 $cardBorderRadius $cardBorderRadius; + padding: 1em 2em; + gap: 1em; + color: #6c757d; + + img { + $imageSize: 4em; + border-radius: 10em; + width: $imageSize; + height: $imageSize; + object-fit: cover; + } + + .publication-date { + font-style: italic; + color: #bdbdbd; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/publication-list/publication-list.component.ts b/frontend/src/app/components/publication-list/publication-list.component.ts new file mode 100644 index 0000000..be463a3 --- /dev/null +++ b/frontend/src/app/components/publication-list/publication-list.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from "@angular/core"; +import { Publication } from "../../core/rest-services/publications/model/publication"; +import { Observable } from "rxjs"; +import { CommonModule } from "@angular/common"; +import { RouterModule } from "@angular/router"; +import { MatTooltipModule } from "@angular/material/tooltip"; + +@Component({ + selector: 'app-publication-list', + standalone: true, + templateUrl: './publication-list.component.html', + styleUrl: './publication-list.component.scss', + imports: [CommonModule, RouterModule, MatTooltipModule] +}) +export class PublicationListComponent { + @Input() + publications$!: Observable; +} \ No newline at end of file diff --git a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html index e32d421..8506529 100644 --- a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html +++ b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html @@ -6,7 +6,7 @@
@for(subCategory of category.subCategories; track subCategory) { - + {{subCategory.name}} } diff --git a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts index 6a6c408..f498224 100644 --- a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts +++ b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, inject, OnInit } from "@angular/core"; +import { Component, EventEmitter, inject, OnInit, Output } from "@angular/core"; import { MatIconModule } from "@angular/material/icon"; import { DisplayableCategory, SideMenuService } from "../side-menu.service"; import { Observable } from "rxjs"; @@ -14,6 +14,8 @@ import { RouterModule } from "@angular/router"; }) export class CategoriesMenuComponent implements OnInit { private sideMenuService = inject(SideMenuService); + @Output() + categoryClicked = new EventEmitter(); ngOnInit(): void { this.sideMenuService.loadCategories(); diff --git a/frontend/src/app/components/side-menu/side-menu.component.html b/frontend/src/app/components/side-menu/side-menu.component.html index 1a336a5..f4fc33a 100644 --- a/frontend/src/app/components/side-menu/side-menu.component.html +++ b/frontend/src/app/components/side-menu/side-menu.component.html @@ -9,6 +9,6 @@

Catégories

- +
diff --git a/frontend/src/app/pages/home/home.component.scss b/frontend/src/app/pages/home/home.component.scss index 21a6bed..01013a5 100644 --- a/frontend/src/app/pages/home/home.component.scss +++ b/frontend/src/app/pages/home/home.component.scss @@ -1,94 +1,6 @@ -$cardBorderRadius: .5em; - :host { display: flex; flex-direction: column; justify-content: center; align-items: center; - - .publication-container { - display: flex; - flex-direction: column; - gap: 2em; - max-width: 50em; - width: 90%; - margin: auto; - - .publication { - display: flex; - flex-direction: column; - border-radius: $cardBorderRadius; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); - transition: box-shadow .2s ease-in-out; - text-decoration: none; - color: black; - background-color: #ffffff; - - &:hover { - box-shadow: 0 4px 8px 0 rgba(0,0,0,.24),0 4px 14px 0 rgba(0,0,0,.16); - } - - img { - object-fit: cover; - height: 15em; - 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 { - display: flex; - flex-direction: column; - padding: 1.5em 2em; - - h1 { - font-size: 1.8em; - margin-bottom: .5em; - } - - h2 { - font-size: 1em; - line-height: 1.4em; - margin: 0; - color: #747373; - font-weight: 400; - } - } - - .footer { - display: flex; - flex-direction: row; - align-items: center; - background-color: #f0f0f0; - border-radius: 0 0 $cardBorderRadius $cardBorderRadius; - padding: 1em 2em; - gap: 1em; - color: #6c757d; - - img { - $imageSize: 4em; - border-radius: 10em; - width: $imageSize; - height: $imageSize; - object-fit: cover; - } - - .publication-date { - font-style: italic; - color: #bdbdbd; - } - } - } - } } \ No newline at end of file diff --git a/frontend/src/app/pages/search-publications/search-publications.component.html b/frontend/src/app/pages/search-publications/search-publications.component.html new file mode 100644 index 0000000..4151968 --- /dev/null +++ b/frontend/src/app/pages/search-publications/search-publications.component.html @@ -0,0 +1,13 @@ +

Search results

+@if((isLoading$ | async) === true) { +

Search in progress...

+ +} @else if((isLoaded$ | async) === true) { + @if((publications$ | async)?.length) { + + } @else { + No any result. + } +} @else { + No any result. +} diff --git a/frontend/src/app/pages/search-publications/search-publications.component.scss b/frontend/src/app/pages/search-publications/search-publications.component.scss new file mode 100644 index 0000000..01013a5 --- /dev/null +++ b/frontend/src/app/pages/search-publications/search-publications.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/frontend/src/app/pages/search-publications/search-publications.component.ts b/frontend/src/app/pages/search-publications/search-publications.component.ts new file mode 100644 index 0000000..89e99b4 --- /dev/null +++ b/frontend/src/app/pages/search-publications/search-publications.component.ts @@ -0,0 +1,48 @@ +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"; + +@Component({ + selector: 'app-search-publications', + templateUrl: './search-publications.component.html', + styleUrl: './search-publications.component.scss', + standalone: true, + imports: [CommonModule, MatProgressSpinner, PublicationListComponent], + providers: [SearchPublicationsService] +}) +export class SearchPublicationsComponent implements OnInit, OnDestroy { + private searchPublicationsService = inject(SearchPublicationsService); + private activatedRoute = inject(ActivatedRoute); + private queryParamsSubscription?: Subscription; + + ngOnInit(): void { + this.queryParamsSubscription = this.activatedRoute.queryParamMap + .subscribe(params => { + const categoryId = params.get('category-id'); + if (categoryId) { + this.searchPublicationsService.loadPublications(`category_id=${categoryId}`); + } + }); + } + + ngOnDestroy(): void { + this.queryParamsSubscription?.unsubscribe(); + } + + get publications$(): Observable { + return this.searchPublicationsService.publications$; + } + + get isLoading$(): Observable { + return this.searchPublicationsService.isLoading$; + } + + get isLoaded$(): Observable { + return this.searchPublicationsService.isLoaded$; + } +} \ No newline at end of file diff --git a/frontend/src/app/pages/search-publications/search-publications.service.ts b/frontend/src/app/pages/search-publications/search-publications.service.ts new file mode 100644 index 0000000..51660af --- /dev/null +++ b/frontend/src/app/pages/search-publications/search-publications.service.ts @@ -0,0 +1,49 @@ +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"; + + +@Injectable() +export class SearchPublicationsService { + private publicationRestService = inject(PublicationRestService); + private publicationsSubject = new BehaviorSubject([]); + private isLoadingSubject = new BehaviorSubject(false); + private isLoadedSubject = new BehaviorSubject(false); + private snackBar = inject(MatSnackBar); + + get publications$(): Observable { + return this.publicationsSubject.asObservable(); + } + + get isLoading$(): Observable { + return this.isLoadingSubject.asObservable(); + } + + get isLoaded$(): Observable { + return this.isLoadedSubject.asObservable(); + } + + loadPublications(searchCriteria: string): void { + this.isLoadingSubject.next(true); + this.isLoadedSubject.next(false); + this.publicationsSubject.next([]); + + this.publicationRestService.search(searchCriteria) + .then(publications => { + this.publicationsSubject.next(publications); + }) + .catch(error => { + if (error.status !== 404) { + const errorMessage = 'An error occured while retrieving publications.'; + console.error(errorMessage, error); + this.snackBar.open(errorMessage, 'Close', { duration: 5000 }); + } + }) + .finally(() => { + this.isLoadingSubject.next(false); + this.isLoadedSubject.next(true); + }); + } +} \ No newline at end of file