The Code
const API_KEY = "...."; //redacted
const getMovies = async () => {
try {
const response = await fetch('https://api.themoviedb.org/3/movie/popular', {
headers: {
"Authorization": `Bearer ${API_KEY}`
}
});
const data = await response.json();
return data;
} catch (error) {
displayError();
console.error(error);
}
}
const displayError = () => {
Swal.fire({
backdrop: false,
title: 'Oh No!',
text: `There was some trouble connecting with TMDB.`,
icon: 'error',
confirmButtonColor: '#3f4d5a'
})
}
const getMovie = async (movieID) => {
try {
const response = await fetch(`https://api.themoviedb.org/3/movie/${movieID}`, {
headers: {
"Authorization": `Bearer ${API_KEY}`
}
});
const data = await response.json();
return data;
} catch (error) {
displayError();
console.error(error);
}
}
const formatDate = date => {
const inputDate = date.toString();
const year = inputDate.slice(0,4);
const month = inputDate.slice(5,7);
const newDate = inputDate.slice(8);
const months = {
"01": "January",
"02": "February",
"03": "March",
"04": "April",
"05": "May",
"06": "June",
"07": "July",
"08": "August",
"09": "September",
"10": "October",
"11": "November",
"12": "December"
};
return `${months[month]} ${newDate}, ${year}`;
}
const formatCurrency = number => {
const numString = number.toString();
let startNum;
let endNum;
if (number > 1000000000) {
startNum = numString.slice(0, numString.length - 9);
endNum = "Billion";
} else if (number > 1000000) {
startNum = numString.slice(0, numString.length - 6);
endNum = "Million";
} else if (number > 1000) {
startNum = numString.slice(0, numString.length - 3)
endNum = "Thousand";
} else {
return "Figure unavailable";
}
return `$${startNum} ${endNum}`;
}
const formatRuntime = totalMins => {
const hours = Math.floor(totalMins/60);
const remainderMins = totalMins % 60;
let hourText = "h";
let minText = "m";
return `${hours}${hourText} ${remainderMins}${minText}`;
}
const displayMovies = async () => {
const data = await getMovies();
const movieListDiv = document.getElementById('movieList');
const moviePosterTemplate = document.getElementById('movieCardTemplate');
if (data?.results) {
const movies = data.results;
removeLoader(movies[0], "main");
for (let i = 0; i < movies.length; i++) {
const movie = movies[i];
const movieCard = moviePosterTemplate.content.cloneNode(true);
const movieImg = movieCard.querySelector('.card-img-top');
movieImg.src = `https://image.tmdb.org/t/p/w500${movie.poster_path}`;
const movieTitle = movieCard.querySelector('.card-body > h5');
movieTitle.innerText = movie.title;
const movieParagraphElement = movieCard.querySelector('.card-text');
movieParagraphElement.innerText = movie.overview;
const movieBtn = movieCard.querySelector('.btn-primary');
movieBtn.setAttribute("data-movieId", movie.id);
movieListDiv.appendChild(movieCard);
}
} else {
displayError();
}
}
const generateGenres = (genres) => {
let genresCode = '';
const genresMap = {
Action: "text-bg-warning",
Thriller: "text-bg-success",
Crime: "text-bg-danger",
Drama: "text-bg-warning",
Mystery: "text-bg-danger",
Horror: "text-bg-danger",
Adventure: "text-bg-info",
"science fiction": "text-bg-light",
Family: "text-bg-primary",
Romance: "text-bg-primary",
Fantasy: "text-bg-light",
Comedy: "text-bg-warning"
}
for (let i = 0; i < genres.length; i++) {
let className = "text-bg-dark";
if (genresMap[genres[i].name]) {
className = genresMap[genres[i].name];
}
const code = `${genres[i].name}`;
genresCode += code;
}
return genresCode;
}
const clearModal = () => {
const modalBody = document.querySelector('#movieModal .modal-content');
const html = `
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel"></h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onclick="clearModal()"
></button>
</div>
<div class="modal-body">
<div class="d-flex justify-content-center loader-div">
<div class="spinner-border text-dark mt-5" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="row">
<div class="col-12 col-lg-4 d-md-flex align-items-md-center">
<img class="poster-img" src="" />
</div>
<div class="col-12 col-lg-8">
<div>
<h2 class="movie-title mt-3"></h2>
<p class="modal-tagline m-0"></p>
<div class="genres py-3 border-bottom border-0 border-light border-opacity-25"></div>
<h3 class="synopsis-heading mt-3 mb-0"></h3>
<p class="synopsis-text py-3 border-bottom border-0 border-light border-opacity-25"></p>
<p class="runtime m-1"></p>
<p class="ratings m-1"></p>
<p class="release-date m-1"></p>
<p class="budget m-1"></p>
<p class="revenue m-1"></p>
</div>
</div>
</div>
</div>
<div class="modal-footer d-flex justify-content-evenly justify-content-md-end">
<a href="#" type="button" target="_blank" class="btn btn-warning imdb-btn">View IMDB</a>
<a href="#" type="button" target="_blank" class="btn btn-success homepage-btn">Movie Homepage</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>`;
modalBody.innerHTML = html;
}
const removeLoader = (details, parentElement) => {
let className;
if (parentElement === "main") {
className = "main-loader-div";
} else {
className = "loader-div";
}
if (details?.title) {
const loaderDiv = document.querySelector(`.${className}`);
loaderDiv.innerHTML = '';
}
}
const showMovieDetails = async (clickedButton) => {
clearModal();
const movieID = clickedButton.getAttribute('data-movieId');
const movieDetails = await getMovie(movieID);
removeLoader(movieDetails, "modal");
if (!movieDetails.title) {
displayError();
} else {
const modalBody = document.querySelector('#movieModal .modal-body');
const modalTitle = document.querySelector('#movieModal .modal-title');
const posterImg = modalBody.querySelector('.poster-img');
const modalTagline = modalBody.querySelector('.modal-tagline');
const releaseDate = modalBody.querySelector('.release-date');
const budget = modalBody.querySelector('.budget');
const revenue = modalBody.querySelector('.revenue');
const runtime = modalBody.querySelector('.runtime');
const ratings = modalBody.querySelector('.ratings');
const modalGenres = modalBody.querySelector('.genres');
const movieTitle = modalBody.querySelector('.movie-title');
const synopsisHeading = modalBody.querySelector('.synopsis-heading');
const synopsisText = modalBody.querySelector('.synopsis-text');
const modalFooter = document.querySelector('.modal-footer');
const imdbBtn = modalFooter.querySelector('.imdb-btn');
const homepageBtn = modalFooter.querySelector('.homepage-btn')
posterImg.src = `https://image.tmdb.org/t/p/w500${movieDetails.poster_path}`
modalTitle.innerText = movieDetails.title;
movieTitle.innerText = movieDetails.title;
modalBody.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.8),rgba(0, 0, 0, 0.8)), url(https://image.tmdb.org/t/p/w500${movieDetails.backdrop_path})`;
modalTagline.innerText = movieDetails.tagline? `"${movieDetails.tagline}"`: "";
const genres = movieDetails.genres;
const genresHTML = generateGenres(genres);
modalGenres.innerHTML = genresHTML;
synopsisHeading.innerText = "Synopsis";
synopsisText.innerText = movieDetails.overview;
runtime.innerHTML = `<span class = "important-info">Runtime:</span> ${formatRuntime(movieDetails.runtime)} <i class="bi bi-clock"></i>`;
releaseDate.innerHTML = `<span class = "important-info">Release Date:</span> ${formatDate(movieDetails.release_date)} <i class="bi bi-calendar-date"></i>`;
budget.innerHTML = `<span class = "important-info">Budget:</span> ${formatCurrency(movieDetails.budget)} <i class="bi bi-coin"></i>`;
revenue.innerHTML = `<span class = "important-info">Revenue:</span> ${formatCurrency(movieDetails.revenue)} <i class="bi bi-coin"></i>`;
ratings.innerHTML = `<span class = "important-info">Ratings:</span> ${movieDetails.vote_average.toFixed(1)} <i class="bi bi-star-fill"></i>`;
imdbBtn.setAttribute("href", `https://www.imdb.com/title/${movieDetails.imdb_id}/`);
homepageBtn.setAttribute("href", movieDetails.homepage);
}
}
Abstract
For this app, the I programmed for the following:
- On Page Load, make an api call to get a list of movies.
- If the response is received as desired, loop through the response array and display a movie card per movie.
- Add an event listener to each card's button that triggers anther fetch call for the clicked movie's details. Display those details in a modal.
- If the response fails on either pageload or clicking a card for details, show an error alert.
- Add a loading spinner on main page and on the modal that is displayed until we get a successful response.
- A minor but important goal, when any modal is opened, it should first be cleared so that we don't see the previously opened modal details until we get the fresh response.
Elephant in the room
Yes, I know. The api key
on line 1 is a major concern and not the best look.
It would've been better if I stored it as an environment variable, added the .env
file to a
gitignore
file to avoid pushing the env variables to github. But even then, it would've been
exposed as a header in the network request.
While out of scope for this exercise,
the ideal solution would've been a proxy API server. So instead of directly calling the
tmdb
api, the app could call the proxy server which would've had the api key and would've
relayed requests from clientside (this app) and relayed responses from the tmdb service. That way, the api
key would've never been lived in the frontend.
Covering all loose ends, the proxy api server should've had a CORS policy only allowing requests from the clientside so that it's not open for bad actors to access.
API calling functions
The purpose of working on this app was to get exposure with calling an api and using async JS functions.
An API call is essentially the clientside of an app using the browser to visit "a server" for getting data or posting data.
In this app, the functions that call an API are getMovies()
(line 3) and
getMovie(movieID)
(line 28). The getMovies()
function fetches a list of movies
with enough data that I could generate movie cards from the data. Whereas, getMovie()
function
requires a movie ID and gets the details for the particular movie. I used this call to generate the modal
when user clicks More Info
button.
Async Await
The await
keyword relies on the function being async
. What
async await
allow us to do is to wait until an asynchronous (not instant) expression is
resolved. So line 11 return data
does not execute until line 10
const data = await response.json()
is resolved.
Error Handling
The api functions use try/catch
codeblocks. In operations that require api calls and rely on
the response, there's significant room for an exception to be thrown in the try
block. If any
such exception is thrown in the try
block, the fail-safe catch
block is executed.
This is used to do effective error handling.
Additionally, even within the code flow of try
blocks, there's conditional checking of the
expected response for happy path. If the desired response is not received, an error will be displayed.
What happens on pageload
Upon pageload, the code triggers the async function displayMovies()
(line 93). This function is
a DOM manipulating function. Since our movies are not hardcoded in the HTML code, this function dynamically
creates the movie cards by going through an array of movie objects.
For the purpose stated above, I call getMovies()
and store the returned value to
data
variable.
I then check whether data
is a truthy value and whether it has a property named
results
(line 98). If it does, I am sure to have received a desirable response from the API
endpoint and I can continue the code-flow of removing the loader and looping through the data and displaying
movie cards on the DOM.
If the condition on line 98 is false, I display an error.
How I show movie details
The button More Info
on each movie card is made as a clone of a template
tag. I
added an event listener to this template tag which passes in the argument of event.target
. This
allows each button to have an event listener without repetitive code.
This event.target
allowed me to access the movie ID in the function
showMovieDetails
(line 211). Because this ID is required in getting the movie details, I call
the function getMovie
with this ID as an argument.
Once the details request gets a response, the loader is removed.
I finally check if the response has one of the desired properties I'm looking for. If the response is bad, I
display an error. If the response is good, I grab all the necessary DOM elements and plug the data in those.
I use many helper functions like formatDate, formatCurrency, formatRuntime, generateGenres
to
make the data user friendly.
Moreover, a user could open the modal for different movies multiple times and unsurprisingly, an api response would take some time. So I took the decision to first clear out the modal data everytime it's opened (line 212). This ensures that the user won't see the previously clicked movie for as long as the latest clicked movie's request is not successful. This bug would be especially evident if the internet was slow. With a cleared modal, a user would only see the spinning loader in an otherwise empty modal.