Open In App

Build a Movie APP Using Next.js

Last Updated : 09 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

We will create a movie database app using Next.js and an external movie API (such as The Movie Database API). The app will enable users to view movies, search for specific movies, and view detailed information about each movie, including the title, release date, rating, brief description, and a list of actors and actresses.

Project Preview

pre
Build a Movie Database Using Next.js

Prerequisites

Approach

  • Set up a new Nextjs project with create-next-app.
  • Create an api.js file to handle API requests.
  • Define functions to fetch movie data, search movies, and fetch movie details with cast information.
  • We will use Axios to fetch data from the TMDb API.
  • Create Navbar.js for navigation links.
  • Create HomePage.js to display movies lists and implement search functionality.
  • Create dynamic route page [id].js to display detailed information and cast for a selected movie.
  • Create pagination component It allows users to navigate between different pages of movie results.

Steps to Build a Movie Database with Next.js:

Step 1: Initialized the Nextjs app and installing the required packages

npx create-next-app@latest moviedb 

Step 2: It will ask you some questions, so choose as the following.

√ Would you like to use TypeScript? ... No
√ Would you like to use ESLint? ... No
√ Would you like to use Tailwind CSS? ... No
√ Would you like to use `src/` directory? ... Yes
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to customize the default import alias (@/*)? ... No

Step 3: Install the necessary package for your project using the following command.

npm i axios bootstrap

Project Structure

struct
Project Structure

Dependencies

"dependencies": {
"autoprefixer": "^10.4.19",
"bootstrap": "^5.3.3",
"next": "14.1.3",
"react": "^18",
"react-dom": "^18",
}

Step 4: Create the required files and write the following code to run this movie database app .

Example: We will create a Movie Database using next.js which allow users to view movies, search for specific ones, and access detailed information.

JavaScript
// Page.js

'use client'
import 'bootstrap/dist/css/bootstrap.min.css';
import HomePage from './components/HomePage';
function App() {
    return (
        <>
        <HomePage/ >
        </>
    );
}

export default App;
JavaScript
// HomePage.js

import React, { useEffect, useState } from 'react';
import { api } from '../api/api'; // Adjust import path as needed
import MovieCard from './MovieCard';
import Pagination from './Pagination'; // Import the Pagination component
import Navbar from './Navbar';

const HomePage = () => {
    const [movies, setMovies] = useState([]);
    const [currentPage, setCurrentPage] = useState(1);
    const [totalPages, setTotalPages] = useState(1);
    const [query, setQuery] = useState('');
    const [searching, setSearching] = useState(false);

    useEffect(() => {
        const fetchMovies = async () => {
            try {
                const endpoint = searching ? '/search/movie' : '/movie/popular';
                const response = await api.get(endpoint, {
                    params: { 
                        page: currentPage, 
                        query: searching ? query : undefined
                    }
                });
                setMovies(response.data.results);
                setTotalPages(response.data.total_pages);
            } catch (error) {
                console.error(error);
            }
        };

        fetchMovies();
    }, [currentPage, query, searching]);

    const handleSearch = (e) => {
        e.preventDefault();
        setCurrentPage(1); // Reset to first page for new search
        setSearching(true); // Set searching state to true
    };

    const handleReset = () => {
        setQuery('');
        setSearching(false); // Reset search state
        setCurrentPage(1); // Reset to first page
    };

    return (
        <>
            <Navbar />
            <div className="container my-4">
                <div className="d-flex justify-content-between mb-4">
                    <form onSubmit={handleSearch} className="d-flex">
                        <input
                            className="form-control me-2"
                            type="search"
                            value={query}
                            onChange={(e) => setQuery(e.target.value)}
                            placeholder="Search for movies..."
                        />
                        <button className="btn btn-outline-success" type="submit">
                            Search
                        </button>
                    </form>
                </div>
                {searching && (
                    <button onClick={handleReset} className="btn 
                    btn-outline-primary mb-4">
                        Reset Search
                    </button>
                )}
                <div className="row">
                    {movies.map(movie => (
                        <div key={movie.id} className="col-12 col-sm-6
                        col-md-4 col-lg-3 mb-4">
                            <MovieCard movie={movie} />
                        </div>
                    ))}
                </div>
                <Pagination 
                    currentPage={currentPage} 
                    totalPages={totalPages} 
                    onPageChange={(page) => setCurrentPage(page)} 
                />
            </div>
        </>
    );
};

export default HomePage;
JavaScript
// MovieCard.js

import React from 'react';
import Link from 'next/link';
import { IMAGE_BASE_URL } from '../api/api.js';

const MovieCard = ({ movie }) => {
    return (
        <div className="card shadow-sm mb-4 bg-gray">
            <Link href={`/movie/${movie.id}`} passHref>
                <div className="position-relative">
                    <img
                        src={`${IMAGE_BASE_URL}${movie.poster_path}`}
                        alt={movie.title}
                        className="card-img-top"
                    />
                    <div className="position-absolute top-0 start-0 m-2
                    bg-danger text-white text-xs 
                    font-weight-bold py-1 px-2 rounded">
                        {new Date(movie.release_date).getFullYear()}
                    </div>
                </div>
            </Link>
            <div className="card-body">
                <h5 className="card-title">{movie.title}</h5>
                <div className="d-flex justify-content-between align-items-center">
                    <span className="bg-warning text-dark text-xs 
                    font-weight-bold px-2 py-1 rounded">
                        ★ {movie.vote_average}
                    </span>
                    <span className="text-secondary text-xs font-weight-bold">
                        Popularity: {movie.popularity}
                    </span>
                </div>
            </div>
        </div>
    );
};

export default MovieCard;
JavaScript
// Navbar.js

import React, { useState } from 'react';
import Link from 'next/link';

const Navbar = () => {
    const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);

    return (
        <nav className="navbar navbar-expand-md 
        navbar-dark bg-dark sticky-top">
            <div className="container">
                <Link href="/" passHref>
                    <div className="navbar-brand" 
                    style={{ textDecoration: 'none', color: 'white' }}>
                        MovieDB
                    </div>
                </Link>
                <button
                    className="navbar-toggler"
                    type="button"
                    onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
                    aria-controls="navbarNav"
                    aria-expanded={isMobileMenuOpen}
                    aria-label="Toggle navigation"
                >
                    <span className="navbar-toggler-icon"></span>
                </button>
                
            </div>
        </nav>
    );
};

export default Navbar;
JavaScript
// Pagination.js

import React from 'react';

const Pagination = ({ currentPage, totalPages, onPageChange }) => {
    const maxButtons = 5;

    // Calculate the start and end of the visible page range
    const startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2));
    const endPage = Math.min(totalPages, startPage + maxButtons - 1);

    // Adjust the start page if the end page is less than the max number of buttons
    const adjustedStartPage = Math.max(1, endPage - maxButtons + 1);

    const pages = Array.from({ length: endPage - adjustedStartPage + 1 }, 
    (_, i) => adjustedStartPage + i);

    return (
        <nav aria-label="Page navigation">
            <ul className="pagination justify-content-center">
                <li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
                    <button
                        className="page-link"
                        onClick={() => onPageChange(currentPage - 1)}
                        disabled={currentPage === 1}
                    >
                        &laquo;
                    </button>
                </li>
                {pages.length > 0 && pages.map(page => (
                    <li
                        key={page}
                        className={`page-item ${currentPage === page ? 'active' : ''}`}
                    >
                        <button
                            className="page-link"
                            onClick={() => onPageChange(page)}
                        >
                            {page}
                        </button>
                    </li>
                ))}
                <li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
                    <button
                        className="page-link"
                        onClick={() => onPageChange(currentPage + 1)}
                        disabled={currentPage === totalPages}
                    >
                        &raquo;
                    </button>
                </li>
            </ul>
        </nav>
    );
};

export default Pagination;
JavaScript
import React, { useEffect, useState } from 'react';
import { api, IMAGE_BASE_URL } from '@/app/api/api';
import { useRouter } from 'next/router';
import Link from 'next/link';
import 'bootstrap/dist/css/bootstrap.min.css';
import Navbar from '@/app/components/Navbar';

const MovieDetailPage = () => {
    const router = useRouter();
    const { id } = router.query;
    const [movie, setMovie] = useState(null);
    const [cast, setCast] = useState([]);

    useEffect(() => {
        if (!id) return;

        // Fetch movie details
        api.get(`/movie/${id}`)
            .then(response => setMovie(response.data))
            .catch(error => console.error
            	('Error fetching movie details:', error));

        // Fetch movie cast
        api.get(`/movie/${id}/credits`)
            .then(response => {
                console.log('Cast response data:', response.data);
                setCast(response.data.cast);
            })
            .catch(error => console.error('Error fetching movie cast:', error));
    }, [id]);

    if (!movie) return <div className="text-center
    	text-white mt-10">Loading...</div>;

    return (
        <>
        <Navbar/>
        <div className="container my-4 text-white">
            <Link href="/" passHref>
                <p className="text-primary mb-4">&larr; Back to List</p>
            </Link>
            <div className="row mb-4 border-bottom border-light pb-4">
                <div className="col-md-4 mb-3 mb-md-0">
                    <img 
                        src={`${IMAGE_BASE_URL}${movie.poster_path}`} 
                        alt={movie.title} 
                        className="img-fluid rounded shadow-lg"
                        onError={(e) => e.target.src = 'https://media.geeksforgeeks.org/
                        wp-content/uploads/20240805102426/pta.jpg'}
                    />
                </div>
                <div className="col-md-8">
                    <h1 className="display-4 mb-2">{movie.title}</h1>
                    <div className="mb-4">
                        <span className="badge bg-warning text-dark mr-2">Rating: 
                        {movie.vote_average}</span>
                        <span className="mr-2">{movie.runtime} min</span>
                        <span>{movie.genres.map(genre => genre.name).join(', ')}</span>
                    </div>
                    <p className="mb-2 text-dark"><strong>Release Date:</strong> 
                    {new Date(movie.release_date).toLocaleDateString()}</p>
                    <h2 className="h4 text-dark mb-2">Overview</h2>
                    <p className='text-dark'>{movie.overview}</p>
                </div>
            </div>
            <h2 className="h4 mb-4 border-bottom border-light pb-2">Cast</h2>
            <div className="row">
                {cast.map(member => (
                    <div key={member.cast_id} className="col-6 col-sm-4 col-md-3 
                    col-lg-2 mb-4 d-flex flex-column align-items-center">
                        <div className="position-relative">
                            <img 
                                src={member.profile_path ? `${IMAGE_BASE_URL}$
                                {member.profile_path}` : 'https://media.geeksforgeeks.org/
                                wp-content/uploads/20240805102426/pta.jpg'} 
                                alt={member.name} 
                                className="img-fluid rounded-circle"
                                style={{ width: '120px', height: '120px', objectFit: 'cover' }}
                                onError={(e) => e.target.src = 'https://media.geeksforgeeks.org
                                /wp-content/uploads/20240805102426/pta.jpg'}
                            />
                        </div>
                        <p className="font-weight-bold text-center mt-2">{member.name}</p>
                        <p className="text-muted text-center">{member.character}</p>
                    </div>
                ))}
            </div>
        </div>
        </>
    );
};

export default MovieDetailPage;
JavaScript
// src/api/api.js
import axios from 'axios';

const API_KEY = 'Your_API_KEY';
const BASE_URL = 'https://api.themoviedb.org/3';
const IMAGE_BASE_URL = 'https://image.tmdb.org/t/p/w500';

const api = axios.create({
    baseURL: BASE_URL,
    params: {
        api_key: API_KEY,
        language: 'en-US'
    }
});

export { api, IMAGE_BASE_URL };

Step 5: Save all files and start the server by using the command.

npm run dev

Output: After running the application this output will be visible on http://localhost:3000/


Next Article

Similar Reads