Commit 96f463fa by PROCOM\nataliak

Adding video details components

parent 9c5e0dc7
...@@ -3,5 +3,7 @@ ...@@ -3,5 +3,7 @@
</div> </div>
<div class="footer-container"> <div class="footer-container">
<img alt="logo" src="https://static.chorus.ai/images/chorus-logo.svg" /> <!-- <img alt="logo" src="https://static.chorus.ai/images/chorus-logo.svg" /> -->
</div> </div>
<div class="border-bottom"></div>
\ No newline at end of file
...@@ -6,15 +6,26 @@ ...@@ -6,15 +6,26 @@
.main-app-container { .main-app-container {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100vw; width: 100%;
height: 100vh; min-height: 100vh;
overflow: auto;
z-index: 1;
overflow: auto;
position: relative;
} }
.footer-container { .footer-container {
border-bottom: 4px solid $color-blue;
display: flex; display: flex;
padding-bottom: 24px; padding-bottom: 24px;
justify-content: center; justify-content: center;
bottom: 4px; bottom: 5px;
position: absolute; position: fixed;
width: 100%;
z-index: 0;
}
.border-bottom {
border-bottom: 4px solid $color-blue;
bottom: 0px;
position: fixed;
width: 100%; width: 100%;
z-index: 2;
} }
\ No newline at end of file
...@@ -5,6 +5,6 @@ import { Component } from '@angular/core'; ...@@ -5,6 +5,6 @@ import { Component } from '@angular/core';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent {
title = 'chorus-video';
} }
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatToolbarModule, MatSnackBarModule } from '@angular/material'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { StoreDevtoolsModule } from "@ngrx/store-devtools"; import { HttpClientModule } from '@angular/common/http';
import { MatToolbarModule, MatSnackBarModule, MatInputModule, MatButtonModule, MatProgressSpinnerModule } from '@angular/material';
import { MatIconModule } from '@angular/material/icon';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { StoreModule } from '@ngrx/store'; import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
...@@ -12,27 +15,37 @@ import { VideoService } from './store/services/video.services'; ...@@ -12,27 +15,37 @@ import { VideoService } from './store/services/video.services';
import { VideoDetailsComponent } from './video-details/video-details.component'; import { VideoDetailsComponent } from './video-details/video-details.component';
import { AppRoutingModule } from './app.routing.module'; import { AppRoutingModule } from './app.routing.module';
import { videoReducer } from './store/reducers/video.reducer'; import { videoReducer } from './store/reducers/video.reducer';
import { HttpClientModule } from '@angular/common/http'; import { EventService } from './store/services/event.service';
import { OrderByPipe } from './store/pipes/order.pipe';
import { FindVideoIdComponent } from './find-video-id/find-video-id.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
VideoDetailsComponent VideoDetailsComponent,
FindVideoIdComponent,
OrderByPipe
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
HttpClientModule, HttpClientModule,
FormsModule,
ReactiveFormsModule,
AppRoutingModule, AppRoutingModule,
MatButtonModule,
MatInputModule,
MatToolbarModule, MatToolbarModule,
MatSnackBarModule, MatSnackBarModule,
MatProgressSpinnerModule,
MatIconModule,
StoreModule.forRoot({ StoreModule.forRoot({
video: videoReducer videoState: videoReducer
}), }),
EffectsModule.forRoot([ VideoEffects ]), EffectsModule.forRoot([ VideoEffects ]),
StoreDevtoolsModule.instrument() StoreDevtoolsModule.instrument()
], ],
providers: [ VideoService ], providers: [ VideoService, EventService ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule { }
import { NgModule } from "@angular/core"; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from "@angular/router"; import { Routes, RouterModule } from '@angular/router';
import { VideoDetailsComponent } from "./video-details/video-details.component"; import { VideoDetailsComponent } from './video-details/video-details.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: "", path: '',
component: VideoDetailsComponent component: VideoDetailsComponent
} }
]; ];
......
<div class="find-video-id-container">
<h1>Please enter a video ID:</h1>
<form (ngSubmit)="onSubmitId()" [formGroup]="videoIdForm">
<mat-form-field class="full-width">
<input matInput placeholder="Video ID" formControlName="videoid" id="videoid" required>
<mat-error *ngIf="videoIdForm.controls.videoid.errors && videoIdForm.controls.videoid.errors.required">
Video ID is <strong>required</strong>
</mat-error>
</mat-form-field>
<button type="submit" mat-flat-button color="primary">Find video</button>
</form>
</div>
\ No newline at end of file
.find-video-id-container {
form {
padding: 13px;
}
.full-width {
width: 100%;
}
button {
margin-top: 20px;
}
}
\ No newline at end of file
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FindVideoIdComponent } from './find-video-id.component';
describe('FindVideoIdComponent', () => {
let component: FindVideoIdComponent;
let fixture: ComponentFixture<FindVideoIdComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FindVideoIdComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FindVideoIdComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { FormControl, Validators, FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { IAppState } from '../store/reducers/video.reducer';
import { Store } from '@ngrx/store';
import { FindTranscriptById } from '../store/actions/video.actions';
@Component({
selector: 'app-find-video-id',
templateUrl: './find-video-id.component.html',
styleUrls: ['./find-video-id.component.scss']
})
export class FindVideoIdComponent implements OnInit {
videoIdForm = this.fb.group({
videoid: ['', Validators.required]
});
constructor(
private fb: FormBuilder,
private router: Router,
private store: Store<IAppState>
) { }
ngOnInit() {
}
onSubmitId() {
if (this.videoIdForm.valid) {
this.store.dispatch(new FindTranscriptById(this.videoIdForm.value.videoid));
this.router.navigateByUrl('/?id=' + this.videoIdForm.value.videoid);
// this.router.navigateByUrl(['/'], { queryParams: { id: this.videoIdForm.value.videoid } });
}
}
}
import { Action } from "@ngrx/store"; import { Action } from '@ngrx/store';
export type IVideoActionsTypes = { export interface IVideoActionsTypes {
LOAD_TRANSCRIPT_BY_ID: string; LOAD_TRANSCRIPT_BY_ID: string;
LOAD_TRANSCRIPT_BY_ID_SUCCESS: string; LOAD_TRANSCRIPT_BY_ID_SUCCESS: string;
LOAD_TRANSCRIPT_BY_ID_ERROR: string; LOAD_TRANSCRIPT_BY_ID_ERROR: string;
}; }
export const VideoActionsTypes: IVideoActionsTypes = { export const VideoActionsTypes: IVideoActionsTypes = {
LOAD_TRANSCRIPT_BY_ID: "LOAD TRANSCRIPT BY ID", LOAD_TRANSCRIPT_BY_ID: 'LOAD TRANSCRIPT BY ID',
LOAD_TRANSCRIPT_BY_ID_SUCCESS: "LOAD TRANSCRIPT BY ID SUCCESS", LOAD_TRANSCRIPT_BY_ID_SUCCESS: 'LOAD TRANSCRIPT BY ID SUCCESS',
LOAD_TRANSCRIPT_BY_ID_ERROR: "LOAD TRANSCRIPT BY ID ERROR" LOAD_TRANSCRIPT_BY_ID_ERROR: 'LOAD TRANSCRIPT BY ID ERROR'
}; };
export class FindTicketById implements Action { export class FindTranscriptById implements Action {
readonly type = VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID; readonly type = VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID;
constructor(public payload: number) {} constructor(public payload: string) {}
} }
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from "@ngrx/effects"; import { Actions, Effect, ofType } from '@ngrx/effects';
import { map, mergeMap, catchError } from "rxjs/operators"; import { map, mergeMap, catchError } from 'rxjs/operators';
import { VideoActionsTypes } from "../actions/video.actions"; import { VideoActionsTypes } from '../actions/video.actions';
import { of } from "rxjs"; import { of } from 'rxjs';
import { MatSnackBar } from "@angular/material"; import { MatSnackBar } from '@angular/material';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
import { VideoService } from "../services/video.services"; import { VideoService } from '../services/video.services';
@Injectable() @Injectable()
export class VideoEffects { export class VideoEffects {
...@@ -22,10 +22,9 @@ export class VideoEffects { ...@@ -22,10 +22,9 @@ export class VideoEffects {
payload: ticket payload: ticket
})), })),
catchError(error => { catchError(error => {
this.router.navigate(["Video"]);
this.snackBar.open( this.snackBar.open(
"ERROR: There were some errors", 'ERROR: There were some errors',
"Error", 'Error',
{ {
duration: 5000 duration: 5000
} }
......
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'orderBy'
})
export class OrderByPipe implements PipeTransform {
transform(array: any, field: string): any[] {
if (!Array.isArray(array)) {
return;
}
array.sort((a: any, b: any) => {
if (a[field] < b[field]) {
return -1;
} else if (a[field] > b[field]) {
return 1;
} else {
return 0;
}
});
return array;
}
}
import { VideoActionsTypes } from "../actions/video.actions"; import { VideoActionsTypes } from '../actions/video.actions';
export interface IAppState {
videoState: IVideoState;
}
export interface IVideoState { export interface IVideoState {
transcript: any, transcript: any;
videoId: string, videoId: string;
loading: boolean; loading: boolean;
error: boolean; error: boolean;
} }
...@@ -15,11 +19,12 @@ export const initialState: IVideoState = { ...@@ -15,11 +19,12 @@ export const initialState: IVideoState = {
}; };
export function videoReducer(state: IVideoState = initialState, action: any): IVideoState { export function videoReducer(state: IVideoState = initialState, action: any): IVideoState {
switch(action.type) { switch (action.type) {
case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID: { case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID: {
return { return {
...state, ...state,
videoId: action.payload,
loading: true loading: true
}; };
} }
...@@ -27,6 +32,7 @@ export function videoReducer(state: IVideoState = initialState, action: any): IV ...@@ -27,6 +32,7 @@ export function videoReducer(state: IVideoState = initialState, action: any): IV
case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID_SUCCESS: { case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID_SUCCESS: {
return { return {
...state, ...state,
transcript: action.payload,
loading: false, loading: false,
error: false error: false
}; };
...@@ -35,6 +41,8 @@ export function videoReducer(state: IVideoState = initialState, action: any): IV ...@@ -35,6 +41,8 @@ export function videoReducer(state: IVideoState = initialState, action: any): IV
case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID_ERROR: { case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID_ERROR: {
return { return {
...state, ...state,
videoId: initialState.videoId,
transcript: initialState.transcript,
loading: false, loading: false,
error: true error: true
}; };
......
import { Injectable, Renderer2 } from '@angular/core';
@Injectable()
export class EventService {
constructor() { }
addEvents(renderer: Renderer2, events): void {
for (const event of events) {
event.dispose = renderer.listen(event.element, event.name, newEvent => event.callback(newEvent));
}
}
removeEvents(events): void {
for (const event of events) {
if (event.dispose) {
event.dispose();
}
}
}
}
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { HttpClient } from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import { ITranscript } from "../models/video.models"; import { ITranscript } from '../models/video.models';
@Injectable() @Injectable()
export class VideoService { export class VideoService {
readonly transcriptUrl = "https://static.chorus.ai/api"; readonly transcriptUrl = 'https://static.chorus.ai/api';
constructor(private httpClient: HttpClient) { } constructor(private httpClient: HttpClient) { }
loadTranscriptById(id: number): Observable<any> { loadTranscriptById(id: number): Observable<any> {
return this.httpClient.get(this.transcriptUrl + '/' + id + ".json"); return this.httpClient.get(this.transcriptUrl + '/' + id + '.json');
} }
} }
<div class="video-details-container"> <div class="video-details-container" *ngIf="!errorLoading; else errorLoadingContainer">
<div class="video-details" *ngIf="(videoId$ | async) as videoId; else noIdContainer">
<h1>Test title</h1> <h1>Test title</h1>
<div class="video-container"> <div class="video-container" #player>
<video #video class="video" src="https://static.chorus.ai/api/4d79041e-f25f-421d-9e5f-3462459b9934.mp4" [attr.autoplay]="autoplay ? true : null" <video #video class="video" [attr.src]="'https://static.chorus.ai/api/' + videoId + '.mp4'" preload="auto">
[preload]="preload ? 'auto' : 'metadata'" [attr.poster]="poster ? poster : null" [attr.loop]="loop ? loop : null">
<ng-content select="source"></ng-content>
<ng-content select="track"></ng-content>
This browser does not support HTML5 video. This browser does not support HTML5 video.
</video> </video>
<div class="controls" *ngIf="videoLoaded" [ngClass]="showPlayButton('visible', 'hidden')">
<button mat-icon-button (click)="toggleVideoPlayback(video)">
<mat-icon *ngIf="!playing">play_arrow</mat-icon>
<mat-icon *ngIf="playing">pause</mat-icon>
</button>
</div>
</div>
<div class="transcript-container" *ngIf="(transcript$ | async) as transcript">
<div class="speaker-container" *ngFor="let item of transcript | orderBy: 'time'; let i = index; trackBy: trackByFn" [ngClass]="item.speaker?.toLowerCase()">
<div class="speaker-status" [class.hidden]="item.speaker === transcript[i+1]?.speaker"></div>
<div>
<div class="speaker-name" *ngIf="item.speaker !== transcript[i-1]?.speaker">{{ item.speaker }}</div>
<div class="snippet">{{ item.snippet }}</div>
</div>
</div> </div>
</div>
</div>
<ng-template #noIdContainer>
<app-find-video-id></app-find-video-id>
</ng-template>
</div> </div>
<ng-template #errorLoadingContainer>
<div class="video-details-container">
<p>Error Loading Video: Please check the video ID or try again later.</p>
<a href="/">Enter new video ID</a>
</div>
</ng-template>
\ No newline at end of file
...@@ -5,16 +5,96 @@ ...@@ -5,16 +5,96 @@
width: 100%; width: 100%;
} }
.video-details-container { .video-details-container {
margin: auto;
background-color: $color-white; background-color: $color-white;
padding: 32px; padding: 32px;
margin: 0 auto 100px;
@media screen and (min-width: 768px) {
max-width: 60%; max-width: 60%;
}
.video-container { .video-container {
width: 300px; width: 300px;
margin: 0 auto; margin: 0 auto 15px;
position: relative;
background: $color-black;
box-shadow: 0 0 8px 0 rgba(0,0,0,0.30);
border-radius: 4px;
overflow: hidden;
video { video {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
} }
.controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
button {
opacity: 0.8;
background: $color-purple;
border-radius: 3px;
border: none;
color: $color-white;
border-radius: 3px;
padding: 11px 11px 9px;
cursor: pointer;
}
&.visible {
visibility: visible;
opacity: 1;
transition: opacity 0.5s linear;
}
&.hidden {
visibility: hidden;
opacity: 0;
transition: visibility 0s 0.5s, opacity 0.5s linear;
}
}
}
.speaker-container {
display: flex;
align-items: flex-end;
.speaker-status {
border: 1px solid;
border-radius: 100px;
width: 27px;
height: 27px;
&.hidden {
visibility: hidden;
}
}
.speaker-name {
padding: 0 13px;
font-size: 12px;
line-height: 16px;
}
.snippet {
font-size: 15px;
line-height: 20px;
padding: 8px 12px;
background: $color-light-grey;
border-radius: 100px;
}
&.rep {
flex-direction: row-reverse;
.speaker-status {
margin-left: 8px;
border-color: $color-blue;
background-color: rgba($color-blue, 0.1);
}
.speaker-name {
text-align: right;
}
}
&.cust {
.speaker-status {
margin-right: 8px;
border-color: $color-pink;
background-color: rgba($color-pink, 0.1);
}
}
} }
} }
\ No newline at end of file
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, AfterViewInit, OnDestroy, ElementRef, ViewChild, Renderer2 } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EventService } from '../store/services/event.service';
import { Store, select } from '@ngrx/store';
import { IAppState } from '../store/reducers/video.reducer';
import { FindTranscriptById } from '../store/actions/video.actions';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'app-video-details', selector: 'app-video-details',
templateUrl: './video-details.component.html', templateUrl: './video-details.component.html',
styleUrls: ['./video-details.component.scss'] styleUrls: ['./video-details.component.scss']
}) })
export class VideoDetailsComponent implements OnInit { export class VideoDetailsComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('player') private player: ElementRef;
@ViewChild('video') private video: ElementRef;
constructor() { } videoIdParam: string;
playing = false;
videoLoaded = false;
errorLoading = false;
transcript$: Observable<any>;
videoId$: Observable<string>;
loading$: Observable<boolean>;
error$: Observable<boolean>;
private isMouseMoving = false;
private events;
constructor(
private route: ActivatedRoute,
private renderer: Renderer2,
private evt: EventService,
private store: Store<IAppState>
) {
// this.videoIdParam = this.route.snapshot.queryParams['id'];
this.transcript$ = store.pipe(select(state => state.videoState.transcript));
this.videoId$ = store.pipe(select(state => state.videoState.videoId));
this.loading$ = store.pipe(select(state => state.videoState.loading));
this.error$ = store.pipe(select(state => state.videoState.error));
}
ngOnInit() { ngOnInit() {
this.route.queryParams.subscribe(queryParams => {
if (queryParams.id) {
this.store.dispatch(new FindTranscriptById(queryParams.id));
if (this.video) {
this.events = [
{ element: this.video.nativeElement, name: 'loadstart', callback: event => this.videoLoaded = false, dispose: null },
{ element: this.video.nativeElement, name: 'loadedmetadata', callback: event => this.onLoadedMetadata(event), dispose: null },
{ element: this.video.nativeElement, name: 'error', callback: event => this.onLoadingError(event), dispose: null },
{ element: this.player.nativeElement, name: 'mouseenter', callback: event => this.onMouseEnter(event), dispose: null },
{ element: this.player.nativeElement, name: 'mouseleave', callback: event => this.onMouseLeave(event), dispose: null }
];
this.evt.addEvents(this.renderer, this.events);
}
}
});
}
ngAfterViewInit() {
// if (this.video) {
// this.events = [
// { element: this.video.nativeElement, name: 'loadstart', callback: event => this.videoLoaded = false, dispose: null },
// { element: this.video.nativeElement, name: 'loadedmetadata', callback: event => this.onLoadedMetadata(event), dispose: null },
// { element: this.video.nativeElement, name: 'error', callback: event => this.onLoadingError(event), dispose: null },
// { element: this.player.nativeElement, name: 'mouseenter', callback: event => this.onMouseEnter(event), dispose: null },
// { element: this.player.nativeElement, name: 'mouseleave', callback: event => this.onMouseLeave(event), dispose: null }
// ];
// // this.evt.addEvents(this.renderer, this.events);
// }
}
ngOnDestroy() {
this.evt.removeEvents(this.events);
}
trackByFn(index, item) {
return index;
}
load() {
if (this.video && this.video.nativeElement) {
this.video.nativeElement.load();
}
}
onLoadingError(event) {
this.errorLoading = true;
console.error('Loading Error', event);
}
onLoadedMetadata(event: any) {
this.videoLoaded = true;
}
onMouseEnter(event: any) {
this.isMouseMoving = true;
}
onMouseLeave(event: any) {
this.isMouseMoving = false;
}
toggleVideoPlayback(video) {
this.playing = !this.playing;
this.updateVideoPlayback(video);
}
updateVideoPlayback(video) {
this.playing ? video.play() : video.pause();
}
showPlayButton(activeClass: string, inactiveClass: string): any {
return (!this.playing || this.isMouseMoving) ? activeClass : inactiveClass;
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment