DEV Community

Cover image for How To Build A Virtual Event Site With Angular (Youtube-Live Clone)
Gospel Darlington
Gospel Darlington

Posted on • Originally published at cometchat.com

8 2

How To Build A Virtual Event Site With Angular (Youtube-Live Clone)

What you’ll be building. Demo, Git Repo Here.

Youtube-Live Clone

Youtube-Live Clone

Introduction

There is a real principle in life that states: "If you want to be great, then start by doing great things in little ways". This also applies to you as a developer aspiring to influence the industry in a great way, "You can start by building great things in a little way". One way to build great things is by starting small. And the cometchat communication SDK offers you the opportunity to integrate some great messaging features into your apps such as text, audio, and video chatting features.

Prerequisites

To follow this tutorial, you must have a basic understanding of the general principles of Angular. This will help you to speedily digest this tutorial.

Installing The App Dependencies

First, you need to have NodeJs installed on your machine, you can go to their website to do that.

Second, you need to also have the Angular-CLI installed on your computer using the command below.

npm install -g @angular/cli
Enter fullscreen mode Exit fullscreen mode

Next, create a new project with the name youtube-live-clone.

ng new youtube-live-clone
Enter fullscreen mode Exit fullscreen mode

The ng new command prompts you for information about features to include in the initial app. Accept the defaults by pressing the Enter or Return key.

The Angular CLI installs the necessary Angular npm packages and other dependencies. This can take a few minutes.

Last, install these essential dependencies for our project using the command below.

ng add @angular/material
npm install @angular/youtube-player
npm install @angular/fire
ng add @angular/fire
Enter fullscreen mode Exit fullscreen mode

Now that we're done with the installations, let's move on to building our youtube-live clone solution.

Installing Comet Chat SDK

  1. Head to CometChat Pro and create an account.
  2. From the dashboard, add a new app called "youtube-clone".
  3. Select this newly added app from the list.
  4. From the Quick Start copy the APP_ID, REGION and AUTH_KEY. These will be used later.
  5. Navigate to the Users tab, and delete all the default users and groups leaving it clean (very important).
  6. Get the Angular CLI installed on your machine by entering this command on your terminal. npm install -g @angular/cli
  7. Open the "environment.ts" file in the project.
  8. Enter your secret keys from comet Chat and Firebase below on the next heading.
  9. Copy the same settings into the "environment.prod.ts" as well.
  10. Run the following command to install the comet chat SDK.
npm install @cometchat-pro/chat@2.3.0 --save
Enter fullscreen mode Exit fullscreen mode

The Environment Variables

The setup below spells out the format for configuring the environment.ts files for this project.


firebase: {
    apiKey: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
    authDomain: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx',
    databaseURL: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
    projectId: 'xxx-xxx-xxx',
    storageBucket: 'xxx-xxx-xxx-xxx-xxx',
    messagingSenderId: 'xxx-xxx-xxx',
    appId: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
    measurementId: 'xxx-xxx-xxx',
  },
  APP_ID: 'xxx-xxx-xxx',
  AUTH_KEY: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
  APP_REGION: 'xx',
Enter fullscreen mode Exit fullscreen mode

Setting Up Firebase Project

Head to Firebase create a new project and activate the email and password authentication service. This is how you do it.

To begin using Firebase, you’ll need a Gmail account. Head over to Firebase and create a new project.

Firebase Console Create Project

Firebase Console Create Project

Firebase provides support for authentication using different providers. For example Social Auth, phone numbers as well as the standard email and password method. Since we’ll be using the email and password authentication method in this tutorial, we need to enable this method for the project we created in Firebase, as it is by default disabled.

Under the authentication tab for your project, click the sign-in method and you should see a list of providers Firebase currently supports.

Firebase Authentication Options

Firebase Authentication Options

Next, click the edit icon on the email/password provider and enable it.

Firebase Enabling Authentication

Firebase Enabling Authentication

Next, you need to go and register your application under your Firebase project. On the project’s overview page, select the add app option and pick web as the platform.

Youtube-Live Clone Project Page

Youtube-Live Clone Project Page

Once you’re done registering the application, you’ll be presented with a screen containing your application credentials. Take note of the second script tag as we’ll be using it shortly in our Angular application.

Congratulations, now that you're done with the installations, let's do some configurations.

Configuring Comet Chat SDK

Inside your project structure, open the main.ts and paste these codes.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { CometChat } from '@cometchat-pro/chat';
if (environment.production) {
enableProdMode();
}
const appID = environment.APP_ID;
const region = environment.APP_REGION;
const appSetting = new CometChat.AppSettingsBuilder()
.subscribePresenceForAllUsers()
.setRegion(region)
.build();
CometChat.init(appID, appSetting).then(
() => {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
console.log('Initialization completed successfully');
},
(error) => {
console.log('Initialization failed with error:', error);
// Check the reason for error and take appropriate action.
}
);
view raw main.ts hosted with ❤ by GitHub
The main.ts file

The above codes initialize comet chat in your app and before our app starts up. The main.ts entry file uses your comet chat API Credentials. The environment.ts file also contains your Firebase Configurations variable file. Please do not share your secret keys on Github.

Setting Up The Router

The app-routing.module.ts file has all the routes available in our app along with their security clearance.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
AngularFireAuthGuard,
redirectUnauthorizedTo,
} from '@angular/fire/auth-guard';
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['login']);
import { EventsComponent } from './events/events.component';
import { EventComponent } from './event/event.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { ProfileComponent } from './profile/profile.component';
import { SearchComponent } from './search/search.component';
import { CreateEventComponent } from './create-event/create-event.component';
import { EditEventComponent } from './edit-event/edit-event.component';
const routes: Routes = [
{
path: '',
component: EventsComponent,
canActivate: [AngularFireAuthGuard],
data: { authGuardPipe: redirectUnauthorizedToLogin },
},
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'profile', component: ProfileComponent },
{
path: 'events/:id',
component: EventComponent,
canActivate: [AngularFireAuthGuard],
data: { authGuardPipe: redirectUnauthorizedToLogin },
},
{
path: 'edit/:id',
component: EditEventComponent,
canActivate: [AngularFireAuthGuard],
data: { authGuardPipe: redirectUnauthorizedToLogin },
},
{
path: 'search/:keyword',
component: SearchComponent,
canActivate: [AngularFireAuthGuard],
data: { authGuardPipe: redirectUnauthorizedToLogin },
},
{
path: 'create',
component: CreateEventComponent,
canActivate: [AngularFireAuthGuard],
data: { authGuardPipe: redirectUnauthorizedToLogin },
},
{
path: '**',
redirectTo: '',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
The App Routing Module file

Project Structure

The image below reveals the project structure. Make sure you see the folder arrangement before proceeding.

Youtube-Live Clone Project Structure

Youtube-Live Clone Project Structure

Now let's make the rest of the project components as seen in the image above.

The App Component

The following code is a wrapper around our app within the angular-router enabling swift navigation. For each route, this component navigates our app to the appropriate URL.

import { Component } from '@angular/core';
import { MatIconRegistry } from "@angular/material/icon";
import { DomSanitizer } from "@angular/platform-browser";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'youtube-clone';
constructor(
private matIconRegistry: MatIconRegistry,
private domSanitizer: DomSanitizer
) {
this.matIconRegistry.addSvgIcon(
"access_point",
this.domSanitizer.bypassSecurityTrustResourceUrl("../assets/access_point.svg")
);
}
}
The app.component.ts file

Replace everything in the app.component.html file with <router-outlet></router-outlet> and remove every style from the app.component.css.

The Angular Material Icons Setup

The following code will configure our app to harness the full power of material icons throughout our project.

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
const materialModules = [
MatIconModule
];
@NgModule({
imports: [
CommonModule,
...materialModules
],
exports: [
...materialModules
],
})
export class AngularMaterialModule { }
The angular-material.module.ts file

The App Module

Paste the codes in your app.module.ts file, this is a very important file that bundles all the components we will be using for our project.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AngularMaterialModule } from './angular-material.module';
import { HttpClientModule } from '@angular/common/http';
import { YouTubePlayerModule } from '@angular/youtube-player';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { environment } from 'src/environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { EventsComponent } from './events/events.component';
import { EventComponent } from './event/event.component';
import { CreateEventComponent } from './create-event/create-event.component';
import { EditEventComponent } from './edit-event/edit-event.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { ProfileComponent } from './profile/profile.component';
import { SearchComponent } from './search/search.component';
import { HeaderComponent } from './components/header.component';
import { SidebarComponent } from './components/sidebar.component';
import { RowsComponent } from './components/rows.component';
import { VideoComponent } from './components/video.component';
import { RelatedComponent } from './components/related.component';
@NgModule({
declarations: [
AppComponent,
EventsComponent,
EventComponent,
LoginComponent,
RegisterComponent,
HeaderComponent,
SidebarComponent,
RowsComponent,
VideoComponent,
RelatedComponent,
CreateEventComponent,
EditEventComponent,
ProfileComponent,
SearchComponent
],
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
BrowserAnimationsModule,
AngularMaterialModule,
HttpClientModule,
YouTubePlayerModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
view raw app.module.ts hosted with ❤ by GitHub
The app.module.ts

The Sidebar Component

The Sidebar component

The Sidebar component

The sidebar component, beautifully crafted with sub-components mirrors the routes of the real YouTube live. You must get to study the markup and styling structure on your own time. This component is reused across two other components in our app. Here is the code that sponsors its operation.

import { Component } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
@Component({
selector: 'app-sidebar',
template: `
<app-rows title="Home" icon="home"></app-rows>
<app-rows title="Trending" icon="whatshot"></app-rows>
<app-rows title="Subscription" icon="subscriptions"></app-rows>
<hr class="sidebar__hr" />
<app-rows title="Library" icon="video_library"></app-rows>
<app-rows title="History" icon="history"></app-rows>
<app-rows selected="true" svg="access_point" title="Live"></app-rows>
<app-rows title="Your Videos" icon="tv"></app-rows>
<app-rows title="Watch Later" icon="watch_ater"></app-rows>
<app-rows title="Liked Videos" icon="thumb_up_alt_outlined"></app-rows>
<app-rows title="Show More" icon="expand_more_outlined"></app-rows>
<hr class="sidebar__hr" />
<button class="logout" (click)="logOut()">Logout</button>
`,
styles: [
`
.sidebar__hr {
height: 1px;
border: 0;
background-color: lightgray;
margin-top: 10px;
margin-bottom: 10px;
}
.logout {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: red;
width: 100%;
border: 0;
padding: 15px;
color: #ffffff;
font-size: 14px;
transition: all 0.3 ease;
cursor: pointer;
}
.logout:hover {
background: rgba(255, 0, 0, 0.829);
}
`,
],
})
export class SidebarComponent {
constructor(private auth: AngularFireAuth, private route: Router) {}
public logOut(): void {
this.auth
.signOut()
.then(() => this.route.navigate(['login']))
.catch((error) => console.log(error.message))
}
}
The Sidebar Component

The Header Component

The Header component

The Header component

The header component, this single-file reusable component carries a bulk of vital features necessary for the functioning of our application. Besides being the navigational agent of our app, it also houses the search-bar component responsible for sorting out relevant events for the user. No big words, here are the codes responsible for its action.

import { Component, OnInit } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
@Component({
selector: 'app-header',
template: `
<div class="header">
<div class="header__left">
<mat-icon class="header__icon">menu</mat-icon>
<img
[routerLink]="['/']"
class="header__logo"
src="/assets/logo.svg"
alt="youtube logo"
/>
</div>
<form
#searchForm="ngForm"
(ngSubmit)="submit(searchForm)"
class="header__middle"
>
<input
ngModel
name="search"
#search="ngModel"
id="search"
class="header__search"
type="search"
placeholder="Search"
required
/>
<button
class="header__searchBtn"
type="submit"
[disabled]="!searchForm.valid"
>
<mat-icon class="header__icon">search</mat-icon>
</button>
</form>
<div class="header__right">
<mat-icon
class="header__icon"
[routerLink]="['/create']"
title="create new event"
>open_in_new</mat-icon
>
<mat-icon class="header__icon">video_call</mat-icon>
<mat-icon class="header__icon">apps</mat-icon>
<mat-icon class="header__icon">notifications</mat-icon>
<img
[routerLink]="['/profile']"
[src]="user?.photoURL"
class="mat-card-avatar"
title="Your Profile"
/>
</div>
</div>
`,
styles: [
`
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
position: sticky;
top: 0;
background-color: white;
z-index: 100;
}
.header__icon {
line-height: unset;
cursor: pointer;
}
.header__logo {
height: 75px;
margin-left: 20px;
cursor: pointer;
}
.header__logo:focus {
outline: none;
}
.header__left,
.header__middle,
.header__right {
display: flex;
align-items: center;
}
.header__middle {
width: 40%;
border: 1px solid black;
}
.header__middle > input {
flex: 1;
border: none;
padding: 0 10px;
}
.header__middle > input:focus,
.header__middle > input:active {
outline: none;
border: none;
}
.header__searchBtn {
width: 70px !important;
background-color: #fafafa;
border: none;
border-left: 1px solid lightgray;
color: gray;
text-align: center;
cursor: pointer;
}
.header__right > .header__icon {
margin-right: 10px;
}
.mat-card-avatar {
height: 40px;
width: 40px;
border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
cursor: pointer;
}
`,
],
})
export class HeaderComponent implements OnInit {
user: any = null;
constructor(private auth: AngularFireAuth, private router: Router) {
this.auth.authState.subscribe((authState) => (this.user = authState));
}
ngOnInit(): void {}
public submit(form: NgForm): void {
if (form.valid) {
this.router.navigate(['search', form.value.search])
}
}
}
The Header Component file

The Video Components

The Video component

The Video component

This is a single-file component used across the app for listing events retrieved from firestore. It carries event information such as the title, description, image URL, and YouTube link. The success of this app owes a big thanks to this very component. The codes below clearly describe the functionality of this component.

import { Component, Input } from '@angular/core';
@Component({
selector: 'app-video',
template: `
<div class="videoCard">
<img class="videoCard__thumbnail" [src]="image" [alt]="channel" />
<div *ngIf="live == 'true'" class="videoCard__indicator">
<mat-icon svgIcon="access_point"></mat-icon>
<p>LIVE</p>
</div>
<div class="video__text">
<h4>{{ title }}</h4>
<p>{{ channel }}</p>
<p>{{ views }} . {{ timestamp }}</p>
</div>
</div>
`,
styles: [
`
.mat-card-avatar {
height: 40px;
width: 40px;
border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
}
.videoCard {
margin-bottom: 40px;
width: 270px;
position: relative;
cursor: pointer;
}
.videoCard:focus,
.videoCard:active,
.videoCard:hover {
border: none;
outline: none;
box-shadow: none;
}
.videoCard__thumbnail {
height: 140px;
width: 250px;
display: block;
}
.video__text {
margin-top: 10px;
padding-right: 30px;
}
.video__text > h4 {
font-size: 14px;
margin-bottom: 5px;
}
.video__text > p {
font-size: 14px;
color: gray;
}
.videoCard__indicator {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 25px;
top: 110px;
padding: 5px;
color: white;
background-color: rgb(216, 2, 2);
border-radius: 4px;
height: 15px;
}
.videoCard__indicator > img {
width: 40px;
}
.videoCard__indicator > p {
font-weight: 500;
margin: 0;
}
`,
],
})
export class VideoComponent {
@Input() image: string = '';
@Input() title: string = '';
@Input() channel: string = '';
@Input() views: string = '';
@Input() timestamp: string = '';
@Input() live: string = 'false';
ngOnInit(): void {}
}
The Video Card component

The Related Video Component

The Related Video Component

The Related Video Component

Like the video.component.ts, this component acts similarly. The only difference is that, while the video components list videos vertically, this component list videos horizontally. Here are the codes for that.

The Related Videos component

The Rows Video Component

The Rows Video Component

The Rows Video Component

This component is responsible for making the sidebar routes look like the image above. It takes an Angular-Material Icon and a title to display what you see above. Below is the code for it.

import { Component, Input } from '@angular/core';
@Component({
selector: 'app-rows',
template: `
<div [ngClass]="['sidebarRow', selected ? 'selected' : '']">
<mat-icon *ngIf="svg == ''" class="rows__icon">{{ icon }}</mat-icon>
<mat-icon
*ngIf="icon == ''"
[svgIcon]="svg"
class="rows__icon"
></mat-icon>
<p class="rows__title">{{ title }}</p>
</div>
`,
styles: [
`
.sidebarRow {
display: flex;
align-items: center;
padding: 10px 20px;
}
.rows__icon {
color: #606060;
font-size: large !important;
}
.rows__title {
flex: 1;
margin: 0 0 0 20px;
font-size: 12px;
font-weight: 500;
}
.sidebarRow:hover {
background-color: lightgray;
cursor: pointer;
}
.sidebarRow:hover > .rows__icon {
color: red;
}
.sidebarRow:hover > .rows__title {
font-weight: bold;
}
.sidebarRow.selected {
background-color: lightgray;
}
.sidebarRow.selected > .rows__icon {
color: red;
}
.sidebarRow.selected > .rows__title {
font-weight: bold;
}
`,
],
})
export class RowsComponent {
@Input() title: string = '';
@Input() icon: string = '';
@Input() selected: boolean = false;
@Input() live: boolean = false;
@Input() svg: string = '';
}
The Rows Video Component

The Registration Component

The Registration Component

The Registration Component

This component combines the power of firebase auth-service and comet chat such that whenever a new user signs up for our app, the user’s details are captured by firebase and also registered on our comet chat account. The code below explains it in detail.


public submit(form: any): void {
    this.loading = true;
    const fullname = form.fullname;
    const email = form.email;
    const password = form.password;
    const avatar = this.generateImageFromIntial(fullname);

    this.auth
      .createUserWithEmailAndPassword(email, password)
      .then((res) => {
        res.user
          .updateProfile({
            displayName: fullname,
            photoURL: avatar,
          })
          .then(() => this.signUpWithCometChat({ ...res.user, avatar }));
      })
      .catch((error) => {
        console.log(error);
        this.loading = false;
      });
  }
  private signUpWithCometChat(data: any) {
    const apiKey = environment.APP_KEY;
    const user = new CometChat.User(data.uid);
    user.setName(data.displayName);
    user.setMetadata({avatar: data.avatar});
    CometChat.createUser(user, apiKey)
      .then(() => this.route.navigate(['login']))
      .catch((error) => {
        console.log(error);
        this.loading = false;
      });
  }
Enter fullscreen mode Exit fullscreen mode

So, whenever a user registers in our app, automatically, he is simultaneously registered on our comet chat account. Here is the full code that explains it all.

import { Component } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
import { CometChat } from '@cometchat-pro/chat';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css'],
})
export class RegisterComponent {
loading: boolean = false;
constructor(private auth: AngularFireAuth, private route: Router) {}
public submit(form: any): void {
this.loading = true;
const fullname = form.fullname;
const email = form.email;
const password = form.password;
const avatar = this.generateImageFromIntial(fullname);
this.auth
.createUserWithEmailAndPassword(email, password)
.then((res) => {
res.user
.updateProfile({
displayName: fullname,
photoURL: avatar,
})
.then(() => this.signUpWithCometChat({ ...res.user, avatar }));
})
.catch((error) => {
console.log(error);
this.loading = false;
});
}
private signUpWithCometChat(data: any) {
const authKey = environment.AUTH_KEY;
const user = new CometChat.User(data.uid);
user.setName(data.displayName);
user.setMetadata({avatar: data.avatar});
CometChat.createUser(user, authKey)
.then(() => this.route.navigate(['login']))
.catch((error) => {
console.log(error);
this.loading = false;
});
}
private generateImageFromIntial(name: any) {
let canvas: any = document.createElement('canvas');
canvas.style.display = 'none';
canvas.width = '32';
canvas.height = '32';
document.body.appendChild(canvas);
let context = canvas.getContext('2d');
context.fillStyle = '#999';
context.fillRect(0, 0, canvas.width, canvas.height);
context.font = '16px Arial';
context.fillStyle = '#ccc';
if (name && name != '') {
let initials = name[0];
context.fillText(initials.toUpperCase(), 10, 23);
let data = canvas.toDataURL();
document.body.removeChild(canvas);
return data;
} else {
return false;
}
}
}
The Registration Component TypeScript File
<div class="app">
<div class="login-page">
<div class="form">
<img
class="header__logo"
src="/assets/logo.svg"
alt="youtube logo"
width="200"
/>
<form
class="login-form"
class="login-form"
#loginForm="ngForm"
(ngSubmit)="submit(loginForm.value)"
>
<input
type="text"
placeholder="Fullname"
required
minlength="3"
ngModel
name="fullname"
#fullname="ngModel"
id="fullname"
/>
<input
type="email"
placeholder="Email"
required
minlength="3"
ngModel
name="email"
#email="ngModel"
id="email"
/>
<input
type="password"
placeholder="Password"
required
minlength="6"
ngModel
name="password"
#password="ngModel"
id="password"
/>
<button
type="submit"
[style]="
!loginForm.valid || loading
? 'background-color: rgb(255, 94, 94);'
: ''
"
type="submit"
[disabled]="!loginForm.valid || loading"
>
{{ loading ? "Registering..." : "Register" }}
</button>
<p class="message">
Already registered? <a [routerLink]="['/login']">Sign In</a>
</p>
</form>
</div>
</div>
</div>
The Registration Component HTML file
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #ffffff;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: red;
width: 100%;
border: 0;
padding: 15px;
color: #ffffff;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,
.form button:active,
.form button:focus {
background: rgba(255, 0, 0, 0.829);
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: red;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before,
.container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #ef3b3a;
}
The Registration Component CSS file

The Login Component

The Login Componentt

The Login Component

Apart from being gorgeously styled, the Login component follows the behavior of the Registration component. For example, if a user named Maxwell registered on our app, he is then navigated to the login page to sign in. Because comet chat also has his details, the moment he signs in, he will also be signed in by comet chat. The piece of code below demonstrates this process better.


public submit(form): void {
    this.loading = true;
    const email = form.email;
    const password = form.password;
    this.auth
      .signInWithEmailAndPassword(email, password)
      .then((res) => this.loginCometChat(res.user))
      .catch((error) => {
        console.log(error);
        this.loading = false;
      });
  }
  private loginCometChat(user: any) {
    const apiKey = environment.APP_KEY;
    CometChat.login(user.uid, apiKey)
      .then(() => this.route.navigate(['']))
      .catch((error) => {
        console.log(error);
        this.loading = false;
      });
  }
Enter fullscreen mode Exit fullscreen mode

Once a user is successfully logged in, he is then redirected to the home page. The route-guard within the app.routing-module.ts ensures that only authenticated users are permitted to access the home page. The following scripts below describes the overall operations of the login component.

import { Component } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
import { CometChat } from '@cometchat-pro/chat';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css'],
})
export class LoginComponent {
loading: boolean = false;
constructor(private auth: AngularFireAuth, private route: Router) {}
public submit(form): void {
this.loading = true;
const email = form.email;
const password = form.password;
this.auth
.signInWithEmailAndPassword(email, password)
.then((res) => this.loginCometChat(res.user))
.catch((error) => {
console.log(error);
this.loading = false;
});
}
private loginCometChat(user: any) {
const authKey = environment.AUTH_KEY;
CometChat.login(user.uid, authKey)
.then(() => this.route.navigate(['']))
.catch((error) => {
console.log(error);
this.loading = false;
});
}
}
The Login Component TypeScipt file
<div class="app">
<div class="login-page">
<div class="form">
<img
class="header__logo"
src="/assets/logo.svg"
alt="youtube logo"
width="200"
/>
<form
class="login-form"
#loginForm="ngForm"
(ngSubmit)="submit(loginForm.value)"
>
<input
type="email"
placeholder="Email"
required
minlength="3"
ngModel
name="email"
#email="ngModel"
id="email"
/>
<input
type="password"
placeholder="Password"
required
minlength="6"
ngModel
name="password"
#password="ngModel"
id="password"
/>
<button
type="submit"
[style]="
!loginForm.valid || loading
? 'background-color: rgb(255, 94, 94);'
: ''
"
[disabled]="!loginForm.valid || loading"
>
{{ loading ? "Logging..." : "login" }}
</button>
<p class="message">
Not registered? <a [routerLink]="['/register']">Create an account</a>
</p>
</form>
</div>
</div>
</div>
The Login Component HTML file
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #ffffff;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: red;
width: 100%;
border: 0;
padding: 15px;
color: #ffffff;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,
.form button:active,
.form button:focus {
background: rgba(255, 0, 0, 0.829);
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: red;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before,
.container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #ef3b3a;
}
The Login Component CSS file

The Profile Component

The Profile Component

The Profile Component

The profile component is charged with the responsibility of updating our data which will reflect across our app. One of the primary tasks of this component is to change a user's profile.

In the registration component, once a user signs up, we generated a placeholder avatar for him using the initial of his name. The profile component allows a user to update their avatar to their preferred choice. Here is the full code performing this operation.

import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
import { CometChat } from '@cometchat-pro/chat';
import { environment } from 'src/environments/environment'
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
loading: boolean = false;
user: any = null;
constructor(private auth: AngularFireAuth, private route: Router) {
this.auth.authState.subscribe((authState) => (this.user = authState));
}
ngOnInit(): void {
}
public submit(form: NgForm): void {
if (form.valid) {
this.loading = true
const photoURL = form.value.avatar
this.auth.authState.subscribe((authState) => {
authState.updateProfile({photoURL})
.then(() => this.setAvatar(photoURL))
});
}
}
private setAvatar(url: string) {
const authKey = environment.AUTH_KEY;
const uid = this.user.uid;
var user = new CometChat.User(uid);
user.setAvatar(url);
CometChat.updateUser(user, authKey)
.then(() => this.route.navigate(['']))
.catch((error) => console.log(error))
.finally(() => this.loading = false);
}
}
The Profile Component TypeScript File
<div class="app">
<div class="login-page">
<div class="form">
<h4>Update Your Profile</h4>
<form
class="login-form"
#profileForm="ngForm"
(ngSubmit)="submit(profileForm)"
>
<input
placeholder="fullname"
required
minlength="3"
[ngModel]="user?.displayName"
name="fullname"
#fullname="ngModel"
id="fullname"
disabled
/>
<input
placeholder="Email"
type="email"
required
minlength="3"
[ngModel]="user?.email"
name="email"
#email="ngModel"
id="email"
disabled
/>
<input
placeholder="Avatar"
required
minlength="3"
[ngModel]="user?.photoURL"
name="avatar"
#Avatar="ngModel"
id="avatar"
/>
<button
[style]="
!profileForm.valid || loading
? 'background-color: rgb(255, 94, 94);'
: ''
"
type="submit"
[disabled]="!profileForm.valid || loading"
>
{{ loading ? "Updating..." : "Update" }}
</button>
<p class="message">
Changed your mind? <a [routerLink]="['/']">Back to home</a>
</p>
</form>
</div>
</div>
</div>
The Profile Component HTML File
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #ffffff;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input,
.form select {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: red;
width: 100%;
border: 0;
padding: 15px;
color: #ffffff;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,
.form button:active,
.form button:focus {
background: rgba(255, 0, 0, 0.829);
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: red;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before,
.container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #ef3b3a;
}
The Profile Component CSS File

The Create-Event Component

The Create-Event Component

The Create-Event Component

This is a major component with many responsibilities associated with the success of our application. This component retains the duty of collecting an event’s information like the video title, link, image URL, description, and so on. The image above clearly describes it.

You must understand some background activities that are transacted whenever an event is added to our platform.

For instance, a user named Musa adds a new event to our platform, two operations were carried out behind the scene. One, firebase stored the details of that event, and comet chat created a group just for that event as well. The code below explains it better.


public submit(form: NgForm): void {
    if (form.valid) {
      this.loading = true;
      const data = form.value;
      data.timestamp = new Date().toJSON();
      data.views = this.randomNumber(100, 300);
      data.uid = this.authState.uid;
      this.firestore
        .collection('events')
        .add(data)
        .then((d) => {
          form.reset();
          const groupName = this.toVideoId(data.videoId);
          const guid = d.id;
          this.cometChatCreateGroup({ groupName, guid });
        });
    }
  }
  private cometChatCreateGroup(data: any) {
    const GUID = data.guid;
    const groupName = data.groupName;
    const groupType = CometChat.GROUP_TYPE.PUBLIC;
    const password = '';
    const group = new CometChat.Group(GUID, groupName, groupType, password);
    CometChat.createGroup(group)
      .then((group) => console.log('Group created successfully:', group))
      .catch((error) => {
        console.log('Group creation failed with exception:', error)
        this.loading = false;
      });
  }
Enter fullscreen mode Exit fullscreen mode

Now that you understand what is happening under the hood, let’s have a look at the full code of this component.

import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { AngularFirestore } from '@angular/fire/firestore';
import { AngularFireAuth } from '@angular/fire/auth';
import { CometChat } from '@cometchat-pro/chat';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-create-event',
templateUrl: './create-event.component.html',
styleUrls: ['./create-event.component.css'],
})
export class CreateEventComponent {
loading: boolean = false;
authState: any = null;
constructor(
private firestore: AngularFirestore,
private auth: AngularFireAuth
) {
this.auth.authState.subscribe((authState) => (this.authState = authState));
}
public submit(form: NgForm): void {
if (form.valid) {
this.loading = true;
const data = form.value;
data.timestamp = new Date().toJSON();
data.views = this.randomNumber(100, 300);
data.uid = this.authState.uid;
this.firestore
.collection('events')
.add(data)
.then((d) => {
form.reset();
const groupName = this.toVideoId(data.videoId);
const guid = d.id;
this.cometChatCreateGroup({ groupName, guid });
});
}
}
private cometChatCreateGroup(data: any) {
const GUID = data.guid;
const groupName = data.groupName;
const groupType = CometChat.GROUP_TYPE.PUBLIC;
const password = '';
const group = new CometChat.Group(GUID, groupName, groupType, password);
CometChat.createGroup(group)
.then((group) => console.log('Group created successfully:', group))
.catch((error) => {
console.log('Group creation failed with exception:', error)
this.loading = false;
});
}
private toVideoId(url: string) {
const regExp = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
return url.match(regExp)[1];
}
private randomNumber(min, max): Number {
const r = Math.random() * (max - min) + min;
return Math.floor(r);
}
}
The Create-Event TypeScript File
<div class="app">
<div class="login-page">
<div class="form">
<h4>Create New Event</h4>
<form
class="login-form"
#createForm="ngForm"
(ngSubmit)="submit(createForm)"
>
<input
placeholder="Title"
required
minlength="3"
ngModel
name="title"
#title="ngModel"
id="title"
/>
<input
placeholder="Youtube Link"
required
minlength="3"
ngModel
name="videoId"
#videoId="ngModel"
id="videoId"
/>
<input
placeholder="Image URL"
required
minlength="3"
ngModel
name="imgURL"
#imgURL="ngModel"
id="imgURL"
/>
<input
placeholder="Channel"
required
minlength="3"
ngModel
name="channel"
#channel="ngModel"
id="channel"
/>
<input
placeholder="Description"
required
minlength="3"
ngModel
name="description"
#description="ngModel"
id="description"
/>
<select name="live" [ngModel]="''" #live="ngModel" id="live" required>
<option value="" selected>Live?</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
<button
[style]="
!createForm.valid || loading
? 'background-color: rgb(255, 94, 94);'
: ''
"
type="submit"
[disabled]="!createForm.valid || loading"
>
{{ loading ? "Creating..." : "Create" }}
</button>
<p class="message">
Changed your mind? <a [routerLink]="['/']">Back to home</a>
</p>
</form>
</div>
</div>
</div>
The Create-Event HTML File
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #ffffff;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input,
.form select {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: red;
width: 100%;
border: 0;
padding: 15px;
color: #ffffff;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,
.form button:active,
.form button:focus {
background: rgba(255, 0, 0, 0.829);
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: red;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before,
.container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #ef3b3a;
}
The Create-Event CSS File

The Edit-Event Component

Like the create-event component, the edit component those almost the same job, only that it modifies the details of the event you added to the platform.

There is an important mode of operation that it does which can be best explained with the code snippets below.

import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { AngularFirestore } from '@angular/fire/firestore';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-edit-event',
templateUrl: './edit-event.component.html',
styleUrls: ['./edit-event.component.css'],
})
export class EditEventComponent {
loading: boolean = false;
authState: any = null;
id: string = '';
event: any = null;
constructor(
private firestore: AngularFirestore,
private router: Router,
private route: ActivatedRoute
) {
this.route.params.subscribe((param) => {
this.getEvent(param.id);
this.id = param.id;
});
}
public submit(form: NgForm): void {
if (form.valid) {
this.loading = true;
const data = form.value;
this.firestore
.collection('events')
.doc(this.id)
.update(data)
.then(() => {
this.loading = false;
this.router.navigate(['events', this.id]);
});
}
}
getEvent(id: string) {
this.firestore
.collection('events')
.doc(id)
.ref.get()
.then((doc: any) => {
const key = doc.id;
const data = doc.data();
this.event = { ...data, key };
});
}
}
The Edit-Event TypeScript File
<div class="app">
<div class="login-page">
<div class="form">
<h4>Update New Event</h4>
<form
class="login-form"
#updateForm="ngForm"
(ngSubmit)="submit(updateForm)"
>
<input
placeholder="Title"
required
minlength="3"
[ngModel]="event?.title"
name="title"
#title="ngModel"
id="title"
/>
<input
placeholder="Youtube Link"
required
minlength="3"
[ngModel]="event?.videoId"
name="videoId"
#videoId="ngModel"
id="videoId"
/>
<input
placeholder="Image URL"
required
minlength="3"
[ngModel]="event?.imgURL"
name="imgURL"
#imgURL="ngModel"
id="imgURL"
/>
<input
placeholder="Channel"
required
minlength="3"
[ngModel]="event?.channel"
name="channel"
#channel="ngModel"
id="channel"
/>
<input
placeholder="Description"
required
minlength="3"
[ngModel]="event?.description"
name="description"
#description="ngModel"
id="description"
/>
<select name="live" [ngModel]="event?.live" #live="ngModel" id="live" required>
<option value="" selected>Live?</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
<button
[style]="
!updateForm.valid || loading
? 'background-color: rgb(255, 94, 94);'
: ''
"
type="submit"
[disabled]="!updateForm.valid || loading"
>
{{ loading ? "Updating..." : "Update" }}
</button>
<p class="message">
Changed your mind? <a [routerLink]="['events', event?.id]">Back to event</a>
</p>
</form>
</div>
</div>
</div>
The Edit-Event HTML File
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #ffffff;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input,
.form select {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: red;
width: 100%;
border: 0;
padding: 15px;
color: #ffffff;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,
.form button:active,
.form button:focus {
background: rgba(255, 0, 0, 0.829);
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: red;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before,
.container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #ef3b3a;
}
The Edit-Event CSS File

The Events Component

The Events Component

The Events Component

This is a big-player component in the overall success of our application. Its assignment is to list elegantly all the events we have created in our app.

It employs the services of the video component for vertically rendering video cards to the view. No more talks, let’s see how it functions code-wise.

import { Component, OnInit } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import { Router } from '@angular/router';
import { CometChat } from '@cometchat-pro/chat';
@Component({
selector: 'app-events',
templateUrl: './events.component.html',
styleUrls: ['./events.component.css'],
})
export class EventsComponent implements OnInit {
about: boolean = false;
events: Array<any> = [];
constructor(private firestore: AngularFirestore, private route: Router) {}
ngOnInit(): void {
this.listEvents();
}
public listEvents() {
this.firestore
.collection('events')
.snapshotChanges()
.subscribe((snapshot) => {
this.events = [];
snapshot.forEach((childSnapshot) => {
const key: string = childSnapshot.payload.doc.id;
const data: any = childSnapshot.payload.doc.data();
this.events.push({ ...data, key });
});
});
}
viewEvent(event: any) {
this.joinGroup(event.key);
}
private joinGroup(guid: string) {
const GUID = guid;
const password = '';
const groupType = CometChat.GROUP_TYPE.PUBLIC;
CometChat.joinGroup(GUID, groupType, password)
.then((group) => {
console.log('Group joined successfully:', group);
this.route.navigate(['events', guid]);
})
.catch((error) => {
if (error.code != 'ERR_ALREADY_JOINED')
console.log('Group joining failed with exception:', error);
this.route.navigate(['events', guid]);
});
}
public timeAgo(date: any) {
const NOW: any = new Date();
date = new Date(date);
const times: any = [
['second', 1],
['minute', 60],
['hour', 3600],
['day', 86400],
['week', 604800],
['month', 2592000],
['year', 31536000],
];
let diff: any = Math.round((NOW - date) / 1000);
for (let t = 0; t < times.length; t++) {
if (diff < times[t][1]) {
if (t == 0) {
return 'Just now';
} else {
diff = Math.round(diff / times[t - 1][1]);
return diff + ' ' + times[t - 1][0] + (diff == 1 ? ' ago' : 's ago');
}
}
}
}
}
The Events Component TypeScript File
<app-header></app-header>
<div class="app">
<app-sidebar class="sidebar"></app-sidebar>
<div class="streams">
<div class="streams__info">
<div class="streams__left">
<img src="/assets/access_point.jpg" alt="youtube live" />
<div class="left__data">
<h2>Live</h2>
<p>13.8M Subscribers</p>
</div>
</div>
<div class="streams__right">
<button class="streams__btn">Subscribe</button>
</div>
</div>
<div class="streams__nav">
<button [ngClass]="[!about ? 'active' : '']" (click)="about = false">
Home
</button>
<button [ngClass]="[about ? 'active' : '']" (click)="about = true">
About
</button>
</div>
<div *ngIf="!about" class="streams__wrapper">
<h2 class="streams__title">Live Now</h2>
<div class="streams__videos">
<app-video
*ngFor="let event of events"
[title]="event.title"
[timestamp]="timeAgo(event.timestamp)"
[channel]="event.channel"
[views]="event.views + 'k views'"
[image]="event.imgURL"
[live]="event.live"
(click)="viewEvent(event)"
></app-video>
</div>
</div>
<div *ngIf="about" class="about__wrapper">
<div class="about__description">
<h4>Description</h4>
<p>
YouTube Live - Watch great live streams, such as live gaming, live
music, live sports, and live news.
</p>
<hr class="streams__hr" />
<h4>Links</h4>
<a href="#">Auto-generated by YouTube</a>
</div>
<div class="about__stats">
<h4>Stats</h4>
<hr class="streams__hr" />
<p>Joined Jan 15, 2015</p>
<hr class="streams__hr" />
<mat-icon>flag_alt</mat-icon>
</div>
</div>
</div>
</div>
The Events Component HTML File
.app {
display: flex;
}
.sidebar {
flex: 0.2;
}
.streams {
flex: 0.8;
background-color: #f9f9f9;
padding-bottom: 0;
}
.streams__title {
margin-left: 5px;
margin-bottom: 20px;
font-weight: 400;
}
.streams__videos {
display: flex;
flex-wrap: wrap;
}
.streams__info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 80px;
}
.streams__btn {
color: white;
background-color: #c00;
border: none;
padding: 10px;
text-transform: uppercase;
font-size: large;
cursor: pointer;
}
.streams__left {
display: flex;
align-items: center;
}
.streams__left > img {
border-radius: 50%;
margin-right: 30px;
}
.left__data > h2 {
font-weight: 400;
}
.left__data > p {
color: #606060;
}
.streams__wrapper,
.about__wrapper {
padding: 40px 20px;
background-color: #f1f1f1;
}
.streams__nav {
margin-left: 80px;
}
.streams__nav > button {
color: gray;
margin-right: 10px;
text-transform: uppercase;
font-weight: 600;
font-size: 15px;
background-color: transparent;
border: none;
padding: 20px 30px;
cursor: pointer;
}
.streams__nav > button:hover {
border-bottom: 3px solid #646464;
color: #030303;
}
.streams__nav > button:focus {
border: none;
border-bottom: 3px solid #646464;
outline: none;
}
.streams__nav > button.active {
border-bottom: 3px solid #646464;
color: #030303;
}
.about__wrapper {
display: flex;
}
.about__description {
flex: 0.6;
padding-right: 20px;
}
.about__stats {
flex: 0.4;
}
.about__wrapper h4 {
margin: 0 0 40px 0;
font-weight: 400;
}
.about__wrapper p {
margin: 10px 0 40px 0;
}
.about__wrapper mat-icon {
color: #929292;
}
.about__description a {
text-decoration: none;
font-size: 13px;
}
.streams__hr {
height: 1px;
border: 0;
background-color: lightgray;
margin-top: 20px;
margin-bottom: 20px;
}
The Events Component CSS File

The Event Component

The Event Component

The Event Component

This component may sound similar to the previous component, but they do totally different things. While the events component displays a list of events this component showcases a single event.

Other than the cool design it features, the event component embodies a full catalog of mind-blowing responsibilities, let’s list them.

  • Event Video Streaming
  • Live-Chatting Activity
  • Related Videos Displaying
  • Performs Real-Time Messaging
  • Event Edit & Delete Abilities

That’s some responsibilities don’t you think so? Allow me to discuss some of the functionalities with you in codes.


// This method listens for a real-time message and renders it to view
private listenForMessage(guid: string) {
    const listenerID = guid;
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          this.messages.push(message);
          this.scrollToEnd();
        },
      })
    );
  }

// This method retreives all the messages for the current event
private getMessages(guid: string) {
    const limit = 50;
    const messagesRequest = new CometChat.MessagesRequestBuilder()
      .setLimit(limit)
      .setGUID(guid)
      .build();
    messagesRequest
      .fetchPrevious()
      .then((messages: Array<any>) => {
        this.messages = messages.filter((m) => m.type == 'text');
      })
      .catch((error) =>
        console.log('Message fetching failed with error:', error)
      );
  }

// This method sends a new message into the group
private sendMessage(data: any, form: NgForm) {
    const receiverID = data.guid;
    const messageText = data.message;
    const receiverType = CometChat.RECEIVER_TYPE.GROUP;
    const textMessage = new CometChat.TextMessage(
      receiverID,
      messageText,
      receiverType
    );
    CometChat.sendMessage(textMessage)
      .then((message) => {
        this.messages.push(message);
        form.reset();
        this.scrollToEnd();
        this.words = 0;
      })
      .catch((error) =>
        console.log('Message sending failed with error:', error)
      );
  }

ngOnDestroy(): void {
    CometChat.removeMessageListener(this.id)
}

// This method joins a user into a group associated with an event
private joinGroup(guid: string) {
    const GUID = guid;
    const password = '';
    const groupType = CometChat.GROUP_TYPE.PUBLIC;
    CometChat.joinGroup(GUID, groupType, password)
      .then((group) => {
        console.log('Group joined successfully:', group);
        this.router.navigate(['events', guid]);
      })
      .catch((error) => {
        if (error.code != 'ERR_ALREADY_JOINED')
          console.log('Group joining failed with exception:', error);
        this.router.navigate(['events', guid]);
      });
  }
Enter fullscreen mode Exit fullscreen mode

Now that you understand the background activities within this component, it’s time you see the full code.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { AngularFirestore } from '@angular/fire/firestore';
import { AngularFireAuth } from '@angular/fire/auth';
import { CometChat } from '@cometchat-pro/chat';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-event',
templateUrl: './event.component.html',
styleUrls: ['./event.component.css'],
})
export class EventComponent implements OnInit {
messages: Array<any> = [];
message: string = '';
events: Array<any> = [];
event: any = null;
words: number = 0;
user: any = null;
id: string = '';
constructor(
private firestore: AngularFirestore,
private auth: AngularFireAuth,
private router: Router,
private route: ActivatedRoute
) {
this.auth.authState.subscribe((authState) => (this.user = authState));
this.route.params.subscribe((param) => {
this.id = param.id;
this.getEvent(param.id);
this.getMessages(param.id);
this.listenForMessage(param.id);
this.listRelatedEvents(param.id);
});
}
ngOnInit(): void {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
document.body.appendChild(tag);
}
ngOnDestroy(): void {
CometChat.removeMessageListener(this.id)
}
public submit(form: NgForm): void {
if (form.valid) {
const data = { message: form.value.message, guid: this.id };
this.sendMessage(data, form);
}
}
private getMessages(guid: string) {
const limit = 50;
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setLimit(limit)
.setGUID(guid)
.build();
messagesRequest
.fetchPrevious()
.then((messages: Array<any>) => {
this.messages = messages.filter((m) => m.type == 'text');
})
.catch((error) =>
console.log('Message fetching failed with error:', error)
);
}
private listenForMessage(guid: string) {
const listenerID = guid;
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => {
this.messages.push(message);
this.scrollToEnd();
},
})
);
}
viewEvent(event: any) {
this.joinGroup(event.key);
}
private joinGroup(guid: string) {
const GUID = guid;
const password = '';
const groupType = CometChat.GROUP_TYPE.PUBLIC;
CometChat.joinGroup(GUID, groupType, password)
.then((group) => {
console.log('Group joined successfully:', group);
this.router.navigate(['events', guid]);
})
.catch((error) => {
if (error.code != 'ERR_ALREADY_JOINED')
console.log('Group joining failed with exception:', error);
this.router.navigate(['events', guid]);
});
}
private sendMessage(data: any, form: NgForm) {
const receiverID = data.guid;
const messageText = data.message;
const receiverType = CometChat.RECEIVER_TYPE.GROUP;
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType
);
CometChat.sendMessage(textMessage)
.then((message) => {
this.messages.push(message);
form.reset();
this.scrollToEnd();
this.words = 0;
})
.catch((error) =>
console.log('Message sending failed with error:', error)
);
}
onDelete() {
if (confirm('Are you sure?')) this.remEvent();
}
private remEvent() {
this.firestore
.collection('events')
.doc(this.event.key)
.delete()
.then(() => this.router.navigate(['']));
}
getEvent(id: string) {
this.firestore
.collection('events')
.doc(id)
.ref.get()
.then((doc: any) => {
const key = doc.id;
const data = doc.data();
data.videoId = this.toVideoId(data.videoId);
this.event = { ...data, key };
});
}
public listRelatedEvents(id: string) {
this.firestore
.collection('events', (ref) => ref.orderBy('timestamp', 'desc').limit(5))
.snapshotChanges()
.subscribe((snapshot) => {
this.events = [];
snapshot.forEach((childSnapshot) => {
const key: string = childSnapshot.payload.doc.id;
const data: any = childSnapshot.payload.doc.data();
if (key != id) this.events.push({ ...data, key });
});
});
}
ngAfterViewChecked(): void {
this.scrollToEnd();
}
public toVideoId(url: string) {
const regExp = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
return url.match(regExp)[1];
}
scrollToEnd(): void {
const elmnt = document.getElementById('messages-container');
elmnt.scrollTop = elmnt.scrollHeight;
}
public timeAgo(date: any) {
const NOW: any = new Date();
date = new Date(date);
const times: any = [
['second', 1],
['minute', 60],
['hour', 3600],
['day', 86400],
['week', 604800],
['month', 2592000],
['year', 31536000],
];
let diff: any = Math.round((NOW - date) / 1000);
for (let t = 0; t < times.length; t++) {
if (diff < times[t][1]) {
if (t == 0) {
return 'Just now';
} else {
diff = Math.round(diff / times[t - 1][1]);
return diff + ' ' + times[t - 1][0] + (diff == 1 ? ' ago' : 's ago');
}
}
}
}
wordCounter(words: string) {
if(words == null) words = ''
this.words = words.split('').length
}
}
The Event Component TypeScript File
<app-header></app-header>
<div class="app">
<app-sidebar class="sidebar"></app-sidebar>
<div class="event">
<div class="event__left">
<div class="event__player">
<youtube-player
width="800"
height="450"
[videoId]="event?.videoId"
></youtube-player>
</div>
<div class="event__info">
<div class="info__left">
<h4>{{ event?.title }}</h4>
<p>Started streaming {{ timeAgo(event?.timestamp) }}</p>
</div>
<div class="info__right">
<span>
<mat-icon class="info__icon">thumb_up</mat-icon>
210
</span>
<span>
<mat-icon class="info__icon">thumb_down</mat-icon>
120
</span>
<span>
<mat-icon class="info__icon">share</mat-icon>
share
</span>
<span>
<mat-icon class="info__icon">playlist_play</mat-icon>
Save
</span>
<span>
<mat-icon class="info__icon">flag</mat-icon>
</span>
</div>
</div>
<hr class="__hr" />
<div class="event__description">
<div>
<img [src]="event?.imgURL" class="mat-card-avatar" />
</div>
<div class="event__data">
<h4>{{ event?.channel }}</h4>
<p>{{ event?.views }}K subscribers</p>
<p class="event__details">
{{ event?.description }}
</p>
<p class="event__links">
<button [routerLink]="['/edit', event?.key]">Edit</button>
<button (click)="onDelete()">Delete</button>
</p>
</div>
<div>
<button class="subscribe__btn">Subscribe</button>
</div>
</div>
</div>
<div class="event__right">
<div class="event__chat">
<div class="chatHeader">
<div class="chatHeader__left">
<span>Top Chat </span>
<span>▼</span>
</div>
<div class="chatHeader__right">
<span>⋮</span>
</div>
</div>
<div id="messages-container" class="chatBody">
<div class="messages">
<div
class="message"
*ngFor="let message of messages; let i = index"
>
<img
[src]="message.sender?.avatar || message.sender.metadata?.avatar"
class="mat-card-avatar"
/>
<span>
<span class="name">{{ message.sender.name }}</span>
{{ message.text }}
</span>
</div>
</div>
</div>
<form
#chatForm="ngForm"
(ngSubmit)="submit(chatForm)"
class="chatFooter"
>
<div class="chatFooter__wrapper">
<div class="avatar">
<img [src]="user?.photoURL" class="mat-card-avatar" />
</div>
<div class="sendForm">
<p>{{ user?.displayName }}</p>
<input
ngModel
name="message"
#message="ngModel"
id="message"
placeholder="Say something..."
maxlength="200"
max="200"
required
(keydown)="wordCounter(message.value)"
/>
</div>
</div>
<div class="chatFooter__btn">
<span class="counter">{{ words }}/200</span>
<button type="submit" [disabled]="!chatForm.valid">
<mat-icon class="icon">send</mat-icon>
</button>
</div>
</form>
</div>
<div class="event__videos">
<div class="badge-pills">
<span class="pill">All</span>
<span class="pill">Related</span>
<span class="pill">Live</span>
<span class="pill">New</span>
</div>
</div>
<div class="event__related">
<app-related
*ngFor="let event of events"
[title]="event.title"
[timestamp]="timeAgo(event.timestamp)"
[channel]="event.channel"
[views]="event.views + 'k views'"
[image]="event.imgURL"
[live]="event.live"
(click)="viewEvent(event)"
></app-related>
</div>
</div>
</div>
</div>
The Event Component HTML File
.app {
display: flex;
}
.sidebar {
flex: 0.2;
}
.event {
flex: 0.8;
background-color: #f9f9f9;
padding-bottom: 0;
display: flex;
}
.mat-card-avatar {
height: 30px;
width: 30px;
border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
}
.event__left,
.event__right {
padding: 0 15px;
margin: 15px 0;
}
.info__left,
.info__right {
flex: 0.5;
}
.event__left {
flex: 0.7;
}
.event__right {
flex: 0.3;
}
.event__info {
display: flex;
justify-content: space-between;
padding: 25px 0;
}
.info__right {
text-align: right;
}
.info__left > h4 {
font-weight: 500;
text-transform: uppercase;
}
.info__left > p {
font-size: 12px;
color: gray;
margin-top: 10px;
}
.info__right {
font-size: 10px;
color: gray;
}
.info__right span {
margin-right: 20px;
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
}
.info__right span:last-child {
margin-right: 0;
}
.info__icon:hover {
color: red;
}
.__hr {
height: 1px;
border: 0;
background-color: lightgray;
margin-top: 10px;
margin-bottom: 10px;
}
.mat-card-avatar {
height: 40px;
width: 40px;
border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
}
.subscribe__btn {
color: white;
background-color: #c00;
border: none;
padding: 10px;
text-transform: uppercase;
font-size: large;
cursor: pointer;
}
.event__description {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.event__description > .mat-card-avatar {
height: 50px;
width: 50px;
}
.event__data {
flex: 0.9;
}
.event__details {
margin: 10px 0;
}
.event__links {
margin-top: 30px;
}
.event__links > button {
text-transform: uppercase;
padding: 5px 15px;
cursor: pointer;
margin-right: 10px;
}
.event__chat {
border: 1px solid lightgray;
height: 608px;
position: relative;
}
.chatHeader {
display: flex;
justify-content: space-between;
background-color: white;
padding: 15px 25px;
border-bottom: 1px solid lightgray;
}
.chatHeader__left > span:last-child {
font-size: 12px;
}
.chatHeader__left > span {
cursor: pointer;
}
.chatHeader__right > span {
font-size: 25px;
cursor: pointer;
}
.chatFooter {
position: absolute;
bottom: 0;
background-color: white;
width: 100%;
height: 130px;
}
.chatFooter__wrapper {
display: flex;
margin: 15px 20px;
}
.chatFooter__wrapper > .avatar {
margin-right: 10px;
}
.chatFooter__wrapper > .sendForm {
flex: 1;
}
.sendForm p {
color: gray;
margin-bottom: 5px;
font-size: 15px;
font-weight: 400;
}
.sendForm input {
border: none;
border-bottom: 1px solid lightgray;
width: 100%;
color: gray;
}
.sendForm input:focus {
outline: none;
border-bottom: 3px solid #305fe4;
}
.chatFooter__btn {
display: flex;
justify-content: space-between;
margin: 15px 20px;
color: gray;
}
.chatFooter__btn > button {
border: none;
background-color: transparent;
cursor: pointer;
color: gray;
}
.chatFooter__btn > button:hover,
.chatFooter__btn > button:active,
.chatFooter__btn > button:focus {
outline: none;
}
.messages {
margin: 15px 20px;
}
.message {
display: flex;
align-items: center;
font-size: 12px;
margin-bottom: 10px;
}
.message > img {
margin-right: 10px;
}
.message > span > .name {
color: gray;
margin-right: 10px;
text-transform: capitalize;
}
.chatBody {
overflow-y: scroll;
height: 68%;
}
.event__videos {
margin-top: 20px;
}
.badge-pills {
padding: 10px 0;
}
.badge-pills > span {
background-color: lightgray;
color: black;
padding: 10px 15px;
margin-right: 10px;
border-radius: 25px;
cursor: pointer;
}
.badge-pills > span:first-child {
background-color: gray;
color: white;
}
.event__related {
margin-top: 25px;
}
/* width */
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background: #f1f1f1;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #888;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #555;
}
The Event Component CSS File

The Search Component

The Search Component

The Search Component

Lastly, let’s discuss how the search component resolves events sorting operations. Obviously, as events are added more into the platform, a threshold will come that will require an ability to search for events. This component offers that solution, let’s see the logic behind it in the codes below.

import { Component, OnInit } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import { ActivatedRoute, Router } from '@angular/router';
import { CometChat } from '@cometchat-pro/chat';
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.css'],
})
export class SearchComponent implements OnInit {
events: Array<any> = [];
constructor(
private firestore: AngularFirestore,
private route: ActivatedRoute,
private router: Router,
) {
this.route.params.subscribe((param) => {
this.searchResult(param.keyword);
});
}
ngOnInit(): void {}
public searchResult(keyword: string) {
this.firestore
.collection('events', (ref) => ref.orderBy('timestamp', 'desc').limit(10))
.snapshotChanges()
.subscribe((snapshot) => {
this.events = [];
snapshot.forEach((childSnapshot) => {
const key: string = childSnapshot.payload.doc.id;
const data: any = childSnapshot.payload.doc.data();
const title = data.title.toLowerCase()
const channel = data.channel.toLowerCase()
if (title.includes(keyword) || channel.includes(keyword))
this.events.push({ ...data, key });
});
});
}
viewEvent(event: any) {
this.joinGroup(event.key);
}
private joinGroup(guid: string) {
const GUID = guid;
const password = '';
const groupType = CometChat.GROUP_TYPE.PUBLIC;
CometChat.joinGroup(GUID, groupType, password)
.then((group) => {
console.log('Group joined successfully:', group);
this.router.navigate(['events', guid]);
})
.catch((error) => {
if (error.code != 'ERR_ALREADY_JOINED')
console.log('Group joining failed with exception:', error);
this.router.navigate(['events', guid]);
});
}
public timeAgo(date: any) {
const NOW: any = new Date();
date = new Date(date);
const times: any = [
['second', 1],
['minute', 60],
['hour', 3600],
['day', 86400],
['week', 604800],
['month', 2592000],
['year', 31536000],
];
let diff: any = Math.round((NOW - date) / 1000);
for (let t = 0; t < times.length; t++) {
if (diff < times[t][1]) {
if (t == 0) {
return 'Just now';
} else {
diff = Math.round(diff / times[t - 1][1]);
return diff + ' ' + times[t - 1][0] + (diff == 1 ? ' ago' : 's ago');
}
}
}
}
}
The Event Component TypeScript File
<app-header></app-header>
<div class="app">
<app-sidebar class="sidebar"></app-sidebar>
<div class="search">
<div class="streams__wrapper">
<h2 class="streams__title">Search Results({{events.length}})</h2>
<div class="streams__videos">
<app-video
*ngFor="let event of events"
[title]="event.title"
[timestamp]="timeAgo(event.timestamp)"
[channel]="event.channel"
[views]="event.views + 'k views'"
[image]="event.imgURL"
[live]="event.live"
(click)="viewEvent(event)"
></app-video>
</div>
</div>
</div>
</div>
The Event Component HTML File
.app {
display: flex;
}
.sidebar {
flex: 0.2;
}
.search {
flex: 0.8;
background-color: #f9f9f9;
padding-bottom: 0;
display: flex;
}
.streams__wrapper {
padding: 40px 20px;
}
.streams__videos {
display: flex;
flex-wrap: wrap;
}
The Event Component CSS File

Once you are done pasting the codes as directed, run the command below to start your application.

ng serve --open
Enter fullscreen mode Exit fullscreen mode

After a few seconds of building in the terminal, your app should be up and running.

Congratulations, you just completed the clone of YouTube-Live, great job!!!

Conclusion

In conclusion, building a virtual event site such as YouTube-Live is a fantastic idea to up your development skill. Especially, the integration of a live-chatting feature using the comet chat SDK makes a dream come true for a fellow like me.

This tutorial has educated you with the abilities needed to pull together a clone of one of the most valuable applications of our time. It's time to get busy and replicate a YouTube-Live Clone.

Top comments (0)