Free Web Design Code & Scripts

Movie Comparison App in JavaScript

Movie Comparison App In Javascript
Code Snippet:Simple Movie Comparison App ( VanillaJS and Axios)
Author: Wesley van Drimmelen
Published: 5 months ago
Last Updated: 5 months ago
Downloads: 194
License: MIT
Edit Code online: View on CodePen
Read More

This JavaScript code snippet helps you to create a movie comparison app. This app is an excellent project for intermediate JavaScript developers looking to solidify their understanding of asynchronous JavaScript, API interactions, and DOM manipulation. This tutorial will walk you through setting up the HTML structure, styling the app with CSS, and implementing the core logic using JavaScript classes and API calls to fetch movie data, ultimately allowing users to compare movies based on various criteria.

Step 1: Include Header Assets

First, you’ll need to include the necessary CSS stylesheet in the <head> section of your HTML file to normalize the styles. This ensures consistency across different browsers.

<link rel="stylesheet" href="https://public.codepenassets.com/css/normalize-5.0.0.min.css">

Step 2: Create the HTML Structure

Here, we’ll define the basic HTML structure of the movie comparison app. This includes the header, two columns for displaying movie information, and placeholders for the autocomplete search boxes and movie details.

<main>
  <header class="header">
    <h1 class="header__title">Movie Comparison</h1>
  </header>
  
  <div class="columns">
    <!--   Left side movie   -->
    <section class="column">
      <div id="left-autocomplete"></div>
      <div id="left-details"></div>
    </section>
    
    <!--   Right side movie   -->
    <section class="column">
      <div id="right-autocomplete"></div>
      <div id="right-details"></div>
    </section>
  </div>

</main>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js'></script><script  src="./script.js"></script>

Step 3: Add CSS Styling

Now, let’s add the CSS styles to make the app visually appealing and user-friendly. These styles will define the layout, colors, and fonts of the various elements in the app.

* {
  box-sizing: border-box;
}

p {
  margin: 0;
}

body {
  margin: 0;
  padding: 0;
  color: #fff;
  background-color: #1D253D;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}

.columns {
  display: flex;
  max-width: 900px;
  margin: 0 auto;
}
.columns .column {
  flex: 0 0 50%;
  padding: 0 1rem;
}

.header {
  padding: 2rem 1rem;
  text-align: center;
}
.header__title {
  margin: 0;
  color: #6478B4;
  font-size: 20px;
  letter-spacing: 2px;
}

.autocomplete__label {
  display: block;
}
.autocomplete__input {
  display: block;
  width: 100%;
  margin-top: 0.5rem;
  padding: 1rem 1.25rem;
  border: 0;
  border-radius: 4px;
  color: #fff;
  background-color: #2d3a60;
  outline: 0;
  font-size: 18px;
  transition: all 0.15s ease-out;
}
.autocomplete__input:focus {
  box-shadow: 0 0 0 3px rgba(100, 120, 180, 0.75);
}
.autocomplete__input:hover {
  background-color: #364471;
}
.autocomplete .dropdown__menu__item img {
  width: 30px;
  margin-right: 1rem;
}

.dropdown {
  position: relative;
  vertical-align: top;
  display: inline-flex;
  width: 100%;
}
.dropdown--active .dropdown__menu {
  display: block;
}
.dropdown__menu {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  overflow-y: scroll;
  display: none;
  width: 100%;
  min-width: 12rem;
  max-height: 300px;
  padding: 0.5rem 0;
  border-radius: 4px;
  background-color: white;
}
.dropdown__menu__link {
  display: flex;
  align-items: center;
  padding: 0.5rem 1rem;
  color: #1D253D;
  cursor: pointer;
  transition: all 0.15s ease-out;
}
.dropdown__menu__link:hover, .dropdown__menu__link:focus {
  background-color: #eee;
}

.media {
  display: block;
  margin-bottom: 2rem;
}
.media__title {
  text-align: center;
}
.media__info {
  height: 175px;
  overflow-y: auto;
}
.media__figure {
  width: 100%;
  margin: 0;
  padding: 1rem;
  border-radius: 10px;
  background-color: #252f4e;
}
.media__figure img {
  display: block;
  margin: 0 auto;
  height: 200px;
  border-radius: 10px;
}

.compare {
  padding: 1.5rem 1rem;
  margin-bottom: 1rem;
  border-radius: 10px;
  color: #1D253D;
}
.compare--winner {
  background-color: #19CF7F;
}
.compare--loser {
  background-color: #F30C81;
}
.compare__title {
  margin-bottom: 0.5rem;
  font-weight: 700;
  font-size: 24px;
}
.compare--winner .compare__subtitle {
  color: #0e7447;
}
.compare--loser .compare__subtitle {
  color: #92074d;
}

Step 4: Implement JavaScript Functionality

This step is the core of the app. We’ll define the JavaScript classes and functions to handle the autocomplete search, fetch movie details from the API, and implement the movie comparison logic.

/**

  If the calls fail, it might be because the api limit has been reached.
  It's able to do 1.000 calls/day, search for 89c28a6e and replace it with your own (you can get one for free at http://omdbapi.com/)

*/

// Some default classes and variables used in the app
const DEFAULTS = {
  DROPDOWN_CLASS_ITEM: "dropdown__menu__item",
  DROPDOWN_CLASS_LINK: "dropdown__menu__link",
  DROPDOWN_CLASS_ACTIVE: "dropdown--active",
  COMPARE_CLASS_WINNER: "compare--winner",
  COMPARE_CLASS_LOSER: "compare--loser",
  API_BASE_URL: "https://www.omdbapi.com",
  API_KEY: "89c28a6e"
};

// Reusable Autocomplete Class
class Autocomplete {
  constructor(config) {
    ({
      root: this.root,
      optionTemplate: this.optionTemplate,
      onOptionSelect: this.onOptionSelect,
      inputValue: this.inputValue,
      fetchData: this.fetchData
    } = config);

    this.createRootTemplate(this.root);

    this.input = this.root.querySelector(".autocomplete__input");
    this.dropdown = this.root.querySelector(".autocomplete__dropdown");
    this.resultsWrapper = this.root.querySelector(".results");

    this.initListeners();
  }

  initListeners() {
    this.input.addEventListener("input", this.debounce(ev => this.onInput(ev), 500));

    document.addEventListener("click", ev => {
      if (!this.root.contains(ev.target)) {
        this.dropdown.classList.remove(DEFAULTS.DROPDOWN_CLASS_ACTIVE);
      }
    });
  }

  async onInput(ev) {
    const items = await this.fetchData(ev[0].target.value);

    if (!items.length) {
      this.dropdown.classList.remove(DEFAULTS.DROPDOWN_CLASS_ACTIVE);
      return;
    }

    this.resultsWrapper.innerHTML = "";
    this.dropdown.classList.add(DEFAULTS.DROPDOWN_CLASS_ACTIVE);

    for (const item of items) {
      const option = document.createElement("li");
      option.classList.add(DEFAULTS.DROPDOWN_CLASS_ITEM);

      const link = document.createElement("a");
      link.classList.add(DEFAULTS.DROPDOWN_CLASS_LINK);
      link.innerHTML = this.optionTemplate(item);

      option.appendChild(link);
      option.addEventListener("click", () => {
        this.dropdown.classList.remove(DEFAULTS.DROPDOWN_CLASS_ACTIVE);
        this.input.value = this.inputValue(item);
        this.onOptionSelect(item);
      });

      this.resultsWrapper.appendChild(option);
    }
  }

  debounce(callback, delay = 1000) {
    return (...args) => {
      if (this.timeoutId) clearTimeout(this.timeoutId);
  
      this.timeoutId = setTimeout(() => {
        callback.call(null, args);
      }, delay);
    };
  }

  createRootTemplate(el) {
    el.innerHTML = `
      <div class="autocomplete">
        <label class="autocomplete__label">
          Search
          <input class="autocomplete__input" />
        </label>

        <div class="autocomplete__dropdown dropdown">
          <ul class="dropdown__menu results">
          </ul>
        </div>
      </div>
    `;
  }
}

// Class to compare movies
class MovieComparison {
  constructor() {
    this.init();
  }

  movieAutocompleteConfig() {
    return {
      optionTemplate(movie) {
        const imgSrc = movie.Poster === "N/A" ? "" : movie.Poster;
        return `
        <img src="${imgSrc}" />
        ${movie.Title} (${movie.Year})
      `;
      },
      inputValue(movie) {
        return movie.Title;
      },
      async fetchData(searchTerm) {
        const response = await axios.get(DEFAULTS.API_BASE_URL, {
          params: {
            apikey: DEFAULTS.API_KEY, // This api can only be used 1000 times a day
            s: searchTerm
          }
        });

        return response.data.Error ? [] : response.data.Search;
      }
    };
  }

  init() {
    new Autocomplete({
      ...this.movieAutocompleteConfig(),
      root: document.querySelector("#left-autocomplete"),
      onOptionSelect: movie => {
        this.onMovieSelect(
          movie,
          document.querySelector("#left-details"),
          "left"
        );
      }
    });

    new Autocomplete({
      ...this.movieAutocompleteConfig(),
      root: document.querySelector("#right-autocomplete"),
      onOptionSelect: movie => {
        this.onMovieSelect(
          movie,
          document.querySelector("#right-details"),
          "right"
        );
      }
    });
  }

  async onMovieSelect(movie, summaryElement, side) {
    const response = await axios.get(DEFAULTS.API_BASE_URL, {
      params: {
        apikey: DEFAULTS.API_KEY,
        i: movie.imdbID
      }
    });

    summaryElement.innerHTML = this.movieTemplate(response.data);

    if (side === "left") {
      this.leftMovie = response.data;
    } else {
      this.rightMovie = response.data;
    }

    if (this.leftMovie && this.rightMovie) {
      this.runComparison();
    }
  }

  runComparison() {
    const leftSideStats = document.querySelectorAll("#left-details .compare");
    const rightSideStats = document.querySelectorAll("#right-details .compare");

    console.log(rightSideStats);

    for (const [i, leftStat] of leftSideStats.entries()) {
      const rightStat = rightSideStats[i];

      const leftSideValue = parseInt(leftStat.dataset.value);
      const rightStatValue = parseInt(rightStat.dataset.value);

      if (rightStatValue > leftSideValue) {
        leftStat.classList.remove(DEFAULTS.COMPARE_CLASS_WINNER);
        leftStat.classList.add(DEFAULTS.COMPARE_CLASS_LOSER);
      } else {
        rightStat.classList.remove(DEFAULTS.COMPARE_CLASS_WINNER);
        rightStat.classList.add(DEFAULTS.COMPARE_CLASS_LOSER);
      }
    }
  }

  movieTemplate(detail) {
    const boxOffice = parseInt(
      detail.BoxOffice.replace(/\$/g, "").replace(/,/g, "")
    );
    const metascore = parseInt(detail.Metascore);
    const imdbScore = parseFloat(detail.imdbRating);
    const imdbVotes = parseInt(detail.imdbVotes.replace(/,/g, ""));
    const awardsCount = detail.Awards.split(" ")
      .filter(Number)
      .reduce((acc, cur) => acc + parseInt(cur), 0);

    return `
      <article class="media">
        <h1 class="media__title">${detail.Title}</h1>
        <figure class="media__figure">
            <img src="${detail.Poster}" />
        </figure>
  
        <div class="media__info">
          <h4>${detail.Genre}</h4>
          <p>${detail.Plot}</p>
        </div>
      </article>
  
      <article class="compare ${DEFAULTS.COMPARE_CLASS_WINNER}" data-value="${awardsCount}">
        <p class="compare__title">${detail.Awards}</p>
        <p class="compare__subtitle">Awards</p>
      </article>
  
      <article class="compare ${DEFAULTS.COMPARE_CLASS_WINNER}" data-value="${boxOffice}">
        <p class="compare__title">${detail.BoxOffice}</p>
        <p class="compare__subtitle">Box Office</p>
      </article>
  
      <article class="compare ${DEFAULTS.COMPARE_CLASS_WINNER}" data-value="${metascore}">
        <p class="compare__title">${detail.Metascore}</p>
        <p class="compare__subtitle">Metascore</p>
      </article>
  
      <article class="compare ${DEFAULTS.COMPARE_CLASS_WINNER}"" data-value="${imdbScore}">
        <p class="compare__title">${detail.imdbRating}</p>
        <p class="compare__subtitle">IMDB Rating</p>
      </article>
  
      <article class="compare ${DEFAULTS.COMPARE_CLASS_WINNER}"" data-value="${imdbVotes}">
        <p class="compare__title">${detail.imdbVotes}</p>
        <p class="compare__subtitle">IMDB Votes</p>
      </article>
    `;
  }
}

const comparison = new MovieComparison();

Conclusion

Congratulations! You have successfully built a movie comparison app in JavaScript. This project showcases the power of JavaScript in creating interactive web applications by fetching data from external APIs and manipulating the DOM to present information effectively. You can further enhance this app by adding more features like user reviews, trailers, and different comparison metrics.

Loading... ...

Loading preview...

Device: Desktop
Dimensions: 1200x800
Lines: 0 Characters: 0 Ln 1, Ch 1

Leave a Comment

About W3Frontend

W3Frontend provides free, open-source web design code and scripts to help developers and designers build faster. Every snippet is reviewed before publishing for quality. Learn more.