This project aims to create a similar experience of the The Movie Database (TMDb) Website using its own API and React.js focused in web responsiveness and user experience (UX).
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
The documentation of the project can be found under Documentation section.
The documentation of TMDb can be found here: TMDb documentation.
The documentation of React and Hooks for reference: React and Hooks.
The first thing to do before starting development is to choose our Code Editor. I prefer using Microsoft Visual Studio Code (VSCode).
“Git_ is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency._” The official description says all.
This project use at least React 16.8, because React introduced Hooks on this release.
I prefer to work with Typescript, but the vast majority of the Web uses Javascript. So, for compatibility, I will work with Javascript and add PropTypes on this project as it adds the Type Enforcement functionality to Javascript.
For increased flexibility, efficiency and organized development I prefer styling React Components with styled-components instead of plain CSS/SASS.
As my package manager I prefer using Yarn.
Let’s see the process as if you do not have any of the softwares required here:
sudo apt install software-properties-common apt-transport-https wget
wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main"
sudo apt update
sudo apt install code
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install yarn
sudo apt install git-all
git clone https://github.com/git-BR/the-movie-palace.git
yarn install
yarn start
With that we already have our React application up and running on the browser in this address: localhost:3000
Open your Terminal. Let’s start with installing our Code Editor here: access VSCode website, download and install.
Install homebrew if you don’t already have it:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
brew install yarn
brew install git
git clone https://github.com/git-BR/the-movie-palace.git
yarn install
yarn start
With that we already have our React application up and running on the browser in this address: localhost:3000
Visit VSCode official website, download the Windows Setup file and install it.
Install Git for Windows. The official Git has linked this project that ported the original Bash Terminal experience to Windows. Right now the version is 2.28.0, donwload and install it. For more information go here: Git for Windows
Now you can git clone in the ported version of Git for Windows with a right-click on a folder in Windows Explorer to access the Bash then with the terminal open execute this:
git clone https://github.com/git-BR/the-movie-palace.git
cd the-movie-palace
this will clone the project folder/files, named the-movie-palace, in the selected place.
yarn install
yarn start
With that we already have our React application up and running on the browser in this address: localhost:3000
Just for reference
This project was bootstrapped with create-react-app. It starts with all the common stuff needed in a React project so we don’t need to add manually one by one. It’s pretty straightforward:
yarn add create-react-app
yarn create react-app my-project-name
cd my-project-name
yarn start
You can open your project directly from terminal to VSCode like this:
cd my-project-name-folder
code .
Now we will install PropTypes in our project:
yarn add prop-types
And last we install Styled Components:
yarn add styled-components
For getting data from TMDb API efficiently we need to store the URLs in variables/constants. This will avoid repeated URLs typing. You can name your constants the way you want, but try to keep it meaningful:
// THE BASE API URL
const API_URL = 'https://api.themoviedb.org/3/';
// GET YOUR KEY FROM TMDB DEVELOPER API
const API_KEY = 'put your KEY from TMDb API';
// THE URL WHICH TRIGGERS SEARCH
const SEARCH_BASE_URL = `${API_URL}search/movie?api_key=${API_KEY}&query=`;
// THE URL WHICH TRIGGERS POPULAR MOVIES
const POPULAR_BASE_URL = `${API_URL}movie/popular?api_key=${API_KEY}`;
// UNFORTUNATELY TMDB LIMITS 20 RESULTS PER PAGE, SO RATINGS FILTER WILL BE
// LIMITED TO THAT JUST FOR DEMO.
const FILTER_BASE_URL = `${API_URL}movie/popular?api_key=${API_KEY}&page=2`;
// THE BASE URL WHICH TRIGGERS IMAGES
const IMAGE_BASE_URL = 'http://image.tmdb.org/t/p/';
// THE SIZE OF ALTERNATIVE BACKDROP IMAGES
const BACKDROP_SIZE = 'w1280';
// THE SIZE OF MOVIES POSTER IMAGES
const POSTER_SIZE = 'w500';
Here we will create two Custom Hooks to avoid too much Hooks code in the Home Component:
useHomeFetch
// TRY TO GET DATA ENDPOINT FROM TMDB API WHILE WAIT FOR JSON PARSE
try {
const result = await (await fetch(endpoint)).json();
setState(prev => ({
...prev,
movies:
isLoadMore !== -1
// SPREAD ANY PREVIOUS PROPS FIRST
? [...prev.movies, ...result.results]
: [...result.results],
// IF ALREADY HAVE ANY heroImage USE THAT OR ELSE GET THE FIRST NEW ONE FROM API
heroImage: prev.heroImage || result.results[0],
currentPage: result.page,
totalPages: result.total_pages,
}));
// CATCH ANY ERRORS IF ANY
} catch (error) {
setError(true);
console.log(error);
}
// FETCH POPULAR MOVIES FOR HOME PAGE
useEffect(() => {
// STORE SESSION STATE IF ALREADY EXISTS OR ELSE GET FROM API
if (sessionStorage.homeState) {
setState(JSON.parse(sessionStorage.homeState));
setLoading(false);
} else {
fetchMovies(POPULAR_BASE_URL);
}
}, []);
useMovieFetch uses almost the same logic, but with localStorage
// CALLED WHEN movieId TRIGGERS
useEffect(() => {
// STORE LOCAL STATE IF ALREADY EXISTS OR ELSE GET FROM API
if (localStorage[movieId]) {
setState(JSON.parse(localStorage[movieId]));
setLoading(false);
} else {
fetchData();
}
}, [fetchData, movieId]);
We will create a const named state which gives us the Popular Movies in a descending order. We will use this many times, so let’s destructuring it beforehand:
// DESTRUCTURING state TO AVOID TOO MANY REPETITIONS
const [
{
state: { movies, currentPage, totalPages, heroImage },
loading,
error,
},
fetchMovies,
] = useHomeFetch(searchTerm);
Now we loop through an array from TMDb API and show that in the MovieWall Component:
<MovieWall header={searchTerm ? 'Search Result' : 'Popular Movies'}>
{/* TO LOOP/MAP AN ARRAY FROM TMDB AND SHOW DATA IN THE MOVIEWALL COMPONENT */}
{movies.map(movie => (
<>
<MovieCard
key={movie.id}
clickable
image={
movie.poster_path
? IMAGE_BASE_URL + POSTER_SIZE + movie.poster_path
: NoImage
}
movieId={movie.id}
movieName={movie.original_title}
/>
</>
))}
To create a Single Page Application (SPA) experience in our app we should stay in the same page, so let’s create a “infinity” load button which calls the loadMoreMovies function to do that:
{loading && <Spinner />}
{/* CHECK IF REACHED THE END */}
{currentPage < totalPages && !loading && (
// INFINITY LOAD THAT TURNS THE EXPERIENCE IN A SPA
<LoadMoreBtn text="Load more..." callback={loadMoreMovies} />
)}
When the user types in the Search Bar it will return movies based on what is typed and will wait a delay of one second without the necessity to press Enter. This gives a better User Experience (UX) and is simple to do:
const doSearch = event => {
// TRACKS THE USER INPUTS event.target
const { value } = event.target;
// SET A DELAY (1s) TO USER SEARCH QUERY AND AVOID ABRUPT RENDERING
clearTimeout(timeOut.current);
// STORE INPUTS IN value AND RETURN DATA WITH callback TO Home COMPONENT
setState(value);
timeOut.current = setTimeout(() => {
callback(value);
}, 1000);
}
This will return the movies inside the callback(value) after 1s
In RatingsFilter Component we will restrict movies based on vote_average of the TMDb API. So movies from 0.0 - 2.0 will show in the first star, movies with 2.0 - 4.0 vote_average will show in two stars and so on. To create that functionality we will use a Radio Input HTML Element which will mimic a star rating system and useState Hook to store the filtered ratings then restrict the results based on the radio id:
// VERIFY ANY VALUE CHANGES IN target.id AND UPDATE ratings ACCORDINGLY
const handleInputChange = (e) => {
setSelected(e.target.id);
};
...
// FILTER ratings STATE AND RETURN THE FILTERED VALUES IN MOVIEWALL COMPONENT
const filteredOneStar =
<MovieWall
header={'Filtered Movies'}
children >
{ratings
.filter(movie => movie.vote_average <= 2 && movie.vote_average >= 0)
.map(movie => (
<>
<MovieCard
clickable
image={
movie.poster_path
? IMAGE_BASE_URL + POSTER_SIZE + movie.poster_path
: null
}
movieId={movie.id}
movieName={movie.original_title}
/>
</>
))}
</MovieWall>
...
return (
...
<input
onClick={handleInputChange}
type="radio"
id="rating-1"
name="rating"
/>
<label htmlFor="rating-1">1</label>
...
)
In order to keep in the SPA principle we need to avoid routes like react-router-dom and focus on staying on the same page yet show another one on top. To do that we will create a Modal Component that will show the MovieDetails Component inside it and triggered by onClick HTML Input:
<Modal
isVisible={isModal}
onClose={() => setModal(false)} >
<MovieDetails movie={movie} />
</Modal>
{/* clickable MAKES THE img CALLS THE Modal JUST ONE TIME */}
{clickable ? (
<>
<img className="clickable" src={image} alt="moviecard" onClick={() => setModal(true)} />
<div className="card-rating">
<div>{movie.vote_average}</div>
</div>
</>
) : (
<img src={image} alt="moviecard" />
)}
With Styled Components the development turns easier after you created several styled components. Let’s see one exemple:
<StyledRatingsFilter>
...
<StyledMovieWall>
<StyledMovieCard>
{
...
}
</StyledMovieCard>
</StyledMovieWall>
</StyledRatingsFilter>
With this example we nested external styled components several times. StyledRatingFilter is styling the StyledMovieWall which is styling the StyledMovieCard and so on.
With PropTypes we can enforce the same behavior by forcing types for every props in React. It’s almost the same as Static Types in TypeScript:
MovieCard.propTypes = {
image: PropTypes.string,
movieId: PropTypes.number,
clickable: PropTypes.bool,
}
Here we are saying that image will always be a string, movieId will always be a number and clickable will always be a boolean. With that feature Javascript does not need to check every other type, just one, thus increasing performance.
Author: git-BR
Source Code: https://github.com/git-BR/the-movie-palace
#react #reactjs #javascript