From 57c36b277e68b764dd34cb2e449f6e2ca3d1e9b6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 19 Jul 2018 16:17:54 +0200 Subject: Begin advanced search --- client/src/app/search/index.ts | 3 + client/src/app/search/search-routing.module.ts | 23 +++++++ client/src/app/search/search.component.html | 19 ++++++ client/src/app/search/search.component.scss | 93 ++++++++++++++++++++++++++ client/src/app/search/search.component.ts | 93 ++++++++++++++++++++++++++ client/src/app/search/search.module.ts | 25 +++++++ client/src/app/search/search.service.ts | 46 +++++++++++++ 7 files changed, 302 insertions(+) create mode 100644 client/src/app/search/index.ts create mode 100644 client/src/app/search/search-routing.module.ts create mode 100644 client/src/app/search/search.component.html create mode 100644 client/src/app/search/search.component.scss create mode 100644 client/src/app/search/search.component.ts create mode 100644 client/src/app/search/search.module.ts create mode 100644 client/src/app/search/search.service.ts (limited to 'client/src/app/search') diff --git a/client/src/app/search/index.ts b/client/src/app/search/index.ts new file mode 100644 index 000000000..40f4e021f --- /dev/null +++ b/client/src/app/search/index.ts @@ -0,0 +1,3 @@ +export * from './search-routing.module' +export * from './search.component' +export * from './search.module' diff --git a/client/src/app/search/search-routing.module.ts b/client/src/app/search/search-routing.module.ts new file mode 100644 index 000000000..0ac9e6b57 --- /dev/null +++ b/client/src/app/search/search-routing.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { MetaGuard } from '@ngx-meta/core' +import { SearchComponent } from '@app/search/search.component' + +const searchRoutes: Routes = [ + { + path: 'search', + component: SearchComponent, + canActivate: [ MetaGuard ], + data: { + meta: { + title: 'Search' + } + } + } +] + +@NgModule({ + imports: [ RouterModule.forChild(searchRoutes) ], + exports: [ RouterModule ] +}) +export class SearchRoutingModule {} diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html new file mode 100644 index 000000000..b8c4d7dc5 --- /dev/null +++ b/client/src/app/search/search.component.html @@ -0,0 +1,19 @@ +
+ No results found +
+ +
+
+ {{ pagination.totalItems | myNumberFormatter }} results for {{ currentSearch }} +
+ +
+ + +
+ {{ video.name }} + {{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views + +
+
+
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss new file mode 100644 index 000000000..06e3c9542 --- /dev/null +++ b/client/src/app/search/search.component.scss @@ -0,0 +1,93 @@ +@import '_variables'; +@import '_mixins'; + +.no-result { + height: 70vh; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: $font-semibold; +} + +.search-result { + margin-left: 40px; + margin-top: 40px; + + .results-counter { + font-size: 15px; + padding-bottom: 20px; + margin-bottom: 30px; + border-bottom: 1px solid #DADADA; + + .search-value { + font-weight: $font-semibold; + } + } + + .entry { + display: flex; + min-height: 130px; + padding-bottom: 20px; + margin-bottom: 20px; + + &.video { + + my-video-thumbnail { + margin-right: 10px; + } + + .video-info { + flex-grow: 1; + + .video-info-name { + @include disable-default-a-behaviour; + + color: #000; + display: block; + width: fit-content; + font-size: 18px; + font-weight: $font-semibold; + } + + .video-info-date-views { + font-size: 14px; + } + + .video-info-account { + @include disable-default-a-behaviour; + + display: block; + width: fit-content; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + color: #585858; + + &:hover { + color: #303030; + } + } + } + } + } +} + +@media screen and (max-width: 800px) { + .entry { + flex-direction: column; + height: auto; + text-align: center; + + &.video { + .video-info-name { + margin: auto; + } + + my-video-thumbnail { + margin-right: 0; + } + } + } +} diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts new file mode 100644 index 000000000..be1cb3689 --- /dev/null +++ b/client/src/app/search/search.component.ts @@ -0,0 +1,93 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { RedirectService } from '@app/core' +import { NotificationsService } from 'angular2-notifications' +import { Subscription } from 'rxjs' +import { SearchService } from '@app/search/search.service' +import { ComponentPagination } from '@app/shared/rest/component-pagination.model' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Video } from '../../../../shared' +import { MetaService } from '@ngx-meta/core' + +@Component({ + selector: 'my-search', + styleUrls: [ './search.component.scss' ], + templateUrl: './search.component.html' +}) +export class SearchComponent implements OnInit, OnDestroy { + videos: Video[] = [] + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10, // It's per object type (so 10 videos, 10 video channels etc) + totalItems: null + } + + private subActivatedRoute: Subscription + private currentSearch: string + + constructor ( + private i18n: I18n, + private route: ActivatedRoute, + private metaService: MetaService, + private redirectService: RedirectService, + private notificationsService: NotificationsService, + private searchService: SearchService + ) { } + + ngOnInit () { + this.subActivatedRoute = this.route.queryParams.subscribe( + queryParams => { + const querySearch = queryParams['search'] + + if (!querySearch) return this.redirectService.redirectToHomepage() + if (querySearch === this.currentSearch) return + + this.currentSearch = querySearch + this.updateTitle() + + this.reload() + }, + + err => this.notificationsService.error('Error', err.text) + ) + } + + ngOnDestroy () { + if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() + } + + search () { + return this.searchService.searchVideos(this.currentSearch, this.pagination) + .subscribe( + ({ videos, totalVideos }) => { + this.videos = this.videos.concat(videos) + this.pagination.totalItems = totalVideos + }, + + error => { + this.notificationsService.error(this.i18n('Error'), error.message) + } + ) + } + + onNearOfBottom () { + // Last page + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + + this.pagination.currentPage += 1 + this.search() + } + + private reload () { + this.pagination.currentPage = 1 + this.pagination.totalItems = null + + this.videos = [] + + this.search() + } + + private updateTitle () { + this.metaService.setTitle(this.i18n('Search') + ' ' + this.currentSearch) + } +} diff --git a/client/src/app/search/search.module.ts b/client/src/app/search/search.module.ts new file mode 100644 index 000000000..c6ec74d20 --- /dev/null +++ b/client/src/app/search/search.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core' +import { SharedModule } from '../shared' +import { SearchComponent } from '@app/search/search.component' +import { SearchService } from '@app/search/search.service' +import { SearchRoutingModule } from '@app/search/search-routing.module' + +@NgModule({ + imports: [ + SearchRoutingModule, + SharedModule + ], + + declarations: [ + SearchComponent + ], + + exports: [ + SearchComponent + ], + + providers: [ + SearchService + ] +}) +export class SearchModule { } diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts new file mode 100644 index 000000000..02d5f5915 --- /dev/null +++ b/client/src/app/search/search.service.ts @@ -0,0 +1,46 @@ +import { catchError, switchMap } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { Observable } from 'rxjs' +import { ComponentPagination } from '@app/shared/rest/component-pagination.model' +import { VideoService } from '@app/shared/video/video.service' +import { RestExtractor, RestService } from '@app/shared' +import { environment } from 'environments/environment' +import { ResultList, Video } from '../../../../shared' +import { Video as VideoServerModel } from '@app/shared/video/video.model' + +export type SearchResult = { + videosResult: { totalVideos: number, videos: Video[] } +} + +@Injectable() +export class SearchService { + static BASE_SEARCH_URL = environment.apiUrl + '/api/v1/search/' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService, + private videoService: VideoService + ) {} + + searchVideos ( + search: string, + componentPagination: ComponentPagination + ): Observable<{ videos: Video[], totalVideos: number }> { + const url = SearchService.BASE_SEARCH_URL + 'videos' + + const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination) + params = params.append('search', search) + + return this.authHttp + .get>(url, { params }) + .pipe( + switchMap(res => this.videoService.extractVideos(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } +} -- cgit v1.2.3