diff --git a/apps/twitch-logs/src/app/app.module.ts b/apps/twitch-logs/src/app/app.module.ts index c07ad01..94803e7 100644 --- a/apps/twitch-logs/src/app/app.module.ts +++ b/apps/twitch-logs/src/app/app.module.ts @@ -25,8 +25,10 @@ import { FlexLayoutModule } from "@angular/flex-layout"; import { AppComponent } from './app.component'; import { VideosComponent } from './videos/videos.component'; import { CommentsComponent } from './comments/comments.component'; +import { SearchComponent } from './search/search.component'; import { SplitBadgesPipe } from './pipes/splitbadges.pipe'; import { SplitMessagePipe } from './pipes/splitmessage.pipe'; +import { ToTimePipe } from './pipes/totime.pipe'; import { ToHMSPipe } from './pipes/tohms.pipe'; import { CommentsService } from './services/comments.service'; import { ImagesService } from './services/images.service'; @@ -35,6 +37,7 @@ import { ImagesService } from './services/images.service'; const routes: Routes = [ { path: '', component: VideosComponent }, { path: 'videos/:id', component: CommentsComponent }, + { path: 'search', component: SearchComponent }, { path: '**', redirectTo: '/' } ]; @@ -43,8 +46,10 @@ const routes: Routes = [ AppComponent, VideosComponent, CommentsComponent, + SearchComponent, SplitBadgesPipe, SplitMessagePipe, + ToTimePipe, ToHMSPipe ], imports: [ diff --git a/apps/twitch-logs/src/app/comments/comments.component.html b/apps/twitch-logs/src/app/comments/comments.component.html index 5a37dd5..2945f9e 100644 --- a/apps/twitch-logs/src/app/comments/comments.component.html +++ b/apps/twitch-logs/src/app/comments/comments.component.html @@ -2,15 +2,15 @@ - + - + - + @@ -25,13 +25,13 @@ - Offset + Time - {{comment.offset | tohms : 'colons'}} + {{comment.offset | totime : comment.video_recorded_at | tohms : 'colons'}} diff --git a/apps/twitch-logs/src/app/models/comment.ts b/apps/twitch-logs/src/app/models/comment.ts index ba6c15e..0bed9ef 100644 --- a/apps/twitch-logs/src/app/models/comment.ts +++ b/apps/twitch-logs/src/app/models/comment.ts @@ -1,6 +1,7 @@ export interface Comment { id: string; video_id: number; + video_recorded_at: Date; offset: number; commenter_id: string; commenter_name: string; diff --git a/apps/twitch-logs/src/app/models/searchresult.ts b/apps/twitch-logs/src/app/models/searchresult.ts new file mode 100644 index 0000000..81492c3 --- /dev/null +++ b/apps/twitch-logs/src/app/models/searchresult.ts @@ -0,0 +1,22 @@ +export interface SearchResult { + id: string; + video_id: number; + video_title: string; + video_game: string; + video_length: number; + video_thumbnail_small: string; + video_thumbnail_medium: string; + video_thumbnail_large: string; + video_recorded_at: Date; + offset: number; + commenter_id: string; + commenter_name: string; + commenter_display_name: string; + commenter_logo: string; + source: string; + message_body: string; + message_user_color: string; + message_user_badges: string; + created_at: Date; + updated_at: Date; +} diff --git a/apps/twitch-logs/src/app/pipes/totime.pipe.ts b/apps/twitch-logs/src/app/pipes/totime.pipe.ts new file mode 100644 index 0000000..9f88254 --- /dev/null +++ b/apps/twitch-logs/src/app/pipes/totime.pipe.ts @@ -0,0 +1,19 @@ +import { + Pipe, + PipeTransform +} from '@angular/core'; + + +@Pipe({ + name: 'totime' +}) +export class ToTimePipe implements PipeTransform { + + transform(offset: number, base: string): number { + let recorded = new Date(base + 'Z'); + let result = new Date(recorded.getTime() + offset * 1000); + let ref = new Date(result.getTime()); + ref.setHours(0, 0, 0, 0); + return (result.getTime() - ref.getTime()) / 1000 >> 0; + } +} diff --git a/apps/twitch-logs/src/app/search/search.component.css b/apps/twitch-logs/src/app/search/search.component.css new file mode 100644 index 0000000..01a13fa --- /dev/null +++ b/apps/twitch-logs/src/app/search/search.component.css @@ -0,0 +1,50 @@ +.comments { + text-align: center; +} + +.comments-table { + text-align: left; +} + +.mat-column-video_title { + white-space: nowrap; +} + +.mat-column-video_title img, .mat-column-video_title a { + vertical-align: middle; +} + +.mat-column-commenter_display_name { + font-weight: 500; +} + +.mat-column-commenter_display_name img { + vertical-align: middle; +} + +.mat-column-commenter_display_name span.commenter { + vertical-align: middle; +} + +.mat-column-message_body span, .mat-column-message_body img { + vertical-align: middle; +} + +.minispacer { + flex: 0.1 1 auto; +} + +.spacer { + flex: 1 1 auto; +} + +.spinner-container { + position: fixed; + left: 50%; + top: 15%; + margin-left: -50px; +} + +.spinner-container mat-spinner { + margin: 6em auto 0 auto; +} diff --git a/apps/twitch-logs/src/app/search/search.component.html b/apps/twitch-logs/src/app/search/search.component.html new file mode 100644 index 0000000..75dca0a --- /dev/null +++ b/apps/twitch-logs/src/app/search/search.component.html @@ -0,0 +1,129 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + Video + + + + {{result.video_title}} + + + + {{result.video_title}} + + + + + + + + + + Recorded + + {{result.video_recorded_at | date : 'yyyy-MM-dd'}} + + + + + + Time + + + + + + {{result.offset | totime : result.video_recorded_at | tohms : 'colons'}} + + + + + + + + + + Commenter + + + + + + + + {{token.title}} + + + + + + + + {{result.commenter_display_name}} + + + + + + + + + + Message + + + + + + {{token.title}} + + {{token}} + + + + + + + + + + + + + + + +
diff --git a/apps/twitch-logs/src/app/search/search.component.ts b/apps/twitch-logs/src/app/search/search.component.ts new file mode 100644 index 0000000..17242d3 --- /dev/null +++ b/apps/twitch-logs/src/app/search/search.component.ts @@ -0,0 +1,124 @@ +import { + AfterViewInit, + Component, + ElementRef, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; + +import { ActivatedRoute } from '@angular/router'; + +import { + MatPaginator, + MatSort, + MatTableDataSource +} from '@angular/material'; + +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +import { + debounceTime, + delay, + distinctUntilChanged, + startWith, + tap +} from 'rxjs/operators'; + +import { merge } from 'rxjs/observable/merge'; +import { fromEvent } from 'rxjs/observable/fromEvent'; + +import { CommentsService } from '../services/comments.service'; +import { ImagesService } from '../services/images.service'; +import { SearchDataSource } from '../services/search.datasource'; + + +@Component({ + selector: 'search', + templateUrl: './search.component.html', + styleUrls: [ './search.component.css' ] +}) +export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { + private badgesSubject = new BehaviorSubject([]); + private emotesSubject = new BehaviorSubject([]); + + dataSource: SearchDataSource; + + displayedColumns = [ + 'video_title', + 'video_recorded_at', + 'offset', + 'commenter_display_name', + 'message_body' + ]; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + @ViewChild('input1') input1: ElementRef; + @ViewChild('input2') input2: ElementRef; + + public badges$ = this.badgesSubject.asObservable(); + public emotes$ = this.emotesSubject.asObservable(); + + constructor(private route: ActivatedRoute, + private commentsService: CommentsService, + private imagesService: ImagesService) { } + + ngOnInit() { + this.dataSource = new SearchDataSource(this.commentsService); + this.dataSource.loadComments('', '', 'video_recorded_at', 'desc', 0, 20); + this.imagesService.getBadges().subscribe( + (data: any[]) => this.badgesSubject.next(data) + ); + this.imagesService.getEmotes().subscribe( + (data: any[]) => this.emotesSubject.next(data) + ); + } + + ngOnDestroy() { + this.badgesSubject.complete(); + this.emotesSubject.complete(); + } + + ngAfterViewInit() { + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + fromEvent(this.input1.nativeElement, 'keyup') + .pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => { + this.paginator.pageIndex = 0; + this.loadCommentsPage(); + }) + ) + .subscribe(); + + fromEvent(this.input2.nativeElement, 'keyup') + .pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => { + this.paginator.pageIndex = 0; + this.loadCommentsPage(); + }) + ) + .subscribe(); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + tap(() => this.loadCommentsPage()) + ) + .subscribe(); + } + + loadCommentsPage() { + this.dataSource.loadComments( + this.input1.nativeElement.value, + this.input2.nativeElement.value, + this.sort.active, + this.sort.direction, + this.paginator.pageIndex, + this.paginator.pageSize); + } +} diff --git a/apps/twitch-logs/src/app/services/comments.service.ts b/apps/twitch-logs/src/app/services/comments.service.ts index 00e7203..0d7dcd7 100644 --- a/apps/twitch-logs/src/app/services/comments.service.ts +++ b/apps/twitch-logs/src/app/services/comments.service.ts @@ -11,6 +11,7 @@ import { map } from 'rxjs/operators'; import { Video } from '../models/video'; import { Comment } from '../models/comment'; +import { SearchResult } from '../models/searchresult'; @Injectable() @@ -56,4 +57,25 @@ export class CommentsService { })) ); } + + searchComments( + commenter = '', term = '', sortBy = 'video_recorded_at', sortOrder = 'desc', + pageNumber = 0, pageSize = 20): Observable { + + return this.http.get('/twitch-logs/api/search', { + observe: 'response', + params: new HttpParams() + .set('commenter', commenter) + .set('term', term) + .set('sort_by', sortBy) + .set('sort_order', sortOrder) + .set('page_number', pageNumber.toString()) + .set('page_size', pageSize.toString()) + }).pipe( + map((res: any) => ({ + results: res.body, + totalCount: parseInt(res.headers.get('X-Total-Count')) + })) + ); + } } diff --git a/apps/twitch-logs/src/app/services/search.datasource.ts b/apps/twitch-logs/src/app/services/search.datasource.ts new file mode 100644 index 0000000..6ef994a --- /dev/null +++ b/apps/twitch-logs/src/app/services/search.datasource.ts @@ -0,0 +1,61 @@ +import { + CollectionViewer, + DataSource +} from '@angular/cdk/collections'; + +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { of } from 'rxjs/observable/of'; + +import { + catchError, + finalize +} from 'rxjs/operators'; + +import { CommentsService } from './comments.service'; + +import { SearchResult } from '../models/searchresult'; + + +export class SearchDataSource implements DataSource { + private resultsSubject = new BehaviorSubject([]); + private countSubject = new BehaviorSubject(0); + private loadingSubject = new BehaviorSubject(false); + + public count$ = this.countSubject.asObservable(); + public loading$ = this.loadingSubject.asObservable(); + + constructor(private commentsService: CommentsService) { } + + loadComments(commenter: string, + term: string, + sortColumn: string, + sortDirection: string, + pageIndex: number, + pageSize: number) { + + this.loadingSubject.next(true); + + this.commentsService.searchComments(commenter, term, sortColumn, sortDirection, + pageIndex, pageSize) + .pipe( + catchError(() => of([])), + finalize(() => this.loadingSubject.next(false)) + ) + .subscribe((data: any) => { + this.resultsSubject.next(data.results); + this.countSubject.next(data.totalCount); + }); + } + + connect(collectionViewer: CollectionViewer): Observable { + console.log('Connecting data source'); + return this.resultsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.resultsSubject.complete(); + this.countSubject.complete(); + this.loadingSubject.complete(); + } +} diff --git a/apps/twitch-logs/src/app/videos/videos.component.html b/apps/twitch-logs/src/app/videos/videos.component.html index d2878b1..df651c5 100644 --- a/apps/twitch-logs/src/app/videos/videos.component.html +++ b/apps/twitch-logs/src/app/videos/videos.component.html @@ -2,14 +2,16 @@ - + - + + +