In this music player tutorial, we see how easy it is to get started using Kendo UI components for Angular, and how easy it is to define the structure of your application using the Splitter component. The components are easy to use and configure.
Kendo UI is a library used for building applications at a faster pace. Kendo UI provides UI components for libraries like jQuery, Angular, React and Vue, and it comes packed with an extensive set of components for creating charts, data tables and drag-and-drop pages.
Kendo UI is customizable. It provides a set of themes for Material UI, Bootstrap, etc., and it comes packed with an icon set and a color palette. Kendo UI components are distributed as multiple NPM packages, so there’s no fear of bloating your application with unnecessary components and increasing your build bundle.
Angular is a JavaScript framework for creating a web application. It offers the best practices and tools to ease the development of applications. Angular uses declarative templates, dependency injection and Observables to power applications that can be run on several platforms.
Kendo UI provides Angular-specific components for ease of development, and we’ll be using some of the components to build a simple music player application. The application will be using Spotify’s API to get tracks and albums, and Kendo UI for creating grids and defining icons.
To follow this tutorial, you need a basic understanding of Angular. Ensure that you have Node and NPM installed before you get started.
If you have no prior knowledge of Angular, kindly follow the official tutorial here. You can return to the article when you’re done.
We’ll be building the application using the following tools:
Here’s a screenshot of the application we’ll be building:
To get access to the Spotify API, you’ll need to create an application in the developer dashboard. Head over to the developer dashboard, and log in using your Spotify credentials. After logging in, you should the screenshot below:
You can see that I have existing applications. If you don’t, click the empty card to create a new application, and fill in the required details to get started.
Since we won’t be setting up OAuth for users of the application, we’ll have to get access tokens from the Spotify developer console. The tokens expire after a couple of requests, but you can always extend the application to get access tokens from logged-in users.
To get a token, head over to the console, select the endpoint you wish to explore, and at the bottom of the page you should see a button titled Get Token
. Click the button to get a new token and test the endpoint to view sample responses.
Copy the generated token, we’ll be using this later in the article.
To get started, we will use the CLI (command line interface) provided by the Angular team to initialize our project.
First, install the CLI by running npm install -g @angular/cli
. NPM is a package manager used for installing packages. It will be available on your PC if you have Node installed; if not, download Node here.
To create a new Angular project using the CLI, open a terminal and run:
ng new kendo-music-player --style=scss
This command is used to initialize a new Angular project; the project will be using SCSS as the pre-processor.
Next, run the following command in the root folder of the project to install dependencies:
ng add @progress/kendo-angular-buttons @progress/kendo-angular-layout
Start the Angular development server by running ng serve
in a terminal in the root folder of your project.
To get started, we’ll define the views for the application, starting from the home page. The home page will house the grid displaying tracks and albums and the header. The default App
component will house the home page of the application. Open the app.component.html
file and update the file to get started:
<main>
<!-- header component will come here -->
<section>
<kendo-splitter style="width: 100%; height: 100%; padding: 40px 0 ">
<kendo-splitter-pane size="60%" min="40%">
<!-- albums grid here -->
</kendo-splitter-pane>
<kendo-splitter-pane min="30%">
<! album tracks list here -->
</kendo-splitter-pane>
</kendo-splitter>
</section>
</main>
First, we define the structure of our application using Kendo UI’s layout component. The layout package provides a splitter component that can be used to split the webpage into different sections. The splitter component allows the user to control the size of each section by dragging and resizing.
The kendo-splitter
can be displayed in two orientations — vertical
and horizontal
. Just provide an orientation
directive on the component. Within the kendo-splitter
component, there are two kendo-splitter-pane
components. These are used for creating columns similar to the .col
elements in Bootstrap.
These pane components are resizable columns, so first we provide the size
directive, which defines the width of the column similar to col-8
in Bootstrap. The min
directive is used on resizable
columns to restrict the resizing to a minimum width. In the example above, the first column can’t be resized past 40%
and the second 30%
.
Next, we’ll include the external assets the application will be using. We’ll be using the Montserrat
and Roboto
fonts. Also, we’ll update style.scss
file to include the Kendo UI component stylesheets and application-wide styles.
Open the index.html
file and include the link to the external fonts:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KendoMusicPlayer</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto:400,500,700,900" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
</html>
Also, we’ll be overriding the default color scheme of Kendo UI’s theme. The first step is to create a file to hold the variables. Create a folder in the src/app
directory called scss-partials
and create a partials file called _variables.scss
within the folder. Open the file and update it with the following variables:
// src/app/scss-partials/_variables.scss
$accent: #7B84FF;
$accent-light: #DBDDFF;
$border-radius: 5px;
$base-text: #525F7F;
Then open the style.scss
file next and import the variables file and component stylesheets next:
// styles.scss
@import "app/scss-partials/variables";
@import "~@progress/kendo-theme-default/scss/button";
@import "~@progress/kendo-theme-default/scss/splitter";
body, html {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
background: #F7F7F7;
}
body,html,main, section{
height: 100%;
}
The header component will display the application logo and very little information. This component is mostly a display component.
Run the following command to create the header component:
ng generate component header
Next, open the src/app/header/header.component.html
file and update it to look like the code below:
<!-- src/app/header/header.component.html -->
<header>
<div class="brand">
<img src="/assets/images/logo.png">
<h5>Just Music</h5>
</div>
<div class="nav-items">
<ul class="nav-items">
<li class="nav-item">
<span>Music</span>
</li>
</ul>
</div>
</header>
Note: Image asset used can be found here in the GitHub repository. The logo was obtained from https://flaticon.com.
Next, we’ll style the header. Open the header.component.scss
file and update it with the snippet below:
@import "../scss-partials/variables";
header {
display: flex;
background-color: #F7F7F7;
align-items: center;
font-family: 'Montserrat', sans-serif;
margin: 0;
padding: 20px 5%;
color: whitesmoke;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
.brand {
flex: 1;
display: flex;
align-items: center;
img {
height: 35px;
border-radius: 50%;
margin-right: 17px;
}
h5 {
font-size: 15px;
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
color: $base-text;
}
}
.nav-items {
margin: 0;
.nav-item {
span {
text-transform: uppercase;
font-size: 13px;
font-weight: bold;
letter-spacing: 0.7px;
padding: 10px 17px;
border-radius: 4px;
background-color: $accent-light;
color: $accent;
border: none;
margin: 0;
}
}
}
}
Just a couple of styles to beautify the header.
If you visit http://localhost:4200, you should see the latest view of the application. The header and the splitter are visible, you can play around with the resizable columns but remember to come back and finish the article. Ensure the Angular dev server is running before testing.
Next, we’ll create a service to fetch the latest releases and tracks from Spotify.
To handle fetching of the latest releases and tracks, we’ll be using a service. Services are recommended to be used for side effects like HTTP requests in your application. Services are reusable and can easily abstract processes like these from components, thus making components less bloated and easier to manage.
To create a service using the CLI, run the following command:
ng generate service album
This should generate file named album.service.ts
in the src/app
directory. Open the file and update it to include new methods for fetching data:
import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {environment} from '../environments/environment';
@Injectable({
providedIn: 'root'
})
export class AlbumService {
constructor(private http: HttpClient) {
}
get headers() {
return new HttpHeaders({'Content-Type': 'application/json', 'Authorization': `Bearer ${environment.spotifySecret}`});
}
getAlbums() {
return this.http.get('https://api.spotify.com/v1/browse/new-releases?limit=10&country=GB', {
headers: this.headers
});
}
getAlbumTracks(id: string) {
return this.http.get(`https://api.spotify.com/v1/albums/${id}/tracks`, {
headers: this.headers
});
}
}
First, we’ll import the HttpClient
, which will be used for making HTTP requests. The HttpHeaders
is useful for creating headers that can be attached to requests since the Spotify endpoints require Authorization
headers.
The headers
getter method generates headers using the HttpHeaders
class and provides a Bearer
token stored in the environment
file. We’ll get to creating this spotifySecret
property in the environment.ts
file.
The next method is the getAlbums
method that returns an Http
request observable made to the new-releases
endpoint. This endpoint returns the latest albums and tracks released on Spotify. You can view the responses from this endpoint in the console here.
Finally, the getAlbumTracks
method takes an id
parameter and calls the album/${id}/tracks
endpoint. This endpoint simply returns the tracks contained in an album.
Next, we’ll update the environment.ts
file with the token we obtained from the console. If your token has expired, you can get a fresh one here.
Open the environment.ts
file and add a new property called spotifySecret
to the object:
export const environment = {
production: false,
spotifySecret: '<ACCESS_TOKEN>',
};
Replace the placeholder string <ACCESS_TOKEN>
with your actual access token obtained from the console.
To use the HttpClient
class, you have to include the HttpClientModule
import in the app.module.ts
file. Open the file and add the import:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
...
import {HttpClientModule} from '@angular/common/http';
@NgModule({
declarations: [
...
],
imports: [
...,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Next, we’ll create the components to display the latest albums from the new releases endpoint.
Run the following commands to generate components for the product item and product list:
ng generate component album
And for the product list, run:
ng generate component album-list
Open the album.component.html
file in the src/app/album
directory and update with the code below:
<div class="album" (click)="onAlbumClick($event)">
<div [ngStyle]="albumCover">
<div class="details">
<h3>{{album.name}}</h3>
<h4>{{getArtiste(album).name}}</h4>
</div>
</div>
</div>
The album has a click
event listener to load the tracks of the selected album. Next, let’s add some styles to the component to give it some life. Open the album.component.scss
file and add the following styles to it:
.album div {
width: 100%;
height: 300px;
border-radius: 6px;
margin: 10px 0;
background-blend-mode: multiply, normal;
background-size: contain;
display: flex;
align-items: flex-start;
overflow: hidden;
cursor: pointer;
&:hover {
h3 {
transition: 0.3s ease-in;
transform: translateY(0);
}
h4 {
transition: 0.3s ease-in;
transform: translateY(0);
}
}
.details{
display: flex;
flex-direction: column;
}
h3 {
font-size: 17px;
font-family: 'Montserrat', sans-serif;
text-transform: uppercase;
letter-spacing: 0.7px;
font-weight: 600;
text-align: left;
color: white;
opacity: 0.9;
margin-left: 15px;
margin-top: 30px;
transform: translateY(-50px);
}
h4 {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.7px;
font-weight: 500;
text-align: left;
color: whitesmoke;
opacity: 0.6;
margin-left: 15px;
margin-top: 6px;
transform: translateY(-70px);
}
}
Next, we’ll update the album.component.ts
file to create the handler for the click event. Open the file and update it with the variables and methods used in the HTML file:
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
@Component({
selector: 'app-album',
templateUrl: './album.component.html',
styleUrls: ['./album.component.scss']
})
export class AlbumComponent implements OnInit {
@Input() album: { images: Array<any> };
@Output() albumSelected = new EventEmitter<Object>();
albumCover: {};
getArtiste(album) {
const {artists} = album;
const [artist] = artists;
return artist;
}
onAlbumClick(e) {
this.albumSelected.emit(this.album);
}
ngOnInit() {
const {images} = this.album;
const [, image] = images;
this.albumCover = {background: `linear-gradient(0deg, #989494, #aba6a6), url(${image.url}), no-repeat`};
}
}
In the component, we have Input
and Output
bindings for component interaction. The component will take an Input
named album
. This will be sent by the album list component. The component also emits events using the Output
binding coupled with the EventEmitter
observable.
The component has two methods. The getArtiste
method gets the album’s artist off the album
input object. The next method is the event handler onAlbumClick
; the method emits the album clicked to the parent component.
Finally, there’s the ngOnInit
lifecycle. In here, we obtain the album image from the album
object, then, using the image, we generate a background image using the linear-gradient
function.
After setting up the album
component, we’ll head over to the album-list
component, open the album-list.component.html
file and update it with the code below:
<section class="album-section">
<div>
<h4>Latest Albums</h4>
</div>
<div class="album-list">
<app-album *ngFor="let album of albums" [album]="album" (albumSelected)="onAlbumSelected($event)"></app-album>
</div>
</section>
Here, we loop through the array of albums
using the *ngFor
directive and rendering them using the album
component. You’ll also notice the (albumSelected)
event emitter created in the album
component.
Next, open the album-list.component.scss
file and update it with the styles below:
@import "../scss-partials/variables";
.album-section {
padding: 30px 45px;
h4 {
font-size: 15px;
color: $base-text;
text-transform: uppercase;
font-family: 'Montserrat', sans-serif;
font-weight: 600;
margin: 0 0 20px;
}
}
.album-list {
line-height: 0;
column-count: 3;
column-gap: 10px;
}
@media (max-width: 1200px) {
.album-list {
column-count: 4;
}
}
@media (max-width: 1000px) {
.album-list {
column-count: 3;
}
}
@media (max-width: 800px) {
.album-list {
column-count: 2;
}
}
@media (max-width: 400px) {
.album-list {
column-count: 1;
}
}
The following styles are used to define a simple grid that presents the images in a random order within the grid.
Next, open the album-list.component.ts
file to create the event handler for the events emitted from the album
component.
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
@Component({
selector: 'app-album-list',
templateUrl: './album-list.component.html',
styleUrls: ['./album-list.component.scss']
})
export class AlbumListComponent implements OnInit {
constructor() {
}
@Input() albums: [] = [];
@Output() albumSelected = new EventEmitter<object>();
onAlbumSelected(album) {
this.albumSelected.emit(album);
}
ngOnInit() {
}
}
In the component, we define Input
and Output
bindings to receive data from the home component and to emit the selected album. The onAlbumSelected
is the event handler for receiving data from the album
component.
Next, we’ll render the album list component in the app.component.html
and also fetch the latest albums using the album
service.
Open the app.component.html
file and update it to be similar to the snippet below:
<main>
<app-header></app-header>
<section>
<kendo-splitter style="width: 100%; height: 100%; padding: 40px 0 ">
<kendo-splitter-pane size="60%" min="40%">
<app-album-list [albums]="albums" (albumSelected)="onAlbumChange($event)"></app-album-list>
</kendo-splitter-pane>
<kendo-splitter-pane min="30%">
<! -- track list here -->
</kendo-splitter-pane>
</kendo-splitter>
</section>
</main>
After rendering the album list, update the App
component to fetch albums and assign them to a variable. Also, we’ll create an event handler to receive the clicked album.
Open the app.component.ts
file and update it to declare the variables used and to fetch the albums:
import {Component, OnInit} from '@angular/core';
import {AlbumService} from './album.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor(private albumService: AlbumService) {
}
albums = [];
currentAlbum: any = {};
tracks = [];
currentAlbumTracks(id) {
this.albumService.getAlbumTracks(id).subscribe((response: any) => {
const {items} = response;
this.tracks = items;
});
}
onAlbumChange(album: any) {
const {id} = album;
this.currentAlbum = album;
this.currentAlbumTracks(id);
}
ngOnInit() {
this.albumService.getAlbums().subscribe((response: any) => {
const {albums} = response;
const {items} = albums;
this.currentAlbum = items[0];
this.albums = items;
this.currentAlbumTracks(this.currentAlbum.id);
});
}
}
First, we import the AlbumService
, then in the OnInit
lifecycle, we fetch the albums using the service. When the request is complete, we get the items
from the response using object restructuring. Then we set the first album in the list as the currentAlbum
.
The currentAlbumTracks
method is for fetching the tracks within an album; the method takes a single parameter id
and uses the AlbumService
to fetch the data.
The onAlbumChange
method is an event handler that is called when an album is clicked. The method gets the id
of the selected album and calls the currentAlbumTracks
method to fetch its tracks. The selected album is assigned to the currentAlbum
property.
After these updates, if we visit the application on the browser on http://localhost:4200/, we should see the albums listed, and it should be similar to the screenshot below. Ensure that the token hasn’t expired. Get a new one if it has.
Next, we’ll create the tracklist and track components. Stay tuned.
Run the following commands to generate components for the song item and songs list:
ng generate component song-item
And for the product list run:
ng generate component song-list
Open the song-item.component.html
file in the src/app/song-item
directory and update with the code below:
<div class="track">
<div class="track-info">
<div class="track-info__name"><h4>{{track.name}}</h4></div>
<div class="track-info__meta">
<span class="track-info__meta-duration">
{{songDuration}}
</span>
<div class="track-info__meta-tags">
<span *ngIf="track.explicit" class="">
Explicit
</span>
</div>
</div>
</div>
<div class="track-playing__indicator">
<img src="/assets/images/short-waves.png" class="bland" alt="">
<img src="/assets/images/player-waves-colored.png" class="colored">
</div>
<div class="track-actions">
<button class="track-actions__play"><span class="k-icon k-i-video-external"></span></button>
</div>
</div>
Note: All image assets used can be found in a GitHub repository here.
In this component, we’ll display the track name, and we’ll display a tag if the track is explicit
. We also used Kendo UI’s icon set to define the play button. Kendo UI has a rich set of icons that can be found here. They are easy to configure and customize.
Let’s apply some styles to the component. Open the song-item.component.scss
file and update it with the following:
@import "../scss-partials/variables";
.track {
padding: 15px 20px;
display: flex;
justify-content: space-between;
background: #F7F7F7;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 10px;
margin: 10px 0;
&:hover {
background: white;
transform: scale(1.06);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.09);
.track {
&-playing__indicator {
.colored {
display: block;
}
.bland {
display: none;
}
}
&-actions {
&__play {
background: $accent;
.k-icon {
color: white;
}
}
}
}
}
&-info {
flex-grow: 1;
flex-basis: 0;
&__name {
margin-bottom: 8px;
h4 {
font-family: 'Montserrat', sans-serif;
opacity: 0.8;
font-size: 14px;
font-weight: 400;
margin: 5px 0;
}
}
&__meta {
display: flex;
align-items: center;
&-duration {
font-size: 13px;
font-weight: 600;
opacity: 0.7;
}
&-tags {
margin-left: 7px;
span {
background: #ffdde2;
color: indianred;
border-radius: 4px;
font-size: 10px;
text-transform: uppercase;
font-weight: 600;
padding: 5px 6px;
letter-spacing: 0.6px;
}
}
}
}
&-playing__indicator {
flex-grow: 1;
flex-basis: 0;
.colored {
display: none;
}
}
&-actions {
&__play {
height: 50px;
width: 50px;
border-radius: 50%;
border: none;
background: #F0F0F0;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 1px 2px 4px 0 rgba(0, 0, 0, 0.09);
cursor: pointer;
.k-icon {
font-size: 22px;
color: #D4D4D4;
}
}
}
}
After applying the styles, the next step is to update the component file to declare the variables used in the view template. Open the song-item.component.ts
file and update it like the snippet below:
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'app-song-item',
templateUrl: './song-item.component.html',
styleUrls: ['./song-item.component.scss']
})
export class SongItemComponent implements OnInit {
constructor() {
}
@Input() track = {duration_ms: 0};
get songDuration() {
const {duration_ms} = this.track;
const songInSeconds = Math.ceil(duration_ms / 1000);
const getMinutes = Math.floor(songInSeconds / 60);
const getSecondsRemainder = songInSeconds % 60;
return `${getMinutes}:${getSecondsRemainder}`;
}
ngOnInit() {
}
}
First, we declare an Input
binding to receive the track data from the parent component, then we declare a getter function songDuration
. This method takes the duration_ms
property, which contains the length of the track in milliseconds and converts it to a minutes:seconds
format seen in most music player applications.
After setting up the song-item
component, we’ll head over to the song-list
component, open the song-list.component.html
file, and update it with the code below:
<div class="song-list">
<div>
<h3>Current Album</h3>
<hr>
<div class="album-info">
<div class="album-info__image">
<img [src]="albumCover.url" [alt]="currentAlbum.name">
</div>
<div class="album-info__name">
<h4>{{currentAlbum.name}}</h4>
<p>{{albumArtiste.name}}</p>
</div>
</div>
</div>
<app-song-item *ngFor="let track of tracks" [track]="track"></app-song-item>
</div>
Here we loop through the array of tracks
using the *ngFor
directive and rendering them using the song-item
component. Also, we display the name of the album and the album cover above the track list. The track list is songs within the album.
Next, open the album-list.component.scss
file and update it with the styles below:
.song-list {
padding: 30px 50px;
h3 {
font-size: 14px;
font-weight: 600;
opacity: 0.7;
text-transform: uppercase;
font-family: 'Montserrat', sans-serif;
margin: 0 0 15px;
}
hr{
border-color: rgba(0,0,0,0.07);
margin-bottom: 15px;
}
.album-info {
display: flex;
align-items: flex-end;
&__image {
img {
width: 50px;
height: 50px;
border-radius: 6px;
}
}
&__name {
h4 {
font-family: 'Montserrat', sans-serif;
text-transform: uppercase;
font-size: 14px;
color: #7B84FF;
font-weight: 600;
opacity: 0.8;
margin: 0 0 0 10px;
}
p{
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
opacity: 0.6;
margin: 0 0 5px 10px;
}
}
}
}
The following styles are used to define the list of tracks to be displayed.
The song-list
component receives Input
bindings from the parent component, so open the song-list.component.ts
file to declare the Input
variables and other variables used in the view template:
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'app-song-list',
templateUrl: './song-list.component.html',
styleUrls: ['./song-list.component.scss']
})
export class SongListComponent implements OnInit {
constructor() {
}
@Input() tracks: [];
@Input() currentAlbum: any;
get albumCover() {
const {images} = this.currentAlbum;
const [image] = images;
return image;
}
get albumArtiste() {
const {artists} = this.currentAlbum;
const [artist] = artists;
return artist;
}
ngOnInit() {
}
}
In the component, we define the bindings to be received by the component; also, we declare two getter
methods albumCover
and albumArtiste
. The method names are pretty self-explanatory, but we’ll still walk through it. The albumCover
method retrieves the image from the currentAlbum
object, and the albumArtiste
method retrieves the album’s artist.
Like we did with the album-list
component, we’ll render the component in the app.component.html
.
Open the app.component.html
file and update it to be similar to the snippet below:
<main>
<app-header></app-header>
<section>
<kendo-splitter style="width: 100%; height: 100%; padding: 40px 0 ">
<kendo-splitter-pane size="60%" min="40%">
<app-album-list [albums]="albums" (albumSelected)="onAlbumChange($event)"></app-album-list>
</kendo-splitter-pane>
<kendo-splitter-pane min="30%">
<app-song-list [tracks]="tracks" [currentAlbum]="currentAlbum"></app-song-list>
</kendo-splitter-pane>
</kendo-splitter>
</section>
</main>
After rendering the list of tracks, if we visit the application on the browser on http://localhost:4200/, we should see both the albums listed and the tracks. If an album is clicked, it will load the tracks within the track list section. Check it out below:
In this tutorial, we’ve seen how easy it is to get started using Kendo UI components for Angular. We’ve seen how easy it is to define the structure of your application using the Splitter component from Kendo UI. The components were easy to use and configure. You can extend this demo application to play each track listed. The Spotify API provides a preview_url
for their tracks and an audio element can be used to play these tracks.
You can find the source code for this demo application here. Happy coding.
Want to learn more about creating great web apps? It all starts out with Kendo UI - the complete UI component library that allows you to quickly build high-quality, responsive apps. It includes everything you need, from grids and charts to dropdowns and gauges.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.