diff --git a/src/app/shared/components/paginator/paginator.component.html b/src/app/shared/components/paginator/paginator.component.html new file mode 100644 index 000000000..d4fb096df --- /dev/null +++ b/src/app/shared/components/paginator/paginator.component.html @@ -0,0 +1,15 @@ +
+ + + + + + + +
\ No newline at end of file diff --git a/src/app/shared/components/paginator/paginator.component.scss b/src/app/shared/components/paginator/paginator.component.scss new file mode 100644 index 000000000..f6a62f420 --- /dev/null +++ b/src/app/shared/components/paginator/paginator.component.scss @@ -0,0 +1,25 @@ +.page{ + font-family: Innerspace; + font-style: normal; + font-weight: bold; + font-size: 13px; + line-height: 15px; + color: #444444; + pointer-events: none; + padding: 1rem; + min-width: 20px !important; + + &-selected{ + color: #3849F9; + } +} +.arrow, .page-selected, .page-unselected{ + pointer-events: all; +} +.arrow:hover{ + color: #3849F9; +} +.page-selected:hover, .page-unselected:hover, .arrow:hover{ + cursor: pointer; + opacity: 0.5; +} diff --git a/src/app/shared/components/paginator/paginator.component.spec.ts b/src/app/shared/components/paginator/paginator.component.spec.ts new file mode 100644 index 000000000..ac0437ba1 --- /dev/null +++ b/src/app/shared/components/paginator/paginator.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; + +import { PaginatorComponent } from './paginator.component'; + +describe('PaginatorComponent', () => { + let component: PaginatorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MatButtonModule, + MatIconModule + ], + declarations: [PaginatorComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PaginatorComponent); + component = fixture.componentInstance; + component.currentPage = { + element: 1, + isActive: true + } + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/paginator/paginator.component.ts b/src/app/shared/components/paginator/paginator.component.ts new file mode 100644 index 000000000..58b21a6f1 --- /dev/null +++ b/src/app/shared/components/paginator/paginator.component.ts @@ -0,0 +1,169 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { Constants } from '../../constants/constants'; +import { PaginationElement } from '../../models/paginationElement.model'; +@Component({ + selector: 'app-paginator', + templateUrl: './paginator.component.html', + styleUrls: ['./paginator.component.scss'] +}) +export class PaginatorComponent implements OnInit, OnChanges { + + private readonly FIRST_PAGINATION_PAGE = 1; + private readonly MAX_PAGE_PAGINATOR_DISPLAY = 7; + private readonly PAGINATION_DOTS = '...'; + private readonly PAGINATION_SHIFT_DELTA = 3; + + @Input() currentPage: PaginationElement; + @Input() totalEntities: number; + + carouselPageList: PaginationElement[] = []; + totalPageAmount: number; + size: number = Constants.WORKSHOPS_PER_PAGE; + + @Output() pageChange = new EventEmitter(); + + constructor() { } + + ngOnInit(): void { + this.totalPageAmount = this.getTotalPageAmount(); + + this.createPageList(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (!changes.currentPage.isFirstChange()) { + const currentPage = this.carouselPageList.find((page: PaginationElement) => page.element === this.currentPage.element); + const isForward: boolean = this.checkIsForwardScrollDirection(changes); + const isRecreationAllowed: boolean = this.checkCarouseleRecreationIsAllowed(isForward, currentPage); + + if (isRecreationAllowed) { + this.createPageList(); + } + } + } + + private checkIsForwardScrollDirection(changes: SimpleChanges): boolean { + return changes.currentPage.previousValue.element < changes.currentPage.currentValue.element; + } + + private checkCarouseleRecreationIsAllowed(isForward: boolean, currentPage: PaginationElement): boolean { + if (isForward) { + return this.carouselPageList.indexOf(currentPage) >= this.PAGINATION_SHIFT_DELTA + } else { + return this.carouselPageList.indexOf(currentPage) <= this.PAGINATION_SHIFT_DELTA + } + } + + private createPageList(): void { + this.carouselPageList = []; + + let firstPage = +this.currentPage.element - this.PAGINATION_SHIFT_DELTA; + firstPage = firstPage < this.FIRST_PAGINATION_PAGE ? this.FIRST_PAGINATION_PAGE : firstPage; + + let lastPage = +this.currentPage.element + this.PAGINATION_SHIFT_DELTA; + lastPage = lastPage > this.totalPageAmount ? this.totalPageAmount : lastPage; + + let pageList = this.createDisplayedPageList(firstPage); + + if (this.totalPageAmount < this.MAX_PAGE_PAGINATOR_DISPLAY) { + this.carouselPageList = pageList; + } else { + this.createCarouselPageList(pageList, pageList[0]?.element !== this.FIRST_PAGINATION_PAGE, true); + } + + } + + onPageChange(page: PaginationElement): void { + this.pageChange.emit(page); + } + + onArroveClick(isForward: boolean): void { + let page: PaginationElement = { + element: '', + isActive: true, + } + if (isForward) { + page.element = +this.currentPage.element + 1; + } else { + page.element = +this.currentPage.element - 1; + } + + this.pageChange.emit(page); + } + + private getTotalPageAmount(): number { + return Math.ceil(this.totalEntities / this.size); + } + + private createDisplayedPageList(startPage: number): PaginationElement[] { + let start: number; + let end: number; + if (this.totalPageAmount > this.MAX_PAGE_PAGINATOR_DISPLAY) { + const isMaxAmountFit = (startPage + this.MAX_PAGE_PAGINATOR_DISPLAY) < this.totalPageAmount; + start = (isMaxAmountFit) ? startPage : this.totalPageAmount - this.MAX_PAGE_PAGINATOR_DISPLAY; + end = this.MAX_PAGE_PAGINATOR_DISPLAY; + } else { + start = startPage; + end = this.totalPageAmount; + } + + let pageList: PaginationElement[] = []; + while (pageList.length < end) { + pageList.push({ + element: start, + isActive: true + }); + start++; + } + return pageList; + } + + private createCarouselPageList(pageList: PaginationElement[], isOnStart?: boolean, isOnEnd?: boolean) { + if (isOnStart) { + let start: PaginationElement[] = [ + { + element: this.FIRST_PAGINATION_PAGE, + isActive: true + } + ]; + + if (pageList[0]?.element !== 2) { + start.push( + { + element: this.PAGINATION_DOTS, + isActive: false + }) + } + + this.carouselPageList = this.carouselPageList.concat(start); + }; + + if (pageList[0]?.element === 2) { + pageList.pop(); + } + + if (pageList[pageList.length - 1]?.element === this.totalPageAmount - 1) { + pageList.shift(); + } + + this.carouselPageList = this.carouselPageList.concat(pageList); + + + if (isOnEnd) { + let end: PaginationElement[] = [ + { + element: this.totalPageAmount, + isActive: true + } + ]; + if (pageList[pageList.length - 1]?.element !== this.totalPageAmount - 1) { + end.unshift({ + element: this.PAGINATION_DOTS, + isActive: false + }) + } + this.carouselPageList = this.carouselPageList.concat(end); + } + } + +} diff --git a/src/app/shared/constants/constants.ts b/src/app/shared/constants/constants.ts index 831a7a11a..4f044b566 100644 --- a/src/app/shared/constants/constants.ts +++ b/src/app/shared/constants/constants.ts @@ -13,6 +13,7 @@ export class Constants { static readonly PHONE_LENGTH = 10; static readonly PROVIDER_ENTITY_TYPE = 1; static readonly WORKSHOP_ENTITY_TYPE = 2; + static readonly WORKSHOPS_PER_PAGE = 8; static readonly RATE_ONE_STAR = 1; static readonly RATE_TWO_STAR = 2; diff --git a/src/app/shared/models/paginationElement.model.ts b/src/app/shared/models/paginationElement.model.ts new file mode 100644 index 000000000..58e969897 --- /dev/null +++ b/src/app/shared/models/paginationElement.model.ts @@ -0,0 +1,4 @@ +export interface PaginationElement { + element: number | string, + isActive: boolean +} \ No newline at end of file diff --git a/src/app/shared/services/workshops/app-workshop/app-workshops.service.ts b/src/app/shared/services/workshops/app-workshop/app-workshops.service.ts index ebb799a64..5bd37c253 100644 --- a/src/app/shared/services/workshops/app-workshop/app-workshops.service.ts +++ b/src/app/shared/services/workshops/app-workshop/app-workshops.service.ts @@ -1,6 +1,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; +import { Constants } from 'src/app/shared/constants/constants'; import { Direction } from 'src/app/shared/models/category.model'; import { WorkshopCard, WorkshopFilterCard } from '../../../models/workshop.model'; @@ -57,6 +58,14 @@ export class AppWorkshopsService { filters.directions.forEach((direction: Direction) => params = params.set('DirectionIds', direction.id.toString())); } + if (filters.currentPage) { + const size: number = Constants.WORKSHOPS_PER_PAGE; + const from: number = size * (+filters.currentPage.element - 1); + + params = params.set('Size', size.toString()); + params = params.set('From', from.toString()); + } + return params; } /** diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index d44c26993..97f98e1b4 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -42,6 +42,7 @@ import { FullSearchBarComponent } from './components/full-search-bar/full-search import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MessageBarComponent } from './components/message-bar/message-bar.component'; import { ShowTooltipIfTruncatedDirective } from './directives/show-tooltip-if-truncated.directive'; +import { PaginatorComponent } from './components/paginator/paginator.component'; import { StarsComponent } from './components/stars/stars.component'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatIconModule } from '@angular/material/icon'; @@ -84,6 +85,7 @@ import { FooterComponent } from '../footer/footer.component'; FullSearchBarComponent, MessageBarComponent, ShowTooltipIfTruncatedDirective, + PaginatorComponent, StarsComponent, FooterComponent, ], @@ -136,6 +138,7 @@ import { FooterComponent } from '../footer/footer.component'; MessageBarComponent, MatProgressBarModule, ShowTooltipIfTruncatedDirective, + PaginatorComponent, ReactiveFormsModule, StarsComponent, FooterComponent, diff --git a/src/app/shared/store/filter.actions.ts b/src/app/shared/store/filter.actions.ts index c3c73d1c7..c884156c0 100644 --- a/src/app/shared/store/filter.actions.ts +++ b/src/app/shared/store/filter.actions.ts @@ -1,5 +1,6 @@ import { Direction } from "../models/category.model"; import { City } from "../models/city.model"; +import { PaginationElement } from "../models/paginationElement.model"; import { WorkingHours } from "../models/workingHours.model"; export class SetCity { static readonly type = '[app] Set City'; @@ -70,4 +71,8 @@ export class SetMinAge { export class SetMaxAge { static readonly type = '[filter] Set Max Age'; constructor(public payload: number) { } +} +export class PageChange { + static readonly type = '[filter] Change Page'; + constructor(public payload: PaginationElement) { } } \ No newline at end of file diff --git a/src/app/shared/store/filter.state.ts b/src/app/shared/store/filter.state.ts index 4fdfde1ed..87b837528 100644 --- a/src/app/shared/store/filter.state.ts +++ b/src/app/shared/store/filter.state.ts @@ -22,8 +22,10 @@ import { FilterChange, SetMinAge, SetMaxAge, + PageChange, } from './filter.actions'; import { AppWorkshopsService } from '../services/workshops/app-workshop/app-workshops.service'; +import { PaginationElement } from '../models/paginationElement.model'; export interface FilterStateModel { directions: Direction[]; maxAge: number; @@ -38,10 +40,11 @@ export interface FilterStateModel { city: City; searchQuery: string; order: string; - filteredWorkshops: WorkshopCard[]; + filteredWorkshops: WorkshopFilterCard; topWorkshops: WorkshopCard[]; withDisabilityOption: boolean; isLoading: boolean; + currentPage: PaginationElement } @State({ name: 'filter', @@ -59,17 +62,21 @@ export interface FilterStateModel { city: undefined, searchQuery: '', order: '', - filteredWorkshops: [], + filteredWorkshops: undefined, topWorkshops: [], withDisabilityOption: false, - isLoading: false + isLoading: false, + currentPage: { + element: 1, + isActive: true + } } }) @Injectable() export class FilterState { @Selector() - static filteredWorkshops(state: FilterStateModel): WorkshopCard[] { return state.filteredWorkshops } + static filteredWorkshops(state: FilterStateModel): WorkshopFilterCard { return state.filteredWorkshops } @Selector() static topWorkshops(state: FilterStateModel): WorkshopCard[] { return state.topWorkshops } @@ -161,7 +168,7 @@ export class FilterState { return this.appWorkshopsService .getFilteredWorkshops(state) - .subscribe((filterResult: WorkshopFilterCard) => patchState(filterResult ? { filteredWorkshops: filterResult.entities, isLoading: false } : { filteredWorkshops: [], isLoading: false }), + .subscribe((filterResult: WorkshopFilterCard) => patchState(filterResult ? { filteredWorkshops: filterResult, isLoading: false } : { filteredWorkshops: undefined, isLoading: false }), () => patchState({ isLoading: false })) } @@ -193,6 +200,12 @@ export class FilterState { dispatch(new FilterChange()); } + @Action(PageChange) + pageChange({ patchState, dispatch }: StateContext, { payload }: PageChange) { + patchState({ currentPage: payload }); + dispatch(new FilterChange()); + } + @Action(FilterChange) filterChange({ }: StateContext, { }: FilterChange) { } } diff --git a/src/app/shell/result/result.component.html b/src/app/shell/result/result.component.html index 456040f3f..938fd32d9 100644 --- a/src/app/shell/result/result.component.html +++ b/src/app/shell/result/result.component.html @@ -3,7 +3,8 @@

Знайдено Х гуртків

- @@ -18,7 +19,7 @@

Знайдено Х гуртків

- +
@@ -29,7 +30,7 @@

Знайдено Х гуртків