🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Build a complete React movie search application
- Integrate with external APIs (OMDb API)
- Implement search functionality and filtering
- Use React Hooks for state management
- Create responsive components
- Handle loading states and errors
📚 Project Overview
We'll build a movie search application that includes: - Movie search functionality - Movie details display - Favorites management - Responsive design - API integration with OMDb
🏗️ Project Structure
movie-search-app/
├── package.json
├── public/
│ └── index.html
├── src/
│ ├── App.js
│ ├── index.js
│ ├── components/
│ │ ├── MovieCard.js
│ │ ├── MovieDetails.js
│ │ ├── SearchBar.js
│ │ └── FavoritesList.js
│ └── hooks/
│ └── useMovies.js
└── styles/
└── App.css
📝 Package.json
{
"name": "movie-search-app",
"version": "1.0.0",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
💻 Complete Implementation
App.js
import React, { useState, useEffect } from 'react';
import './styles/App.css';
import SearchBar from './components/SearchBar';
import MovieCard from './components/MovieCard';
import MovieDetails from './components/MovieDetails';
import FavoritesList from './components/FavoritesList';
import { useMovies } from './hooks/useMovies';
function App() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedMovie, setSelectedMovie] = useState(null);
const [favorites, setFavorites] = useState([]);
const [showFavorites, setShowFavorites] = useState(false);
const { movies, loading, error, searchMovies } = useMovies();
useEffect(() => {
const savedFavorites = localStorage.getItem('movieFavorites');
if (savedFavorites) {
setFavorites(JSON.parse(savedFavorites));
}
}, []);
useEffect(() => {
localStorage.setItem('movieFavorites', JSON.stringify(favorites));
}, [favorites]);
const handleSearch = (term) => {
setSearchTerm(term);
if (term.trim()) {
searchMovies(term);
}
};
const handleMovieSelect = (movie) => {
setSelectedMovie(movie);
};
const handleAddToFavorites = (movie) => {
if (!favorites.find(fav => fav.imdbID === movie.imdbID)) {
setFavorites(prev => [...prev, movie]);
}
};
const handleRemoveFromFavorites = (movieId) => {
setFavorites(prev => prev.filter(fav => fav.imdbID !== movieId));
};
const isFavorite = (movieId) => {
return favorites.some(fav => fav.imdbID === movieId);
};
return (
<div className="app">
<header className="app-header">
<h1>Movie Search App</h1>
<button
className="favorites-toggle"
onClick={() => setShowFavorites(!showFavorites)}
>
{showFavorites ? 'Hide' : 'Show'} Favorites ({favorites.length})
</button>
</header>
<main className="app-main">
<SearchBar onSearch={handleSearch} />
{showFavorites ? (
<FavoritesList
favorites={favorites}
onRemove={handleRemoveFromFavorites}
onSelect={handleMovieSelect}
/>
) : (
<div className="movies-container">
{loading && <div className="loading">Searching movies...</div>}
{error && <div className="error">Error: {error}</div>}
{movies.length > 0 && (
<div className="movies-grid">
{movies.map(movie => (
<MovieCard
key={movie.imdbID}
movie={movie}
onSelect={handleMovieSelect}
onAddToFavorites={handleAddToFavorites}
isFavorite={isFavorite(movie.imdbID)}
/>
))}
</div>
)}
{searchTerm && movies.length === 0 && !loading && (
<div className="no-results">
No movies found for "{searchTerm}"
</div>
)}
</div>
)}
{selectedMovie && (
<MovieDetails
movie={selectedMovie}
onClose={() => setSelectedMovie(null)}
onAddToFavorites={handleAddToFavorites}
isFavorite={isFavorite(selectedMovie.imdbID)}
/>
)}
</main>
</div>
);
}
export default App;
hooks/useMovies.js
import { useState, useCallback } from 'react';
export function useMovies() {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const searchMovies = useCallback(async (searchTerm) => {
if (!searchTerm.trim()) return;
setLoading(true);
setError(null);
try {
const response = await fetch(
`https://www.omdbapi.com/?s=${encodeURIComponent(searchTerm)}&apikey=YOUR_API_KEY`
);
if (!response.ok) {
throw new Error('Failed to fetch movies');
}
const data = await response.json();
if (data.Response === 'True') {
setMovies(data.Search || []);
} else {
setMovies([]);
setError(data.Error || 'No movies found');
}
} catch (err) {
setError(err.message);
setMovies([]);
} finally {
setLoading(false);
}
}, []);
return { movies, loading, error, searchMovies };
}
components/MovieCard.js
import React from 'react';
function MovieCard({ movie, onSelect, onAddToFavorites, isFavorite }) {
const handleClick = () => {
onSelect(movie);
};
const handleAddToFavorites = (e) => {
e.stopPropagation();
onAddToFavorites(movie);
};
return (
<div className="movie-card" onClick={handleClick}>
<div className="movie-poster">
{movie.Poster !== 'N/A' ? (
<img src={movie.Poster} alt={movie.Title} />
) : (
<div className="no-poster">No Image</div>
)}
</div>
<div className="movie-info">
<h3 className="movie-title">{movie.Title}</h3>
<p className="movie-year">{movie.Year}</p>
<p className="movie-type">{movie.Type}</p>
</div>
<button
className={`favorite-btn ${isFavorite ? 'active' : ''}`}
onClick={handleAddToFavorites}
>
{isFavorite ? '♥' : '♡'}
</button>
</div>
);
}
export default MovieCard;
components/MovieDetails.js
import React, { useState, useEffect } from 'react';
function MovieDetails({ movie, onClose, onAddToFavorites, isFavorite }) {
const [details, setDetails] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDetails = async () => {
try {
const response = await fetch(
`https://www.omdbapi.com/?i=${movie.imdbID}&apikey=YOUR_API_KEY`
);
const data = await response.json();
setDetails(data);
} catch (error) {
console.error('Error fetching movie details:', error);
} finally {
setLoading(false);
}
};
fetchDetails();
}, [movie.imdbID]);
const handleAddToFavorites = () => {
onAddToFavorites(movie);
};
if (loading) {
return (
<div className="movie-details-overlay">
<div className="movie-details">
<div className="loading">Loading details...</div>
</div>
</div>
);
}
return (
<div className="movie-details-overlay" onClick={onClose}>
<div className="movie-details" onClick={(e) => e.stopPropagation()}>
<button className="close-btn" onClick={onClose}>×</button>
<div className="details-content">
<div className="details-poster">
{details.Poster !== 'N/A' ? (
<img src={details.Poster} alt={details.Title} />
) : (
<div className="no-poster">No Image</div>
)}
</div>
<div className="details-info">
<h2>{details.Title}</h2>
<p><strong>Year:</strong> {details.Year}</p>
<p><strong>Rated:</strong> {details.Rated}</p>
<p><strong>Runtime:</strong> {details.Runtime}</p>
<p><strong>Genre:</strong> {details.Genre}</p>
<p><strong>Director:</strong> {details.Director}</p>
<p><strong>Actors:</strong> {details.Actors}</p>
<p><strong>Plot:</strong> {details.Plot}</p>
<p><strong>IMDb Rating:</strong> {details.imdbRating}</p>
<button
className={`favorite-btn ${isFavorite ? 'active' : ''}`}
onClick={handleAddToFavorites}
>
{isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}
</button>
</div>
</div>
</div>
</div>
);
}
export default MovieDetails;
🎯 Key Features Implemented
- Movie Search - Search movies by title
- Movie Details - View detailed information
- Favorites Management - Save and remove favorites
- Responsive Design - Works on all devices
- Loading States - Visual feedback during API calls
- Error Handling - Graceful error management
- Local Storage - Persist favorites between sessions
🎯 Key Takeaways
- React Hooks manage component state and side effects
- API integration connects to external data sources
- Component composition builds complex UIs from simple parts
- State management handles application data
- User experience considerations for loading and errors
🚀 Next Steps
Congratulations! You've built a complete movie search application. In the next lesson, we'll create:
- Notes App - React Hooks for state management
- Local storage integration - Persist data between sessions
- CRUD operations - Create, read, update, delete notes
- Component reusability - Build modular components
Next Lesson: Lesson 10: Mini Project - Notes App