Commit 96f463fa by PROCOM\nataliak

Adding video details components

parent 9c5e0dc7
......@@ -3,5 +3,7 @@
</div>
<div class="footer-container">
<img alt="logo" src="https://static.chorus.ai/images/chorus-logo.svg" />
</div>
\ No newline at end of file
<!-- <img alt="logo" src="https://static.chorus.ai/images/chorus-logo.svg" /> -->
</div>
<div class="border-bottom"></div>
\ No newline at end of file
......@@ -6,15 +6,26 @@
.main-app-container {
display: flex;
align-items: center;
width: 100vw;
height: 100vh;
width: 100%;
min-height: 100vh;
overflow: auto;
z-index: 1;
overflow: auto;
position: relative;
}
.footer-container {
border-bottom: 4px solid $color-blue;
display: flex;
padding-bottom: 24px;
justify-content: center;
bottom: 4px;
position: absolute;
bottom: 5px;
position: fixed;
width: 100%;
z-index: 0;
}
.border-bottom {
border-bottom: 4px solid $color-blue;
bottom: 0px;
position: fixed;
width: 100%;
z-index: 2;
}
\ No newline at end of file
......@@ -5,6 +5,6 @@ import { Component } from '@angular/core';
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'chorus-video';
}
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MatToolbarModule, MatSnackBarModule } from '@angular/material';
import { StoreDevtoolsModule } from "@ngrx/store-devtools";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
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 { EffectsModule } from '@ngrx/effects';
......@@ -12,27 +15,37 @@ import { VideoService } from './store/services/video.services';
import { VideoDetailsComponent } from './video-details/video-details.component';
import { AppRoutingModule } from './app.routing.module';
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({
declarations: [
AppComponent,
VideoDetailsComponent
VideoDetailsComponent,
FindVideoIdComponent,
OrderByPipe
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
AppRoutingModule,
MatButtonModule,
MatInputModule,
MatToolbarModule,
MatSnackBarModule,
MatProgressSpinnerModule,
MatIconModule,
StoreModule.forRoot({
video: videoReducer
videoState: videoReducer
}),
EffectsModule.forRoot([ VideoEffects ]),
StoreDevtoolsModule.instrument()
],
providers: [ VideoService ],
providers: [ VideoService, EventService ],
bootstrap: [AppComponent]
})
export class AppModule { }
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { VideoDetailsComponent } from "./video-details/video-details.component";
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { VideoDetailsComponent } from './video-details/video-details.component';
const routes: Routes = [
{
path: "",
path: '',
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_SUCCESS: string;
LOAD_TRANSCRIPT_BY_ID_ERROR: string;
};
}
export const VideoActionsTypes: IVideoActionsTypes = {
LOAD_TRANSCRIPT_BY_ID: "LOAD TRANSCRIPT BY ID",
LOAD_TRANSCRIPT_BY_ID_SUCCESS: "LOAD TRANSCRIPT BY ID SUCCESS",
LOAD_TRANSCRIPT_BY_ID_ERROR: "LOAD TRANSCRIPT BY ID ERROR"
LOAD_TRANSCRIPT_BY_ID: 'LOAD TRANSCRIPT BY ID',
LOAD_TRANSCRIPT_BY_ID_SUCCESS: 'LOAD TRANSCRIPT BY ID SUCCESS',
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;
constructor(public payload: number) {}
constructor(public payload: string) {}
}
import { Injectable } from "@angular/core";
import { Actions, Effect, ofType } from "@ngrx/effects";
import { map, mergeMap, catchError } from "rxjs/operators";
import { VideoActionsTypes } from "../actions/video.actions";
import { of } from "rxjs";
import { MatSnackBar } from "@angular/material";
import { Router } from "@angular/router";
import { VideoService } from "../services/video.services";
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { VideoActionsTypes } from '../actions/video.actions';
import { of } from 'rxjs';
import { MatSnackBar } from '@angular/material';
import { Router } from '@angular/router';
import { VideoService } from '../services/video.services';
@Injectable()
export class VideoEffects {
......@@ -22,10 +22,9 @@ export class VideoEffects {
payload: ticket
})),
catchError(error => {
this.router.navigate(["Video"]);
this.snackBar.open(
"ERROR: There were some errors",
"Error",
'ERROR: There were some errors',
'Error',
{
duration: 5000
}
......@@ -44,4 +43,4 @@ export class VideoEffects {
private router: Router,
private snackBar: MatSnackBar
) { }
}
\ No newline at end of file
}
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 {
transcript: any,
videoId: string,
transcript: any;
videoId: string;
loading: boolean;
error: boolean;
}
......@@ -15,11 +19,12 @@ export const initialState: IVideoState = {
};
export function videoReducer(state: IVideoState = initialState, action: any): IVideoState {
switch(action.type) {
switch (action.type) {
case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID: {
return {
...state,
videoId: action.payload,
loading: true
};
}
......@@ -27,6 +32,7 @@ export function videoReducer(state: IVideoState = initialState, action: any): IV
case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID_SUCCESS: {
return {
...state,
transcript: action.payload,
loading: false,
error: false
};
......@@ -35,6 +41,8 @@ export function videoReducer(state: IVideoState = initialState, action: any): IV
case VideoActionsTypes.LOAD_TRANSCRIPT_BY_ID_ERROR: {
return {
...state,
videoId: initialState.videoId,
transcript: initialState.transcript,
loading: false,
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 { Observable } from "rxjs";
import { HttpClient } from "@angular/common/http";
import { ITranscript } from "../models/video.models";
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { ITranscript } from '../models/video.models';
@Injectable()
export class VideoService {
readonly transcriptUrl = "https://static.chorus.ai/api";
readonly transcriptUrl = 'https://static.chorus.ai/api';
constructor(private httpClient: HttpClient) { }
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">
<h1>Test title</h1>
<div class="video-container">
<video #video class="video" src="https://static.chorus.ai/api/4d79041e-f25f-421d-9e5f-3462459b9934.mp4" [attr.autoplay]="autoplay ? true : null"
[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.
</video>
<div class="video-details-container" *ngIf="!errorLoading; else errorLoadingContainer">
<div class="video-details" *ngIf="(videoId$ | async) as videoId; else noIdContainer">
<h1>Test title</h1>
<div class="video-container" #player>
<video #video class="video" [attr.src]="'https://static.chorus.ai/api/' + videoId + '.mp4'" preload="auto">
This browser does not support HTML5 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>
<ng-template #noIdContainer>
<app-find-video-id></app-find-video-id>
</ng-template>
</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>
</div>
\ No newline at end of file
</ng-template>
\ No newline at end of file
......@@ -5,16 +5,96 @@
width: 100%;
}
.video-details-container {
margin: auto;
background-color: $color-white;
padding: 32px;
max-width: 60%;
margin: 0 auto 100px;
@media screen and (min-width: 768px) {
max-width: 60%;
}
.video-container {
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 {
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({
selector: 'app-video-details',
templateUrl: './video-details.component.html',
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() {
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