parent
47cbd5695c
commit
489ad86c10
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
<div class="results">
|
||||||
|
|
||||||
|
<mat-toolbar>
|
||||||
|
|
||||||
|
<mat-input-container class="search">
|
||||||
|
|
||||||
|
<input matInput placeholder="Search commenter" #input1>
|
||||||
|
|
||||||
|
</mat-input-container>
|
||||||
|
|
||||||
|
<span class="minispacer"></span>
|
||||||
|
|
||||||
|
<mat-input-container class="search">
|
||||||
|
|
||||||
|
<input matInput placeholder="Search message" #input2>
|
||||||
|
|
||||||
|
</mat-input-container>
|
||||||
|
|
||||||
|
<span class="spacer"></span>
|
||||||
|
|
||||||
|
<button class="back-button" color="primary" mat-raised-button [routerLink]="['/']">Back</button>
|
||||||
|
|
||||||
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<div class="spinner-container" *ngIf="dataSource.loading$ | async">
|
||||||
|
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-table class="results-table mat-elevation-z8" [dataSource]="dataSource"
|
||||||
|
matSort matSortActive="video_recorded_at" matSortDirection="desc" matSortDisableClear>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="video_title">
|
||||||
|
|
||||||
|
<mat-header-cell fxFlex="20%" *matHeaderCellDef mat-sort-header>Video</mat-header-cell>
|
||||||
|
|
||||||
|
<mat-cell fxFlex="20%" *matCellDef="let result">
|
||||||
|
|
||||||
|
<img src="{{result.video_thumbnail_small}}" alt="{{result.video_title}}">
|
||||||
|
|
||||||
|
<a mat-button [routerLink]="['/videos', result.video_id]">
|
||||||
|
|
||||||
|
{{result.video_title}}
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</mat-cell>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="video_recorded_at">
|
||||||
|
|
||||||
|
<mat-header-cell fxFlex="8%" *matHeaderCellDef mat-sort-header>Recorded</mat-header-cell>
|
||||||
|
|
||||||
|
<mat-cell fxFlex="8%" *matCellDef="let result">{{result.video_recorded_at | date : 'yyyy-MM-dd'}}</mat-cell>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="offset">
|
||||||
|
|
||||||
|
<mat-header-cell fxFlex="8%" *matHeaderCellDef mat-sort-header>Time</mat-header-cell>
|
||||||
|
|
||||||
|
<mat-cell fxFlex="8%" *matCellDef="let result">
|
||||||
|
|
||||||
|
<a mat-button href="https://www.twitch.tv/videos/{{videoID}}?comment={{result.id}}&t={{result.offset | tohms}}" target="_blank">
|
||||||
|
|
||||||
|
{{result.offset | totime : result.video_recorded_at | tohms : 'colons'}}
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</mat-cell>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="commenter_display_name">
|
||||||
|
|
||||||
|
<mat-header-cell fxFlex="18%" *matHeaderCellDef mat-sort-header>Commenter</mat-header-cell>
|
||||||
|
|
||||||
|
<mat-cell fxFlex="18%" *matCellDef="let result">
|
||||||
|
|
||||||
|
<span *ngIf="result.message_user_badges">
|
||||||
|
|
||||||
|
<ng-template ngFor let-token [ngForOf]="badges$ | async | splitbadges : result.message_user_badges">
|
||||||
|
|
||||||
|
<img src="{{token.url}}" alt="{{token.title}}" *ngIf="token.hasOwnProperty('url')">
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="commenter" [ngStyle]="{color: result.message_user_color}">
|
||||||
|
|
||||||
|
{{result.commenter_display_name}}
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</mat-cell>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="message_body">
|
||||||
|
|
||||||
|
<mat-header-cell fxFlex="46%" *matHeaderCellDef mat-sort-header>Message</mat-header-cell>
|
||||||
|
|
||||||
|
<mat-cell fxFlex="46%" *matCellDef="let result">
|
||||||
|
|
||||||
|
<ng-template ngFor let-token [ngForOf]="emotes$ | async | splitmessage : result.message_body">
|
||||||
|
|
||||||
|
<img src="{{token.url}}" alt="{{token.title}}" *ngIf="token.hasOwnProperty('url')">
|
||||||
|
|
||||||
|
<span *ngIf="!token.hasOwnProperty('url')">{{token}}</span>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</mat-cell>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
|
|
||||||
|
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||||
|
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator [length]="dataSource?.count$ | async" [pageSize]="20"
|
||||||
|
[pageSizeOptions]="[10, 20, 50, 100, 200]"></mat-paginator>
|
||||||
|
|
||||||
|
</div>
|
@ -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<any[]>([]);
|
||||||
|
private emotesSubject = new BehaviorSubject<any[]>([]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<SearchResult> {
|
||||||
|
private resultsSubject = new BehaviorSubject<SearchResult[]>([]);
|
||||||
|
private countSubject = new BehaviorSubject<number>(0);
|
||||||
|
private loadingSubject = new BehaviorSubject<boolean>(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<SearchResult[]> {
|
||||||
|
console.log('Connecting data source');
|
||||||
|
return this.resultsSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(collectionViewer: CollectionViewer): void {
|
||||||
|
this.resultsSubject.complete();
|
||||||
|
this.countSubject.complete();
|
||||||
|
this.loadingSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue