aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/search
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/search')
-rw-r--r--client/src/app/search/index.ts3
-rw-r--r--client/src/app/search/search-routing.module.ts23
-rw-r--r--client/src/app/search/search.component.html19
-rw-r--r--client/src/app/search/search.component.scss93
-rw-r--r--client/src/app/search/search.component.ts93
-rw-r--r--client/src/app/search/search.module.ts25
-rw-r--r--client/src/app/search/search.service.ts46
7 files changed, 302 insertions, 0 deletions
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 @@
1export * from './search-routing.module'
2export * from './search.component'
3export * 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 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { SearchComponent } from '@app/search/search.component'
5
6const searchRoutes: Routes = [
7 {
8 path: 'search',
9 component: SearchComponent,
10 canActivate: [ MetaGuard ],
11 data: {
12 meta: {
13 title: 'Search'
14 }
15 }
16 }
17]
18
19@NgModule({
20 imports: [ RouterModule.forChild(searchRoutes) ],
21 exports: [ RouterModule ]
22})
23export 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 @@
1<div i18n *ngIf="pagination.totalItems === 0" class="no-result">
2 No results found
3</div>
4
5<div myInfiniteScroller [autoLoading]="true" (nearOfBottom)="onNearOfBottom()" class="search-result">
6 <div i18n *ngIf="pagination.totalItems" class="results-counter">
7 {{ pagination.totalItems | myNumberFormatter }} results for <span class="search-value">{{ currentSearch }}</span>
8 </div>
9
10 <div *ngFor="let video of videos" class="entry video">
11 <my-video-thumbnail [video]="video"></my-video-thumbnail>
12
13 <div class="video-info">
14 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
15 <span i18n class="video-info-date-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
16 <a class="video-info-account" [routerLink]="[ '/accounts', video.by ]">{{ video.by }}</a>
17 </div>
18 </div>
19</div>
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 @@
1@import '_variables';
2@import '_mixins';
3
4.no-result {
5 height: 70vh;
6 display: flex;
7 align-items: center;
8 justify-content: center;
9 font-size: 16px;
10 font-weight: $font-semibold;
11}
12
13.search-result {
14 margin-left: 40px;
15 margin-top: 40px;
16
17 .results-counter {
18 font-size: 15px;
19 padding-bottom: 20px;
20 margin-bottom: 30px;
21 border-bottom: 1px solid #DADADA;
22
23 .search-value {
24 font-weight: $font-semibold;
25 }
26 }
27
28 .entry {
29 display: flex;
30 min-height: 130px;
31 padding-bottom: 20px;
32 margin-bottom: 20px;
33
34 &.video {
35
36 my-video-thumbnail {
37 margin-right: 10px;
38 }
39
40 .video-info {
41 flex-grow: 1;
42
43 .video-info-name {
44 @include disable-default-a-behaviour;
45
46 color: #000;
47 display: block;
48 width: fit-content;
49 font-size: 18px;
50 font-weight: $font-semibold;
51 }
52
53 .video-info-date-views {
54 font-size: 14px;
55 }
56
57 .video-info-account {
58 @include disable-default-a-behaviour;
59
60 display: block;
61 width: fit-content;
62 overflow: hidden;
63 text-overflow: ellipsis;
64 white-space: nowrap;
65 font-size: 14px;
66 color: #585858;
67
68 &:hover {
69 color: #303030;
70 }
71 }
72 }
73 }
74 }
75}
76
77@media screen and (max-width: 800px) {
78 .entry {
79 flex-direction: column;
80 height: auto;
81 text-align: center;
82
83 &.video {
84 .video-info-name {
85 margin: auto;
86 }
87
88 my-video-thumbnail {
89 margin-right: 0;
90 }
91 }
92 }
93}
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 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute } from '@angular/router'
3import { RedirectService } from '@app/core'
4import { NotificationsService } from 'angular2-notifications'
5import { Subscription } from 'rxjs'
6import { SearchService } from '@app/search/search.service'
7import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { Video } from '../../../../shared'
10import { MetaService } from '@ngx-meta/core'
11
12@Component({
13 selector: 'my-search',
14 styleUrls: [ './search.component.scss' ],
15 templateUrl: './search.component.html'
16})
17export class SearchComponent implements OnInit, OnDestroy {
18 videos: Video[] = []
19 pagination: ComponentPagination = {
20 currentPage: 1,
21 itemsPerPage: 10, // It's per object type (so 10 videos, 10 video channels etc)
22 totalItems: null
23 }
24
25 private subActivatedRoute: Subscription
26 private currentSearch: string
27
28 constructor (
29 private i18n: I18n,
30 private route: ActivatedRoute,
31 private metaService: MetaService,
32 private redirectService: RedirectService,
33 private notificationsService: NotificationsService,
34 private searchService: SearchService
35 ) { }
36
37 ngOnInit () {
38 this.subActivatedRoute = this.route.queryParams.subscribe(
39 queryParams => {
40 const querySearch = queryParams['search']
41
42 if (!querySearch) return this.redirectService.redirectToHomepage()
43 if (querySearch === this.currentSearch) return
44
45 this.currentSearch = querySearch
46 this.updateTitle()
47
48 this.reload()
49 },
50
51 err => this.notificationsService.error('Error', err.text)
52 )
53 }
54
55 ngOnDestroy () {
56 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
57 }
58
59 search () {
60 return this.searchService.searchVideos(this.currentSearch, this.pagination)
61 .subscribe(
62 ({ videos, totalVideos }) => {
63 this.videos = this.videos.concat(videos)
64 this.pagination.totalItems = totalVideos
65 },
66
67 error => {
68 this.notificationsService.error(this.i18n('Error'), error.message)
69 }
70 )
71 }
72
73 onNearOfBottom () {
74 // Last page
75 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
76
77 this.pagination.currentPage += 1
78 this.search()
79 }
80
81 private reload () {
82 this.pagination.currentPage = 1
83 this.pagination.totalItems = null
84
85 this.videos = []
86
87 this.search()
88 }
89
90 private updateTitle () {
91 this.metaService.setTitle(this.i18n('Search') + ' ' + this.currentSearch)
92 }
93}
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 @@
1import { NgModule } from '@angular/core'
2import { SharedModule } from '../shared'
3import { SearchComponent } from '@app/search/search.component'
4import { SearchService } from '@app/search/search.service'
5import { SearchRoutingModule } from '@app/search/search-routing.module'
6
7@NgModule({
8 imports: [
9 SearchRoutingModule,
10 SharedModule
11 ],
12
13 declarations: [
14 SearchComponent
15 ],
16
17 exports: [
18 SearchComponent
19 ],
20
21 providers: [
22 SearchService
23 ]
24})
25export 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 @@
1import { catchError, switchMap } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { Observable } from 'rxjs'
5import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
6import { VideoService } from '@app/shared/video/video.service'
7import { RestExtractor, RestService } from '@app/shared'
8import { environment } from 'environments/environment'
9import { ResultList, Video } from '../../../../shared'
10import { Video as VideoServerModel } from '@app/shared/video/video.model'
11
12export type SearchResult = {
13 videosResult: { totalVideos: number, videos: Video[] }
14}
15
16@Injectable()
17export class SearchService {
18 static BASE_SEARCH_URL = environment.apiUrl + '/api/v1/search/'
19
20 constructor (
21 private authHttp: HttpClient,
22 private restExtractor: RestExtractor,
23 private restService: RestService,
24 private videoService: VideoService
25 ) {}
26
27 searchVideos (
28 search: string,
29 componentPagination: ComponentPagination
30 ): Observable<{ videos: Video[], totalVideos: number }> {
31 const url = SearchService.BASE_SEARCH_URL + 'videos'
32
33 const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
34
35 let params = new HttpParams()
36 params = this.restService.addRestGetParams(params, pagination)
37 params = params.append('search', search)
38
39 return this.authHttp
40 .get<ResultList<VideoServerModel>>(url, { params })
41 .pipe(
42 switchMap(res => this.videoService.extractVideos(res)),
43 catchError(err => this.restExtractor.handleError(err))
44 )
45 }
46}