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