diff --git a/.env b/.env new file mode 100644 index 00000000..408647ac --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_KEY=abdf86b5a5bfd53f8efefa7cdcb50fa9 diff --git a/README.md b/README.md index f768e33f..d9d6dde1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,64 @@ -# React + Vite +## Unit Assignment: Flixster -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Submitted by: Dara Oyedun -Currently, two official plugins are available: +Estimated time spent: 50 hours spent in total -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +Deployed Application (optional): [Flixster Deployed Site](https://flixster-starter-w73s.onrender.com) + +### Application Features + +#### CORE FEATURES + + +- [x] **Display Movies** + - [x] Users can view a list of current movies from The Movie Database API. + - [x] For each movie displayed, users can see its title, poster image, and votes. + - [x] Users can load more current movies by clicking a button at the bottom of the list (page should not be reloaded). +- [x] **Search Functionality** + - [x] Users can search for movies and view the results in a grid. + - [x] Users can clear results and view previous current movies displayed. +- [x] **Accessibility Features** + - [x] Website implements accessibility features (semantic HTML, color contrast, font sizing, alt text for images). +- [x] **Responsive Design** + - [x] Website implements responsive web design. +- [x] **Movie Details** + - [x] Users can view more details about a movie in a popup, such as runtime in minutes, backdrop poster, release date, genres, and/or an overview. +- [x] **Sorting Options** + - [x] Users can click on a filter by drop down to sort product by type (alphabetic, release date, rating). +- [x] **Layout** + - [x] Website displays header, banner, search, movie grid, about, contact, and footer section. + +#### STRETCH FEATURES + +- [x] **Deployment** + - [x] Website is deployed via Render. +- [x] **Embedded Movie Trailers** + - [x] Within the popup displaying a movie's details, users can play the movie trailer. +- [x] **Watched Checkbox** + - [x] For each movie displayed, users can mark the movie as watched. +- [x] **Favorite Button** + - [x] For each movie displayed, users can favorite the movie. +- [x] **Sidebar** + - [x] Users can open a sidebar + - [x] The sidebar displays the user's favorited and watched movies + +### Walkthrough Video + +'https://www.loom.com/share/94d48f59afcd45d3853c479b60ee7728?sid=d083ebdc-4df4-4f91-9c69-1d2ef9884188' + +### Reflection + +* Did the topics discussed in your labs prepare you to complete the assignment? Be specific, which features in your weekly assignment did you feel unprepared to complete? +The labs helped me as well as the code demos done in class. that really helped me understand why I could get any errors and how I could resolve them. it also ensured that I was able to get the foundational knowledge I needed to grasp React + +* If you had more time, what would you have done differently? Would you have added additional features? Changed the way your project responded to a particular event, etc. +If I had more time, I would implement more CSS features and add some animations. I would also want a button that if clicked on would take you back to the top as scrolling is tedious. Also, i would want my search bar to immediately start displaying movies as soon as the user starts to type on the search bar. + +* Reflect on your project demo, what went well? Were there things that maybe didn't go as planned? Did you notice something that your peer did that you would like to try next time? +I like how my modal turned out and how I was able to also add the embedded video trailer. I nticed that one of my peers used an image from the movies as the header and it displayed some of the information on it. I would like to try it next time. + +### Open-source libraries used +TMDB API {'https://developer.themoviedb.org/docs/getting-started'} +### Shout out +To Sammy, Ayoub, Destiny and Gabriella diff --git a/Untitled design (3).jpg b/Untitled design (3).jpg new file mode 100644 index 00000000..27cfb262 Binary files /dev/null and b/Untitled design (3).jpg differ diff --git a/index.html b/index.html index 0abcfe5f..84237a99 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,9 @@ + + + Flixster @@ -9,5 +12,6 @@
+ diff --git a/package-lock.json b/package-lock.json index 92a683d2..941b4915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", - "vite": "^5.2.0" + "vite": "^5.2.13" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4100,9 +4100,9 @@ } }, "node_modules/vite": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", - "integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==", + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", + "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", "dev": true, "dependencies": { "esbuild": "^0.20.1", diff --git a/package.json b/package.json index eded5715..374006bf 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,6 @@ "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", - "vite": "^5.2.0" + "vite": "^5.2.13" } } diff --git a/src/App.css b/src/App.css index 0bf65669..b5491426 100644 --- a/src/App.css +++ b/src/App.css @@ -1,28 +1,82 @@ -.App { +.title-header{ text-align: center; + padding-right: auto; + color: white; } - -.App-header { - background-color: #282c34; - display: flex; +.loadMoreButton{ + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + justify-content: center; + background-color: white; + color: black +} +footer{ + display: inline-block; + /* position: fixed; */ + margin-left: 50%; + left: 0; + bottom: 0; + margin-right: 50%; + width: 200px; + height: 50px; + background-color:black; +} +.button{ + margin-left: auto; + margin-right: auto; +} +.search-bar{ + width: 80% +} +.body{ + background-color: #0d0f16; + margin-left: 20%; + display: inline-flex; + flex-wrap: wrap; flex-direction: row; - align-items: center; - justify-content: space-evenly; - color: white; - padding: 20px; } +.hide{ + display: none; +} +.show{ + display: flex; +} +.nav-bar{ + list-style: none; + margin: 0; + padding: 0; + display: flex; + backdrop-filter: blur(5px); -@media (max-width: 600px) { - .movie-card { - width: 100%; - } +} +.whole-body{ + background-color: #0d0f16; +} - .search-bar { - flex-direction: column; - gap: 10px; - } +.button { + font-family: "Poppins"; + border-radius: 10px; + font-size: 110%; + border: none; +} +.button:hover { + background-color: rgba(255, 82, 122, .6); - .search-bar form { - flex-direction: column; - } +} +.load-more{ + margin-left: 10%; + margin-right: 10%; + align-items: center; + display: flex; + flex-direction: column; +} +header { + font-family: Arial, sans-serif; + font-size: 20px; + color: #333; + background-image: url('/src/Background.jpg'); + width: 100%; + border-bottom: 1px solid #ccc; + height:100% } diff --git a/src/App.jsx b/src/App.jsx index 48215b3f..04539c0d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,212 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import './App.css' +import MovieList from './MovieList.jsx'; +import SideBar from './SideBar'; -const App = () => { -
- -
-} -export default App +function App() { + //state variable to store movie data + const [movies, setMovieData] = useState([]); + + //state variable to store page number + const [pageNumber, setPageNumber] = useState(1); + + //state variable to store user's search items + const [searchData, setSearchData] = useState(''); + + // to switch between tabs between nowPlaying and Search buttons + const [nowPlayingButton, setnowPlayingButton] = useState('active'); + const [searchButton, setSearchButton] = useState('inactive'); + + // to hide and show search bar + const [displaySearchBarButton, setdisplaySearchBarButton] = useState('hide'); + + //import API key from .env file and assign it to a variable + const apiKey = import.meta.env.VITE_API_KEY; + let fetchURL = 'https://api.themoviedb.org/3/discover/movie?api_key=abdf86b5a5bfd53f8efefa7cdcb50fa9&language=en-US'; + + //create new states FOR SWITCHING BETWEEN FUNCTIONS + const [fetchBetweenTabs, setFetchBetweenTabs] = useState('now-playing-home'); + + //state variable to store sort type + const [sortType, setSortType] = useState(''); + + //state variable to store search data + const [searchTerm, setSearchTerm] = useState(''); + + //state variable to liked movies information + const [likedMovies, setLikedMovies] = useState([]); + + //state variable to store watched movies information + const [watchedMovies, setWatchedMovies] = useState([]); + + //state variable to store genre type + const [genreType, setGenreType] = useState(''); + + //function to choose what to display on the page. If it's now playing, it will fetch the data from now-playing endpoint. + // If sort button is clicked, then it'll fetch the data from sort endpoint + async function handleDiscoverRequest() { + try { + if (fetchBetweenTabs === 'now-playing-home') { + if (sortType != "") { + fetchURL += '&sort_by=' + sortType; + } + setMovieData([]); + setPageNumber(1); + const response = await fetch(fetchURL); + const data = await response.json(); + setMovieData(data.results); + } + else { + setMovieData([]); + setFetchBetweenTabs('search-results'); + } + } + catch (err) { + console.error(err); + } + } + + //function to fetch new page of data when user clicks on load more button + const fetchNewPageMovieData = async () => { + try { + if (sortType != "") { + fetchURL += '&sort_by=' + sortType; + } + fetchURL += '&page=' + pageNumber; + const response = await fetch(fetchURL); + const data = await response.json(); + setMovieData(movies.concat(data.results)); + } + catch (err) { + console.log(err); + } + } + + //useEffect to fetch if the tab is now playing or search + useEffect(() => { + if (fetchBetweenTabs === 'now-playing-home' && searchTerm === '') { + fetchNewPageMovieData() + } + else{ + searchDataValue(searchTerm); + } + }, [pageNumber, searchTerm]); + + //function to load more pages of data when user clicks on load more button + function loadMorePages() { + setPageNumber(prevpageNumber => prevpageNumber + 1); + } + + //function to fetch data from search endpoint + function searchDataValue() { + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhYmRmODZiNWE1YmZkNTNmOGVmZWZhN2NkY2I1MGZhOSIsInN1YiI6IjY2Njg4NzllNmU0MTZkMDlhODhiNzYzOCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.h4y2UevgrG927Du8ysa-yc4y9k7D3Yh7zDK2TU8eYEE' + } + }; + fetch(`https://api.themoviedb.org/3/search/movie?query=${searchData}&include_adult=false&language=en-US&page=${pageNumber}`, options) + .then(response => response.json()) + .then(response => setMovieData(movies.concat(response.results))) + .catch(err => console.error(err)); + } + + //function to display search bar and search button when clicked or to hide them when clicked on now-playing button + function NowPlaying(event) { + if (event.target.id === "now-playing") { + setnowPlayingButton('active'); + setSearchButton('inactive'); + setdisplaySearchBarButton('hide'); + setSortType(''); + setGenreType('') + setMovieData([]); + setSearchTerm(''); + handleDiscoverRequest(); + } + else { + setnowPlayingButton('inactive'); + setSearchButton('active'); + setdisplaySearchBarButton('show'); + setMovieData([]); + setSearchTerm(''); + } + } + //function to fetch data from sort endpoint based on sort + useEffect(() => { + handleDiscoverRequest(); + }, [sortType]); + + //function to move liked movies to the sidebar + function handlesetLikedMovies(movieID){ + if(likedMovies.includes(movieID)){ + setLikedMovies(previousIDs => previousIDs.filter(previousID => previousID !== movieID)); + } + else{ + setLikedMovies(previousID => [...previousID, movieID]); + } + } + + //function to move watched movies to the sidebar + function handlesetWatchedMovies(movieID){ + if(watchedMovies.includes(movieID)){ + setWatchedMovies(previousIDs => previousIDs.filter(previousID => previousID !== movieID)); + } + else{ + setWatchedMovies(previousID => [...previousID, movieID]); + } + } + + + return ( +
+
+ +
+ +
+
+
+
+

Flixster

+
+ + + +
+
+
+
+ setSearchData(e.target.value.toLowerCase())} /> + +
+
+
+ +
+ + +
+
+ ) +} +export default App; diff --git a/src/Background.jpg b/src/Background.jpg new file mode 100644 index 00000000..27cfb262 Binary files /dev/null and b/src/Background.jpg differ diff --git a/src/Modal.css b/src/Modal.css new file mode 100644 index 00000000..12c56bd7 --- /dev/null +++ b/src/Modal.css @@ -0,0 +1,81 @@ +.modalOverlay{ + position: fixed; + justify-content: center; + align-items: center; + width: 50%; + height: 70%; + top: 25%; + left: 25%; + z-index: 1; + + + + +} +.modalContainer{ + padding: 10px; + border-radius: 10px; + width: 100%; + /* background: transparent; */ + color: white; + border: solid white; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6); + background-size: 100%; + + +} +.closeButton{ + background: none; + border: none; + font-size: 1.5rem; + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + color: red; +} +.modalContent{ + margin-top: 10px; + background-color: rgba(255, 255, 255, 0.7); + border: solid white; + + +} +.hide{ + display: none; +} +.show{ + display: flex; +} +.main-image{ + width: 25%; + border-right: white; +} +.image-video{ + display: flex; +} +.title-of-modal{ + text-align: center; + font-size: 120%; + font-weight: bolder; + color: goldenrod; + background-color: black; + background-position: center; + margin-left: 10%; + margin-right: 10% ; + +} +.over-view{ + font-weight: bold; + margin-top: 2%; + margin-bottom: 2%; + text-wrap: wrap; +} +.youtube{ + width: 100%; + display: flex; +} +.modal-Information{ + background-color: black; + padding: 5%; +} diff --git a/src/Modal.jsx b/src/Modal.jsx new file mode 100644 index 00000000..67dd57c8 --- /dev/null +++ b/src/Modal.jsx @@ -0,0 +1,83 @@ +import './Modal.css'; +import React, { useEffect, useState } from 'react'; +import MovieCard from './MovieCard'; +function Modal(props){ + const [getTrailerKey, setGetTrailerKey] = useState(""); + const apiKey = import.meta.env.VITE_API_KEY; + + + //use effect to get the trailer data from the API and find the trailers and their keys + useEffect(()=>{ + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhYmRmODZiNWE1YmZkNTNmOGVmZWZhN2NkY2I1MGZhOSIsInN1YiI6IjY2Njg4NzllNmU0MTZkMDlhODhiNzYzOCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.h4y2UevgrG927Du8ysa-yc4y9k7D3Yh7zDK2TU8eYEE' + } + }; + fetch(`https://api.themoviedb.org/3/movie/${props.data.movieID}/videos?api_key=${apiKey}`, options) + .then(response => response.json()) + .then(response => response.results.find( + (movie)=>movie.site === "YouTube" && movie.type === "Trailer" + )) + .then((movie) => setGetTrailerKey(`https://www.youtube.com/embed/${movie.key}?autoplay=1`)) + .catch(err => console.error(err)); + },[props.data.movieID, apiKey]); + + const Genres = { + 28:"Action", + 12:"Adventure", + 16:"Animation", + 35:"Comedy", + 80:"Crime", + 99:"Documentary", + 18:"Drama", + 10751:"Family", + 14:"Fantasy", + 36:"History", + 27:"Horror", + 10402:"Music", + 9648:"Mystery", + 10749:"Romance", + 878:"Science Fiction", + 10770:"TV Movie", + 53:"Thriller", + 10752:"War", + 37:"Western", + } + function getGenres(genres){ + return genres.map((genre)=> Genres[genre]).join(", "); + } + + return( +
+
+
+
Title: {props.data.title}
+ +
+
+
+ + +
+
+
{props.data.overview}
+
Original Title: {props.data.originalTitle}
+
Genres: {getGenres(props.data.genres)}
+
Release Date: {props.data.releaseDate}
+
+
+
+ +
+ +
+
+ ) +} +export default Modal; diff --git a/src/MovieCard.css b/src/MovieCard.css new file mode 100644 index 00000000..c107b701 --- /dev/null +++ b/src/MovieCard.css @@ -0,0 +1,88 @@ +.movie-card{ + margin: 2% 0% 2% 2%; + border-radius: 10px; + margin-left: auto; + margin-right: auto; + width: 250px; + transition-duration: 0.3s; + box-shadow: 0px 0px 5px 5px rgba(255,255, 255, 0.2); + +} + +.movie-card:hover{ + transform: scale(1.1); + cursor: pointer; + background-color: #A9A9A9; +} + +.movieImage{ + width: 100%; + display: block; + border-top-right-radius: 10px; + border-top-left-radius: 10px; +} + +p{ + text-align: center; + text-wrap: wrap; + color: white; +} + +.view-details{ + border-radius: 10px; + margin-bottom: 1%; + margin-left: 1%; + width: 60%; + font-size: 70%; + background-color: blue; +} +.title{ + font-size: 90%; +} +.watch-button{ + background: none; + border: none; + font-size: 1em; + cursor: pointer; + color: rgba(255,255, 255, 0.5); + transition: color 0.3s; + color:white; +} +.watch-button.watched{ + color: gold; +} +.heart{ + background: none; + border: none; + font-size: 1em; + cursor: pointer; + color: rgba(255,255, 255, 0.5); + transition: color 0.3s; + color:white; +} +.heart.clicked{ + color: red; +} + +.button { + font-family: "Poppins"; + border-radius: 10px; + margin: .5vh 1vh; + font-size: 110%; + background-color: rgba(255, 255, 255, 0.2); + border: none; + color: white; + backdrop-filter: blur(5px); + + transition-duration: .5s; + } + .button:hover { + background-color: rgba(255, 82, 122, .6); + + } +/* .movie-card-information{ + box-shadow: inset 0 0 2000px rgba(0, 0, 0, .8); + backdrop-filter: blur(10px); + height: 20%; + border-radius: 0px 0px 20px 20px; +} */ diff --git a/src/MovieCard.jsx b/src/MovieCard.jsx new file mode 100644 index 00000000..0807d063 --- /dev/null +++ b/src/MovieCard.jsx @@ -0,0 +1,58 @@ +import './MovieCard.css'; +import React from 'react'; +import { useState } from 'react'; +import Modal from './Modal.jsx'; +import SideBar from './SideBar'; +function MovieCard(props) { + const [isModalOpen, setIsModalOpen] = useState(false); + const [liked, setLiked] = useState(false); + const [watched, setWatched] = useState(false); + function openModal() { + setIsModalOpen(true); + } + function closeModal() { + setIsModalOpen(false); + } + const likeButton = () =>{ + setLiked(prevState => !prevState); + } + const watchButton = () =>{ + setWatched(prevState => !prevState); + } + return ( + <> +
+ Movie Image +
+

{props.title}

+

{props.rating}

+ + + + +
+ +
+
+
+ {isModalOpen && } + + + ) +} +export default MovieCard; diff --git a/src/MovieList.css b/src/MovieList.css new file mode 100644 index 00000000..8ebfc7ea --- /dev/null +++ b/src/MovieList.css @@ -0,0 +1,5 @@ +.movieCards{ + display: flex; + flex-wrap: wrap; + margin-top: 3%; +} diff --git a/src/MovieList.jsx b/src/MovieList.jsx new file mode 100644 index 00000000..28bb552d --- /dev/null +++ b/src/MovieList.jsx @@ -0,0 +1,42 @@ +import './MovieList.css'; +import MovieCard from './MovieCard.jsx'; +import SideBar from './SideBar'; +function MovieList(props) { + const toggleLikeList = (movieID, isRemove) => { + if(isRemove){ + props.setLikedMovies((prev) => { + return props.likedMovies.filter((movieDataID) => movieDataID !== movieID); + }) + } + else{ + props.setLikedMovies((prev) => { + return [...props.likedMovies, movieID]; + }); + } + } + function createMovieCards(card, i) { + return ( + props.setFavoriteMovies(card.id)} + setWatchedMovies={() => props.setWatchedMovie(card.id)} + /> + ) + } + return ( + <> +
+ {props.data.map((v, i) => createMovieCards(v, i))} +
+ + ) +} +export default MovieList; diff --git a/src/SideBar.css b/src/SideBar.css new file mode 100644 index 00000000..21ea77b1 --- /dev/null +++ b/src/SideBar.css @@ -0,0 +1,26 @@ +.sidebar{ + position: fixed; + height: 100%; + width: 20%; + background-color: rgba(255, 255, 255, .15); + backdrop-filter: blur(5px); + color: white; + } + +/* .favorite-cards{ + background-size: 100%; + width: 80%; + height: 10vh; + text-align: left; + border-radius: 6px; + margin-top: 2%; + padding: 1% 4%; + box-shadow: inset 0 0 0 100px rgba(0, 0, 0, 0.6); +} */ +.title-liked{ + width: 100%; + margin: 0% 4%; +} +.texts{ + text-align: center; +} diff --git a/src/SideBar.jsx b/src/SideBar.jsx new file mode 100644 index 00000000..c8037665 --- /dev/null +++ b/src/SideBar.jsx @@ -0,0 +1,30 @@ +import './SideBar.css' +import { useEffect, useState } from 'react'; +import MovieCard from './MovieCard'; + +function SideBar(props){ + function MovieCardForSideBar(movieID, index){ + // get selected movie from the list of movies + const movie = props.movies.find((item) => item.id === movieID); + return( + // display updated state to the user (in the browser) +
+ Movie Poster +

{movie.title}

+
+ ) + } + return( +
+
+

Favorites

+
+
+ {props.favoriteMovies.map(MovieCardForSideBar)} +
+

Watched Movies:

+ {props.watched.map(MovieCardForSideBar)} +
+ ) +} +export default SideBar; diff --git a/src/index.css b/src/index.css index e1faed1a..c241e471 100644 --- a/src/index.css +++ b/src/index.css @@ -1,7 +1,7 @@ body { + font-family: Poppins; margin: 0; - font-family: Arial, sans-serif; - background-color: #f4f4f4; + background-color: #0d0f16; } button {