React Movie Search Application

Software Engineering

Cisco ISE

Overview:

The Movie Search React Application is a user-friendly, responsive web application that allows users to search for movies by title or actor and view relevant details about the selected movies. This project demonstrates the power of React, a popular JavaScript library for building interactive user interfaces, combined with external API calls to fetch and display data dynamically. This document provides an overview of the project's objectives, requirements, and how they were met.

Objective:

The primary objective of this project is to create a visually appealing and easy-to-use React application for searching movies. The app should provide a seamless experience for users to search for movies, view the results, and access detailed information about each movie. To achieve this objective, the project incorporates the following key functionalities:

  1. Search movies by title or actor using the OMDb API.
  2. Display search results in an organized list format, including movie posters and essential details.
  3. Allow users to click on a movie item to view more information.
  4. Implement a sorting functionality to enable users to sort search results by relevance, year, or alphabetically.

Requirements and Implementation:

Create a search functionality to find movies using the OMDb API.
  • This requirement is met by implementing the 'searchMovies' function within the React app. The function fetches movie data from the OMDb API using the provided API key and API URL. The API call is made using JavaScript's Fetch API, which returns a Promise that resolves to the Response object representing the response to the request. The data is then converted to JSON format, and error handling is implemented if an error occurs during the API call. The search results are returned as an array of movie objects.

const API_KEY = '96d39d7c';
const API_URL = 'http://www.omdbapi.com/';

const searchMovies = async (query) => {
   const response = await fetch(`${API_URL}?s=${query}&apikey=${API_KEY}`);
   const data = await response.json();
   if (data.Error) {
       console.error(data.Error);
       return [];
   } else {
       return data.Search;
   }
};

Display search results in a list format with movie posters and details.
  • To display the search results in an organized and visually appealing manner, a 'MovieList' component is created. The component maps through the movies array and displays each movie item as a list element. Each movie item includes the poster, title, and release year. The movie posters use the 'img' tag with appropriate alt text, while the movie titles and years are displayed using 'h2' and 'p' elements, respectively. CSS styles are applied to ensure a clean and consistent layout for the movie list.

const MovieList = ({movies}) => {
   const [selectedMovie, setSelectedMovie] = useState(null);

   const handleClick = async (movie) => {
       const details = await searchMoviesById(movie.imdbID);
       setSelectedMovie(details);
   };

   const handleClose = () => {
       setSelectedMovie(null);
   };

   return (<>
       {selectedMovie && (<MovieDetailsPanel movie={selectedMovie} onClose={handleClose}/>)}
       <ul className="movie-list">
           {movies.map((movie) => (<li
               key={movie.imdbID}
               className="movie-item"
               onClick={() => handleClick(movie)}
           >
               <img
                   src={movie.Poster}
                   alt={`${movie.Title} poster`}
                   className="movie-poster"
               />
               <div className="movie-details">
                   <h2 className="movie-title">{movie.Title}</h2>
                   <p className="movie-year">{movie.Year}</p>
               </div>
           </li>))}
       </ul>
   </>);
};

Allow users to view more details about a movie by clicking on the movie item.
  • To enhance user experience, the project allows users to view more information about a movie by clicking on a movie item in the list. This functionality is implemented in the 'handleClick' function within the 'MovieList' component. When a user clicks on a movie item, the 'searchMoviesById' function is called with the movie's IMDb ID as a parameter. This function fetches detailed information about the selected movie using another API call to the OMDb API.

const searchMoviesById = async (id) => {
   const response = await fetch(`${API_URL}?i=${id}&apikey=${API_KEY}`);
   const movie = await response.json();
   if (movie.Error) {
       console.error(movie.Error);
       return null;
   } else {
       return movie;
   }
};

  • The fetched data is then used to create a 'MovieDetailsPanel' component that displays the movie's title, year, genre, director, actors, and plot. To allow users to close the details panel, a 'close' button is included, which triggers the 'handleClose' function that sets the 'selectedMovie' state back to 'null,' effectively hiding the details panel.

const MovieDetailsPanel = ({movie, onClose}) => {
   return (<div className="movie-details-panel">
       <h2>{movie.Title}</h2>
       <p>Year: {movie.Year}</p>
       <p>Genre: {movie.Genre}</p>
       <p>Director: {movie.Director}</p>
       <p>Actors: {movie.Actors}</p>
       <p>Plot: {movie.Plot}</p>
       <button className="close-button" onClick={onClose}>
           &times;
       </button>
   </div>);
};

Implement a sort functionality to allow users to sort search results by relevance, year, or alphabetically.
  • To provide users with more control over the search results display, a sorting functionality is implemented using the 'SortDropdown' component. The component renders a dropdown menu with three sorting options: relevance, year, and alphabetical. When a user selects a sorting option, the 'handleSortChange' function is triggered, which calls the 'handleSort' function with the selected sort option as a parameter. The 'handleSort' function then sorts the 'movies' array based on the selected option using JavaScript's built-in array sort method.

const SortDropdown = ({onSort}) => {
   const handleSortChange = (e) => {
       onSort(e.target.value);
   };

   return (<div className="sort-dropdown">
       <select onChange={handleSortChange}>
           <option value="relevance">Relevance</option>
           <option value="year">Year</option>
           <option value="alphabetical">Alphabetical</option>
       </select>
   </div>);
};

  • For sorting by year, the movies are sorted in ascending order based on their release year. For alphabetical sorting, the movies are sorted by their titles. For relevance, the original search results are displayed by setting the 'movies' state back to the 'originalMovies' state.

const MovieSearch = () => {
   const [movies, setMovies] = useState([]);
   const [originalMovies, setOriginalMovies] = useState([]);

   const handleSearch = async (query) => {
       const results = await searchMovies(query);
       setMovies(results);
       setOriginalMovies(results);
       localStorage.setItem('movies', JSON.stringify(results));
   };

   const handleSort = (sortOption) => {
       switch (sortOption) {
           case 'year':
               setMovies([...movies].sort((a, b) => a.Year - b.Year));
               break;
           case 'alphabetical':
               setMovies([...movies].sort((a, b) => a.Title.localeCompare(b.Title)));
               break;
           case 'relevance':
               setMovies([...originalMovies]);
               break;
           default:
               break;
       }
   };

   useEffect(() => {
       const storedMovies = JSON.parse(localStorage.getItem('movies'));
       if (storedMovies) {
           setMovies(storedMovies);
           setOriginalMovies(storedMovies);
       }
   }, []);



MovieSearch.js Code:

import React, {useState, useEffect} from 'react';
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
import './App.css';

// API access constants
const API_KEY = '****';
const API_URL = 'http://www.omdbapi.com/';

// Search movies using OMDb API
const searchMovies = async (query) => {
   const response = await fetch(`${API_URL}?s=${query}&apikey=${API_KEY}`);
   const data = await response.json();
   if (data.Error) {
       console.error(data.Error);
       return [];
   } else {
       return data.Search;
   }
};

// SearchFrom with input and submit button to search
const SearchForm = ({onSearch}) => {
   const [query, setQuery] = useState('');

   const handleSubmit = (event) => {
       event.preventDefault();
       onSearch(query);
   };

   return (<form onSubmit={handleSubmit} className="search-form">
       <input
           type="text"
           placeholder="Search for a movie or actor"
           value={query}
           onChange={(e) => setQuery(e.target.value)}
       />
       <button type="submit">Search</button>
   </form>);
};

// Search movies by ID using OMDb API
const searchMoviesById = async (id) => {
   const response = await fetch(`${API_URL}?i=${id}&apikey=${API_KEY}`);
   const movie = await response.json();
   if (movie.Error) {
       console.error(movie.Error);
       return null;
   } else {
       return movie;
   }
};

// show detailed information about movie
const MovieDetailsPanel = ({movie, onClose}) => {
   return (<div className="movie-details-panel">
       <h2>{movie.Title}</h2>
       <p>Year: {movie.Year}</p>
       <p>Genre: {movie.Genre}</p>
       <p>Director: {movie.Director}</p>
       <p>Actors: {movie.Actors}</p>
       <p>Plot: {movie.Plot}</p>
       <button className="close-button" onClick={onClose}>
           &times;
       </button>
   </div>);
};

// Display list of movies with clickable items
const MovieList = ({movies}) => {
   const [selectedMovie, setSelectedMovie] = useState(null);

   const handleClick = async (movie) => {
       const details = await searchMoviesById(movie.imdbID);
       setSelectedMovie(details);
   };

   const handleClose = () => {
       setSelectedMovie(null);
   };

   return (<>
       {selectedMovie && (<MovieDetailsPanel movie={selectedMovie} onClose={handleClose}/>)}
       <ul className="movie-list">
           {movies.map((movie) => (<li
               key={movie.imdbID}
               className="movie-item"
               onClick={() => handleClick(movie)}
           >
               <img
                   src={movie.Poster}
                   alt={`${movie.Title} poster`}
                   className="movie-poster"
               />
               <div className="movie-details">
                   <h2 className="movie-title">{movie.Title}</h2>
                   <p className="movie-year">{movie.Year}</p>
               </div>
           </li>))}
       </ul>
   </>);
};

// Sort movies based on different criteria
const SortDropdown = ({onSort}) => {
   const handleSortChange = (e) => {
       onSort(e.target.value);
   };

   return (<div className="sort-dropdown">
       <select onChange={handleSortChange}>
           <option value="relevance">Relevance</option>
           <option value="year">Year</option>
           <option value="alphabetical">Alphabetical</option>
       </select>
   </div>);
};

// Manage and display movie search
const MovieSearch = () => {
   const [movies, setMovies] = useState([]);
   const [originalMovies, setOriginalMovies] = useState([]);

   // movie search and update state
   const handleSearch = async (query) => {
       const results = await searchMovies(query);
       setMovies(results);
       setOriginalMovies(results);
       localStorage.setItem('movies', JSON.stringify(results));
   };

   // sort movies
   const handleSort = (sortOption) => {
       switch (sortOption) {
           case 'year':
               setMovies([...movies].sort((a, b) => a.Year - b.Year));
               break;
           case 'alphabetical':
               setMovies([...movies].sort((a, b) => a.Title.localeCompare(b.Title)));
               break;
           case 'relevance':
               setMovies([...originalMovies]);
               break;
           default:
               break;
       }
   };

   // hook to load movies from storage
   useEffect(() => {
       const storedMovies = JSON.parse(localStorage.getItem('movies'));
       if (storedMovies) {
           setMovies(storedMovies);
           setOriginalMovies(storedMovies);
       }
   }, []);

   return (<Router>
       <div className="App">

          // CSS Styles
           <style>
               {`
               body {
                   background-color: rgb(18, 20, 29);
                   color: white;
               }
               .title {
                   font-size: 2rem;
                   font-weight: bold;
                   margin-bottom: 1rem;
                   text-align: center;
                   text-transform: uppercase;
               }        
               .movie-list {
                   display: flex;
                   flex-wrap: wrap;
                   gap: 1rem;
                   list-style-type: none;
                   margin: 0;
                   padding: 0;
                   justify-content: center;
               }
               .movie-item {
                   background-color: rgb(0, 0, 0);
                   border: 1px solid #e0e0e0;
                   border-radius: 4px;
                   display: flex;
                   flex-direction: column;
                   overflow: hidden;
                   padding: 1rem;
                   width: 300px;
               }
               .movie-poster {
                   align-self: center;
                   max-height: 350px;
                   max-width: 100%;
                   object-fit: contain;
                   transition: transform 0.3s ease, box-shadow 0.3s ease;
               }
               .movie-poster:hover {
                   transform: scale(1.1);
                   box-shadow: 0 0 30px rgba(104, 213, 255, 0.8);
               }
               .movie-details {
                   display: flex;
                   flex-direction: column;
                   align-items: center;
               }
               .movie-title {
                   font-size: 18px;
                   font-weight: bold;
                   margin: 1rem 0;
                   text-align: center;
                   color: rgb(59, 207, 145)
               }
               .movie-year {
                   font-size: 14px;
                   font-weight: normal;
                   margin: 0;
                   text-align: center;
                   font-weight: bold;
                   color: rgb(255, 110, 110);
               }
               .movie-details-panel {
                   background-color: rgba(255, 255, 255, 0.8);
                   border: 1px solid #e0e0e0;
                   border-radius: 4px;
                   max-width: 500px;
                   padding: 1rem;
                   position: fixed;
                   top: 50%;
                   left: 50%;
                   transform: translate(-50%, -50%);
                   z-index: 10;
                   backdrop-filter: blur(20px);
                   color: black;
               }
               .close-button {
                   background-color: transparent;
                   border: none;
                   font-size: 24px;
                   position: absolute;
                   right: 5px;
                   top: 5px;
               }
               .close-button:focus {
                   outline: none;
               }
               .search-form {
                   display: flex;
                   justify-content: center;
                   margin-bottom: 1rem;
               }
               .search-form input {
                   border: 1px solid #ccc;
                   border-radius: 4px;
                   font-size: 1rem;
                   padding: 0.5rem;
                   width: 300px;
               }
               .search-form input:focus {
                   outline: none;
                   border-color: rgb(104, 213, 255);
                   box-shadow: 0 0 0 0.2rem rgba(104, 213, 255, 0.25);
               }
               .search-form button {
                   background-color: #333;
                   border: none;
                   border-radius: 4px;
                   color: #fff;
                   font-size: 1rem;
                   margin-left: 0.5rem;
                   padding: 0.5rem 1rem;
                   text-transform: uppercase;
               }
               .search-form button:hover {
                   background-color: #666;
                   cursor: pointer;
               }
               .sort-dropdown {
                   display: flex;
                   justify-content: center;
                   margin-bottom: 1rem;
               }
               .sort-dropdown select {
                   border: 1px solid #ccc;
                   border-radius: 4px;
                   font-size: 1rem;
                   padding: 0.5rem;
               }    
               h1 {
                   font-size: 3rem;
                   font-weight: bold;
                   margin-bottom: 1rem;
                   text-align: center;
                   text-transform: uppercase;
                   color: white;
               }
         `}
           </style>
           <h1>Movie Search</h1>
           <Routes>
               <Route
                   path="/"
                   element={<div>
                       <SearchForm onSearch={handleSearch}/>
                       <SortDropdown onSort={handleSort}/>
                       <MovieList movies={movies}/>
                   </div>}
               />
           </Routes>
       </div>
   </Router>);
};

export default MovieSearch;

View this project on GitHub

Contact Me

Lets Work Together

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Stay in touch

Ready to Talk

Feel free to contact me