Website to search movies with React and TMDb API

THE MOVIE PALACE

demo

About

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).

🏁 Getting Started

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.

Prerequisites

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.

Installing locally

Let’s see the process as if you do not have any of the softwares required here:

Ubuntu Linux

  • Open your Terminal. Let’s start with installing our Code Editor VSCode
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
  • Installing Git on Ubuntu:
sudo apt install git-all
  • Now we can git clone this project locally in any folder of your computer, install dependencies and run on the web browser:
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


macOS

  • 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)"
  • Then install Yarn package manager in terminal. This will also install Node.js if it is not already installed. Reference: Yarn macOS
brew install yarn
  • Installing Git on macOS:
brew install git
  • Now we can git clone this project locally in any folder of your computer, install dependencies and run on the web browser:
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


Windows

  • 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.

  • Install the LTS release of the Node.js.
  • Got to Yarn official website, download for Windows button and install it.
  • Now with the opened Git Bash terminal (if you closed just go where you git cloned/extracted the project files, right-click and select BASH to open it) execute this:
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

📜 Documentation

API Environment

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';

🖇 Custom Hooks

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]);

Popular Movies Home Page

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} />
      )}

🔎 Search Movies

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

🌟 Ratings Star Filter

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>

    ...

)

Keep the SPA principle

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" />
          )}

Styled Components

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.

PropTypes

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.


⛏️ Built Using

  • VSCode - Code Editor written in TypeScript by Microsoft
  • Git - Git is a free and open source distributed version control system
  • React - Web Framework originally created by Jordan Walke at Facebook.
  • Hooks - React Hooks (state management).
  • PropTypes - Type Enforcement in Javascript like in Typescript (originally inside of React and now an external module).
  • Styled Components - Create CSS/SASS styling inside a React Component.
  • Javascript - The common Web Language.
  • TMDb - Instructions to get a The Movie Database Developer API and start development.
  • Krita - Powerful free painting software.

🚧 TODO

  • Simplify logic even more
  • Design a Figma UI that share single source of truth with Web and Mobile
  • Add more styling and animations
  • Look for bugs

Download Details:

Author: git-BR

Source Code: https://github.com/git-BR/the-movie-palace

#react #reactjs #javascript

Website to search movies with React and TMDb API
24.90 GEEK