Introduction
In this blog, we will walk through the process of building a Movie Finder website using React and the OMDB API. The website allows users to browse movies by categories like Avengers, Star Wars, and Series, and search for movies using specific queries. Each movie has its detail page, making it easy to explore more about your favorite films.
Project Overview
Movie Finder Website enables users to:
- Browse categories like Avengers and Star Wars.
- Search for movies by keywords.
- View detailed movie information (poster, genre, director, actors, and more).
- Navigate easily through the website with a clean, modern design.
Features
- Fetch data dynamically using the OMDB API.
- Responsive design for a better user experience.
- Search functionality that provides instant results.
- Loading indicators while data is being fetched.
- View details of individual movies on a separate page.
Technologies Used
- React: Frontend library for building UI components.
- React Router: For navigation and routing.
- Axios: For making HTTP requests to the OMDB API.
- OMDB API: To fetch movie details.
- CSS: For styling the application.
Project Structure
Here is the directory structure for the project:
movie-finder/
├── public/
├── src/
│ ├── components/
│ │ └── Navbar.js
│ │ └── Footer.js
│ ├── pages/
│ │ └── Home.js
│ │ └── Movies.js
│ │ └── Series.js
│ │ └── SearchResults.js
│ │ └── MovieDetail.js
│ └── App.js
│ └── App.css
└── package.json
Installation
-
Clone the repository:
git clone https://github.com/abhishekgurjar-in/movie-finder.git cd movie-finder
-
Install dependencies:
npm install
Get your API key from OMDB API.
-
Create a
.env
file in the project root and add your API key:
REACT_APP_OMDB_API_KEY=yourapikey
-
Run the project:
npm start
Usage
1. Home Page
The Home Page showcases two categories of movies: Avengers and Star Wars. When the user clicks on a movie card, they are redirected to the detailed movie page.
Code Snippet from Home.js
:
import React, { useEffect, useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import Movies from "./Movies";
import Series from "./Series";
const Home = () => {
const [avengersMovies, setAvengersMovies] = useState([]);
const [starWarsMovies, setStarWarsMovies] = useState([]);
const [loadingAvengers, setLoadingAvengers] = useState(true);
const [loadingStarWars, setLoadingStarWars] = useState(true);
const navigate = useNavigate();
useEffect(() => {
fetchMovies("Avengers", setAvengersMovies, setLoadingAvengers);
fetchMovies("Star Wars", setStarWarsMovies, setLoadingStarWars);
}, []);
const fetchMovies = (query, setMovies, setLoading) => {
axios
.get(`http://www.omdbapi.com/?s=${query}&apikey=you_api_key`)
.then((response) => {
if (response.data.Search) {
setMovies(response.data.Search);
} else {
setMovies([]); // Clear movies if no results
}
})
.catch((error) => {
console.error(`There was an error fetching the ${query} movies!`, error);
setMovies([]); // Clear movies if there is an error
})
.finally(() => {
setLoading(false);
});
};
const handleCardClick = (id) => {
navigate(`/movie/${id}`);
};
const renderMovies = (movies, loading) => (
loading ? (
<div className="loader"><div className="load"></div></div>
) : (
<div className="cards">
{movies.length > 0 ? (
movies.map((movie) => (
<div key={movie.imdbID} className="card" onClick={() => handleCardClick(movie.imdbID)}>
<img src={movie.Poster} alt={movie.Title} />
<h2>{movie.Title}</h2>
</div>
))
) : (
<p>No movies found.</p>
)}
</div>
)
);
return (
<>
<div className="home">
<div className="movie-category">
<h4>Avengers Movies</h4>
{renderMovies(avengersMovies, loadingAvengers)}
</div>
<br />
<br />
<div className="movie-category">
<h4>Star Wars Movies</h4>
{renderMovies(starWarsMovies, loadingStarWars)}
</div>
</div>
<Movies />
<Series />
</>
);
};
export default Home;
2. Search Functionality
The user can search for any movie using the search bar at the top of the website. Search results are fetched from the OMDB API based on the user’s query.
Code Snippet from SearchResults.js
:
import React, { useEffect, useState } from "react";
import axios from "axios";
import { useParams, useNavigate } from "react-router-dom";
const SearchResults = () => {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(false);
const { query } = useParams();
const navigate = useNavigate(); // Add this line to use navigate
useEffect(() => {
if (query) {
setLoading(true); // Set loading to true before starting the fetch
axios
.get(`http://www.omdbapi.com/?s=${query}&apikey=your_api_key`)
.then((response) => {
if (response.data.Search) {
setMovies(response.data.Search);
} else {
setMovies([]); // Clear movies if no results
}
})
.catch((error) => {
console.error("There was an error fetching the movie data!", error);
})
.finally(() => {
setLoading(false); // Set loading to false once fetch is complete
});
}
}, [query]);
const handleCardClick = (id) => {
navigate(`/movie/${id}`); // Navigate to movie detail page
};
return (
<div className="search-results">
<h4>Search Results for "{query}"</h4>
{loading ? (
<div className="loader"><div className="load"></div></div> // Loader
) : (
<div className="cards">
{movies.length > 0 ? (
movies.map((movie) => (
<div key={movie.imdbID} className="card" onClick={() => handleCardClick(movie.imdbID)}>
<img src={movie.Poster} alt={movie.Title} />
<h2>{movie.Title}</h2>
</div>
))
) : (
<p>No results found.</p>
)}
</div>
)}
</div>
);
};
export default SearchResults;
3. Movie Detail Page
When a user clicks on a movie, they are redirected to the movie detail page. This page shows the movie’s title, poster, plot, actors, and more.
Code Snippet from MovieDetail.js
:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useParams } from 'react-router-dom';
const MovieDetail = () => {
const [movie, setMovie] = useState(null);
const [loading, setLoading] = useState(true);
const { id } = useParams();
useEffect(() => {
axios.get(`http://www.omdbapi.com/?i=${id}&apikey=your_api_key`)
.then((response) => {
setMovie(response.data);
})
.catch((error) => {
console.error("There was an error fetching the movie details!", error);
})
.finally(() => {
setLoading(false);
});
}, [id]);
if (loading) return <div className="loader">
<div className="load"></div>
</div>;
if (!movie) return <div className='loader'>No movie data found!</div>;
return (
<div className="movie-detail">
<div className="detail-box">
<h1>{movie.Title}</h1>
<p><strong>Year:</strong> {movie.Year}</p>
<p><strong>Rating:</strong> {movie.imdbRating}</p>
<p><strong>Genre:</strong> {movie.Genre}</p>
<p><strong>Director:</strong> {movie.Director}</p>
<p><strong>Actors:</strong> {movie.Actors}</p>
<p><strong>Plot:</strong> {movie.Plot}</p>
<p><strong>Runtime:</strong> {movie.Runtime}</p>
<p><strong>Language:</strong> {movie.Language}</p>
<p><strong>Country:</strong> {movie.Country}</p>
<p><strong>Awards:</strong> {movie.Awards}</p>
</div>
<div className="img-box">
<img src={movie.Poster} alt={movie.Title} />
</div>
</div>
);
};
export default MovieDetail;
4. Movies and Series Pages
Movies.js and Series.js pages list movies and TV series, respectively.
Code Snippet from Movies.js
:
import React, { useEffect, useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
const Movies = () => {
const [movies, setMovies] = useState([]);
const navigate = useNavigate();
useEffect(() => {
axios
.get(`http://www.omdbapi.com/?s=Avengers&type=movie&apikey=${process.env.REACT_APP_OMDB_API_KEY}`)
.then(response => setMovies(response.data.Search || []))
.catch(error => console.error(error));
}, []);
const handleCardClick = (id) => {
navigate(`/detail/${id}`);
};
return (
<div className="movies">
<h2>Movies</h2>
<div className="cards">
{movies.map(movie => (
<div key={movie.imdbID} className="card" onClick={() => handleCardClick(movie.imdbID)}>
<img src={movie.Poster} alt={movie.Title} />
<h3>{movie.Title}</h3>
</div>
))}
</div>
</div>
);
};
export default Movies;
Code Snippet from Series.js
:
import React, { useEffect, useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
const Series = () => {
const [series, setSeries] = useState([]);
const navigate = useNavigate();
useEffect(() => {
axios
.get(`http://www.omdbapi.com/?s=Star Wars&type=series&apikey=${process.env.REACT_APP_OMDB_API_KEY}`)
.then(response => setSeries(response.data.Search || []))
.catch(error => console.error(error));
}, []);
const handleCardClick = (id) => {
navigate(`/detail/${id}`);
};
return (
<div className="series">
<h2>TV Series</h2>
<div className="cards">
{series.map(show => (
<div key={show.imdbID} className="card" onClick={() => handleCardClick(show.imdbID)}>
<img src={show.Poster} alt={show.Title} />
<h3>{show.Title}</h3>
</div>
))}
</div>
</div>
);
};
export default Series;
5. Navbar Component
The Navbar
component allows users to navigate between different pages and perform a search.
Updated Navbar.js
import React, { useState } from "react";
import { NavLink, Link } from "react-router-dom";
const Navbar = () => {
const [searchQuery, setSearchQuery] = useState("");
const handleSearch = (event) => {
if (event.key === 'Enter' && searchQuery.trim()) {
document.getElementById('search-link').click();
}
};
return (
<div className="navbar">
<div className="logo">
<h1>Movie Finder</h1>
</div>
<div className="page-list">
<NavLink to="/">
<h4>Home</h4>
</NavLink>
<NavLink to="/movies">
<h4>Movies</h4>
</NavLink>
<NavLink to="/series">
<h4>TV Series</h4>
</NavLink>
</div>
<div className="search-box">
<input
type="text"
placeholder="Search for movies or series"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearch}
/>
<Link to={`/search/${searchQuery}`} id="search-link">
<button>Search</button>
</Link>
</div>
</div>
);
};
export default Navbar;
6. Footer Component
The Footer
component provides a simple footer message.
Footer.js
import React from 'react';
const Footer = () => {
return (
<div className='footer'>
Made with <span>❤️</span> by Abhishek Gurjar
</div>
);
};
export default Footer;
*{
box-sizing: border-box;
}
body{
font-family: sans-serif;
margin: 0;
padding: 0;
}
.navbar {
padding-inline: 100px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: red;
}
.search-btn{
background-color: red;
}
.logo h1{
font-size: 25px;
color:black;
}
.page-list {
display: flex;
align-items: center;
gap: 40px;
}
.page-list a{
color: white;
text-decoration: none;
font-size: 20px;
}
.page-list a:hover{
color: black;
}
.page-list a.active{
color: black;
}
.search-box{
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
background-color:white;
color: gray;
width: 250px;
height: 40px;
border-radius: 50px;
overflow: hidden;
}
.search-box input{
width: 200px;
height: 40px;
margin-left: 10px;
border: none;
outline: none;
}
.home{
margin-block: 40px;
margin-inline: 60px;
}
.home h4{
font-size: 16px;
}
.movies{
margin-block: 40px;
margin-inline: 60px;
}
.movies h4{
font-size: 16px;
}
.cards{
display: flex;
flex-wrap: wrap;
align-items:center ;
justify-content: space-between;
gap: 10px;
}
.card{
width:200px;
height:360px;
border-radius: 10px;
overflow: hidden;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
}
.card img{
width: 200px;
height: 290px;
object-fit: cover;
}
.card h2{
margin: 10px;
font-size: 16px;
text-align: center;
}
.series{
margin-block: 40px;
margin-inline: 60px;
}
.series h4{
font-size: 16px;
}
.home{
margin-block: 40px;
margin-inline: 60px;
}
.search-results{
margin-block: 40px;
margin-inline: 60px;
}
.search-results h4{
font-size: 16px;
}
.loader{
min-height: 90vh;
display: flex;
align-items: center;
justify-content: center;
}
/* HTML: <div class="loader"></div> */
.load {
width: 50px;
padding: 8px;
aspect-ratio: 1;
border-radius: 50%;
background: #ff1900;
--_m:
conic-gradient(#0000 10%,#000),
linear-gradient(#000 0 0) content-box;
-webkit-mask: var(--_m);
mask: var(--_m);
-webkit-mask-composite: source-out;
mask-composite: subtract;
animation: l3 1s infinite linear;
}
@keyframes l3 {to{transform: rotate(1turn)}}
.movie-detail {
margin-block: 40px;
margin-inline: 60px;
display: flex;
align-items: flex-start;
justify-content: space-between;
}
img-box{
width: 50%;
}
.movie-detail img {
border-radius: 10px;
width: 330px;
height: auto;
object-fit: cover;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
}
.detail-box{
width: 50%;
}
.movie-detail p {
font-size: 18px;
margin: 10px 0;
}
.movie-detail a {
display: inline-block;
margin-top: 20px;
color: #007bff;
text-decoration: none;
}
.movie-detail a:hover {
text-decoration: underline;
}
.footer{
width: 100%;
background-color: red;
text-align: center;
color: white;
padding: 20px;
}
Live Demo
You can check out the live demo of the Movie Finder website here.
Conclusion
In this blog, we learned how to create a Movie Finder website using React, React Router, and Axios. This project demonstrates how to interact with a public API, manage state in React, and create a simple yet functional movie browsing experience.
Feel free to customize the design and add more features, like user reviews or movie ratings, to make it more dynamic!
Credits
Author
Abhishek Gurjar is a dedicated web developer passionate about creating practical and functional web applications. Check out more of his projects on GitHub.
Top comments (6)
Amazing Brother, Keep it up..
React router has inbuilt loader method for each route, it allows to fetch data outside react component. Fetching inside
useEffect
is 👎 practice....That is also not optimal and may not always trigger.
In general if you need to trigger search on "Enter" tap, better way is to wrap input in form element and add
onSubmit
handler with preventing reload.You may also use ky instead of axios. Axios is using XHR under the hood which is slower than fetch.
Bad mobile experience. Must upgrade it
Great project! I'm curious, how long did it take you to build the whole website? Also, what challenges did you face when working with the movie API?
This is a cool project. Good job!
hi