From ddba76795ca411cc7a423dc6ea7705c866f9e6ac Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Thu, 25 Jul 2024 13:12:26 +0700 Subject: [PATCH 01/64] Create pagination JC-660 --- .../dashboard/dashboard.component.html | 66 ++++++++++++++++ .../features/dashboard/dashboard.component.ts | 79 +++++++++++++++++++ .../src/core/services/anime-api.service.ts | 1 - 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 apps/angular/src/app/features/dashboard/dashboard.component.html create mode 100644 apps/angular/src/app/features/dashboard/dashboard.component.ts diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html new file mode 100644 index 00000000..d5977cd5 --- /dev/null +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -0,0 +1,66 @@ +
+ @if (animeListPage$ | async; as animeListPage) { +
+ + + + {{columnsHeaders.Image}} + +
+ +
+
+
+ + + {{columnsHeaders.EnglishTitle}} + +

+ {{element.englishTitle | empty}} +

+
+
+ + + {{columnsHeaders.JapaneseTitle}} + +

+ {{element.japaneseTitle | empty}} +

+
+
+ + + {{columnsHeaders.AiredStart}} + {{element.aired.start | date: "mediumDate" | empty}} + + + + {{columnsHeaders.Type}} + {{element.type | empty}} + + + + {{columnsHeaders.Status}} + {{element.status | empty}} + + + + + +
+ + +
+ } @else { + + } +
diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts new file mode 100644 index 00000000..a13fb17d --- /dev/null +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -0,0 +1,79 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { MatTableModule } from '@angular/material/table'; +import { AnimeApiService } from '@js-camp/angular/core/services/anime-api.service'; +import { AsyncPipe, DatePipe, NgOptimizedImage } from '@angular/common'; +import { EmptyPipe } from '@js-camp/angular/shared/pipes/empty.pipe'; +import { ProgressBarComponent } from '@js-camp/angular/shared/components/progress-bar/progress-bar.component'; +import { Anime } from '@js-camp/core/models/anime'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; + +/** Column headers to be displayed in table. */ +export enum ColumnsHeaders { + Image = 'Image', + EnglishTitle = 'English title', + JapaneseTitle = 'Japanese title', + AiredStart = 'Aired starts with', + Type = 'Type', + Status = 'Status', +} + +/** Dashboard component. Contains table with list of anime. */ +@Component({ + selector: 'camp-dashboard', + standalone: true, + templateUrl: './dashboard.component.html', + styleUrl: './dashboard.component.css', + imports: [ + MatTableModule, + MatPaginator, + AsyncPipe, + DatePipe, + EmptyPipe, + ProgressBarComponent, + NgOptimizedImage, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardComponent { + + private readonly animeApiService = inject(AnimeApiService); + + /** Stream of anime. */ + // protected readonly animeList$ = this.animeApiService.getList(); + + /** */ + protected page = 0; + + /** */ + protected pageSize = 25; + + /** */ + protected animeListPage$ = this.animeApiService.getPage({}); + + /** + * Page. + * @param event - Page. + */ + protected pageChanged(event: PageEvent): void { + this.page = event.pageIndex; + this.pageSize = event.pageSize; + this.animeListPage$ = this.animeApiService.getPage({ page: event.pageIndex, pageSize: event.pageSize }); + } + + /** + * Track by function for anime list. + * @param index - Anime list item id. + * @param item - Item of anime list. + * @returns Item's id. + */ + protected trackByAnime(index: number, item: Anime): Anime['id'] { + return item.id; + } + + /** Property containing enum with column headers. */ + protected readonly columnsHeaders = ColumnsHeaders; + + /** List of column headers. */ + protected readonly columnsToDisplay = Object.values(ColumnsHeaders); + +} diff --git a/apps/angular/src/core/services/anime-api.service.ts b/apps/angular/src/core/services/anime-api.service.ts index 02eca6a7..52c44176 100644 --- a/apps/angular/src/core/services/anime-api.service.ts +++ b/apps/angular/src/core/services/anime-api.service.ts @@ -11,7 +11,6 @@ import { Pagination } from '@js-camp/core/models/pagination'; import { PaginationMapper } from '@js-camp/core/mappers/pagination.mapper'; import { AnimeParams } from '@js-camp/core/models/anime-params'; import { AnimeParamsMapper } from '@js-camp/core/mappers/anime-params.mapper'; - import { UrlConfigService } from './url-config.service'; /** Anime API access service. */ From 75d1b19293e7f672195514088d243d260b45de48 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Thu, 25 Jul 2024 13:29:14 +0700 Subject: [PATCH 02/64] Create table sort JC-660 --- .../dashboard/dashboard.component.html | 18 ++++-- .../features/dashboard/dashboard.component.ts | 64 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index d5977cd5..c6778b6c 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,7 +1,11 @@
@if (animeListPage$ | async; as animeListPage) {
- + {{columnsHeaders.Image}} @@ -16,7 +20,9 @@ - {{columnsHeaders.EnglishTitle}} + {{columnsHeaders.EnglishTitle}}

{{element.englishTitle | empty}} @@ -34,7 +40,9 @@ - {{columnsHeaders.AiredStart}} + {{columnsHeaders.AiredStart}} {{element.aired.start | date: "mediumDate" | empty}} @@ -44,7 +52,9 @@ - {{columnsHeaders.Status}} + {{columnsHeaders.Status}} {{element.status | empty}} diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index a13fb17d..c096d320 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -6,6 +6,7 @@ import { EmptyPipe } from '@js-camp/angular/shared/pipes/empty.pipe'; import { ProgressBarComponent } from '@js-camp/angular/shared/components/progress-bar/progress-bar.component'; import { Anime } from '@js-camp/core/models/anime'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort, Sort, MatSortModule } from '@angular/material/sort'; /** Column headers to be displayed in table. */ export enum ColumnsHeaders { @@ -26,6 +27,8 @@ export enum ColumnsHeaders { imports: [ MatTableModule, MatPaginator, + MatSort, + MatSortModule, AsyncPipe, DatePipe, EmptyPipe, @@ -76,4 +79,65 @@ export class DashboardComponent { /** List of column headers. */ protected readonly columnsToDisplay = Object.values(ColumnsHeaders); + /** */ + protected sortOrder = { + title_eng: '', + aired__startswith: '', + status: '', + }; + + // dataSource = new MatTableDataSource(); + + /** Announce the change in sort state for assistive technology. + * @param sortState - Anime list item id. + */ + protected sortData(sortState: Sort): void { + console.log(sortState); + + let sortField: keyof typeof this.sortOrder; + + switch (sortState.active) { + case 'English title': + sortField = 'title_eng'; + break; + case 'Aired starts with': + sortField = 'aired__startswith'; + break; + case 'Status': + sortField = 'status'; + break; + default: + sortField = 'status'; + } + // console.log(sortField); + + switch (sortState.direction) { + case 'asc': + this.sortOrder[sortField] = sortField; + break; + case 'desc': + this.sortOrder[sortField] = `-${sortField}`; + break; + default: + this.sortOrder[sortField] = ''; + } + + // console.log(Object.values(this.sortOrder)); + + let orderString = ''; + Object.values(this.sortOrder).forEach(item => { + if (item) { + if (orderString) { + orderString = `${orderString},${item}`; + } else { + orderString = item; + } + + } + }); + + // const orderString = Object.values(this.sortOrder).join(','); + // console.log(orderString); + this.animeListPage$ = this.animeApiService.getPage({page: this.page, pageSize: this.pageSize, ordering: orderString}); + } } From 0b3879b64d1051c739160cb192e603536a9423e7 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Thu, 25 Jul 2024 16:11:43 +0700 Subject: [PATCH 03/64] Create search JC-644 --- .../dashboard/dashboard.component.html | 20 +++++++++++++++ .../features/dashboard/dashboard.component.ts | 25 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index c6778b6c..e9262e18 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,4 +1,24 @@

+
+ + Search + + + +
+ @if (animeListPage$ | async; as animeListPage) {
Date: Thu, 25 Jul 2024 17:04:08 +0700 Subject: [PATCH 04/64] Create filter by type JC-660 --- .../dashboard/dashboard.component.html | 47 ++++++++++++------- .../features/dashboard/dashboard.component.ts | 31 +++++++++++- libs/core/mappers/anime-type.mapper.ts | 11 +++++ 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index e9262e18..2585969c 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,23 +1,34 @@
-
- - Search - + + @if (animeListPage$ | async; as animeListPage) {
diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index 109acddc..11e13c2b 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -12,6 +12,10 @@ import { Anime } from '@js-camp/core/models/anime'; import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatSort, Sort, MatSortModule } from '@angular/material/sort'; +import { MatSelectModule } from '@angular/material/select'; +import { AnimeType } from '@js-camp/core/models/anime-type'; +import { toTypeDto } from '@js-camp/core/mappers/anime-type.mapper'; +import { AnimeTypeDto } from '@js-camp/core/dtos/anime-type.dto'; /** Column headers to be displayed in table. */ export enum ColumnsHeaders { @@ -38,6 +42,7 @@ export enum ColumnsHeaders { MatInputModule, MatButtonModule, MatIconModule, + MatSelectModule, ReactiveFormsModule, AsyncPipe, DatePipe, @@ -49,6 +54,27 @@ export enum ColumnsHeaders { }) export class DashboardComponent { + /** */ + protected types = new FormControl(); + + /** */ + protected typesList = Object.values(AnimeType); + + /** */ + protected onSelect(): void { + // TODO: Use EventEmitter with form value + console.log(this.types.value); + const typesArr = this.types.value as AnimeType[]; + let typesDtoArr: AnimeTypeDto[] = []; + + if (typesArr) { + typesDtoArr = typesArr.map(type => toTypeDto(type)); + } + const typesString = typesDtoArr.join(','); + console.log(typesDtoArr.join(',')); + this.animeListPage$ = this.animeApiService.getPage({ type__in: typesString }); + } + /** */ protected searchForm = new FormGroup({ term: new FormControl(''), @@ -58,7 +84,7 @@ export class DashboardComponent { protected onSubmit(): void { // TODO: Use EventEmitter with form value console.log(this.searchForm.value.term); - this.animeListPage$ = this.animeApiService.getPage({search: String(this.searchForm.value.term)}); + this.animeListPage$ = this.animeApiService.getPage({ search: String(this.searchForm.value.term) }); } /** */ @@ -134,6 +160,7 @@ export class DashboardComponent { default: sortField = 'status'; } + // console.log(sortField); switch (sortState.direction) { @@ -163,6 +190,6 @@ export class DashboardComponent { // const orderString = Object.values(this.sortOrder).join(','); // console.log(orderString); - this.animeListPage$ = this.animeApiService.getPage({page: this.page, pageSize: this.pageSize, ordering: orderString}); + this.animeListPage$ = this.animeApiService.getPage({ page: this.page, pageSize: this.pageSize, ordering: orderString }); } } diff --git a/libs/core/mappers/anime-type.mapper.ts b/libs/core/mappers/anime-type.mapper.ts index 9f5e9e93..3ec05ce9 100644 --- a/libs/core/mappers/anime-type.mapper.ts +++ b/libs/core/mappers/anime-type.mapper.ts @@ -42,3 +42,14 @@ export namespace AnimeTypeMapper { return ANIME_TYPE_MAP_TO_DTO[type]; } } + +/** + * Map type dto. + * @param type - Type dto. + * @returns Type model. + */ +export function toTypeDto(type: AnimeType): AnimeTypeDto { + const keyId = Object.values(AnimeType).indexOf(type); + const enumKey = Object.keys(AnimeType)[keyId] as TypeKey; + return AnimeTypeDto[enumKey]; +} From cb24b48f4701b681c4379e74ed42a2fd118efa19 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Thu, 25 Jul 2024 17:48:23 +0700 Subject: [PATCH 05/64] Add query params JC-660 --- .../features/dashboard/dashboard.component.ts | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index 11e13c2b..5a60f503 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { MatTableModule } from '@angular/material/table'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -16,6 +16,7 @@ import { MatSelectModule } from '@angular/material/select'; import { AnimeType } from '@js-camp/core/models/anime-type'; import { toTypeDto } from '@js-camp/core/mappers/anime-type.mapper'; import { AnimeTypeDto } from '@js-camp/core/dtos/anime-type.dto'; +import { ActivatedRoute, Router } from '@angular/router'; /** Column headers to be displayed in table. */ export enum ColumnsHeaders { @@ -52,7 +53,20 @@ export enum ColumnsHeaders { ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DashboardComponent { +export class DashboardComponent implements OnInit { + + private readonly route = inject(ActivatedRoute); + + private readonly router = inject(Router); + + /** */ + public ngOnInit(): void { + this.route.queryParams + .subscribe(params => { + console.log(params); + this.animeListPage$ = this.animeApiService.getPage(params); + }); + } /** */ protected types = new FormControl(); @@ -63,7 +77,7 @@ export class DashboardComponent { /** */ protected onSelect(): void { // TODO: Use EventEmitter with form value - console.log(this.types.value); + const typesArr = this.types.value as AnimeType[]; let typesDtoArr: AnimeTypeDto[] = []; @@ -71,8 +85,14 @@ export class DashboardComponent { typesDtoArr = typesArr.map(type => toTypeDto(type)); } const typesString = typesDtoArr.join(','); - console.log(typesDtoArr.join(',')); - this.animeListPage$ = this.animeApiService.getPage({ type__in: typesString }); + + this.router.navigate( + [''], + { + queryParams: { type__in: typesString }, + queryParamsHandling: 'merge', + }, + ); } /** */ @@ -82,9 +102,14 @@ export class DashboardComponent { /** */ protected onSubmit(): void { - // TODO: Use EventEmitter with form value - console.log(this.searchForm.value.term); - this.animeListPage$ = this.animeApiService.getPage({ search: String(this.searchForm.value.term) }); + + this.router.navigate( + [''], + { + queryParams: { search: String(this.searchForm.value.term) }, + queryParamsHandling: 'merge', + }, + ); } /** */ @@ -92,15 +117,6 @@ export class DashboardComponent { private readonly animeApiService = inject(AnimeApiService); - /** Stream of anime. */ - // protected readonly animeList$ = this.animeApiService.getList(); - - /** */ - protected page = 0; - - /** */ - protected pageSize = 25; - /** */ protected animeListPage$ = this.animeApiService.getPage({}); @@ -109,9 +125,14 @@ export class DashboardComponent { * @param event - Page. */ protected pageChanged(event: PageEvent): void { - this.page = event.pageIndex; - this.pageSize = event.pageSize; - this.animeListPage$ = this.animeApiService.getPage({ page: event.pageIndex, pageSize: event.pageSize }); + + this.router.navigate( + [''], + { + queryParams: { page: event.pageIndex, pageSize: event.pageSize }, + queryParamsHandling: 'merge', + }, + ); } /** @@ -137,13 +158,10 @@ export class DashboardComponent { status: '', }; - // dataSource = new MatTableDataSource(); - /** Announce the change in sort state for assistive technology. * @param sortState - Anime list item id. */ protected sortData(sortState: Sort): void { - console.log(sortState); let sortField: keyof typeof this.sortOrder; @@ -161,8 +179,6 @@ export class DashboardComponent { sortField = 'status'; } - // console.log(sortField); - switch (sortState.direction) { case 'asc': this.sortOrder[sortField] = sortField; @@ -174,8 +190,6 @@ export class DashboardComponent { this.sortOrder[sortField] = ''; } - // console.log(Object.values(this.sortOrder)); - let orderString = ''; Object.values(this.sortOrder).forEach(item => { if (item) { @@ -188,8 +202,12 @@ export class DashboardComponent { } }); - // const orderString = Object.values(this.sortOrder).join(','); - // console.log(orderString); - this.animeListPage$ = this.animeApiService.getPage({ page: this.page, pageSize: this.pageSize, ordering: orderString }); + this.router.navigate( + [''], + { + queryParams: { ordering: orderString }, + queryParamsHandling: 'merge', + }, + ); } } From fcc162fe4ea1d8c8df8f4cf9744d085d532db111 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Mon, 29 Jul 2024 09:26:16 +0700 Subject: [PATCH 06/64] Create filters, sort and search JC-660 --- .../dashboard/dashboard.component.html | 106 +-------- .../features/dashboard/dashboard.component.ts | 210 ++++++++---------- .../data-retrieval-form.component.css | 17 ++ .../data-retrieval-form.component.html | 29 +++ .../data-retrieval-form.component.ts | 53 +++++ .../app/features/table/table.component.css | 66 ++++++ .../app/features/table/table.component.html | 73 ++++++ .../src/app/features/table/table.component.ts | 78 +++++++ .../src/core/services/query-params.service.ts | 35 +++ .../src/core/services/table-sort.service.ts | 49 ++++ libs/core/dtos/query-params.dto.ts | 18 ++ libs/core/dtos/sorting-columns.dto.ts | 6 + libs/core/mappers/query-params.mapper.ts | 19 ++ libs/core/mappers/sorting-columns.mapper.ts | 16 ++ libs/core/models/pagination.ts | 2 +- libs/core/models/query-params.ts | 31 +++ libs/core/models/sorting-columns.ts | 6 + 17 files changed, 591 insertions(+), 223 deletions(-) create mode 100644 apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.css create mode 100644 apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.html create mode 100644 apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts create mode 100644 apps/angular/src/app/features/table/table.component.css create mode 100644 apps/angular/src/app/features/table/table.component.html create mode 100644 apps/angular/src/app/features/table/table.component.ts create mode 100644 apps/angular/src/core/services/query-params.service.ts create mode 100644 apps/angular/src/core/services/table-sort.service.ts create mode 100644 libs/core/dtos/query-params.dto.ts create mode 100644 libs/core/dtos/sorting-columns.dto.ts create mode 100644 libs/core/mappers/query-params.mapper.ts create mode 100644 libs/core/mappers/sorting-columns.mapper.ts create mode 100644 libs/core/models/query-params.ts create mode 100644 libs/core/models/sorting-columns.ts diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index 2585969c..eb231743 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,106 +1,12 @@
- + - @if (animeListPage$ | async; as animeListPage) { -
- - - - {{columnsHeaders.Image}} - -
- -
-
-
- - - {{columnsHeaders.EnglishTitle}} - -

- {{element.englishTitle | empty}} -

-
-
- - - {{columnsHeaders.JapaneseTitle}} - -

- {{element.japaneseTitle | empty}} -

-
-
- - - {{columnsHeaders.AiredStart}} - {{element.aired.start | date: "mediumDate" | empty}} - - - - {{columnsHeaders.Type}} - {{element.type | empty}} - - - - {{columnsHeaders.Status}} - {{element.status | empty}} - - - - - -
- - -
+ @if (animeListPage) { + } @else { } diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index 5a60f503..a49b309d 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -1,22 +1,18 @@ -import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; -import { MatTableModule } from '@angular/material/table'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatIconModule } from '@angular/material/icon'; -import { MatButtonModule } from '@angular/material/button'; -import { AnimeApiService } from '@js-camp/angular/core/services/anime-api.service'; -import { AsyncPipe, DatePipe, NgOptimizedImage } from '@angular/common'; -import { EmptyPipe } from '@js-camp/angular/shared/pipes/empty.pipe'; -import { ProgressBarComponent } from '@js-camp/angular/shared/components/progress-bar/progress-bar.component'; +import { Component, ChangeDetectionStrategy, inject, OnInit, OnDestroy } from '@angular/core'; +import { PageEvent } from '@angular/material/paginator'; +import { Sort } from '@angular/material/sort'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription, switchMap } from 'rxjs'; import { Anime } from '@js-camp/core/models/anime'; -import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; -import { MatPaginator, PageEvent } from '@angular/material/paginator'; -import { MatSort, Sort, MatSortModule } from '@angular/material/sort'; -import { MatSelectModule } from '@angular/material/select'; import { AnimeType } from '@js-camp/core/models/anime-type'; -import { toTypeDto } from '@js-camp/core/mappers/anime-type.mapper'; -import { AnimeTypeDto } from '@js-camp/core/dtos/anime-type.dto'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Pagination } from '@js-camp/core/models/pagination'; +import { QueryParamsDto } from '@js-camp/core/dtos/query-params.dto'; +import { QueryParamsService } from '@js-camp/angular/core/services/query-params.service'; +import { AnimeApiService } from '@js-camp/angular/core/services/anime-api.service'; +import { ProgressBarComponent } from '@js-camp/angular/shared/components/progress-bar/progress-bar.component'; + +import { TableComponent } from '../table/table.component'; +import { DataRetrievalFormComponent } from '../data-retrieval-form/data-retrieval-form.component'; /** Column headers to be displayed in table. */ export enum ColumnsHeaders { @@ -28,6 +24,15 @@ export enum ColumnsHeaders { Status = 'Status', } +/** Column headers to be displayed in table. */ +export enum ParamsNames { + Limit = 'limit', + Offset = 'offset', + Search = 'search', + Type = 'type__in', + Ordering = 'ordering', +} + /** Dashboard component. Contains table with list of anime. */ @Component({ selector: 'camp-dashboard', @@ -35,101 +40,110 @@ export enum ColumnsHeaders { templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.css', imports: [ - MatTableModule, - MatPaginator, - MatSort, - MatSortModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatIconModule, - MatSelectModule, - ReactiveFormsModule, - AsyncPipe, - DatePipe, - EmptyPipe, ProgressBarComponent, - NgOptimizedImage, + TableComponent, + DataRetrievalFormComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DashboardComponent implements OnInit { +export class DashboardComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); - /** */ - public ngOnInit(): void { - this.route.queryParams - .subscribe(params => { - console.log(params); - this.animeListPage$ = this.animeApiService.getPage(params); - }); - } + private readonly animeApiService = inject(AnimeApiService); + + private readonly queryParamsService = inject(QueryParamsService); /** */ - protected types = new FormControl(); + protected params: QueryParamsDto = { + offset: 0, + limit: 25, + }; /** */ - protected typesList = Object.values(AnimeType); + protected animeListPage?: Pagination; + + public constructor() { + + } /** */ - protected onSelect(): void { - // TODO: Use EventEmitter with form value + public animeSubscription?: Subscription; - const typesArr = this.types.value as AnimeType[]; - let typesDtoArr: AnimeTypeDto[] = []; + /** Subscribes on route parameters when the component is initialized. */ + public ngOnInit(): void { - if (typesArr) { - typesDtoArr = typesArr.map(type => toTypeDto(type)); - } - const typesString = typesDtoArr.join(','); + this.route.queryParams.pipe( + switchMap(params => { + + Object.assign(this.params, params); + + return this.animeApiService.getPage(this.params); + }), + ) + .subscribe(res => { + this.animeListPage = res; + }); + } + + /** Subscribes on route parameters when the component is initialized. */ + public ngOnDestroy(): void { + this.animeSubscription?.unsubscribe(); + } + + /** */ + protected onTypeSelect(event: AnimeType[]): void { + + const typesString = this.queryParamsService.composeTypeParam(event); this.router.navigate( [''], { - queryParams: { type__in: typesString }, + queryParams: { [ParamsNames.Type]: typesString }, queryParamsHandling: 'merge', }, ); } /** */ - protected searchForm = new FormGroup({ - term: new FormControl(''), - }); + protected onSearchSubmit(event: string): void { - /** */ - protected onSubmit(): void { + const queryParams: QueryParamsDto = { + offset: 0, + limit: 25, + }; + + if (ParamsNames.Type in this.params) { + queryParams[ParamsNames.Type] = this.params[ParamsNames.Type] as string; + } + if (ParamsNames.Ordering in this.params) { + queryParams[ParamsNames.Ordering] = this.params[ParamsNames.Ordering] as string; + } + queryParams[ParamsNames.Search] = event; this.router.navigate( [''], { - queryParams: { search: String(this.searchForm.value.term) }, - queryParamsHandling: 'merge', + queryParams, }, ); } - /** */ - protected searchKeywordFilter = new FormControl(); - - private readonly animeApiService = inject(AnimeApiService); - - /** */ - protected animeListPage$ = this.animeApiService.getPage({}); - /** * Page. * @param event - Page. */ - protected pageChanged(event: PageEvent): void { + protected setPage(event: PageEvent): void { + + const limit = event.pageSize; + const offset = event.pageIndex * event.pageSize; this.router.navigate( [''], { - queryParams: { page: event.pageIndex, pageSize: event.pageSize }, + queryParams: { [ParamsNames.Offset]: offset, [ParamsNames.Limit]: limit }, queryParamsHandling: 'merge', }, ); @@ -145,62 +159,13 @@ export class DashboardComponent implements OnInit { return item.id; } - /** Property containing enum with column headers. */ - protected readonly columnsHeaders = ColumnsHeaders; - - /** List of column headers. */ - protected readonly columnsToDisplay = Object.values(ColumnsHeaders); - - /** */ - protected sortOrder = { - title_eng: '', - aired__startswith: '', - status: '', - }; - - /** Announce the change in sort state for assistive technology. - * @param sortState - Anime list item id. + /** + * 1. + * @param sortState - 1. */ - protected sortData(sortState: Sort): void { - - let sortField: keyof typeof this.sortOrder; - - switch (sortState.active) { - case 'English title': - sortField = 'title_eng'; - break; - case 'Aired starts with': - sortField = 'aired__startswith'; - break; - case 'Status': - sortField = 'status'; - break; - default: - sortField = 'status'; - } - - switch (sortState.direction) { - case 'asc': - this.sortOrder[sortField] = sortField; - break; - case 'desc': - this.sortOrder[sortField] = `-${sortField}`; - break; - default: - this.sortOrder[sortField] = ''; - } - - let orderString = ''; - Object.values(this.sortOrder).forEach(item => { - if (item) { - if (orderString) { - orderString = `${orderString},${item}`; - } else { - orderString = item; - } + public setOrdering(sortState: Sort): void { - } - }); + const orderString = this.queryParamsService.composeOrderingParam(sortState); this.router.navigate( [''], @@ -210,4 +175,5 @@ export class DashboardComponent implements OnInit { }, ); } + } diff --git a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.css b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.css new file mode 100644 index 00000000..01236856 --- /dev/null +++ b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.css @@ -0,0 +1,17 @@ +.data-retrieval-form { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--space-s); +} + +.search-form { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--space-s); +} + +.search-form__button { + margin-bottom: 20px; +} diff --git a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.html b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.html new file mode 100644 index 00000000..fa72f5c2 --- /dev/null +++ b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.html @@ -0,0 +1,29 @@ +
+ + Types + + @for (type of typesList; track type) { + {{type}} + } + + +
+ + Attack on Titan + + + +
+
diff --git a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts new file mode 100644 index 00000000..2dad9ab5 --- /dev/null +++ b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts @@ -0,0 +1,53 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; +import { AnimeType } from '@js-camp/core/models/anime-type'; +import { MatSelectModule } from '@angular/material/select'; + +/** Dashboard component. Contains table with list of anime. */ +@Component({ + selector: 'camp-data-retrieval-form', + standalone: true, + templateUrl: './data-retrieval-form.component.html', + styleUrl: './data-retrieval-form.component.css', + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatSelectModule, + ], +}) +export class DataRetrievalFormComponent { + + /** Types select control. */ + protected types = new FormControl(); + + /** */ + protected typesList = Object.values(AnimeType); + + /** */ + @Output() public typeSelectEvent = new EventEmitter(); + + /** */ + protected onTypeSelect(): void { + this.typeSelectEvent.emit(this.types.value); + } + + /** Search form. */ + protected searchForm = new FormGroup({ + term: new FormControl(''), + }); + + /** */ + @Output() public searchEvent = new EventEmitter(); + + /** */ + protected onSearchSubmit(): void { + this.searchEvent.emit(String(this.searchForm.value.term)); + } +} diff --git a/apps/angular/src/app/features/table/table.component.css b/apps/angular/src/app/features/table/table.component.css new file mode 100644 index 00000000..aa11f404 --- /dev/null +++ b/apps/angular/src/app/features/table/table.component.css @@ -0,0 +1,66 @@ +.table-wrapper { + box-shadow: + inset 0 5px 5px -3px rgb(0 0 0 / 20%), + 0 8px 10px 1px rgb(0 0 0 / 14%), + 0 3px 14px 2px rgb(0 0 0 / 12%); +} + +.table { + width: 100%; +} + +.table__title { + justify-content: center; + text-transform: uppercase; + font-size: var(--font-size-sm); + text-align: center; +} + +.table__content { + justify-content: center; + font-size: var(--font-size-xs); +} + +.content__text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cover__wrapper { + display: flex; + justify-content: center; + padding: var(--space-xs) 0; + width: 150px; + height: 150px; + margin: 0 auto; +} + +.cover__image { + width: 100%; + height: auto; + object-fit: contain; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (min-width: 786px) { + .table__title { + font-size: var(--font-size-base); + } + + .table__content { + font-size: var(--font-size-sm); + } +} + +@media (min-width: 1280px) { + .table__title { + font-size: var(--font-size-md); + } + + .table__content { + font-size: var(--font-size-base); + } +} diff --git a/apps/angular/src/app/features/table/table.component.html b/apps/angular/src/app/features/table/table.component.html new file mode 100644 index 00000000..fa81b3d0 --- /dev/null +++ b/apps/angular/src/app/features/table/table.component.html @@ -0,0 +1,73 @@ +@if (pageData) { +
+ + + + {{columnsHeaders.Image}} + +
+ +
+
+
+ + + {{columnsHeaders.EnglishTitle}} + +

+ {{element.englishTitle | empty}} +

+
+
+ + + {{columnsHeaders.JapaneseTitle}} + +

+ {{element.japaneseTitle | empty}} +

+
+
+ + + {{columnsHeaders.AiredStart}} + {{element.aired.start | date: "mediumDate" | empty}} + + + + {{columnsHeaders.Type}} + {{element.type | empty}} + + + + {{columnsHeaders.Status}} + {{element.status | empty}} + + + + + +
+ + +
+ +} diff --git a/apps/angular/src/app/features/table/table.component.ts b/apps/angular/src/app/features/table/table.component.ts new file mode 100644 index 00000000..4e017e6c --- /dev/null +++ b/apps/angular/src/app/features/table/table.component.ts @@ -0,0 +1,78 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatTableModule } from '@angular/material/table'; +import { Anime } from '@js-camp/core/models/anime'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort, Sort, MatSortModule } from '@angular/material/sort'; +import { AsyncPipe, DatePipe } from '@angular/common'; +import { EmptyPipe } from '@js-camp/angular/shared/pipes/empty.pipe'; +import { Pagination } from '@js-camp/core/models/pagination'; + +/** Column headers to be displayed in table. */ +export enum ColumnsHeaders { + Image = 'Image', + EnglishTitle = 'English title', + JapaneseTitle = 'Japanese title', + AiredStart = 'Aired starts with', + Type = 'Type', + Status = 'Status', +} + +/** Dashboard component. Contains table with list of anime. */ +@Component({ + selector: 'camp-table', + standalone: true, + templateUrl: './table.component.html', + styleUrl: './table.component.css', + imports: [ + MatTableModule, + MatPaginator, + MatSort, + MatSortModule, + AsyncPipe, + DatePipe, + EmptyPipe, + ], +}) +export class TableComponent { + + /** */ + @Input() public pageData?: Pagination; + + /** + * Track by function for anime list. + * @param index - Anime list item id. + * @param item - Item of anime list. + * @returns Item's id. + */ + protected trackByAnime(index: number, item: Anime): Anime['id'] { + return item.id; + } + + /** Property containing enum with column headers. */ + protected readonly columnsHeaders = ColumnsHeaders; + + /** List of column headers. */ + protected readonly columnsToDisplay = Object.values(ColumnsHeaders); + + /** */ + @Output() public paginationEvent = new EventEmitter(); + + /** + * Page. + * @param event - Page. + */ + protected pageChanged(event: PageEvent): void { + this.paginationEvent.emit(event); + } + + /** */ + @Output() public sortEvent = new EventEmitter(); + + /** Announce the change in sort state for assistive technology. + * @param sortState - Anime list item id. + */ + protected sortData(sortState: Sort): void { + this.sortEvent.emit(sortState); + } + +} diff --git a/apps/angular/src/core/services/query-params.service.ts b/apps/angular/src/core/services/query-params.service.ts new file mode 100644 index 00000000..e7d6135e --- /dev/null +++ b/apps/angular/src/core/services/query-params.service.ts @@ -0,0 +1,35 @@ +import { inject, Injectable } from '@angular/core'; +import { Sort } from '@angular/material/sort'; +import { toTypeDto } from '@js-camp/core/mappers/anime-type.mapper'; +import { AnimeType } from '@js-camp/core/models/anime-type'; + +import { TableSortService } from './table-sort.service'; + +/** Anime API Access Service. */ +@Injectable({ providedIn: 'root' }) +export class QueryParamsService { + + private readonly tableSortService = inject(TableSortService); + + /** + * AnimeType. + * @param typesArr - AnimeType. + * @returns AnimeType. + */ + public composeTypeParam(typesArr: AnimeType[]): string { + if (typesArr) { + const typesDtoArr = typesArr.map(type => toTypeDto(type)); + return typesDtoArr.join(','); + } + return ''; + } + + /** + * AnimeType. + * @param sortState - AnimeType. + * @returns AnimeType. + */ + public composeOrderingParam(sortState: Sort): string { + return this.tableSortService.composeOrderingString(sortState); + } +} diff --git a/apps/angular/src/core/services/table-sort.service.ts b/apps/angular/src/core/services/table-sort.service.ts new file mode 100644 index 00000000..9b5b29b9 --- /dev/null +++ b/apps/angular/src/core/services/table-sort.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { Sort } from '@angular/material/sort'; +import { SortingColumnsDto } from '@js-camp/core/dtos/sorting-columns.dto'; +import { toSortingColumnsDto } from '@js-camp/core/mappers/sorting-columns.mapper'; +import { SortingColumns } from '@js-camp/core/models/sorting-columns'; + +/** Anime API Access Service. */ +@Injectable({ providedIn: 'root' }) +export class TableSortService { + + /** */ + protected sortOrder: Record = { + [SortingColumnsDto.EnglishTitle]: '', + [SortingColumnsDto.AiredStart]: '', + [SortingColumnsDto.Status]: '', + }; + + /** + * AnimeType. + * @param sortState - AnimeType. + * @returns AnimeType. + */ + public composeOrderingString(sortState: Sort): string { + + console.log(sortState); + + if (sortState) { + + const sortField = toSortingColumnsDto(sortState.active as SortingColumns); +console.log(sortField); + switch (sortState.direction) { + case 'asc': + this.sortOrder[sortField] = sortField; + break; + case 'desc': + this.sortOrder[sortField] = `-${sortField}`; + break; + default: + this.sortOrder[sortField] = ''; + } + + const orderString = Object.values(this.sortOrder).filter(item => item) + .join(','); + + return orderString; + } + return ''; + } +} diff --git a/libs/core/dtos/query-params.dto.ts b/libs/core/dtos/query-params.dto.ts new file mode 100644 index 00000000..e6489d4f --- /dev/null +++ b/libs/core/dtos/query-params.dto.ts @@ -0,0 +1,18 @@ +/** Params DTO. */ +export type QueryParamsDto = { + + /** Offset. */ + offset: number; + + /** Limit. */ + limit: number; + + /** Search. */ + search?: string; + + /** Type. */ + type__in?: string; + + /** Ordering. */ + ordering?: string; +}; diff --git a/libs/core/dtos/sorting-columns.dto.ts b/libs/core/dtos/sorting-columns.dto.ts new file mode 100644 index 00000000..31baae38 --- /dev/null +++ b/libs/core/dtos/sorting-columns.dto.ts @@ -0,0 +1,6 @@ +/** Column headers to be displayed in table. */ +export enum SortingColumnsDto { + EnglishTitle = 'title_eng', + AiredStart = 'aired__startswith', + Status = 'status', +} diff --git a/libs/core/mappers/query-params.mapper.ts b/libs/core/mappers/query-params.mapper.ts new file mode 100644 index 00000000..1934f524 --- /dev/null +++ b/libs/core/mappers/query-params.mapper.ts @@ -0,0 +1,19 @@ +// import { QueryParamsDto } from '../dtos/query-params.dto'; +// import { QueryParams } from '../models/query-params'; + +// export namespace GenreMapper { + +// /** +// * Maps dto to model. +// * @param dto Genre dto. +// */ +// export function toDto(dto: QueryParams): QueryParamsDto { +// return { +// offset: Number(dto.page) * Number(dto.pageSize), +// limit: dto.pageSize, +// search: dto.search, +// type__in: dto.type, +// ordering: dto.ordering, +// }; +// } +// } diff --git a/libs/core/mappers/sorting-columns.mapper.ts b/libs/core/mappers/sorting-columns.mapper.ts new file mode 100644 index 00000000..754eba7f --- /dev/null +++ b/libs/core/mappers/sorting-columns.mapper.ts @@ -0,0 +1,16 @@ +import { SortingColumnsDto } from '../dtos/sorting-columns.dto'; +import { SortingColumns } from '../models/sorting-columns'; + +/** Type of anime type enum's keys. */ +export type ColumnsKey = keyof typeof SortingColumnsDto; + +/** + * Map type dto. + * @param type - Type dto. + * @returns Type model. + */ +export function toSortingColumnsDto(type: SortingColumns): SortingColumnsDto { + const keyId = Object.values(SortingColumns).indexOf(type); + const enumKey = Object.keys(SortingColumns)[keyId] as ColumnsKey; + return SortingColumnsDto[enumKey]; +} diff --git a/libs/core/models/pagination.ts b/libs/core/models/pagination.ts index 83643b49..c3c5f968 100644 --- a/libs/core/models/pagination.ts +++ b/libs/core/models/pagination.ts @@ -13,7 +13,7 @@ export class Pagination extends Immerable { public readonly previous: string; /** Array of items requested. */ - public readonly results: readonly T[]; + public results: T[]; public constructor(data: PaginationConstructorData) { super(); diff --git a/libs/core/models/query-params.ts b/libs/core/models/query-params.ts new file mode 100644 index 00000000..b920e0b3 --- /dev/null +++ b/libs/core/models/query-params.ts @@ -0,0 +1,31 @@ +import { Immerable, OmitImmerable } from './immerable'; + +/** Genre. */ +export class QueryParams extends Immerable { + + /** Offset. */ + public readonly page: string; + + /** Limit. */ + public readonly pageSize: string; + + /** Search. */ + public readonly search: string; + + /** Name. */ + public readonly type: string; + + /** Name. */ + public readonly ordering: string; + + public constructor(data: QueryParamsConstructorData) { + super(); + this.page = data.page; + this.pageSize = data.pageSize; + this.search = data.search; + this.type = data.type; + this.ordering = data.ordering; + } +} + +type QueryParamsConstructorData = OmitImmerable; diff --git a/libs/core/models/sorting-columns.ts b/libs/core/models/sorting-columns.ts new file mode 100644 index 00000000..9e941506 --- /dev/null +++ b/libs/core/models/sorting-columns.ts @@ -0,0 +1,6 @@ +/** Column headers to be displayed in table. */ +export enum SortingColumns { + EnglishTitle = 'English title', + AiredStart = 'Aired starts with', + Status = 'Status', +} From 234859a8f6850a903c4a7fd02a36b54269459f83 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Mon, 29 Jul 2024 15:34:31 +0700 Subject: [PATCH 07/64] Refactoring sort JC-660 --- .../dashboard/dashboard.component.html | 5 +- .../features/dashboard/dashboard.component.ts | 21 +++++--- .../app/features/table/table.component.html | 5 +- .../src/app/features/table/table.component.ts | 40 +++++++++++++-- .../src/core/services/table-sort.service.ts | 49 ++++++++++++------- 5 files changed, 88 insertions(+), 32 deletions(-) diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index eb231743..b86e0ad4 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,10 +1,11 @@
- + @if (animeListPage) { } @else { diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index a49b309d..117942d3 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, inject, OnInit, OnDestroy } from '@angular/core'; +import { Component, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { Sort } from '@angular/material/sort'; import { ActivatedRoute, Router } from '@angular/router'; @@ -13,6 +13,7 @@ import { ProgressBarComponent } from '@js-camp/angular/shared/components/progres import { TableComponent } from '../table/table.component'; import { DataRetrievalFormComponent } from '../data-retrieval-form/data-retrieval-form.component'; +import { TableSortService } from '@js-camp/angular/core/services/table-sort.service'; /** Column headers to be displayed in table. */ export enum ColumnsHeaders { @@ -39,6 +40,7 @@ export enum ParamsNames { standalone: true, templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.css', + // changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ProgressBarComponent, TableComponent, @@ -63,15 +65,16 @@ export class DashboardComponent implements OnInit, OnDestroy { }; /** */ - protected animeListPage?: Pagination; - - public constructor() { + protected ordering: Sort = { active: '', direction: '' }; - } + /** */ + protected animeListPage?: Pagination; /** */ public animeSubscription?: Subscription; + private readonly tableSortService = inject(TableSortService); + /** Subscribes on route parameters when the component is initialized. */ public ngOnInit(): void { @@ -79,6 +82,10 @@ export class DashboardComponent implements OnInit, OnDestroy { switchMap(params => { Object.assign(this.params, params); + if ('ordering' in params) { + this.ordering = this.tableSortService.fromOrderingString(params['ordering']); + // console.log(this.ordering) + } return this.animeApiService.getPage(this.params); }), @@ -94,7 +101,7 @@ export class DashboardComponent implements OnInit, OnDestroy { } /** */ - protected onTypeSelect(event: AnimeType[]): void { + protected setTypeSelect(event: AnimeType[]): void { const typesString = this.queryParamsService.composeTypeParam(event); @@ -108,7 +115,7 @@ export class DashboardComponent implements OnInit, OnDestroy { } /** */ - protected onSearchSubmit(event: string): void { + protected setSearchSubmit(event: string): void { const queryParams: QueryParamsDto = { offset: 0, diff --git a/apps/angular/src/app/features/table/table.component.html b/apps/angular/src/app/features/table/table.component.html index fa81b3d0..d5ff2f14 100644 --- a/apps/angular/src/app/features/table/table.component.html +++ b/apps/angular/src/app/features/table/table.component.html @@ -1,10 +1,12 @@ -@if (pageData) { +@if (pageData && ordering) {
@@ -71,3 +73,4 @@
} + diff --git a/apps/angular/src/app/features/table/table.component.ts b/apps/angular/src/app/features/table/table.component.ts index 4e017e6c..0e8a95c6 100644 --- a/apps/angular/src/app/features/table/table.component.ts +++ b/apps/angular/src/app/features/table/table.component.ts @@ -1,8 +1,8 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { MatTableModule } from '@angular/material/table'; +import { Component, EventEmitter, Input, Output, ViewChild, OnInit } from '@angular/core'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { Anime } from '@js-camp/core/models/anime'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; -import { MatSort, Sort, MatSortModule } from '@angular/material/sort'; +import { MatSort, Sort, MatSortModule, SortDirection, MatSortable } from '@angular/material/sort'; import { AsyncPipe, DatePipe } from '@angular/common'; import { EmptyPipe } from '@js-camp/angular/shared/pipes/empty.pipe'; import { Pagination } from '@js-camp/core/models/pagination'; @@ -17,6 +17,28 @@ export enum ColumnsHeaders { Status = 'Status', } +/** Column headers to be displayed in table. */ +export enum SortingColumnsDto { + EnglishTitle = 'title_eng', + AiredStart = 'aired__startswith', + Status = 'status', +} + +const sortingColumnsDtoMap: Readonly> = { + [SortingColumnsDto.EnglishTitle]: ColumnsHeaders.EnglishTitle, + [SortingColumnsDto.AiredStart]: ColumnsHeaders.AiredStart, + [SortingColumnsDto.Status]: ColumnsHeaders.Status, +}; + +/** + * SortingColumnsDto. + * @param enumKey SortingColumnsDto. + * @returns SortingColumnsDto. + */ +export function fromSortingColumnsDto(enumKey: SortingColumnsDto): ColumnsHeaders { + return sortingColumnsDtoMap[enumKey]; +} + /** Dashboard component. Contains table with list of anime. */ @Component({ selector: 'camp-table', @@ -33,11 +55,21 @@ export enum ColumnsHeaders { EmptyPipe, ], }) -export class TableComponent { +export class TableComponent implements OnInit { + + /** Subscribes on route parameters when the component is initialized. */ + public ngOnInit(): void { + // if (this.ordering) { + // this.ordering.active = fromSortingColumnsDto(this.ordering.active as SortingColumnsDto); + // } + } /** */ @Input() public pageData?: Pagination; + /** */ + @Input() public ordering?: Sort; + /** * Track by function for anime list. * @param index - Anime list item id. diff --git a/apps/angular/src/core/services/table-sort.service.ts b/apps/angular/src/core/services/table-sort.service.ts index 9b5b29b9..30c57452 100644 --- a/apps/angular/src/core/services/table-sort.service.ts +++ b/apps/angular/src/core/services/table-sort.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Sort } from '@angular/material/sort'; -import { SortingColumnsDto } from '@js-camp/core/dtos/sorting-columns.dto'; +import { fromSortingColumnsDto, SortingColumnsDto } from '@js-camp/angular/app/features/table/table.component'; import { toSortingColumnsDto } from '@js-camp/core/mappers/sorting-columns.mapper'; import { SortingColumns } from '@js-camp/core/models/sorting-columns'; @@ -8,13 +8,6 @@ import { SortingColumns } from '@js-camp/core/models/sorting-columns'; @Injectable({ providedIn: 'root' }) export class TableSortService { - /** */ - protected sortOrder: Record = { - [SortingColumnsDto.EnglishTitle]: '', - [SortingColumnsDto.AiredStart]: '', - [SortingColumnsDto.Status]: '', - }; - /** * AnimeType. * @param sortState - AnimeType. @@ -22,28 +15,48 @@ export class TableSortService { */ public composeOrderingString(sortState: Sort): string { - console.log(sortState); + let orderString = ''; if (sortState) { - const sortField = toSortingColumnsDto(sortState.active as SortingColumns); -console.log(sortField); + switch (sortState.direction) { case 'asc': - this.sortOrder[sortField] = sortField; + orderString = sortField; break; case 'desc': - this.sortOrder[sortField] = `-${sortField}`; + orderString = `-${sortField}`; break; default: - this.sortOrder[sortField] = ''; + orderString = ''; } + } - const orderString = Object.values(this.sortOrder).filter(item => item) - .join(','); + return orderString; + } - return orderString; + /** + * AnimeType. + * @param ordering - AnimeType. + * @returns AnimeType. + */ + public fromOrderingString(ordering: string): Sort { + + const sortState: Sort = { active: '', direction: '' }; + + if (ordering === '') { + return sortState; } - return ''; + + if (ordering[0] === '-') { + sortState.active = fromSortingColumnsDto(ordering.slice(1) as SortingColumnsDto); + sortState.direction = 'desc'; + return sortState; + } + + sortState.active = fromSortingColumnsDto(ordering as SortingColumnsDto); + sortState.direction = 'asc'; + + return sortState; } } From 8997c7033e7878bfdbd69aee2b5f2a0d7c2d23d3 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Mon, 29 Jul 2024 19:28:34 +0700 Subject: [PATCH 08/64] Create services JC-660 --- .../dashboard/dashboard.component.html | 5 +- .../features/dashboard/dashboard.component.ts | 125 ++++++++++-------- .../data-retrieval-form.component.ts | 49 ++++++- .../src/app/features/table/table.component.ts | 37 +----- ...t.service.ts => ordering-param.service.ts} | 14 +- ...arams.service.ts => type-param.service.ts} | 18 +-- libs/core/mappers/query-params.mapper.ts | 48 ++++--- libs/core/mappers/sorting-columns.mapper.ts | 42 ++++-- libs/core/models/query-params.ts | 14 +- 9 files changed, 205 insertions(+), 147 deletions(-) rename apps/angular/src/core/services/{table-sort.service.ts => ordering-param.service.ts} (65%) rename apps/angular/src/core/services/{query-params.service.ts => type-param.service.ts} (50%) diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index b86e0ad4..b17a6879 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,9 +1,10 @@
+ (searchEvent)="setSearchSubmit($event)" + [searchTerm]="search"/> - @if (animeListPage) { + @if (animeListPage$ | async; as animeListPage) { ; + protected search = ''; /** */ - public animeSubscription?: Subscription; + protected type: AnimeType[] = []; - private readonly tableSortService = inject(TableSortService); + /** */ + protected ordering: Sort = { active: '', direction: '' }; - /** Subscribes on route parameters when the component is initialized. */ - public ngOnInit(): void { + /** */ + protected animeListPage$: Observable> = EMPTY; - this.route.queryParams.pipe( - switchMap(params => { + private getValuesFromParams(params: Params): void { + if ('offset' in params) { + this.offset = params['offset']; + } - Object.assign(this.params, params); - if ('ordering' in params) { - this.ordering = this.tableSortService.fromOrderingString(params['ordering']); - // console.log(this.ordering) - } + if ('limit' in params) { + this.limit = params['limit']; + } - return this.animeApiService.getPage(this.params); - }), - ) - .subscribe(res => { - this.animeListPage = res; - }); + if ('search' in params) { + this.search = params['search']; + } + + if ('type' in params) { + this.type = this.typeParamService.composeTypeArray(params['type']); + } + + if ('ordering' in params) { + this.ordering = this.orderingParamService.composeOrderingState(params['ordering']); + } } + private defaultParams: QueryParamsDto = { + limit: 25, + offset: 0, + }; + /** Subscribes on route parameters when the component is initialized. */ - public ngOnDestroy(): void { - this.animeSubscription?.unsubscribe(); + public ngOnInit(): void { + + this.animeListPage$ = this.route.queryParams.pipe( + switchMap(params => { + this.getValuesFromParams(params); + return this.animeApiService.getPage(params as QueryParamsDto); + }), + ); } /** */ protected setTypeSelect(event: AnimeType[]): void { - const typesString = this.queryParamsService.composeTypeParam(event); + const queryParams: QueryParamsDto = { ...this.defaultParams }; + + queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(event); + + if (this.ordering) { + queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); + } + if (this.search) { + queryParams[ParamsNames.Search] = this.search; + } this.router.navigate( [''], { - queryParams: { [ParamsNames.Type]: typesString }, - queryParamsHandling: 'merge', + queryParams, }, ); } @@ -117,16 +135,13 @@ export class DashboardComponent implements OnInit, OnDestroy { /** */ protected setSearchSubmit(event: string): void { - const queryParams: QueryParamsDto = { - offset: 0, - limit: 25, - }; + const queryParams: QueryParamsDto = { ...this.defaultParams }; - if (ParamsNames.Type in this.params) { - queryParams[ParamsNames.Type] = this.params[ParamsNames.Type] as string; + if (this.type.length > 0) { + queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(this.type); } - if (ParamsNames.Ordering in this.params) { - queryParams[ParamsNames.Ordering] = this.params[ParamsNames.Ordering] as string; + if (this.ordering) { + queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); } queryParams[ParamsNames.Search] = event; @@ -172,7 +187,7 @@ export class DashboardComponent implements OnInit, OnDestroy { */ public setOrdering(sortState: Sort): void { - const orderString = this.queryParamsService.composeOrderingParam(sortState); + const orderString = this.orderingParamService.composeOrderingString(sortState); this.router.navigate( [''], diff --git a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts index 2dad9ab5..ab439393 100644 --- a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts +++ b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, OnInit, ChangeDetectorRef, AfterViewInit, AfterContentInit } from '@angular/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatIconModule } from '@angular/material/icon'; @@ -39,9 +39,9 @@ export class DataRetrievalFormComponent { } /** Search form. */ - protected searchForm = new FormGroup({ - term: new FormControl(''), - }); + // protected searchForm = new FormGroup({ + // term: new FormControl(''), + // }); /** */ @Output() public searchEvent = new EventEmitter(); @@ -50,4 +50,45 @@ export class DataRetrievalFormComponent { protected onSearchSubmit(): void { this.searchEvent.emit(String(this.searchForm.value.term)); } + + /** Search form. */ + protected searchForm: FormGroup = new FormGroup({ + term: new FormControl(''), + }); + + /** */ + // @Input() public searchTerm = ''; + + // Child component + @Input() public set searchTerm(value: string) { + // This setter function will execute whenever parent input `searchText` is changed. + console.log(value); // New value from parent + this.searchForm.controls['term'].setValue(value); + this.cdr.detectChanges(); + } + + public constructor(private cdr: ChangeDetectorRef) { + // this.searchForm = new FormGroup({ + // term: new FormControl(''), + // }); + } + + /** Subscribes on route parameters when the component is initialized. */ + // public ngAfterContentInit(): void { + // console.log(this.searchTerm); + // this.searchForm.setValue({ + // term: this.searchTerm, + // }); + // this.cdr.detectChanges(); + + // // this.searchForm.get('term')?.setValue(this.searchTerm); + // // patchValue(this.searchTerm); + // } + + /** */ + // public ngAfterViewInit(): void { + // this.searchForm.setValue({ + // term: this.searchTerm, + // }); + // } } diff --git a/apps/angular/src/app/features/table/table.component.ts b/apps/angular/src/app/features/table/table.component.ts index 0e8a95c6..a42722b6 100644 --- a/apps/angular/src/app/features/table/table.component.ts +++ b/apps/angular/src/app/features/table/table.component.ts @@ -1,8 +1,8 @@ -import { Component, EventEmitter, Input, Output, ViewChild, OnInit } from '@angular/core'; -import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatTableModule } from '@angular/material/table'; import { Anime } from '@js-camp/core/models/anime'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; -import { MatSort, Sort, MatSortModule, SortDirection, MatSortable } from '@angular/material/sort'; +import { MatSort, Sort, MatSortModule } from '@angular/material/sort'; import { AsyncPipe, DatePipe } from '@angular/common'; import { EmptyPipe } from '@js-camp/angular/shared/pipes/empty.pipe'; import { Pagination } from '@js-camp/core/models/pagination'; @@ -17,28 +17,6 @@ export enum ColumnsHeaders { Status = 'Status', } -/** Column headers to be displayed in table. */ -export enum SortingColumnsDto { - EnglishTitle = 'title_eng', - AiredStart = 'aired__startswith', - Status = 'status', -} - -const sortingColumnsDtoMap: Readonly> = { - [SortingColumnsDto.EnglishTitle]: ColumnsHeaders.EnglishTitle, - [SortingColumnsDto.AiredStart]: ColumnsHeaders.AiredStart, - [SortingColumnsDto.Status]: ColumnsHeaders.Status, -}; - -/** - * SortingColumnsDto. - * @param enumKey SortingColumnsDto. - * @returns SortingColumnsDto. - */ -export function fromSortingColumnsDto(enumKey: SortingColumnsDto): ColumnsHeaders { - return sortingColumnsDtoMap[enumKey]; -} - /** Dashboard component. Contains table with list of anime. */ @Component({ selector: 'camp-table', @@ -55,14 +33,7 @@ export function fromSortingColumnsDto(enumKey: SortingColumnsDto): ColumnsHeader EmptyPipe, ], }) -export class TableComponent implements OnInit { - - /** Subscribes on route parameters when the component is initialized. */ - public ngOnInit(): void { - // if (this.ordering) { - // this.ordering.active = fromSortingColumnsDto(this.ordering.active as SortingColumnsDto); - // } - } +export class TableComponent { /** */ @Input() public pageData?: Pagination; diff --git a/apps/angular/src/core/services/table-sort.service.ts b/apps/angular/src/core/services/ordering-param.service.ts similarity index 65% rename from apps/angular/src/core/services/table-sort.service.ts rename to apps/angular/src/core/services/ordering-param.service.ts index 30c57452..01677806 100644 --- a/apps/angular/src/core/services/table-sort.service.ts +++ b/apps/angular/src/core/services/ordering-param.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@angular/core'; import { Sort } from '@angular/material/sort'; -import { fromSortingColumnsDto, SortingColumnsDto } from '@js-camp/angular/app/features/table/table.component'; -import { toSortingColumnsDto } from '@js-camp/core/mappers/sorting-columns.mapper'; import { SortingColumns } from '@js-camp/core/models/sorting-columns'; +import { ColumnsMapper } from '@js-camp/core/mappers/sorting-columns.mapper'; +import { SortingColumnsDto } from '@js-camp/core/dtos/sorting-columns.dto'; /** Anime API Access Service. */ @Injectable({ providedIn: 'root' }) -export class TableSortService { +export class OrderingParamService { /** * AnimeType. @@ -18,7 +18,7 @@ export class TableSortService { let orderString = ''; if (sortState) { - const sortField = toSortingColumnsDto(sortState.active as SortingColumns); + const sortField = ColumnsMapper.toSortingColumnsDto(sortState.active as SortingColumns); switch (sortState.direction) { case 'asc': @@ -40,7 +40,7 @@ export class TableSortService { * @param ordering - AnimeType. * @returns AnimeType. */ - public fromOrderingString(ordering: string): Sort { + public composeOrderingState(ordering: string): Sort { const sortState: Sort = { active: '', direction: '' }; @@ -49,12 +49,12 @@ export class TableSortService { } if (ordering[0] === '-') { - sortState.active = fromSortingColumnsDto(ordering.slice(1) as SortingColumnsDto); + sortState.active = ColumnsMapper.fromSortingColumnsDto(ordering.slice(1) as SortingColumnsDto); sortState.direction = 'desc'; return sortState; } - sortState.active = fromSortingColumnsDto(ordering as SortingColumnsDto); + sortState.active = ColumnsMapper.fromSortingColumnsDto(ordering as SortingColumnsDto); sortState.direction = 'asc'; return sortState; diff --git a/apps/angular/src/core/services/query-params.service.ts b/apps/angular/src/core/services/type-param.service.ts similarity index 50% rename from apps/angular/src/core/services/query-params.service.ts rename to apps/angular/src/core/services/type-param.service.ts index e7d6135e..7104f0f2 100644 --- a/apps/angular/src/core/services/query-params.service.ts +++ b/apps/angular/src/core/services/type-param.service.ts @@ -1,22 +1,17 @@ -import { inject, Injectable } from '@angular/core'; -import { Sort } from '@angular/material/sort'; +import { Injectable } from '@angular/core'; import { toTypeDto } from '@js-camp/core/mappers/anime-type.mapper'; import { AnimeType } from '@js-camp/core/models/anime-type'; -import { TableSortService } from './table-sort.service'; - /** Anime API Access Service. */ @Injectable({ providedIn: 'root' }) -export class QueryParamsService { - - private readonly tableSortService = inject(TableSortService); +export class TypeParamService { /** * AnimeType. * @param typesArr - AnimeType. * @returns AnimeType. */ - public composeTypeParam(typesArr: AnimeType[]): string { + public composeTypeString(typesArr: AnimeType[]): string { if (typesArr) { const typesDtoArr = typesArr.map(type => toTypeDto(type)); return typesDtoArr.join(','); @@ -26,10 +21,11 @@ export class QueryParamsService { /** * AnimeType. - * @param sortState - AnimeType. + * @param typesString - AnimeType. * @returns AnimeType. */ - public composeOrderingParam(sortState: Sort): string { - return this.tableSortService.composeOrderingString(sortState); + public composeTypeArray(typesString: string): AnimeType[] { + return typesString.split(',') as AnimeType[]; } + } diff --git a/libs/core/mappers/query-params.mapper.ts b/libs/core/mappers/query-params.mapper.ts index 1934f524..796627e5 100644 --- a/libs/core/mappers/query-params.mapper.ts +++ b/libs/core/mappers/query-params.mapper.ts @@ -1,19 +1,33 @@ -// import { QueryParamsDto } from '../dtos/query-params.dto'; -// import { QueryParams } from '../models/query-params'; +import { QueryParamsDto } from '../dtos/query-params.dto'; +import { QueryParams } from '../models/query-params'; -// export namespace GenreMapper { +export namespace QueryParamsMapper { -// /** -// * Maps dto to model. -// * @param dto Genre dto. -// */ -// export function toDto(dto: QueryParams): QueryParamsDto { -// return { -// offset: Number(dto.page) * Number(dto.pageSize), -// limit: dto.pageSize, -// search: dto.search, -// type__in: dto.type, -// ordering: dto.ordering, -// }; -// } -// } + /** + * Maps dto to model. + * @param model Genre dto. + */ + export function toQueryParamsDto(model: QueryParams): QueryParamsDto { + return { + offset: model.offset, + limit: model.limit, + search: model.search, + type__in: model.type, + ordering: model.ordering, + }; + } + + /** + * Maps dto to model. + * @param dto Genre dto. + */ + export function fromQueryParamsDto(dto: QueryParamsDto): QueryParams { + return new QueryParams({ + offset: dto.offset, + limit: dto.limit, + search: dto.search, + type: dto.type__in, + ordering: dto.ordering, + }); + } +} diff --git a/libs/core/mappers/sorting-columns.mapper.ts b/libs/core/mappers/sorting-columns.mapper.ts index 754eba7f..58dbe5dc 100644 --- a/libs/core/mappers/sorting-columns.mapper.ts +++ b/libs/core/mappers/sorting-columns.mapper.ts @@ -1,16 +1,36 @@ import { SortingColumnsDto } from '../dtos/sorting-columns.dto'; import { SortingColumns } from '../models/sorting-columns'; -/** Type of anime type enum's keys. */ -export type ColumnsKey = keyof typeof SortingColumnsDto; +export namespace ColumnsMapper { + + const sortingColumnsDtoMap: Readonly> = { + [SortingColumnsDto.EnglishTitle]: SortingColumns.EnglishTitle, + [SortingColumnsDto.AiredStart]: SortingColumns.AiredStart, + [SortingColumnsDto.Status]: SortingColumns.Status, + }; + + /** + * SortingColumnsDto. + * @param column SortingColumnsDto. + * @returns SortingColumnsDto. + */ + export function fromSortingColumnsDto(column: SortingColumnsDto): SortingColumns { + return sortingColumnsDtoMap[column]; + } + + const sortingColumnsMap: Readonly> = { + [SortingColumns.EnglishTitle]: SortingColumnsDto.EnglishTitle, + [SortingColumns.AiredStart]: SortingColumnsDto.AiredStart, + [SortingColumns.Status]: SortingColumnsDto.Status, + }; + + /** + * SortingColumnsDto. + * @param column SortingColumnsDto. + * @returns SortingColumnsDto. + */ + export function toSortingColumnsDto(column: SortingColumns): SortingColumnsDto { + return sortingColumnsMap[column]; + } -/** - * Map type dto. - * @param type - Type dto. - * @returns Type model. - */ -export function toSortingColumnsDto(type: SortingColumns): SortingColumnsDto { - const keyId = Object.values(SortingColumns).indexOf(type); - const enumKey = Object.keys(SortingColumns)[keyId] as ColumnsKey; - return SortingColumnsDto[enumKey]; } diff --git a/libs/core/models/query-params.ts b/libs/core/models/query-params.ts index b920e0b3..8fa5f122 100644 --- a/libs/core/models/query-params.ts +++ b/libs/core/models/query-params.ts @@ -4,24 +4,24 @@ import { Immerable, OmitImmerable } from './immerable'; export class QueryParams extends Immerable { /** Offset. */ - public readonly page: string; + public readonly offset: number; /** Limit. */ - public readonly pageSize: string; + public readonly limit: number; /** Search. */ - public readonly search: string; + public readonly search?: string; /** Name. */ - public readonly type: string; + public readonly type?: string; /** Name. */ - public readonly ordering: string; + public readonly ordering?: string; public constructor(data: QueryParamsConstructorData) { super(); - this.page = data.page; - this.pageSize = data.pageSize; + this.offset = data.offset; + this.limit = data.limit; this.search = data.search; this.type = data.type; this.ordering = data.ordering; From bf77abf04aab6e3a0dbd8d168126c63d9749942e Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Tue, 30 Jul 2024 13:28:27 +0700 Subject: [PATCH 09/64] Create observable for params JC-660 --- .../dashboard/dashboard.component.html | 18 ++-- .../features/dashboard/dashboard.component.ts | 97 +++++++------------ .../data-retrieval-form.component.ts | 94 ------------------ .../filter-form.component.css} | 0 .../filter-form.component.html} | 2 +- .../filter-form/filter-form.component.ts | 66 +++++++++++++ .../src/core/services/query-params.service.ts | 75 ++++++++++++++ libs/core/mappers/query-params.mapper.ts | 52 +++++----- libs/core/models/query-params.ts | 6 +- 9 files changed, 219 insertions(+), 191 deletions(-) delete mode 100644 apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts rename apps/angular/src/app/features/{data-retrieval-form/data-retrieval-form.component.css => filter-form/filter-form.component.css} (100%) rename apps/angular/src/app/features/{data-retrieval-form/data-retrieval-form.component.html => filter-form/filter-form.component.html} (90%) create mode 100644 apps/angular/src/app/features/filter-form/filter-form.component.ts create mode 100644 apps/angular/src/core/services/query-params.service.ts diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index b17a6879..ed0967ba 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,15 +1,21 @@
- + @if (animeParams$ | async; as animeParams) { - @if (animeListPage$ | async; as animeListPage) { + + + @if (animeListPage$ | async; as animeListPage) { - } @else { + } @else { + } + } +
diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index 47077a55..2c437476 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -1,14 +1,14 @@ -import { Component, inject, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { Sort } from '@angular/material/sort'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { EMPTY, Observable, switchMap } from 'rxjs'; +import { combineLatest, EMPTY, map, Observable, of, switchMap, tap } from 'rxjs'; import { Anime } from '@js-camp/core/models/anime'; -import { AnimeType } from '@js-camp/core/models/anime-type'; import { Pagination } from '@js-camp/core/models/pagination'; import { QueryParamsDto } from '@js-camp/core/dtos/query-params.dto'; import { AnimeApiService } from '@js-camp/angular/core/services/anime-api.service'; import { ProgressBarComponent } from '@js-camp/angular/shared/components/progress-bar/progress-bar.component'; +import { AnimeType } from '@js-camp/core/models/anime-type'; import { OrderingParamService } from '@js-camp/angular/core/services/ordering-param.service'; @@ -16,8 +16,11 @@ import { AsyncPipe } from '@angular/common'; import { TypeParamService } from '@js-camp/angular/core/services/type-param.service'; +import { Subscription } from 'react-redux'; + import { TableComponent } from '../table/table.component'; -import { DataRetrievalFormComponent } from '../data-retrieval-form/data-retrieval-form.component'; +import { DataRetrievalFormComponent } from '../filter-form/filter-form.component'; +import { QueryParams, QueryParamsService } from '@js-camp/angular/core/services/query-params.service'; /** Column headers to be displayed in table. */ export enum ParamsNames { @@ -50,64 +53,34 @@ export class DashboardComponent implements OnInit { private readonly animeApiService = inject(AnimeApiService); + private readonly queryParamsService = inject(QueryParamsService); + private readonly orderingParamService = inject(OrderingParamService); private readonly typeParamService = inject(TypeParamService); - /** */ - protected offset = 0; - - /** */ - protected limit = 25; - - /** */ - protected search = ''; - - /** */ - protected type: AnimeType[] = []; - - /** */ - protected ordering: Sort = { active: '', direction: '' }; - - /** */ - protected animeListPage$: Observable> = EMPTY; - - private getValuesFromParams(params: Params): void { - if ('offset' in params) { - this.offset = params['offset']; - } - - if ('limit' in params) { - this.limit = params['limit']; - } - - if ('search' in params) { - this.search = params['search']; - } - - if ('type' in params) { - this.type = this.typeParamService.composeTypeArray(params['type']); - } - - if ('ordering' in params) { - this.ordering = this.orderingParamService.composeOrderingState(params['ordering']); - } - } - private defaultParams: QueryParamsDto = { limit: 25, offset: 0, }; - /** Subscribes on route parameters when the component is initialized. */ + /** */ + protected animeListPage$: Observable> = EMPTY; + + /** */ + protected animeParams$: Observable = EMPTY; + + /** @inheritdoc */ public ngOnInit(): void { + this.animeParams$ = this.route.queryParams.pipe( + switchMap(params => of(this.queryParamsService.fromQueryParams(params))), + ); + this.animeListPage$ = this.route.queryParams.pipe( - switchMap(params => { - this.getValuesFromParams(params); - return this.animeApiService.getPage(params as QueryParamsDto); - }), + switchMap(params => this.animeApiService.getPage(params as QueryParamsDto)), ); + } /** */ @@ -117,12 +90,12 @@ export class DashboardComponent implements OnInit { queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(event); - if (this.ordering) { - queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); - } - if (this.search) { - queryParams[ParamsNames.Search] = this.search; - } + // if (this.ordering) { + // queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); + // } + // if (this.search) { + // queryParams[ParamsNames.Search] = this.search; + // } this.router.navigate( [''], @@ -135,14 +108,16 @@ export class DashboardComponent implements OnInit { /** */ protected setSearchSubmit(event: string): void { + // console.log(this.animeParams) + const queryParams: QueryParamsDto = { ...this.defaultParams }; - if (this.type.length > 0) { - queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(this.type); - } - if (this.ordering) { - queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); - } + // if (this.type.length > 0) { + // queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(this.type); + // } + // if (this.ordering) { + // queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); + // } queryParams[ParamsNames.Search] = event; this.router.navigate( diff --git a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts b/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts deleted file mode 100644 index ab439393..00000000 --- a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Component, EventEmitter, Input, Output, OnInit, ChangeDetectorRef, AfterViewInit, AfterContentInit } from '@angular/core'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatIconModule } from '@angular/material/icon'; -import { MatButtonModule } from '@angular/material/button'; -import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; -import { AnimeType } from '@js-camp/core/models/anime-type'; -import { MatSelectModule } from '@angular/material/select'; - -/** Dashboard component. Contains table with list of anime. */ -@Component({ - selector: 'camp-data-retrieval-form', - standalone: true, - templateUrl: './data-retrieval-form.component.html', - styleUrl: './data-retrieval-form.component.css', - imports: [ - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatIconModule, - MatSelectModule, - ], -}) -export class DataRetrievalFormComponent { - - /** Types select control. */ - protected types = new FormControl(); - - /** */ - protected typesList = Object.values(AnimeType); - - /** */ - @Output() public typeSelectEvent = new EventEmitter(); - - /** */ - protected onTypeSelect(): void { - this.typeSelectEvent.emit(this.types.value); - } - - /** Search form. */ - // protected searchForm = new FormGroup({ - // term: new FormControl(''), - // }); - - /** */ - @Output() public searchEvent = new EventEmitter(); - - /** */ - protected onSearchSubmit(): void { - this.searchEvent.emit(String(this.searchForm.value.term)); - } - - /** Search form. */ - protected searchForm: FormGroup = new FormGroup({ - term: new FormControl(''), - }); - - /** */ - // @Input() public searchTerm = ''; - - // Child component - @Input() public set searchTerm(value: string) { - // This setter function will execute whenever parent input `searchText` is changed. - console.log(value); // New value from parent - this.searchForm.controls['term'].setValue(value); - this.cdr.detectChanges(); - } - - public constructor(private cdr: ChangeDetectorRef) { - // this.searchForm = new FormGroup({ - // term: new FormControl(''), - // }); - } - - /** Subscribes on route parameters when the component is initialized. */ - // public ngAfterContentInit(): void { - // console.log(this.searchTerm); - // this.searchForm.setValue({ - // term: this.searchTerm, - // }); - // this.cdr.detectChanges(); - - // // this.searchForm.get('term')?.setValue(this.searchTerm); - // // patchValue(this.searchTerm); - // } - - /** */ - // public ngAfterViewInit(): void { - // this.searchForm.setValue({ - // term: this.searchTerm, - // }); - // } -} diff --git a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.css b/apps/angular/src/app/features/filter-form/filter-form.component.css similarity index 100% rename from apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.css rename to apps/angular/src/app/features/filter-form/filter-form.component.css diff --git a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.html b/apps/angular/src/app/features/filter-form/filter-form.component.html similarity index 90% rename from apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.html rename to apps/angular/src/app/features/filter-form/filter-form.component.html index fa72f5c2..d0a778e9 100644 --- a/apps/angular/src/app/features/data-retrieval-form/data-retrieval-form.component.html +++ b/apps/angular/src/app/features/filter-form/filter-form.component.html @@ -1,7 +1,7 @@
Types - + @for (type of typesList; track type) { {{type}} } diff --git a/apps/angular/src/app/features/filter-form/filter-form.component.ts b/apps/angular/src/app/features/filter-form/filter-form.component.ts new file mode 100644 index 00000000..87836b69 --- /dev/null +++ b/apps/angular/src/app/features/filter-form/filter-form.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, Output, ChangeDetectorRef, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; +import { AnimeType } from '@js-camp/core/models/anime-type'; +import { MatSelectModule } from '@angular/material/select'; + +/** Dashboard component. Contains table with list of anime. */ +@Component({ + selector: 'camp-filter-form', + standalone: true, + templateUrl: './filter-form.component.html', + styleUrl: './filter-form.component.css', + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatSelectModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DataRetrievalFormComponent implements OnInit { + + /** */ + @Input() public typesValue: AnimeType[] = []; + + /** Types select control. */ + protected typesControl = new FormControl(); + + /** */ + protected typesList = Object.values(AnimeType); + + /** */ + @Output() public typeSelectEvent = new EventEmitter(); + + /** */ + protected onTypeSelect(): void { + this.typeSelectEvent.emit(this.typesControl.value); + } + + /** */ + @Output() public searchEvent = new EventEmitter(); + + /** */ + protected onSearchSubmit(): void { + this.searchEvent.emit(String(this.searchForm.value.term)); + } + + /** Search form. */ + protected searchForm: FormGroup = new FormGroup({ + term: new FormControl(''), + }); + + /** */ + @Input() public searchValue = ''; + + /** @inheritdoc */ + public ngOnInit(): void { + this.searchForm.controls['term'].setValue(this.searchValue); + this.typesControl.setValue(this.typesValue); + } +} diff --git a/apps/angular/src/core/services/query-params.service.ts b/apps/angular/src/core/services/query-params.service.ts new file mode 100644 index 00000000..be2578ab --- /dev/null +++ b/apps/angular/src/core/services/query-params.service.ts @@ -0,0 +1,75 @@ +import { inject, Injectable } from '@angular/core'; + +import { AnimeType } from '@js-camp/core/models/anime-type'; +import { Sort } from '@angular/material/sort'; +import { Params } from '@angular/router'; + +import { TypeParamService } from './type-param.service'; +import { OrderingParamService } from './ordering-param.service'; + +/** */ +export enum ParamsNames { + Limit = 'limit', + Offset = 'offset', + Search = 'search', + Type = 'type__in', + Ordering = 'ordering', +} + +/** Params. */ +export type QueryParams = { + + /** Offset. */ + offset: number; + + /** Limit. */ + limit: number; + + /** Search. */ + search: string; + + /** Type. */ + type: AnimeType[]; + + /** Ordering. */ + ordering: Sort; +}; + +/** Anime API Access Service. */ +@Injectable({ providedIn: 'root' }) +export class QueryParamsService { + + private orderingParamService = inject(OrderingParamService); + + private typeParamService = inject(TypeParamService); + + /** + * Maps dto to model. + * @param params Genre dto. + */ + public fromQueryParams(params: Params): QueryParams { + const queryParams: QueryParams = { + offset: 0, + limit: 25, + search: '', + type: [], + ordering: { active: '', direction: '' }, + }; + if ('offset' in params) { + queryParams.offset = params['offset']; + } + if ('limit' in params) { + queryParams.limit = params['limit']; + } + if ('search' in params) { + queryParams.search = params['search']; + } + if ('type__in' in params) { + queryParams.type = this.typeParamService.composeTypeArray(params['type__in']); + } + if ('ordering' in params) { + queryParams.ordering = this.orderingParamService.composeOrderingState(params['ordering']); + } + return queryParams; + } +} diff --git a/libs/core/mappers/query-params.mapper.ts b/libs/core/mappers/query-params.mapper.ts index 796627e5..b090b24f 100644 --- a/libs/core/mappers/query-params.mapper.ts +++ b/libs/core/mappers/query-params.mapper.ts @@ -3,31 +3,31 @@ import { QueryParams } from '../models/query-params'; export namespace QueryParamsMapper { - /** - * Maps dto to model. - * @param model Genre dto. - */ - export function toQueryParamsDto(model: QueryParams): QueryParamsDto { - return { - offset: model.offset, - limit: model.limit, - search: model.search, - type__in: model.type, - ordering: model.ordering, - }; - } + // /** + // * Maps dto to model. + // * @param model Genre dto. + // */ + // export function toQueryParamsDto(model: QueryParams): QueryParamsDto { + // return { + // offset: model.offset, + // limit: model.limit, + // search: model.search, + // type__in: model.type, + // ordering: model.ordering, + // }; + // } - /** - * Maps dto to model. - * @param dto Genre dto. - */ - export function fromQueryParamsDto(dto: QueryParamsDto): QueryParams { - return new QueryParams({ - offset: dto.offset, - limit: dto.limit, - search: dto.search, - type: dto.type__in, - ordering: dto.ordering, - }); - } + // /** + // * Maps dto to model. + // * @param dto Genre dto. + // */ + // export function fromQueryParamsDto(dto: QueryParamsDto): QueryParams { + // return new QueryParams({ + // offset: dto.offset, + // limit: dto.limit, + // search: dto.search, + // type: dto.type__in, + // ordering: dto.ordering, + // }); + // } } diff --git a/libs/core/models/query-params.ts b/libs/core/models/query-params.ts index 8fa5f122..5e87146b 100644 --- a/libs/core/models/query-params.ts +++ b/libs/core/models/query-params.ts @@ -10,13 +10,13 @@ export class QueryParams extends Immerable { public readonly limit: number; /** Search. */ - public readonly search?: string; + public readonly search: string; /** Name. */ - public readonly type?: string; + public readonly type: string; /** Name. */ - public readonly ordering?: string; + public readonly ordering: string; public constructor(data: QueryParamsConstructorData) { super(); From a356d611503d781c3dcc609af317211a3bc7bee9 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Tue, 30 Jul 2024 15:19:53 +0700 Subject: [PATCH 10/64] Realize work with query params service JC-660 --- .../features/dashboard/dashboard.component.ts | 110 +++++++++--------- .../src/core/services/query-params.service.ts | 61 +++++++++- .../src/core/services/type-param.service.ts | 8 +- libs/core/mappers/anime-type.mapper.ts | 29 +++-- 4 files changed, 133 insertions(+), 75 deletions(-) diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index 2c437476..4cd7aefa 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -1,26 +1,19 @@ -import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, OnDestroy } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { Sort } from '@angular/material/sort'; -import { ActivatedRoute, Params, Router } from '@angular/router'; -import { combineLatest, EMPTY, map, Observable, of, switchMap, tap } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EMPTY, Observable, of, Subscription, switchMap } from 'rxjs'; import { Anime } from '@js-camp/core/models/anime'; import { Pagination } from '@js-camp/core/models/pagination'; -import { QueryParamsDto } from '@js-camp/core/dtos/query-params.dto'; import { AnimeApiService } from '@js-camp/angular/core/services/anime-api.service'; import { ProgressBarComponent } from '@js-camp/angular/shared/components/progress-bar/progress-bar.component'; import { AnimeType } from '@js-camp/core/models/anime-type'; -import { OrderingParamService } from '@js-camp/angular/core/services/ordering-param.service'; - import { AsyncPipe } from '@angular/common'; - -import { TypeParamService } from '@js-camp/angular/core/services/type-param.service'; - -import { Subscription } from 'react-redux'; +import { QueryParams, QueryParamsDto, QueryParamsService } from '@js-camp/angular/core/services/query-params.service'; import { TableComponent } from '../table/table.component'; import { DataRetrievalFormComponent } from '../filter-form/filter-form.component'; -import { QueryParams, QueryParamsService } from '@js-camp/angular/core/services/query-params.service'; /** Column headers to be displayed in table. */ export enum ParamsNames { @@ -45,7 +38,7 @@ export enum ParamsNames { ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DashboardComponent implements OnInit { +export class DashboardComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); @@ -55,14 +48,7 @@ export class DashboardComponent implements OnInit { private readonly queryParamsService = inject(QueryParamsService); - private readonly orderingParamService = inject(OrderingParamService); - - private readonly typeParamService = inject(TypeParamService); - - private defaultParams: QueryParamsDto = { - limit: 25, - offset: 0, - }; + private subs: Subscription[] = []; /** */ protected animeListPage$: Observable> = EMPTY; @@ -70,6 +56,15 @@ export class DashboardComponent implements OnInit { /** */ protected animeParams$: Observable = EMPTY; + /** */ + protected queryParams: QueryParams = { + offset: 0, + limit: 25, + search: '', + type: [], + ordering: { active: '', direction: '' }, + }; + /** @inheritdoc */ public ngOnInit(): void { @@ -77,30 +72,36 @@ export class DashboardComponent implements OnInit { switchMap(params => of(this.queryParamsService.fromQueryParams(params))), ); + this.subs.push(this.animeParams$.subscribe(params => { + this.queryParams = params; + })); + this.animeListPage$ = this.route.queryParams.pipe( switchMap(params => this.animeApiService.getPage(params as QueryParamsDto)), ); } + /** @inheritdoc */ + public ngOnDestroy(): void { + this.subs.forEach(sub => { + sub.unsubscribe(); + }); + } + /** */ protected setTypeSelect(event: AnimeType[]): void { - const queryParams: QueryParamsDto = { ...this.defaultParams }; - - queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(event); + const queryParams = { ...this.queryParams }; - // if (this.ordering) { - // queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); - // } - // if (this.search) { - // queryParams[ParamsNames.Search] = this.search; - // } + queryParams.type = event; + queryParams.limit = 25; + queryParams.offset = 0; this.router.navigate( [''], { - queryParams, + queryParams: this.queryParamsService.toQueryParams(queryParams), }, ); } @@ -108,22 +109,16 @@ export class DashboardComponent implements OnInit { /** */ protected setSearchSubmit(event: string): void { - // console.log(this.animeParams) + const queryParams = { ...this.queryParams }; - const queryParams: QueryParamsDto = { ...this.defaultParams }; - - // if (this.type.length > 0) { - // queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(this.type); - // } - // if (this.ordering) { - // queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(this.ordering); - // } - queryParams[ParamsNames.Search] = event; + queryParams.search = event; + queryParams.limit = 25; + queryParams.offset = 0; this.router.navigate( [''], { - queryParams, + queryParams: this.queryParamsService.toQueryParams(queryParams), }, ); } @@ -134,43 +129,46 @@ export class DashboardComponent implements OnInit { */ protected setPage(event: PageEvent): void { - const limit = event.pageSize; - const offset = event.pageIndex * event.pageSize; + const queryParams = { ...this.queryParams }; + + queryParams.limit = event.pageSize; + queryParams.offset = event.pageIndex * event.pageSize; this.router.navigate( [''], { - queryParams: { [ParamsNames.Offset]: offset, [ParamsNames.Limit]: limit }, + queryParams: this.queryParamsService.toQueryParams(queryParams), queryParamsHandling: 'merge', }, ); } - /** - * Track by function for anime list. - * @param index - Anime list item id. - * @param item - Item of anime list. - * @returns Item's id. - */ - protected trackByAnime(index: number, item: Anime): Anime['id'] { - return item.id; - } - /** * 1. * @param sortState - 1. */ public setOrdering(sortState: Sort): void { - const orderString = this.orderingParamService.composeOrderingString(sortState); + const queryParams = { ...this.queryParams }; + + queryParams.ordering = sortState; this.router.navigate( [''], { - queryParams: { ordering: orderString }, + queryParams: this.queryParamsService.toQueryParams(queryParams), queryParamsHandling: 'merge', }, ); } + /** + * Track by function for anime list. + * @param index - Anime list item id. + * @param item - Item of anime list. + * @returns Item's id. + */ + protected trackByAnime(index: number, item: Anime): Anime['id'] { + return item.id; + } } diff --git a/apps/angular/src/core/services/query-params.service.ts b/apps/angular/src/core/services/query-params.service.ts index be2578ab..427b466f 100644 --- a/apps/angular/src/core/services/query-params.service.ts +++ b/apps/angular/src/core/services/query-params.service.ts @@ -16,6 +16,25 @@ export enum ParamsNames { Ordering = 'ordering', } +/** */ +export type QueryParamsDto = { + + /** Offset. */ + [ParamsNames.Limit]: number; + + /** Limit. */ + [ParamsNames.Offset]: number; + + /** Search. */ + [ParamsNames.Search]: string; + + /** Type. */ + [ParamsNames.Type]: string; + + /** Ordering. */ + [ParamsNames.Ordering]: string; +}; + /** Params. */ export type QueryParams = { @@ -55,20 +74,50 @@ export class QueryParamsService { type: [], ordering: { active: '', direction: '' }, }; + if (ParamsNames.Offset in params) { + queryParams.offset = params[ParamsNames.Offset]; + } + if (ParamsNames.Limit in params) { + queryParams.limit = params[ParamsNames.Limit]; + } + if (ParamsNames.Search in params) { + queryParams.search = params[ParamsNames.Search]; + } + if (ParamsNames.Type in params) { + queryParams.type = this.typeParamService.composeTypeArray(params[ParamsNames.Type]); + } + if (ParamsNames.Ordering in params) { + queryParams.ordering = this.orderingParamService.composeOrderingState(params[ParamsNames.Ordering]); + } + return queryParams; + } + + /** + * Maps dto to model. + * @param params Genre dto. + */ + public toQueryParams(params: Params): QueryParamsDto { + const queryParams: QueryParamsDto = { + [ParamsNames.Offset]: 0, + [ParamsNames.Limit]: 25, + [ParamsNames.Search]: '', + [ParamsNames.Type]: '', + [ParamsNames.Ordering]: '', + }; if ('offset' in params) { - queryParams.offset = params['offset']; + queryParams[ParamsNames.Offset] = params['offset']; } if ('limit' in params) { - queryParams.limit = params['limit']; + queryParams[ParamsNames.Limit] = params['limit']; } if ('search' in params) { - queryParams.search = params['search']; + queryParams[ParamsNames.Search] = params['search']; } - if ('type__in' in params) { - queryParams.type = this.typeParamService.composeTypeArray(params['type__in']); + if ('type' in params) { + queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(params['type']); } if ('ordering' in params) { - queryParams.ordering = this.orderingParamService.composeOrderingState(params['ordering']); + queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(params['ordering']); } return queryParams; } diff --git a/apps/angular/src/core/services/type-param.service.ts b/apps/angular/src/core/services/type-param.service.ts index 7104f0f2..3a13477f 100644 --- a/apps/angular/src/core/services/type-param.service.ts +++ b/apps/angular/src/core/services/type-param.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; -import { toTypeDto } from '@js-camp/core/mappers/anime-type.mapper'; +import { AnimeTypeDto } from '@js-camp/core/dtos/anime-type.dto'; +import { AnimeTypeMapper } from '@js-camp/core/mappers/anime-type.mapper'; import { AnimeType } from '@js-camp/core/models/anime-type'; /** Anime API Access Service. */ @@ -13,7 +14,7 @@ export class TypeParamService { */ public composeTypeString(typesArr: AnimeType[]): string { if (typesArr) { - const typesDtoArr = typesArr.map(type => toTypeDto(type)); + const typesDtoArr = typesArr.map(type => AnimeTypeMapper.toDto(type)); return typesDtoArr.join(','); } return ''; @@ -25,7 +26,8 @@ export class TypeParamService { * @returns AnimeType. */ public composeTypeArray(typesString: string): AnimeType[] { - return typesString.split(',') as AnimeType[]; + const typesArr = typesString.split(',') ?? []; + return typesArr.map(type => AnimeTypeMapper.fromDto(type as AnimeTypeDto)); } } diff --git a/libs/core/mappers/anime-type.mapper.ts b/libs/core/mappers/anime-type.mapper.ts index 3ec05ce9..afda642b 100644 --- a/libs/core/mappers/anime-type.mapper.ts +++ b/libs/core/mappers/anime-type.mapper.ts @@ -41,15 +41,24 @@ export namespace AnimeTypeMapper { export function toDto(type: AnimeType): AnimeTypeDto { return ANIME_TYPE_MAP_TO_DTO[type]; } -} -/** - * Map type dto. - * @param type - Type dto. - * @returns Type model. - */ -export function toTypeDto(type: AnimeType): AnimeTypeDto { - const keyId = Object.values(AnimeType).indexOf(type); - const enumKey = Object.keys(AnimeType)[keyId] as TypeKey; - return AnimeTypeDto[enumKey]; + const animeTypeMap: Readonly> = { + [AnimeType.Movie]: AnimeTypeDto.Movie, + [AnimeType.Music]: AnimeTypeDto.Music, + [AnimeType.ONA]: AnimeTypeDto.ONA, + [AnimeType.OVA]: AnimeTypeDto.OVA, + [AnimeType.Promotional]: AnimeTypeDto.Promotional, + [AnimeType.Special]: AnimeTypeDto.Special, + [AnimeType.TV]: AnimeTypeDto.TV, + [AnimeType.Unknown]: AnimeTypeDto.Unknown, + }; + + /** + * Map type dto. + * @param type - Type dto. + * @returns Type model. + */ + export function toDto(type: AnimeType): AnimeTypeDto { + return animeTypeMap[type]; + } } From f74d6b916bde9986daa0c94160af00e0a609f997 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Tue, 30 Jul 2024 18:31:23 +0700 Subject: [PATCH 11/64] Improving code JC-660 --- .../dashboard/dashboard.component.html | 10 +- .../features/dashboard/dashboard.component.ts | 107 +++++++--------- .../filter-form/filter-form.component.css | 2 +- .../filter-form/filter-form.component.html | 6 +- .../filter-form/filter-form.component.ts | 32 ++--- .../app/features/table/table.component.css | 7 +- .../app/features/table/table.component.html | 20 +-- .../src/app/features/table/table.component.ts | 53 +++++--- .../core/services/ordering-param.service.ts | 22 ++-- .../src/core/services/query-params.service.ts | 120 ++++++------------ .../src/core/services/type-param.service.ts | 16 +-- libs/core/dtos/params-names.dto.ts | 7 + libs/core/dtos/query-params.dto.ts | 12 +- libs/core/mappers/query-params.mapper.ts | 33 ----- libs/core/mappers/sorting-columns.mapper.ts | 4 +- libs/core/models/query-params.ts | 43 +++---- libs/core/tsconfig.app.json | 2 +- 17 files changed, 218 insertions(+), 278 deletions(-) create mode 100644 libs/core/dtos/params-names.dto.ts delete mode 100644 libs/core/mappers/query-params.mapper.ts diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index ed0967ba..45cecfe2 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -2,16 +2,18 @@ @if (animeParams$ | async; as animeParams) { - @if (animeListPage$ | async; as animeListPage) { + [limit]="animeParams.limit" + [offset]="animeParams.offset" + (sortEvent)="onOrderingChange($event)" + (paginationEvent)="onPageChange($event)"/> } @else { } diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index 4cd7aefa..e443ba66 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, inject, OnInit, OnDestroy } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { Sort } from '@angular/material/sort'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router'; import { EMPTY, Observable, of, Subscription, switchMap } from 'rxjs'; import { Anime } from '@js-camp/core/models/anime'; import { Pagination } from '@js-camp/core/models/pagination'; @@ -10,33 +10,28 @@ import { ProgressBarComponent } from '@js-camp/angular/shared/components/progres import { AnimeType } from '@js-camp/core/models/anime-type'; import { AsyncPipe } from '@angular/common'; -import { QueryParams, QueryParamsDto, QueryParamsService } from '@js-camp/angular/core/services/query-params.service'; +import { QueryParamsService } from '@js-camp/angular/core/services/query-params.service'; + +import { QueryParams } from '@js-camp/core/models/query-params'; + +import { QueryParamsDto } from '@js-camp/core/dtos/query-params.dto'; import { TableComponent } from '../table/table.component'; import { DataRetrievalFormComponent } from '../filter-form/filter-form.component'; -/** Column headers to be displayed in table. */ -export enum ParamsNames { - Limit = 'limit', - Offset = 'offset', - Search = 'search', - Type = 'type__in', - Ordering = 'ordering', -} - /** Dashboard component. Contains table with list of anime. */ @Component({ selector: 'camp-dashboard', standalone: true, templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ProgressBarComponent, TableComponent, DataRetrievalFormComponent, AsyncPipe, ], - changeDetection: ChangeDetectionStrategy.OnPush, }) export class DashboardComponent implements OnInit, OnDestroy { @@ -50,20 +45,14 @@ export class DashboardComponent implements OnInit, OnDestroy { private subs: Subscription[] = []; - /** */ + /** Stream of anime page. */ protected animeListPage$: Observable> = EMPTY; - /** */ + /** Stream of anime page params. */ protected animeParams$: Observable = EMPTY; - /** */ - protected queryParams: QueryParams = { - offset: 0, - limit: 25, - search: '', - type: [], - ordering: { active: '', direction: '' }, - }; + /** Anime page params. */ + protected queryParams: QueryParams = this.queryParamsService.defaultQueryParams; /** @inheritdoc */ public ngOnInit(): void { @@ -89,77 +78,73 @@ export class DashboardComponent implements OnInit, OnDestroy { }); } - /** */ - protected setTypeSelect(event: AnimeType[]): void { - - const queryParams = { ...this.queryParams }; - - queryParams.type = event; - queryParams.limit = 25; - queryParams.offset = 0; - + private navigate(queryParams: QueryParams, queryParamsHandling: QueryParamsHandling): void { this.router.navigate( [''], { queryParams: this.queryParamsService.toQueryParams(queryParams), + queryParamsHandling, }, ); } - /** */ - protected setSearchSubmit(event: string): void { + /** + * Triggers when the list of selected anime types is changed. + * Forms a new query parameters object with new type list and navigate with these parameters. + * @param event - List of selected anime types. + */ + protected onTypeChange(event: AnimeType[]): void { + + const queryParams = { ...this.queryParams }; + + queryParams.type = event; + queryParams.offset = 0; + + this.navigate(queryParams, ''); + } + + /** + * Triggers when search term is changed. + * Forms a new query parameters object with a new search term and navigate with these parameters. + * @param event - List of selected anime types. + */ + protected onSearchChange(event: string): void { const queryParams = { ...this.queryParams }; queryParams.search = event; - queryParams.limit = 25; queryParams.offset = 0; - this.router.navigate( - [''], - { - queryParams: this.queryParamsService.toQueryParams(queryParams), - }, - ); + this.navigate(queryParams, ''); } /** - * Page. - * @param event - Page. + * Triggers when pagination is changed. + * Forms a new query parameters object with a new pagination data and navigate with these parameters. + * @param event - List of selected anime types. */ - protected setPage(event: PageEvent): void { + protected onPageChange(event: PageEvent): void { const queryParams = { ...this.queryParams }; queryParams.limit = event.pageSize; queryParams.offset = event.pageIndex * event.pageSize; - this.router.navigate( - [''], - { - queryParams: this.queryParamsService.toQueryParams(queryParams), - queryParamsHandling: 'merge', - }, - ); + this.navigate(queryParams, 'merge'); } /** - * 1. - * @param sortState - 1. + * Triggers when ordering is changed. + * Forms a new query parameters object with a new ordering data and navigate with these parameters. + * @param event - List of selected anime types. */ - public setOrdering(sortState: Sort): void { + public onOrderingChange(event: Sort): void { const queryParams = { ...this.queryParams }; - queryParams.ordering = sortState; + queryParams.ordering = event; - this.router.navigate( - [''], - { - queryParams: this.queryParamsService.toQueryParams(queryParams), - queryParamsHandling: 'merge', - }, - ); + this.navigate(queryParams, 'merge'); } /** diff --git a/apps/angular/src/app/features/filter-form/filter-form.component.css b/apps/angular/src/app/features/filter-form/filter-form.component.css index 01236856..d6e103ed 100644 --- a/apps/angular/src/app/features/filter-form/filter-form.component.css +++ b/apps/angular/src/app/features/filter-form/filter-form.component.css @@ -1,4 +1,4 @@ -.data-retrieval-form { +.filter-form { display: flex; flex-direction: row; align-items: center; diff --git a/apps/angular/src/app/features/filter-form/filter-form.component.html b/apps/angular/src/app/features/filter-form/filter-form.component.html index d0a778e9..b7fd5745 100644 --- a/apps/angular/src/app/features/filter-form/filter-form.component.html +++ b/apps/angular/src/app/features/filter-form/filter-form.component.html @@ -1,4 +1,4 @@ -
+
Types @@ -9,10 +9,10 @@
+ class="filter-form__search-form search-form"> - Attack on Titan + Anime title (); - /** */ + /** Anime type selection handler. */ protected onTypeSelect(): void { this.typeSelectEvent.emit(this.typesControl.value); } - /** */ - @Output() public searchEvent = new EventEmitter(); - - /** */ - protected onSearchSubmit(): void { - this.searchEvent.emit(String(this.searchForm.value.term)); - } + /** Initial value for search control. */ + @Input() public searchValue = ''; /** Search form. */ protected searchForm: FormGroup = new FormGroup({ term: new FormControl(''), }); - /** */ - @Input() public searchValue = ''; + /** Event of search form submitting. */ + @Output() public searchEvent = new EventEmitter(); + + /** Search form submitting handler. */ + protected onSearchSubmit(): void { + this.searchEvent.emit(String(this.searchForm.value.term)); + } /** @inheritdoc */ public ngOnInit(): void { diff --git a/apps/angular/src/app/features/table/table.component.css b/apps/angular/src/app/features/table/table.component.css index aa11f404..f057f1a4 100644 --- a/apps/angular/src/app/features/table/table.component.css +++ b/apps/angular/src/app/features/table/table.component.css @@ -19,6 +19,7 @@ .table__content { justify-content: center; font-size: var(--font-size-xs); + padding: var(--space-xs) var(--space-s); } .content__text { @@ -28,21 +29,17 @@ } .cover__wrapper { + position: relative; display: flex; justify-content: center; - padding: var(--space-xs) 0; width: 150px; height: 150px; - margin: 0 auto; } .cover__image { width: 100%; height: auto; object-fit: contain; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } @media (min-width: 786px) { diff --git a/apps/angular/src/app/features/table/table.component.html b/apps/angular/src/app/features/table/table.component.html index d5ff2f14..dcd2a7e3 100644 --- a/apps/angular/src/app/features/table/table.component.html +++ b/apps/angular/src/app/features/table/table.component.html @@ -4,7 +4,7 @@ [dataSource]="pageData.results" [trackBy]="trackByAnime" matSort - (matSortChange)="sortData($event)" + (matSortChange)="onSortData($event)" [matSortActive]="ordering.active" [matSortDirection]="ordering.direction" > @@ -13,7 +13,8 @@ {{columnsHeaders.Image}}
-
@@ -24,7 +25,7 @@ {{columnsHeaders.EnglishTitle}} - +

{{element.englishTitle | empty}}

@@ -33,7 +34,7 @@ {{columnsHeaders.JapaneseTitle}} - +

{{element.japaneseTitle | empty}}

@@ -63,13 +64,12 @@ - - + (page)="onPageChanged($event)" + aria-label="Select page"/>
} diff --git a/apps/angular/src/app/features/table/table.component.ts b/apps/angular/src/app/features/table/table.component.ts index a42722b6..8f55bdef 100644 --- a/apps/angular/src/app/features/table/table.component.ts +++ b/apps/angular/src/app/features/table/table.component.ts @@ -1,9 +1,9 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { MatTableModule } from '@angular/material/table'; import { Anime } from '@js-camp/core/models/anime'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatSort, Sort, MatSortModule } from '@angular/material/sort'; -import { AsyncPipe, DatePipe } from '@angular/common'; +import { AsyncPipe, DatePipe, NgOptimizedImage } from '@angular/common'; import { EmptyPipe } from '@js-camp/angular/shared/pipes/empty.pipe'; import { Pagination } from '@js-camp/core/models/pagination'; @@ -17,12 +17,13 @@ export enum ColumnsHeaders { Status = 'Status', } -/** Dashboard component. Contains table with list of anime. */ +/** Anime table component. */ @Component({ selector: 'camp-table', standalone: true, templateUrl: './table.component.html', styleUrl: './table.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ MatTableModule, MatPaginator, @@ -31,16 +32,37 @@ export enum ColumnsHeaders { AsyncPipe, DatePipe, EmptyPipe, + NgOptimizedImage, ], }) -export class TableComponent { +export class TableComponent implements OnInit { - /** */ + /** Anime list page data. */ @Input() public pageData?: Pagination; - /** */ + /** Sorting ordering settings. */ @Input() public ordering?: Sort; + /** Limit of anime for one page. */ + @Input() public limit = 25; + + /** Offset of anime list. */ + @Input() public offset = 0; + + /** Index of current page. */ + protected pageIndex = 0; + + /** Pages count. */ + protected pagesCount = 0; + + /** @inheritdoc */ + public ngOnInit(): void { + this.pageIndex = this.offset / this.limit; + if (this.pageData) { + this.pagesCount = this.pageData.count / this.limit; + } + } + /** * Track by function for anime list. * @param index - Anime list item id. @@ -57,25 +79,26 @@ export class TableComponent { /** List of column headers. */ protected readonly columnsToDisplay = Object.values(ColumnsHeaders); - /** */ + /** Event of pagination change. */ @Output() public paginationEvent = new EventEmitter(); /** - * Page. - * @param event - Page. + * Pagination changing handler. + * @param event - Pagination settings. */ - protected pageChanged(event: PageEvent): void { + protected onPageChanged(event: PageEvent): void { this.paginationEvent.emit(event); } - /** */ + /** Event of table sorting. */ @Output() public sortEvent = new EventEmitter(); - /** Announce the change in sort state for assistive technology. - * @param sortState - Anime list item id. + /** + * Handler for changes in sorting settings. + * @param event - Sorting settings. */ - protected sortData(sortState: Sort): void { - this.sortEvent.emit(sortState); + protected onSortData(event: Sort): void { + this.sortEvent.emit(event); } } diff --git a/apps/angular/src/core/services/ordering-param.service.ts b/apps/angular/src/core/services/ordering-param.service.ts index 01677806..cc0c0d6a 100644 --- a/apps/angular/src/core/services/ordering-param.service.ts +++ b/apps/angular/src/core/services/ordering-param.service.ts @@ -4,21 +4,21 @@ import { SortingColumns } from '@js-camp/core/models/sorting-columns'; import { ColumnsMapper } from '@js-camp/core/mappers/sorting-columns.mapper'; import { SortingColumnsDto } from '@js-camp/core/dtos/sorting-columns.dto'; -/** Anime API Access Service. */ +/** Ordering query param service. */ @Injectable({ providedIn: 'root' }) export class OrderingParamService { /** - * AnimeType. - * @param sortState - AnimeType. - * @returns AnimeType. + * Compose value for query param from sort settings object. + * @param sortState - Sort settings. + * @returns Query param value. */ public composeOrderingString(sortState: Sort): string { let orderString = ''; if (sortState) { - const sortField = ColumnsMapper.toSortingColumnsDto(sortState.active as SortingColumns); + const sortField = ColumnsMapper.toDto(sortState.active as SortingColumns); switch (sortState.direction) { case 'asc': @@ -36,11 +36,11 @@ export class OrderingParamService { } /** - * AnimeType. - * @param ordering - AnimeType. - * @returns AnimeType. + * Compose sort settings object from query param value. + * @param ordering - Query param value. + * @returns Sort settings. */ - public composeOrderingState(ordering: string): Sort { + public composeOrderingState(ordering: SortingColumnsDto | ''): Sort { const sortState: Sort = { active: '', direction: '' }; @@ -49,12 +49,12 @@ export class OrderingParamService { } if (ordering[0] === '-') { - sortState.active = ColumnsMapper.fromSortingColumnsDto(ordering.slice(1) as SortingColumnsDto); + sortState.active = ColumnsMapper.fromDto(ordering.slice(1) as SortingColumnsDto); sortState.direction = 'desc'; return sortState; } - sortState.active = ColumnsMapper.fromSortingColumnsDto(ordering as SortingColumnsDto); + sortState.active = ColumnsMapper.fromDto(ordering as SortingColumnsDto); sortState.direction = 'asc'; return sortState; diff --git a/apps/angular/src/core/services/query-params.service.ts b/apps/angular/src/core/services/query-params.service.ts index 427b466f..e0c90719 100644 --- a/apps/angular/src/core/services/query-params.service.ts +++ b/apps/angular/src/core/services/query-params.service.ts @@ -1,60 +1,15 @@ import { inject, Injectable } from '@angular/core'; -import { AnimeType } from '@js-camp/core/models/anime-type'; -import { Sort } from '@angular/material/sort'; import { Params } from '@angular/router'; -import { TypeParamService } from './type-param.service'; -import { OrderingParamService } from './ordering-param.service'; - -/** */ -export enum ParamsNames { - Limit = 'limit', - Offset = 'offset', - Search = 'search', - Type = 'type__in', - Ordering = 'ordering', -} - -/** */ -export type QueryParamsDto = { - - /** Offset. */ - [ParamsNames.Limit]: number; - - /** Limit. */ - [ParamsNames.Offset]: number; - - /** Search. */ - [ParamsNames.Search]: string; - - /** Type. */ - [ParamsNames.Type]: string; - - /** Ordering. */ - [ParamsNames.Ordering]: string; -}; +import { QueryParams } from '@js-camp/core/models/query-params'; +import { ParamsNamesDto } from '@js-camp/core/dtos/params-names.dto'; +import { QueryParamsDto } from '@js-camp/core/dtos/query-params.dto'; -/** Params. */ -export type QueryParams = { - - /** Offset. */ - offset: number; - - /** Limit. */ - limit: number; - - /** Search. */ - search: string; - - /** Type. */ - type: AnimeType[]; - - /** Ordering. */ - ordering: Sort; -}; +import { OrderingParamService } from './ordering-param.service'; +import { TypeParamService } from './type-param.service'; -/** Anime API Access Service. */ +/** Query params service. */ @Injectable({ providedIn: 'root' }) export class QueryParamsService { @@ -62,62 +17,65 @@ export class QueryParamsService { private typeParamService = inject(TypeParamService); + /** Default query params. */ + public defaultQueryParams: QueryParams = { + offset: 0, + limit: 25, + search: '', + type: [], + ordering: { active: '', direction: '' }, + }; + /** * Maps dto to model. - * @param params Genre dto. + * @param params Query params. */ public fromQueryParams(params: Params): QueryParams { - const queryParams: QueryParams = { - offset: 0, - limit: 25, - search: '', - type: [], - ordering: { active: '', direction: '' }, - }; - if (ParamsNames.Offset in params) { - queryParams.offset = params[ParamsNames.Offset]; + const queryParams: QueryParams = { ...this.defaultQueryParams }; + if (ParamsNamesDto.Offset in params) { + queryParams.offset = params[ParamsNamesDto.Offset]; } - if (ParamsNames.Limit in params) { - queryParams.limit = params[ParamsNames.Limit]; + if (ParamsNamesDto.Limit in params) { + queryParams.limit = params[ParamsNamesDto.Limit]; } - if (ParamsNames.Search in params) { - queryParams.search = params[ParamsNames.Search]; + if (ParamsNamesDto.Search in params) { + queryParams.search = params[ParamsNamesDto.Search]; } - if (ParamsNames.Type in params) { - queryParams.type = this.typeParamService.composeTypeArray(params[ParamsNames.Type]); + if (ParamsNamesDto.Type in params) { + queryParams.type = this.typeParamService.composeTypeArray(params[ParamsNamesDto.Type]); } - if (ParamsNames.Ordering in params) { - queryParams.ordering = this.orderingParamService.composeOrderingState(params[ParamsNames.Ordering]); + if (ParamsNamesDto.Ordering in params) { + queryParams.ordering = this.orderingParamService.composeOrderingState(params[ParamsNamesDto.Ordering]); } return queryParams; } /** - * Maps dto to model. - * @param params Genre dto. + * Maps model to dto. + * @param params Query params. */ public toQueryParams(params: Params): QueryParamsDto { const queryParams: QueryParamsDto = { - [ParamsNames.Offset]: 0, - [ParamsNames.Limit]: 25, - [ParamsNames.Search]: '', - [ParamsNames.Type]: '', - [ParamsNames.Ordering]: '', + [ParamsNamesDto.Offset]: 0, + [ParamsNamesDto.Limit]: 25, + [ParamsNamesDto.Search]: '', + [ParamsNamesDto.Type]: '', + [ParamsNamesDto.Ordering]: '', }; if ('offset' in params) { - queryParams[ParamsNames.Offset] = params['offset']; + queryParams[ParamsNamesDto.Offset] = params['offset']; } if ('limit' in params) { - queryParams[ParamsNames.Limit] = params['limit']; + queryParams[ParamsNamesDto.Limit] = params['limit']; } if ('search' in params) { - queryParams[ParamsNames.Search] = params['search']; + queryParams[ParamsNamesDto.Search] = params['search']; } if ('type' in params) { - queryParams[ParamsNames.Type] = this.typeParamService.composeTypeString(params['type']); + queryParams[ParamsNamesDto.Type] = this.typeParamService.composeTypeString(params['type']); } if ('ordering' in params) { - queryParams[ParamsNames.Ordering] = this.orderingParamService.composeOrderingString(params['ordering']); + queryParams[ParamsNamesDto.Ordering] = this.orderingParamService.composeOrderingString(params['ordering']); } return queryParams; } diff --git a/apps/angular/src/core/services/type-param.service.ts b/apps/angular/src/core/services/type-param.service.ts index 3a13477f..75034968 100644 --- a/apps/angular/src/core/services/type-param.service.ts +++ b/apps/angular/src/core/services/type-param.service.ts @@ -3,14 +3,14 @@ import { AnimeTypeDto } from '@js-camp/core/dtos/anime-type.dto'; import { AnimeTypeMapper } from '@js-camp/core/mappers/anime-type.mapper'; import { AnimeType } from '@js-camp/core/models/anime-type'; -/** Anime API Access Service. */ +/** Anime type query param service. */ @Injectable({ providedIn: 'root' }) export class TypeParamService { /** - * AnimeType. - * @param typesArr - AnimeType. - * @returns AnimeType. + * Compose value for query param from types array. + * @param typesArr - Array of selected types. + * @returns Value for query param. */ public composeTypeString(typesArr: AnimeType[]): string { if (typesArr) { @@ -21,12 +21,12 @@ export class TypeParamService { } /** - * AnimeType. - * @param typesString - AnimeType. - * @returns AnimeType. + * Compose types array from query param value. + * @param typesString - Query param value. + * @returns Array of anime types. */ public composeTypeArray(typesString: string): AnimeType[] { - const typesArr = typesString.split(',') ?? []; + const typesArr = typesString.split(','); return typesArr.map(type => AnimeTypeMapper.fromDto(type as AnimeTypeDto)); } diff --git a/libs/core/dtos/params-names.dto.ts b/libs/core/dtos/params-names.dto.ts new file mode 100644 index 00000000..e1f4800d --- /dev/null +++ b/libs/core/dtos/params-names.dto.ts @@ -0,0 +1,7 @@ +export enum ParamsNamesDto { + Limit = 'limit', + Offset = 'offset', + Search = 'search', + Type = 'type__in', + Ordering = 'ordering', +} diff --git a/libs/core/dtos/query-params.dto.ts b/libs/core/dtos/query-params.dto.ts index e6489d4f..965b7581 100644 --- a/libs/core/dtos/query-params.dto.ts +++ b/libs/core/dtos/query-params.dto.ts @@ -1,18 +1,20 @@ +import { ParamsNamesDto } from './params-names.dto'; + /** Params DTO. */ export type QueryParamsDto = { /** Offset. */ - offset: number; + [ParamsNamesDto.Limit]: number; /** Limit. */ - limit: number; + [ParamsNamesDto.Offset]: number; /** Search. */ - search?: string; + [ParamsNamesDto.Search]: string; /** Type. */ - type__in?: string; + [ParamsNamesDto.Type]: string; /** Ordering. */ - ordering?: string; + [ParamsNamesDto.Ordering]: string; }; diff --git a/libs/core/mappers/query-params.mapper.ts b/libs/core/mappers/query-params.mapper.ts deleted file mode 100644 index b090b24f..00000000 --- a/libs/core/mappers/query-params.mapper.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { QueryParamsDto } from '../dtos/query-params.dto'; -import { QueryParams } from '../models/query-params'; - -export namespace QueryParamsMapper { - - // /** - // * Maps dto to model. - // * @param model Genre dto. - // */ - // export function toQueryParamsDto(model: QueryParams): QueryParamsDto { - // return { - // offset: model.offset, - // limit: model.limit, - // search: model.search, - // type__in: model.type, - // ordering: model.ordering, - // }; - // } - - // /** - // * Maps dto to model. - // * @param dto Genre dto. - // */ - // export function fromQueryParamsDto(dto: QueryParamsDto): QueryParams { - // return new QueryParams({ - // offset: dto.offset, - // limit: dto.limit, - // search: dto.search, - // type: dto.type__in, - // ordering: dto.ordering, - // }); - // } -} diff --git a/libs/core/mappers/sorting-columns.mapper.ts b/libs/core/mappers/sorting-columns.mapper.ts index 58dbe5dc..41d16b08 100644 --- a/libs/core/mappers/sorting-columns.mapper.ts +++ b/libs/core/mappers/sorting-columns.mapper.ts @@ -14,7 +14,7 @@ export namespace ColumnsMapper { * @param column SortingColumnsDto. * @returns SortingColumnsDto. */ - export function fromSortingColumnsDto(column: SortingColumnsDto): SortingColumns { + export function fromDto(column: SortingColumnsDto): SortingColumns { return sortingColumnsDtoMap[column]; } @@ -29,7 +29,7 @@ export namespace ColumnsMapper { * @param column SortingColumnsDto. * @returns SortingColumnsDto. */ - export function toSortingColumnsDto(column: SortingColumns): SortingColumnsDto { + export function toDto(column: SortingColumns): SortingColumnsDto { return sortingColumnsMap[column]; } diff --git a/libs/core/models/query-params.ts b/libs/core/models/query-params.ts index 5e87146b..0b53c1c7 100644 --- a/libs/core/models/query-params.ts +++ b/libs/core/models/query-params.ts @@ -1,31 +1,30 @@ -import { Immerable, OmitImmerable } from './immerable'; +import { AnimeType } from './anime-type'; -/** Genre. */ -export class QueryParams extends Immerable { +/** */ +export type Sort = { + + /** */ + active: string; + + /** */ + direction: '' | 'asc' | 'desc'; +}; + +/** Params. */ +export type QueryParams = { /** Offset. */ - public readonly offset: number; + offset: number; /** Limit. */ - public readonly limit: number; + limit: number; /** Search. */ - public readonly search: string; - - /** Name. */ - public readonly type: string; - - /** Name. */ - public readonly ordering: string; + search: string; - public constructor(data: QueryParamsConstructorData) { - super(); - this.offset = data.offset; - this.limit = data.limit; - this.search = data.search; - this.type = data.type; - this.ordering = data.ordering; - } -} + /** Type. */ + type: AnimeType[]; -type QueryParamsConstructorData = OmitImmerable; + /** Ordering. */ + ordering: Sort; +}; diff --git a/libs/core/tsconfig.app.json b/libs/core/tsconfig.app.json index 5a299096..545924c2 100644 --- a/libs/core/tsconfig.app.json +++ b/libs/core/tsconfig.app.json @@ -5,5 +5,5 @@ "outDir": "../../dist/out-tsc", "types": ["node"] }, - "include": ["**/*.ts"] + "include": ["**/*.ts", "models/query-params.ts"] } From 852f2dc9073fc9cb442dcf550c04cbd65f9d7a59 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 12:14:05 +0700 Subject: [PATCH 12/64] Write documentation JC-660 --- .../dashboard/dashboard.component.html | 5 ----- .../features/dashboard/dashboard.component.ts | 18 ++++-------------- .../filter-form/filter-form.component.css | 8 +++++++- .../filter-form/filter-form.component.html | 3 ++- .../filter-form/filter-form.component.ts | 5 ++++- .../app/features/table/table.component.html | 1 - .../src/app/features/table/table.component.ts | 4 ++-- libs/core/dtos/params-names.dto.ts | 7 ------- libs/core/dtos/query-params.dto.ts | 4 +--- libs/core/mappers/anime-sort.mapper.ts | 2 ++ libs/core/models/pagination.ts | 2 +- 11 files changed, 23 insertions(+), 36 deletions(-) delete mode 100644 libs/core/dtos/params-names.dto.ts diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.html b/apps/angular/src/app/features/dashboard/dashboard.component.html index 45cecfe2..c563955c 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.html +++ b/apps/angular/src/app/features/dashboard/dashboard.component.html @@ -1,12 +1,9 @@
- @if (animeParams$ | async; as animeParams) { - - @if (animeListPage$ | async; as animeListPage) { } - } -
diff --git a/apps/angular/src/app/features/dashboard/dashboard.component.ts b/apps/angular/src/app/features/dashboard/dashboard.component.ts index e443ba66..a48b2ada 100644 --- a/apps/angular/src/app/features/dashboard/dashboard.component.ts +++ b/apps/angular/src/app/features/dashboard/dashboard.component.ts @@ -19,7 +19,7 @@ import { QueryParamsDto } from '@js-camp/core/dtos/query-params.dto'; import { TableComponent } from '../table/table.component'; import { DataRetrievalFormComponent } from '../filter-form/filter-form.component'; -/** Dashboard component. Contains table with list of anime. */ +/** Dashboard component. Contains table and form components. */ @Component({ selector: 'camp-dashboard', standalone: true, @@ -106,7 +106,7 @@ export class DashboardComponent implements OnInit, OnDestroy { /** * Triggers when search term is changed. * Forms a new query parameters object with a new search term and navigate with these parameters. - * @param event - List of selected anime types. + * @param event - Search term. */ protected onSearchChange(event: string): void { @@ -121,7 +121,7 @@ export class DashboardComponent implements OnInit, OnDestroy { /** * Triggers when pagination is changed. * Forms a new query parameters object with a new pagination data and navigate with these parameters. - * @param event - List of selected anime types. + * @param event - Pagination settings. */ protected onPageChange(event: PageEvent): void { @@ -136,7 +136,7 @@ export class DashboardComponent implements OnInit, OnDestroy { /** * Triggers when ordering is changed. * Forms a new query parameters object with a new ordering data and navigate with these parameters. - * @param event - List of selected anime types. + * @param event - Ordering settings. */ public onOrderingChange(event: Sort): void { @@ -146,14 +146,4 @@ export class DashboardComponent implements OnInit, OnDestroy { this.navigate(queryParams, 'merge'); } - - /** - * Track by function for anime list. - * @param index - Anime list item id. - * @param item - Item of anime list. - * @returns Item's id. - */ - protected trackByAnime(index: number, item: Anime): Anime['id'] { - return item.id; - } } diff --git a/apps/angular/src/app/features/filter-form/filter-form.component.css b/apps/angular/src/app/features/filter-form/filter-form.component.css index d6e103ed..c8f09b2f 100644 --- a/apps/angular/src/app/features/filter-form/filter-form.component.css +++ b/apps/angular/src/app/features/filter-form/filter-form.component.css @@ -1,3 +1,7 @@ +:host { + --button-margin-bottom: 20px; +} + .filter-form { display: flex; flex-direction: row; @@ -13,5 +17,7 @@ } .search-form__button { - margin-bottom: 20px; + /* Used margin, because mat-form-field component creates an empty component of this height from below itself. + Which makes it difficult to align the mat-form-field and the button relative to each other */ + margin-bottom: var(--button-margin-bottom); } diff --git a/apps/angular/src/app/features/filter-form/filter-form.component.html b/apps/angular/src/app/features/filter-form/filter-form.component.html index b7fd5745..d8a9c518 100644 --- a/apps/angular/src/app/features/filter-form/filter-form.component.html +++ b/apps/angular/src/app/features/filter-form/filter-form.component.html @@ -16,7 +16,8 @@ + class="search-form__input" + type="search"/>
- } diff --git a/apps/angular/src/app/features/table/table.component.ts b/apps/angular/src/app/features/table/table.component.ts index 8f55bdef..d3ca47bd 100644 --- a/apps/angular/src/app/features/table/table.component.ts +++ b/apps/angular/src/app/features/table/table.component.ts @@ -37,10 +37,10 @@ export enum ColumnsHeaders { }) export class TableComponent implements OnInit { - /** Anime list page data. */ + /** Anime page data. */ @Input() public pageData?: Pagination; - /** Sorting ordering settings. */ + /** Ordering settings for sort. */ @Input() public ordering?: Sort; /** Limit of anime for one page. */ diff --git a/libs/core/dtos/params-names.dto.ts b/libs/core/dtos/params-names.dto.ts deleted file mode 100644 index e1f4800d..00000000 --- a/libs/core/dtos/params-names.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ParamsNamesDto { - Limit = 'limit', - Offset = 'offset', - Search = 'search', - Type = 'type__in', - Ordering = 'ordering', -} diff --git a/libs/core/dtos/query-params.dto.ts b/libs/core/dtos/query-params.dto.ts index 965b7581..acd90b56 100644 --- a/libs/core/dtos/query-params.dto.ts +++ b/libs/core/dtos/query-params.dto.ts @@ -1,6 +1,4 @@ -import { ParamsNamesDto } from './params-names.dto'; - -/** Params DTO. */ +/** Params DTO. Data for query params and params for request to the server. */ export type QueryParamsDto = { /** Offset. */ diff --git a/libs/core/mappers/anime-sort.mapper.ts b/libs/core/mappers/anime-sort.mapper.ts index 00f3874a..9a44fc00 100644 --- a/libs/core/mappers/anime-sort.mapper.ts +++ b/libs/core/mappers/anime-sort.mapper.ts @@ -9,6 +9,8 @@ export namespace AnimeSortMapper { const DESCENDING_PREFIX = '-'; + const DESCENDING_PREFIX = '-'; + /** * Map object with active field and sorting direction to ordering param. * @param ordering - Object with settings for sort. diff --git a/libs/core/models/pagination.ts b/libs/core/models/pagination.ts index c3c5f968..83643b49 100644 --- a/libs/core/models/pagination.ts +++ b/libs/core/models/pagination.ts @@ -13,7 +13,7 @@ export class Pagination extends Immerable { public readonly previous: string; /** Array of items requested. */ - public results: T[]; + public readonly results: readonly T[]; public constructor(data: PaginationConstructorData) { super(); From 90190cd9899960b8909643631e8f9d1c55dd8ce2 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 16:24:48 +0700 Subject: [PATCH 13/64] Add navigation JC-679 --- apps/angular/src/app/app.component.css | 30 +++++++++++++++++++++++++ apps/angular/src/app/app.component.html | 6 ++++- apps/angular/src/app/app.component.ts | 8 +++++-- apps/angular/src/app/app.routes.ts | 10 +++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/apps/angular/src/app/app.component.css b/apps/angular/src/app/app.component.css index bc082c5e..870765ec 100644 --- a/apps/angular/src/app/app.component.css +++ b/apps/angular/src/app/app.component.css @@ -3,6 +3,36 @@ } .header { + display: flex; + flex-direction: row; + justify-content: space-between; width: 100%; border-bottom: 2px var(--border-color) solid; + padding: 0 var(--space-m); +} + +.header__nav { + display: flex; + flex-direction: row; + justify-content: end; + gap: var(--space-s); +} + +.header__logo-link { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-medium); +} + +.header__link { + font-size: var(--font-size-md); + text-decoration: none; + color: var(--text-color); +} + +.header__link:active { + color: var(--active-link-color); +} + +.header__link:hover { + text-decoration: underline; } diff --git a/apps/angular/src/app/app.component.html b/apps/angular/src/app/app.component.html index cae376c6..5026206a 100644 --- a/apps/angular/src/app/app.component.html +++ b/apps/angular/src/app/app.component.html @@ -1,4 +1,8 @@ -

Anime app

+ Anime app +
diff --git a/apps/angular/src/app/app.component.ts b/apps/angular/src/app/app.component.ts index f5e0b303..24470051 100644 --- a/apps/angular/src/app/app.component.ts +++ b/apps/angular/src/app/app.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, RouterLink } from '@angular/router'; import { MatToolbarModule } from '@angular/material/toolbar'; /** Main component of application. */ @@ -8,6 +8,10 @@ import { MatToolbarModule } from '@angular/material/toolbar'; templateUrl: './app.component.html', styleUrls: ['./app.component.css'], standalone: true, - imports: [RouterModule, MatToolbarModule], + imports: [ + MatToolbarModule, + RouterModule, + RouterLink, + ], }) export class AppComponent {} diff --git a/apps/angular/src/app/app.routes.ts b/apps/angular/src/app/app.routes.ts index f0eb5716..8edd38b7 100644 --- a/apps/angular/src/app/app.routes.ts +++ b/apps/angular/src/app/app.routes.ts @@ -8,4 +8,14 @@ export const appRoutes: Routes = [ path: '', component: AnimeDashboardComponent, }, + { + path: 'login', + loadComponent: () => import('./features/login/login-form.component') + .then(c => c.LoginFormComponent), + }, + { + path: 'registration', + loadComponent: () => import('./features/registration/registration-form.component') + .then(c => c.RegistrationFormComponent), + }, ]; From 1cb1ff8f095474a5d8008f0ccd0d858a8008da0c Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 16:25:27 +0700 Subject: [PATCH 14/64] Create login component JC-679 --- .../app/features/login/login-form.component.css | 5 +++++ .../app/features/login/login-form.component.html | 3 +++ .../app/features/login/login-form.component.ts | 15 +++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 apps/angular/src/app/features/login/login-form.component.css create mode 100644 apps/angular/src/app/features/login/login-form.component.html create mode 100644 apps/angular/src/app/features/login/login-form.component.ts diff --git a/apps/angular/src/app/features/login/login-form.component.css b/apps/angular/src/app/features/login/login-form.component.css new file mode 100644 index 00000000..e46dde1c --- /dev/null +++ b/apps/angular/src/app/features/login/login-form.component.css @@ -0,0 +1,5 @@ +.login-form { + display: flex; + flex-direction: column; + gap: var(--space-s); +} diff --git a/apps/angular/src/app/features/login/login-form.component.html b/apps/angular/src/app/features/login/login-form.component.html new file mode 100644 index 00000000..2879f6ad --- /dev/null +++ b/apps/angular/src/app/features/login/login-form.component.html @@ -0,0 +1,3 @@ + diff --git a/apps/angular/src/app/features/login/login-form.component.ts b/apps/angular/src/app/features/login/login-form.component.ts new file mode 100644 index 00000000..5a870873 --- /dev/null +++ b/apps/angular/src/app/features/login/login-form.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; + +/** Component with form for authorization. */ +@Component({ + selector: 'camp-login-form', + standalone: true, + templateUrl: './login-form.component.html', + styleUrl: './login-form.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ReactiveFormsModule], +}) +export class LoginFormComponent { + +} From d38c2fef818c879977ad5a087719521afd8a37e2 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 16:26:10 +0700 Subject: [PATCH 15/64] Create registration component JC-679 --- .../registration-form.component.css | 27 +++++++ .../registration-form.component.html | 74 +++++++++++++++++++ .../registration-form.component.ts | 42 +++++++++++ 3 files changed, 143 insertions(+) create mode 100644 apps/angular/src/app/features/registration/registration-form.component.css create mode 100644 apps/angular/src/app/features/registration/registration-form.component.html create mode 100644 apps/angular/src/app/features/registration/registration-form.component.ts diff --git a/apps/angular/src/app/features/registration/registration-form.component.css b/apps/angular/src/app/features/registration/registration-form.component.css new file mode 100644 index 00000000..0b5a756a --- /dev/null +++ b/apps/angular/src/app/features/registration/registration-form.component.css @@ -0,0 +1,27 @@ +.registration-form-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + align-content: center; + padding: var(--space-xl); +} + +.registration-header { + font-size: var(--font-size-xl); +} + +.registration-form { + min-width: 360px; + padding: var(--space-m); + border-radius: 24px; + border: 2px var(--border-color) solid; + display: flex; + flex-direction: column; + gap: var(--space-s); + align-items: center; +} + +.registration-form__field { + width: 100%; +} diff --git a/apps/angular/src/app/features/registration/registration-form.component.html b/apps/angular/src/app/features/registration/registration-form.component.html new file mode 100644 index 00000000..33c672c2 --- /dev/null +++ b/apps/angular/src/app/features/registration/registration-form.component.html @@ -0,0 +1,74 @@ +
+

Registration

+ + + + Email + + @if (registrationForm.get('email')?.hasError('required') && registrationForm.get('email')?.touched) { + Email is required. + } @else if (registrationForm.get('email')?.hasError('email') && registrationForm.get('email')?.touched) { + Enter a valid email address. + } + + + + First Name + + @if (registrationForm.get('firstName')?.hasError('required') && registrationForm.get('firstName')?.touched) { + First name is required. + } + + + + Last Name + + @if (registrationForm.get('lastName')?.hasError('required') && registrationForm.get('lastName')?.touched) { + Last name is required. + } + + + + Password + + @if (registrationForm.get('password')?.hasError('required') && registrationForm.get('password')?.touched) { + Password is required. + } @else if (registrationForm.get('password')?.hasError('mustMatch') && registrationForm.get('password')?.touched) { + Passwords must match. + } + + + + Repeat the password + + @if (registrationForm.get('retypedPassword')?.hasError('required') && registrationForm.get('retypedPassword')?.touched) { + Retyped password is required. + } @else if (registrationForm.get('retypedPassword')?.hasError('mustMatch') && registrationForm.get('retypedPassword')?.touched) { + Passwords must match. + } + + +
+ +
+ + +
diff --git a/apps/angular/src/app/features/registration/registration-form.component.ts b/apps/angular/src/app/features/registration/registration-form.component.ts new file mode 100644 index 00000000..0eb75d9d --- /dev/null +++ b/apps/angular/src/app/features/registration/registration-form.component.ts @@ -0,0 +1,42 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { FormFieldsService } from '@js-camp/angular/core/services/form-fields.service'; + +/** Component with form for registration. */ +@Component({ + selector: 'camp-registration-form', + standalone: true, + templateUrl: './registration-form.component.html', + styleUrl: './registration-form.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatButtonModule, + ], +}) +export class RegistrationFormComponent { + + private readonly formFieldsService = inject(FormFieldsService); + + /** Form for registration. */ + public registrationForm = new FormGroup({ + email: new FormControl('', [Validators.required, Validators.email]), + firstName: new FormControl('', [Validators.required]), + lastName: new FormControl('', [Validators.required]), + password: new FormControl('', [Validators.required]), + retypedPassword: new FormControl('', [Validators.required]), + }, { validators: this.formFieldsService.matchFieldsValidator('password', 'retypedPassword') }); + + /** */ + protected onRegistrationSubmit(): void { + console.warn(this.registrationForm.value); + } + +} From dc5e5d9751c67f76ed0d9f795c9be464e09680a6 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 16:26:40 +0700 Subject: [PATCH 16/64] Create service for form fields compare JC-679 --- .../src/core/services/form-fields.service.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 apps/angular/src/core/services/form-fields.service.ts diff --git a/apps/angular/src/core/services/form-fields.service.ts b/apps/angular/src/core/services/form-fields.service.ts new file mode 100644 index 00000000..3459e656 --- /dev/null +++ b/apps/angular/src/core/services/form-fields.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { AbstractControl, ValidatorFn } from '@angular/forms'; + +/** CompareFormFieldsServiceservice. */ +@Injectable({ providedIn: 'root' }) +export class FormFieldsService { + + /** + * AbstractControl. + * @param formField AbstractControl. + * @param anotherFormField AbstractControl. + * @returns AbstractControl. + */ + public matchFieldsValidator(formField: string, anotherFormField: string): ValidatorFn { + return (control: AbstractControl): { [key: string]: boolean; } | null => { + const formFieldControl = control.get(formField); + const anotherFormFieldControl = control.get(anotherFormField); + + if (!formFieldControl || !anotherFormFieldControl) { + return null; + } + + // Check if the passwords do not match + if (formFieldControl.value !== anotherFormFieldControl.value) { + // Set the error on the retypedPassword control + formFieldControl.setErrors({ mustMatch: true }); + anotherFormFieldControl.setErrors({ mustMatch: true }); + return { mustMatch: true }; + } + + // Clear the error if the passwords match + formFieldControl.setErrors(null); + anotherFormFieldControl.setErrors(null); + return null; + + }; + } +} From 251d141e213901eb1c07c6b450793409a03767b8 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 16:26:58 +0700 Subject: [PATCH 17/64] Add css variables JC-679 --- libs/theme/src/variables.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/theme/src/variables.css b/libs/theme/src/variables.css index 252ef95a..3e959ee1 100644 --- a/libs/theme/src/variables.css +++ b/libs/theme/src/variables.css @@ -10,6 +10,8 @@ --background-contrast-color: rgb(0 0 0); --error-color: rgb(255 0 0); --border-color: rgb(0 0 0 / 15%); + --text-color: var(--primary-light-contrast-color); + --active-link-color: var(--primary-color); /* Variables for spaces */ --space-xs: 8px; @@ -25,4 +27,10 @@ --font-size-md: 1.125rem; --font-size-lg: 1.25rem; --font-size-xl: 1.5rem; + + /* Variables for font weights */ + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; } From a3b942bf7ee726fc04e8b0a520e402f1b3832a7e Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 18:27:17 +0700 Subject: [PATCH 18/64] Create dto, model and mapper for registration JC-679 --- libs/core/dtos/registration.dto.ts | 7 ++++++ libs/core/mappers/registration.mapper.ts | 22 +++++++++++++++++++ libs/core/models/authorization-tokens.ts | 9 ++++++++ libs/core/models/registration.ts | 27 ++++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 libs/core/dtos/registration.dto.ts create mode 100644 libs/core/mappers/registration.mapper.ts create mode 100644 libs/core/models/authorization-tokens.ts create mode 100644 libs/core/models/registration.ts diff --git a/libs/core/dtos/registration.dto.ts b/libs/core/dtos/registration.dto.ts new file mode 100644 index 00000000..71d73ce9 --- /dev/null +++ b/libs/core/dtos/registration.dto.ts @@ -0,0 +1,7 @@ +/** Registration data DTO. */ +export type RegistrationDto = { + readonly email: string; + readonly first_name: string; + readonly last_name: string; + readonly password: string; +}; diff --git a/libs/core/mappers/registration.mapper.ts b/libs/core/mappers/registration.mapper.ts new file mode 100644 index 00000000..7615efa2 --- /dev/null +++ b/libs/core/mappers/registration.mapper.ts @@ -0,0 +1,22 @@ +import { RegistrationDto } from '../dtos/registration.dto'; +import { Registration } from '../models/registration'; + +export namespace RegistrationMapper { + + /** + * Maps dto to model. + * @param registration Registration model. + * @returns Registration dto. + */ + export function toDto(registration: Registration): RegistrationDto { + // Disable eslint because these properties must be written in snake case. + return { + email: registration.email, + // eslint-disable-next-line @typescript-eslint/naming-convention + first_name: registration.firstName, + // eslint-disable-next-line @typescript-eslint/naming-convention + last_name: registration.lastName, + password: registration.password, + }; + } +} diff --git a/libs/core/models/authorization-tokens.ts b/libs/core/models/authorization-tokens.ts new file mode 100644 index 00000000..8a16b706 --- /dev/null +++ b/libs/core/models/authorization-tokens.ts @@ -0,0 +1,9 @@ +/** Tokens for user authorization. */ +export type AuthorizationTokens = { + + /** Token for refresh access token. */ + readonly refresh: string; + + /** Access token. */ + readonly access: string; +}; diff --git a/libs/core/models/registration.ts b/libs/core/models/registration.ts new file mode 100644 index 00000000..b5b34b93 --- /dev/null +++ b/libs/core/models/registration.ts @@ -0,0 +1,27 @@ +import { Immerable, OmitImmerable } from './immerable'; + +/** User data for registration. */ +export class Registration extends Immerable { + + /** Email. */ + public readonly email: string; + + /** First name. */ + public readonly firstName: string; + + /** Last name. */ + public readonly lastName: string; + + /** Password. */ + public readonly password: string; + + public constructor(data: RegistrationConstructorData) { + super(); + this.email = data.email; + this.firstName = data.firstName; + this.lastName = data.lastName; + this.password = data.password; + } +} + +type RegistrationConstructorData = OmitImmerable; From f4a610323320243da80ff2db210174c82cd2e9fe Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Wed, 31 Jul 2024 18:27:57 +0700 Subject: [PATCH 19/64] Realize registration JC-679 --- .../registration-form.component.html | 11 ++---- .../registration-form.component.ts | 26 +++++++++---- .../services/authorization-api.service.ts | 39 +++++++++++++++++++ .../src/core/services/form-fields.service.ts | 37 +++++++++--------- 4 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 apps/angular/src/core/services/authorization-api.service.ts diff --git a/apps/angular/src/app/features/registration/registration-form.component.html b/apps/angular/src/app/features/registration/registration-form.component.html index 33c672c2..e70d7f0b 100644 --- a/apps/angular/src/app/features/registration/registration-form.component.html +++ b/apps/angular/src/app/features/registration/registration-form.component.html @@ -32,6 +32,7 @@

Registration

Last Name @if (registrationForm.get('lastName')?.hasError('required') && registrationForm.get('lastName')?.touched) { Last name is required. @@ -46,8 +47,6 @@

Registration

type="password"> @if (registrationForm.get('password')?.hasError('required') && registrationForm.get('password')?.touched) { Password is required. - } @else if (registrationForm.get('password')?.hasError('mustMatch') && registrationForm.get('password')?.touched) { - Passwords must match. }
@@ -64,11 +63,9 @@

Registration

} -
- -
+
diff --git a/apps/angular/src/app/features/registration/registration-form.component.ts b/apps/angular/src/app/features/registration/registration-form.component.ts index 0eb75d9d..ef636328 100644 --- a/apps/angular/src/app/features/registration/registration-form.component.ts +++ b/apps/angular/src/app/features/registration/registration-form.component.ts @@ -5,6 +5,8 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { FormFieldsService } from '@js-camp/angular/core/services/form-fields.service'; +import { Registration } from '@js-camp/core/models/registration'; +import { AuthorizationApiService } from '@js-camp/angular/core/services/authorization-api.service'; /** Component with form for registration. */ @Component({ @@ -23,20 +25,30 @@ import { FormFieldsService } from '@js-camp/angular/core/services/form-fields.se }) export class RegistrationFormComponent { - private readonly formFieldsService = inject(FormFieldsService); + private formFieldsService = inject(FormFieldsService); + + private authorizationApiService = inject(AuthorizationApiService); /** Form for registration. */ public registrationForm = new FormGroup({ - email: new FormControl('', [Validators.required, Validators.email]), - firstName: new FormControl('', [Validators.required]), - lastName: new FormControl('', [Validators.required]), - password: new FormControl('', [Validators.required]), - retypedPassword: new FormControl('', [Validators.required]), + email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }), + firstName: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + lastName: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + password: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + retypedPassword: new FormControl('', { nonNullable: true, validators: [Validators.required] }), }, { validators: this.formFieldsService.matchFieldsValidator('password', 'retypedPassword') }); /** */ protected onRegistrationSubmit(): void { - console.warn(this.registrationForm.value); + const formData = this.registrationForm.getRawValue(); + + const registrationData = new Registration({ + email: formData.email, + firstName: formData.firstName, + lastName: formData.lastName, + password: formData.password, + }); + this.authorizationApiService.register(registrationData); } } diff --git a/apps/angular/src/core/services/authorization-api.service.ts b/apps/angular/src/core/services/authorization-api.service.ts new file mode 100644 index 00000000..051b4eb1 --- /dev/null +++ b/apps/angular/src/core/services/authorization-api.service.ts @@ -0,0 +1,39 @@ +// import { Observable, map, tap } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Registration } from '@js-camp/core/models/registration'; +import { RegistrationMapper } from '@js-camp/core/mappers/registration.mapper'; +import { AuthorizationTokens } from '@js-camp/core/models/authorization-tokens'; + +/** Authorization API access service. */ +@Injectable({ providedIn: 'root' }) +export class AuthorizationApiService { + + private readonly http = inject(HttpClient); + + /** + * Get page of anime list. + * @param registrationData - Params for request. + * @returns Page of anime list. + */ + public register(registrationData: Registration): void { + + // console.log(RegistrationMapper.toDto(registrationData)); + + this.http.post('auth/register/', RegistrationMapper.toDto(registrationData)) + .subscribe(response => { + // console.log('registrationData:', response); + this.saveTokens(response); + }); + } + + /** + * Get page of anime list. + * @param tokens - Params for request. + * @returns Page of anime list. + */ + public saveTokens(tokens: AuthorizationTokens): void { + localStorage.setItem('accessToken', tokens.access); + localStorage.setItem('refreshToken', tokens.refresh); + } +} diff --git a/apps/angular/src/core/services/form-fields.service.ts b/apps/angular/src/core/services/form-fields.service.ts index 3459e656..dfd13666 100644 --- a/apps/angular/src/core/services/form-fields.service.ts +++ b/apps/angular/src/core/services/form-fields.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms'; /** CompareFormFieldsServiceservice. */ @Injectable({ providedIn: 'root' }) @@ -7,32 +7,33 @@ export class FormFieldsService { /** * AbstractControl. - * @param formField AbstractControl. - * @param anotherFormField AbstractControl. + * @param controlName AbstractControl. + * @param matchingControlName AbstractControl. * @returns AbstractControl. */ - public matchFieldsValidator(formField: string, anotherFormField: string): ValidatorFn { - return (control: AbstractControl): { [key: string]: boolean; } | null => { - const formFieldControl = control.get(formField); - const anotherFormFieldControl = control.get(anotherFormField); + public matchFieldsValidator(controlName: string, matchingControlName: string): ValidatorFn { + return (formGroup: AbstractControl): { [key: string]: unknown; } | null => { + const formGroupControl = formGroup as FormGroup; + const control = formGroupControl.controls[controlName]; + const matchingControl = formGroupControl.controls[matchingControlName]; - if (!formFieldControl || !anotherFormFieldControl) { + if (!control || !matchingControl) { + return null; // If the controls are not found, return null + } + + // If the matching control has any error other than 'mustMatch', return and don't override it + if (matchingControl.errors && !matchingControl.errors['mustMatch']) { return null; } - // Check if the passwords do not match - if (formFieldControl.value !== anotherFormFieldControl.value) { - // Set the error on the retypedPassword control - formFieldControl.setErrors({ mustMatch: true }); - anotherFormFieldControl.setErrors({ mustMatch: true }); - return { mustMatch: true }; + // Set error on matchingControl if validation fails + if (control.value !== matchingControl.value) { + matchingControl.setErrors({ mustMatch: true }); + } else { + matchingControl.setErrors(null); // Clear the error if the values match } - // Clear the error if the passwords match - formFieldControl.setErrors(null); - anotherFormFieldControl.setErrors(null); return null; - }; } } From 0d62fcef62c91dd9f7feb37d1e447585bc28fbd4 Mon Sep 17 00:00:00 2001 From: TebyakinaEkaterina Date: Thu, 1 Aug 2024 12:25:10 +0700 Subject: [PATCH 20/64] Development login JC-679 --- .../features/login/login-form.component.css | 22 ++++ .../features/login/login-form.component.html | 39 ++++++- .../features/login/login-form.component.ts | 107 +++++++++++++++++- .../registration-form.component.ts | 4 +- .../services/authorization-api.service.ts | 47 +++++++- libs/core/models/login.ts | 9 ++ 6 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 libs/core/models/login.ts diff --git a/apps/angular/src/app/features/login/login-form.component.css b/apps/angular/src/app/features/login/login-form.component.css index e46dde1c..e3630f15 100644 --- a/apps/angular/src/app/features/login/login-form.component.css +++ b/apps/angular/src/app/features/login/login-form.component.css @@ -1,5 +1,27 @@ +.login-form-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + align-content: center; + padding: var(--space-xl); +} + +.login-header { + font-size: var(--font-size-xl); +} + .login-form { + min-width: 360px; + padding: var(--space-m); + border-radius: 24px; + border: 2px var(--border-color) solid; display: flex; flex-direction: column; gap: var(--space-s); + align-items: center; +} + +.login-form__field { + width: 100%; } diff --git a/apps/angular/src/app/features/login/login-form.component.html b/apps/angular/src/app/features/login/login-form.component.html index 2879f6ad..b4d97183 100644 --- a/apps/angular/src/app/features/login/login-form.component.html +++ b/apps/angular/src/app/features/login/login-form.component.html @@ -1,3 +1,40 @@ -