diff --git a/.env b/.env index c954a05..07f4209 100644 --- a/.env +++ b/.env @@ -1,6 +1,2 @@ VITE_APP_TMDB_API_KEY=2edf9f02e088272f6ff2eab6bf5fa21a -MOVIES_GENRE_LIST_URL="https://api.themoviedb.org/3/genre/movie/list?api_key=2edf9f02e088272f6ff2eab6bf5fa21a&language=en-US" -TV_SERIES_GENRE_LIST_URL="https://api.themoviedb.org/3/genre/movie/list?api_key=2edf9f02e088272f6ff2eab6bf5fa21a&language=en-US" - -MOVIE_CREDITS="https://api.themoviedb.org/3/movie/505642/credits?api_key=2edf9f02e088272f6ff2eab6bf5fa21a&language=en-US" \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..3b0b403 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/src/components/Genre.jsx b/src/components/Genre.jsx index f9c0056..ce2ca9e 100644 --- a/src/components/Genre.jsx +++ b/src/components/Genre.jsx @@ -1,15 +1,23 @@ -import React, { useContext } from 'react' +import { motion } from 'framer-motion' +import React from 'react' import { Badge } from 'react-bootstrap' +import { item } from '../helpers/framerMotion' + export const Genre = ({title,handleGenres,id,active}) => { return ( -<h3> -<Badge onClick={()=>handleGenres(id)} id='badge' style={{ +<motion.h3 + whileTap={{ + scale: 0.8, +}} + +onClick={()=>handleGenres(id)} variants={item}> +<Badge id='badge' style={{ cursor:'pointer', }} bg={active?"primary":"dark"}>{title}</Badge> -</h3> +</motion.h3> ) } diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 6b4e72b..e94a416 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,14 +1,26 @@ import React from "react"; import { Card } from "react-bootstrap"; import { LazyLoadImage } from "react-lazy-load-image-component"; -import { IMAGE_LINK } from "../constants"; +import { IMAGE_LINK, IMAGE_UNAVAILABLE_PLACEHOLDER } from "../constants"; import { useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; -export const MovieCard = ({ movie, tvShow }) => { + +export const MovieCard = ({ movie }) => { const navigate = useNavigate(); return ( - <Card + <motion.div + initial={{ scale: 0,opacity:0 }} + animate={{ opacity:1, scale: 1 }} + transition={{ + type: "spring", + stiffness: 260, + damping: 40, + }} + + > + <Card style={{ width: "100%", background: "#161616", @@ -20,7 +32,7 @@ export const MovieCard = ({ movie, tvShow }) => { > <Card.Body> <LazyLoadImage - src={`${IMAGE_LINK}${movie.poster_path || movie.backdrop_path}`} + src={!movie.poster_path||!movie.backdrop_path ?IMAGE_UNAVAILABLE_PLACEHOLDER :`${IMAGE_LINK}${movie.backdrop_path}`} width={"100%"} height={350} alt="movie" @@ -36,5 +48,6 @@ export const MovieCard = ({ movie, tvShow }) => { </Card.Title> </Card.Body> </Card> + </motion.div> ); }; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 15a595e..4683822 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,81 +1,61 @@ import React from "react"; import { Button, Container, Form, Nav, Navbar } from "react-bootstrap"; -import { FireIcon, TvIcon, FilmIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -import {Link,useParams,useLocation} from 'react-router-dom' +import { + FireIcon, + TvIcon, + FilmIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/outline"; +import { Link, useParams, useLocation } from "react-router-dom"; +import { nav_links } from "../helpers/data"; export default function NavComp() { - const params=useLocation(); -const currentPath=params.pathname + const params = useLocation(); + const currentPath = params.pathname; return ( - <Navbar className="navbar" expand="lg" style={{position:'sticky',height:'fit-content',top:0,zIndex:2}}> + <Navbar + className="navbar" + expand="lg" + style={{ position: "sticky", height: "fit-content", top: 0, zIndex: 2 }} + > <Container> <Navbar.Brand style={{ color: "white", fontWeight: "bold" }} href="#"> MoviesHub </Navbar.Brand> - <Navbar.Toggle - style={{background:'white'}} - aria-controls="navbarScroll" /> + <Navbar.Toggle + style={{ background: "white" }} + aria-controls="navbarScroll" + /> <Navbar.Collapse id="navbarScroll"> <Nav className="me-auto my-2 my-lg-0 d-flex gap-3" style={{ maxHeight: "100px" }} navbarScroll > - <Link to='/' style={{ color: "white", display: "flex", alignItems: "center",fontWeight:currentPath==='/'?'bold':'medium' }}> - - - - - <FireIcon + {nav_links.map((item) => ( + <Link + key={item.id} + to={item.href} style={{ - width: 22, - height: 22, - marginRight: 6, + color:currentPath === item.href ? "white" : "#afafaf", + display: "flex", + alignItems: "center", + fontWeight: currentPath === item.href ? "bold" : "medium", }} - /> - Trendings - - </Link> - <Link to='/movies' style={{ color: "white", display: "flex", alignItems: "center",fontWeight:currentPath==='/movies'?'bold':'medium' }}> - - <FilmIcon - style={{ - width: 22, - height: 22, - marginRight: 6, - }} - /> - Movies - - </Link> - <Link to='/tv-series' style={{ color: "white", display: "flex", alignItems: "center" ,fontWeight:currentPath==='/tv-series'?'bold':'medium' }}> - - <TvIcon - style={{ - width: 22, - height: 22, - marginRight: 6, - }} - /> - TV Series - - </Link> - <Link to='/search' style={{ color: "white", display: "flex", alignItems: "center" ,fontWeight:currentPath==='/search'?'bold':'medium' }}> - - <MagnifyingGlassIcon - style={{ - width: 22, - height: 22, - marginRight: 6, - }} - /> -Search - - </Link> + > + <item.icon + style={{ + width: 22, + height: 22, + marginRight: 6, + }} + /> + {item.title} + </Link> + ))} </Nav> - </Navbar.Collapse> </Container> </Navbar> diff --git a/src/components/SearchComp.jsx b/src/components/SearchComp.jsx new file mode 100644 index 0000000..2821de9 --- /dev/null +++ b/src/components/SearchComp.jsx @@ -0,0 +1,31 @@ + +import React, { useContext } from 'react' +import { Form,Button } from 'react-bootstrap' +import { MovieContext } from '../context/MovieContext' + +export const SearchComp = () => { + const{setQuery,query,SearchMovies}=useContext(MovieContext); + + +const handleSearch=(e)=>{ + e.preventDefault() +SearchMovies() +} + + + return ( +<Form onSubmit={handleSearch} id='form' className=' d-flex gap-3 w-50'> + <Form.Control + className='search-box py-2' + placeholder='search movies' value={query} onChange={e=>setQuery(e.target.value)}/> + <Button + onClick={handleSearch} + style={{ + background:"#00CE79", + border:'none', + color:'black' + }} + className='search-btn'>Search</Button> +</Form> + ) +} diff --git a/src/components/TvCard.jsx b/src/components/TvCard.jsx index 436c680..afe2cd0 100644 --- a/src/components/TvCard.jsx +++ b/src/components/TvCard.jsx @@ -1,16 +1,25 @@ -import React, { useContext } from "react"; +import React from "react"; import { Card } from "react-bootstrap"; import { LazyLoadImage } from "react-lazy-load-image-component"; -import { IMAGE_LINK } from "../constants"; -import {useNavigate} from 'react-router-dom'; -import { MovieContext } from "../context/MovieContext"; +import { IMAGE_LINK, IMAGE_UNAVAILABLE_PLACEHOLDER } from "../constants"; +import { useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; -export const TVCard = ({ movie ,tvShow}) => { - const navigate=useNavigate() +export const TVCard = ({ movie}) => { + const navigate = useNavigate(); return ( - <Card + <motion.div + initial={{ scale: 0,opacity:0 }} + animate={{ opacity:1, scale: 1 }} + transition={{ + type: "spring", + stiffness: 260, + damping: 40 + }} + > + <Card style={{ width: "100%", background: "#161616", @@ -22,7 +31,7 @@ export const TVCard = ({ movie ,tvShow}) => { > <Card.Body> <LazyLoadImage - src={`${IMAGE_LINK}${movie.poster_path || movie.backdrop_path}`} + src={!movie.poster_path||!movie.backdrop_path ?IMAGE_UNAVAILABLE_PLACEHOLDER :`${IMAGE_LINK}${movie.backdrop_path}`} width={"100%"} height={350} alt="movie" @@ -30,12 +39,15 @@ export const TVCard = ({ movie ,tvShow}) => { style={{ objectFit: "cover" }} /> <Card.Title - onClick={()=>navigate(`/tv/${movie.id}`)} - className="text-center mt-3" style={{ cursor: "pointer" }}> + onClick={() => navigate(`/tv/${movie.id}`)} + className="text-center mt-3" + style={{ cursor: "pointer" }} + > {movie.name || movie.title} </Card.Title> - </Card.Body> </Card> + + </motion.div> ); }; diff --git a/src/components/VideoPlayerModal.jsx b/src/components/VideoPlayerModal.jsx index d8e77b1..67a04c5 100644 --- a/src/components/VideoPlayerModal.jsx +++ b/src/components/VideoPlayerModal.jsx @@ -20,8 +20,9 @@ export const VideoPlayerModal = (props) => { </Modal.Header> <Modal.Body style={{background:'#161616'}} > <ReactPlayer + width={'100%'} - playing controls url={`https://youtu.be/${props.videoId}`} /> + playing controls url={`https://youtu.be/${props?.videoid}`} /> </Modal.Body> <Modal.Footer style={{background:"#161616",border:'none'}}> <Button onClick={props.onHide}>Close</Button> diff --git a/src/constants.js b/src/constants.js index 266e968..00008a4 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,7 +1,7 @@ -export const API_KEY = "2edf9f02e088272f6ff2eab6bf5fa21a"; +export const API_KEY = import.meta.env.VITE_APP_TMDB_API_KEY; export const IMAGE_LINK = "https://image.tmdb.org/t/p/w500"; - +export const IMAGE_UNAVAILABLE_PLACEHOLDER="https://upload.wikimedia.org/wikipedia/en/6/60/No_Picture.jpg" export const TRENDINGS = (pageNumber = 1) => `https://api.themoviedb.org/3/trending/movie/week?api_key=${API_KEY}&page=${pageNumber}`; @@ -17,4 +17,7 @@ export const TV_SHOWS=(pageNumber=1) => `https://api.themoviedb.org/3/tv/on_the_ export const FILTERED_MOVIES_WITH_GENRES=(pageNumber=1,id)=>`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${pageNumber}&with_genres=${id}&with_watch_monetization_types=flatrate` -export const FILTERED_TV_SHOWS_WITH_GENRES=(pageNumber=1,id)=>`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&language=en-US&sort_by=popularity.desc&page=${pageNumber}&timezone=America%2FNew_York&with_genres=${id}&include_null_first_air_dates=false&with_watch_monetization_types=flatrate&with_status=0&with_type=0` \ No newline at end of file +export const FILTERED_TV_SHOWS_WITH_GENRES=(pageNumber=1,id)=>`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&language=en-US&sort_by=popularity.desc&page=${pageNumber}&timezone=America%2FNew_York&with_genres=${id}&include_null_first_air_dates=false&with_watch_monetization_types=flatrate&with_status=0&with_type=0` + + +export const SEARCH_MOVIES=(pageNumber=1,query="")=>`https://api.themoviedb.org/3/search/movie?api_key=${API_KEY}&language=en-US&query=${query}&page=${pageNumber}&include_adult=false` \ No newline at end of file diff --git a/src/context/MovieContext.jsx b/src/context/MovieContext.jsx index d17a509..d8e6a11 100644 --- a/src/context/MovieContext.jsx +++ b/src/context/MovieContext.jsx @@ -1,11 +1,11 @@ import axios from "axios"; import { createContext, useEffect, useState } from "react"; import { - API_KEY, FILTERED_MOVIES_WITH_GENRES, FILTERED_TV_SHOWS_WITH_GENRES, LATEST, MOVIE_GENRES, + SEARCH_MOVIES, TRENDINGS, TV_GENRES, TV_SHOWS, @@ -22,8 +22,11 @@ export const MovieContextProvider = ({ children }) => { const [tvGenres, setTVGenres] = useState([]); const [trendingTotalPages, setTrendingTotalPages] = useState(); const [latestTotalPages, setLatestTotalPages] = useState(); - const [tvTotalPages,setTvTotalPages] = useState(); + const [tvTotalPages, setTvTotalPages] = useState(); const [currentPage, setCurrentPage] = useState(1); + const [movieSearchResults, setMovieSearchResults] = useState([]); + const [query, setQuery] = useState(""); + const [searchResultsTotalPages, setSearchResultsTotalPages] = useState(); const fetchTrendings = async () => { const { data: movies } = await fetchData(TRENDINGS(currentPage)); @@ -48,7 +51,14 @@ export const MovieContextProvider = ({ children }) => { const fetchTvShows = async () => { const { data } = await fetchData(TV_SHOWS(currentPage)); setTvShows(data.results); - setTvTotalPages(data.total_pages) + setTvTotalPages(data.total_pages); + }; + + const SearchMovies = async () => { + if (!query) return; + const { data } = await fetchData(SEARCH_MOVIES(currentPage, query)); + setMovieSearchResults(data.results); + setSearchResultsTotalPages(data.total_pages); }; useEffect(() => { @@ -57,6 +67,7 @@ export const MovieContextProvider = ({ children }) => { fetchGenres(); fetchTvShows(); fetchTvGenres(); + SearchMovies(); }, [currentPage]); const handleGenres = async (id) => { @@ -68,7 +79,9 @@ export const MovieContextProvider = ({ children }) => { ) ); - const { data } = await axios.get(FILTERED_MOVIES_WITH_GENRES(currentPage,id)); + const { data } = await axios.get( + FILTERED_MOVIES_WITH_GENRES(currentPage, id) + ); setMovies(data.results); }; const handleTvGenres = async (id) => { @@ -80,10 +93,14 @@ export const MovieContextProvider = ({ children }) => { ) ); - const { data } = await axios.get(FILTERED_TV_SHOWS_WITH_GENRES(currentPage,id)); + const { data } = await axios.get( + FILTERED_TV_SHOWS_WITH_GENRES(currentPage, id) + ); setTvShows(data.results); }; + + return ( <MovieContext.Provider value={{ @@ -100,7 +117,12 @@ export const MovieContextProvider = ({ children }) => { setCurrentPage, setLatestTotalPages, latestTotalPages, - tvTotalPages + tvTotalPages, + searchResultsTotalPages, + setQuery, + SearchMovies, + movieSearchResults, + query, }} > {children} diff --git a/src/helpers/ShowTvInfo.jsx b/src/helpers/ShowTvInfo.jsx index 9203d30..9039607 100644 --- a/src/helpers/ShowTvInfo.jsx +++ b/src/helpers/ShowTvInfo.jsx @@ -102,7 +102,7 @@ useEffect(()=>{ </div> </Container> <VideoPlayerModal - videoId={movieVideoId&&movieVideoId} + videoid={movieVideoId&&movieVideoId} title={movieInfo?.name} show={modalShow} onHide={() => setModalShow(false)} diff --git a/src/helpers/data.jsx b/src/helpers/data.jsx new file mode 100644 index 0000000..39c2b4e --- /dev/null +++ b/src/helpers/data.jsx @@ -0,0 +1,28 @@ +import { FilmIcon, FireIcon, MagnifyingGlassIcon, TvIcon } from "@heroicons/react/24/outline"; + +export const nav_links=[ + { + id:1, + title:"Trendings", + icon:FireIcon, + href:"/" + }, + { + id:2, + title:"Movies", + icon:FilmIcon, + href:"/movies" + }, + { + id:3, + title:"TV Series", + icon:TvIcon, + href:"/tv-series" + }, + { + id:4, + title:"Search", + icon:MagnifyingGlassIcon, + href:"/search" + }, +] \ No newline at end of file diff --git a/src/helpers/framerMotion.js b/src/helpers/framerMotion.js new file mode 100644 index 0000000..20fe3c7 --- /dev/null +++ b/src/helpers/framerMotion.js @@ -0,0 +1,20 @@ +const container = { + hidden: { opacity: 1, scale: 0 }, + visible: { + opacity: 1, + scale: 1, + transition: { + staggerChildren: 0.08 + } + } + } + + const item = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1 + } + } + + export {container,item} \ No newline at end of file diff --git a/src/index.css b/src/index.css index a737fcb..f523af6 100644 --- a/src/index.css +++ b/src/index.css @@ -49,6 +49,9 @@ outline: none; #movie-info{ flex-direction: column; } + #form{ + width: 100% !important; + } } .paginate{ @@ -93,3 +96,11 @@ outline: none; } + + +.search-box{ + background: #1e1e1e !important; + display: none; + border: none !important; + color: white !important; +} \ No newline at end of file diff --git a/src/pages/Movies.jsx b/src/pages/Movies.jsx index 26bbed8..9bb1131 100644 --- a/src/pages/Movies.jsx +++ b/src/pages/Movies.jsx @@ -1,50 +1,53 @@ -import React, { useContext, useEffect } from 'react' -import { Col, Container, Row } from 'react-bootstrap' -import { Genre } from '../components/Genre' -import { MovieCard } from '../components/MovieCard' -import NavComp from '../components/Navbar' -import { PaginationComp } from '../components/PaginationComp' -import { MovieContext } from '../context/MovieContext' +import { motion } from "framer-motion"; +import React, { useContext } from "react"; +import { Col, Container, Row } from "react-bootstrap"; +import { Genre } from "../components/Genre"; +import { MovieCard } from "../components/MovieCard"; +import NavComp from "../components/Navbar"; +import { PaginationComp } from "../components/PaginationComp"; +import { MovieContext } from "../context/MovieContext"; +import { container } from "../helpers/framerMotion"; export default function Movies() { - const {movies,movieGenres,handleGenres,latestTotalPages}=useContext(MovieContext); - - - - + const { movies, movieGenres, handleGenres, latestTotalPages } = + useContext(MovieContext); return ( <> - <NavComp/> - <Container className='mt-4'> - <div className="genres d-flex flex-wrap gap-2"> - {movieGenres?.map(item=> - <Genre id={item.id} key={item.id} title={item.name} - active={item.active} - handleGenres={handleGenres} - /> - )} - - </div> + <NavComp /> + <Container className="mt-4"> + <motion.div + variants={container} + initial="hidden" + animate="visible" + className="genres d-flex flex-wrap " + style={{ + gap:'5px 15px' + }} + > + {movieGenres?.map((item) => ( + <Genre + id={item.id} + key={item.id} + title={item.name} + active={item.active} + handleGenres={handleGenres} + /> + ))} + </motion.div> <div className="wrapper mt-4"> - - <Row md={3} xs={1} lg={4} className="g-4"> - {movies?.map((item)=> - <Col key={item.id}> - <MovieCard movie={item} tvShow={false}/> - </Col> - )} - - </Row> + <Row md={3} xs={1} lg={4} className="g-4"> + {movies?.map((item) => ( + <Col key={item.id}> + <MovieCard movie={item} tvShow={false} /> + </Col> + ))} + </Row> </div> <div className="mt-5 d-flex justify-content-center"> - <PaginationComp - totalPages={latestTotalPages} - /> + <PaginationComp totalPages={latestTotalPages} /> </div> </Container> - - </> - ) + ); } diff --git a/src/pages/Search.jsx b/src/pages/Search.jsx index 43f6096..d6e9220 100644 --- a/src/pages/Search.jsx +++ b/src/pages/Search.jsx @@ -1,31 +1,33 @@ -import React from 'react' -import { Button, Container, Form, Row } from 'react-bootstrap' +import React, { useContext } from 'react' +import { Col, Container, Form, Row } from 'react-bootstrap' import NavComp from '../components/Navbar' +import { SearchComp } from '../components/SearchComp' +import { PaginationComp } from '../components/PaginationComp' +import { MovieCard } from '../components/MovieCard' +import { MovieContext } from '../context/MovieContext'; export default function Search() { + const{movieSearchResults,searchResultsTotalPages}=useContext(MovieContext); return ( <> <NavComp/> <Container className='mt-4'> <div className="wrapper mt-4"> -<Form> - <Form.Control placeholder='search anything'/> - <Button>Search</Button> -</Form> - <Row md={3} xs={1} lg={4} className="g-4"> - {/* {movies?.map((item)=> +<SearchComp/> + <Row md={3} xs={1} lg={4} className="g-4 mt-3"> + {movieSearchResults?.map((item)=> <Col key={item.id}> - <MovieCard movie={item} tvShow={false}/> + <MovieCard movie={item} /> </Col> - )} */} + )} </Row> </div> - {/* <div className="mt-5 d-flex justify-content-center"> + {movieSearchResults.length>0&&<div className="mt-5 d-flex justify-content-center"> <PaginationComp - totalPages={latestTotalPages} + totalPages={searchResultsTotalPages} /> - </div> */} + </div>} </Container> diff --git a/src/pages/ShowInfo.jsx b/src/pages/ShowInfo.jsx index 0c1193a..920fb02 100644 --- a/src/pages/ShowInfo.jsx +++ b/src/pages/ShowInfo.jsx @@ -38,7 +38,7 @@ const fetchMovieCast=async()=>{ const fetchMovieVideoId=async()=>{ const {data}=await axios.get(`https://api.themoviedb.org/3/movie/${movieId}/videos?api_key=${API_KEY}&language=en-US`) -setMovieVideoId(data?.results[0].key) +setMovieVideoId(data?.results[0]?.key) } useEffect(()=>{ @@ -104,7 +104,7 @@ useEffect(()=>{ </div> </Container> <VideoPlayerModal - videoId={movieVideoId&&movieVideoId} + videoid={movieVideoId&&movieVideoId} title={movieInfo?.title} show={modalShow} onHide={() => setModalShow(false)} diff --git a/src/pages/TvSeries.jsx b/src/pages/TvSeries.jsx index 60ac7e1..7dc9c44 100644 --- a/src/pages/TvSeries.jsx +++ b/src/pages/TvSeries.jsx @@ -1,3 +1,4 @@ +import { motion } from "framer-motion"; import React, { useContext } from "react"; import { Col, Container, Row } from "react-bootstrap"; import { Genre } from "../components/Genre"; @@ -5,6 +6,8 @@ import NavComp from "../components/Navbar"; import { PaginationComp } from "../components/PaginationComp"; import { TVCard } from "../components/TvCard"; import { MovieContext } from "../context/MovieContext"; +import { container } from "../helpers/framerMotion"; + export default function TvSeries() { const { tvShows, tvGenres,handleTvGenres,tvTotalPages } = useContext(MovieContext); @@ -12,14 +15,19 @@ export default function TvSeries() { <> <NavComp /> <Container className="mt-4"> - <div className="genres d-flex flex-wrap gap-2"> + <motion.div variants={container} initial="hidden" + animate="visible" className="genres d-flex flex-wrap " + style={{ + gap:'5px 15px' + }} + > {tvGenres?.map((item) => ( <Genre key={item.id} title={item.name} id={item.id} active={item.active} handleGenres={handleTvGenres} /> ))} - </div> + </motion.div> <div className="wrapper mt-4"> <Row md={3} xs={1} lg={4} className="g-4"> {tvShows.map((item) => (