diff --git a/.gitignore b/.gitignore index a547bf36..7ceb59f8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env diff --git a/index.html b/index.html index 0abcfe5f..de73b4dd 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + Flixster diff --git a/src/App.css b/src/App.css index 0bf65669..376d2ad8 100644 --- a/src/App.css +++ b/src/App.css @@ -17,12 +17,13 @@ width: 100%; } - .search-bar { + .searchBar { flex-direction: column; gap: 10px; } - .search-bar form { + .searchBar form { flex-direction: column; } } + diff --git a/src/App.jsx b/src/App.jsx index 48215b3f..ad60a4c5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,68 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import './App.css' +import Header from "./components/Header"; +import.meta.env.VITE_API_KEY; +import NowPlayingScreen from './components/NowPlayingScreen'; +import SearchScreen from './components/SearchScreen.jsx'; +import Footer from './components/Footer.jsx'; const App = () => { -
- -
+ const [isSearching, setSearching] = useState(false); + const [criteria, setCriteria] = useState(""); + + const [isFave, setFave] = useState([]); + const [isWatched, setWatched] = useState([]); + + function loadFromLocalStorage() { + let savedLists = localStorage.getItem("userLists"); + if (savedLists == undefined || savedLists == null ||savedLists == "") { + return; + } + let ListsObject = JSON.parse(savedLists); + setFave(ListsObject.favList); + setWatched(ListsObject.watchedList); + } + + /** + * If the watchlist are now empty, check for stored (usually after a page load) + * else: put the current state into localstorage + */ + useEffect(() => { + if (isWatched.length == 0 && isFave.length == 0) { + loadFromLocalStorage() + } else { + localStorage.setItem("userLists", JSON.stringify( + { + favList: isFave, + watchedList: isWatched + } + )) + } + }, [isFave, isWatched]) + + const handleNewCriteria = (criteriaFromHeader) => { + setCriteria(criteriaFromHeader) + } + + const handleSetFav = (newVal) => { + setFave(newVal); + } + + const handleSetWatched = (newVal) => { + setWatched(newVal); + } + + return ( +
+
+ {!isSearching ? + + : + + } +
+ ); } -export default App +export default App; diff --git a/src/components/FavMovies.jsx b/src/components/FavMovies.jsx new file mode 100644 index 00000000..2276bd91 --- /dev/null +++ b/src/components/FavMovies.jsx @@ -0,0 +1,10 @@ +function FavMovies(props){ + return( +
+ +

{props.title}

+
+ ) +} + +export default FavMovies \ No newline at end of file diff --git a/src/components/Footer.css b/src/components/Footer.css new file mode 100644 index 00000000..a95a4929 --- /dev/null +++ b/src/components/Footer.css @@ -0,0 +1,59 @@ +.footer { + background-color: #141414; + color: white; + padding: 20px 0; + text-align: center; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.footer-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; +} + +.footer-links { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-bottom: 20px; +} + +.footer-link { + color: #e50914; + margin: 0 10px; + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-link:hover { + color: #ff1e1e; +} + +.footer-social { + margin-bottom: 20px; +} + +.social-link { + color: white; + margin: 0 10px; + text-decoration: none; + font-size: 18px; + transition: color 0.3s ease; +} + +.social-link:hover { + color: #e50914; +} + +.footer-bottom { + margin-top: 20px; +} + +.footer-bottom p { + margin: 0; + font-size: 14px; + color: #888; +} \ No newline at end of file diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx new file mode 100644 index 00000000..9e03c515 --- /dev/null +++ b/src/components/Footer.jsx @@ -0,0 +1,29 @@ +import './Footer.css'; + +const Footer = () => { + return ( + + ); +}; + +export default Footer; \ No newline at end of file diff --git a/src/components/Header.css b/src/components/Header.css new file mode 100644 index 00000000..f85f9aa9 --- /dev/null +++ b/src/components/Header.css @@ -0,0 +1,44 @@ +.header { + background-color:red; + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; + color: white; +} + +.header h1 { + margin: 0; +} + +.toggleButton { + display: flex; + align-items: center; +} + +.header input { + padding: 5px; + font-size: 16px; + border: none; + border-radius: 5px; + margin-right: 10px; +} + +.header button { + background-color: white; + color: #ff0000; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + margin-right: 10px; +} + +.header .sortBy { + background-color: #ff0000; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 00000000..30dc11bb --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,21 @@ +import './Header.css'; + +function Header({setSearching,sortMovies }) { + + return ( +
+

Flixster

+
+ + + +
+
+ ); +} + +export default Header; \ No newline at end of file diff --git a/src/components/ModalContainer.css b/src/components/ModalContainer.css new file mode 100644 index 00000000..d39e5820 --- /dev/null +++ b/src/components/ModalContainer.css @@ -0,0 +1,86 @@ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + backdrop-filter: blur(5px); + overflow: auto; + background: rgba(0, 0, 0, 0.75); +} + +.modalContent { + background: #141414; + margin: auto; + margin-top: 10%; + padding: 20px; + border-radius: 10px; + width: 50%; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + position: relative; + color: white; + overflow: auto; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.modalContent div { + margin-bottom: 10px; +} + +.closeButton { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: white; +} + +.modalBody { + margin-top: 20px; +} + +.modalBody div { + margin-bottom: 10px; + font-size: 16px; +} + +.modalBody img { + border-radius: 5px; +} + +.modalBody div:first-child { + font-size: 24px; + font-weight: bold; + color: #e50914; +} + +.modalBody div:nth-child(3) { + font-size: 18px; + color: #e50914; +} + +.modalContent div:nth-child(4){ + color:white; +} + +.modalContent div:nth-child(5){ + color:white; +} + +button { + background-color: #e50914; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + font-weight: bold; +} + +button:hover { + background-color: #ff1e1e; +} \ No newline at end of file diff --git a/src/components/ModalContainer.jsx b/src/components/ModalContainer.jsx new file mode 100644 index 00000000..35948180 --- /dev/null +++ b/src/components/ModalContainer.jsx @@ -0,0 +1,53 @@ +import "./ModalContainer.css"; +import {useEffect, useState} from "react"; + +function ModalContainer(props){ + const[trailerUrl, setTrailerUrl]= useState("") + const apiKey = import.meta.env.VITE_API_KEY; + + useEffect(()=> { + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkYWU5YTZiOThlMTBjZDkyZTcxN2Y4OWIzZDYxYjdjNSIsInN1YiI6IjY2NjY1MTQ1Y2M3MDc0ZDliNjFjMWM2ZiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.IDf1-04fWbMoc-zzed3BAcZLflL14UG-mdjcZobVjxA' + } + }; + + fetch(`https://api.themoviedb.org/3/movie/${props.data.id}/videos?api_key=${apiKey}`, options) + .then(response => response.json()) + .then(response => response.results.find( + (video) => video.site === "YouTube" && video.type === "Trailer" + )) + .then((trailer) => setTrailerUrl(`https://www.youtube.com/embed/${trailer.key}`)) + .catch(err => console.error(err)); + }, [props.data.id, apiKey]) + + + return( +
+
+ +
+
Title:{props.data.title}
+ +
Original Title: {props.data.title}
+
Overview: {props.data.overview}
+
Release Date: {props.data.releaseDate}
+
+ + +
+
+ ); +} + +export default ModalContainer; \ No newline at end of file diff --git a/src/components/MovieCards.jsx b/src/components/MovieCards.jsx new file mode 100644 index 00000000..38f276de --- /dev/null +++ b/src/components/MovieCards.jsx @@ -0,0 +1,49 @@ +import "./movieCards.css" +import { useState } from "react"; +import ModalContainer from "./ModalContainer"; + + +function MovieCard (props) { + const[isModalOpen, setModal]=useState(false) + const[isFav, setIsFave] = useState(false); + const[watched, setWatched]= useState(false) + + function openModal(){ + setModal(true) + } + function closeModal() + { + setModal(false) + } + + return( + <> +
+ +

{props.title}

+

Rating: {props.rating}

+
+
+ +
+
+ +
+
+
+ {isModalOpen ? : null} + + ); +} + +export default MovieCard; \ No newline at end of file diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx new file mode 100644 index 00000000..d3075bf9 --- /dev/null +++ b/src/components/MovieList.jsx @@ -0,0 +1,29 @@ +import MovieCards from "./MovieCards" +import "./movieCards.css" + +function MovieList(props){ + function createCards(card, index){ + return( + props.favMovies(card.id)} + watchedMovies={() =>props.watchedMovies(card.id)} + /> + ) + } + + return( +
+ {props.data.map(createCards)} +
+ ) +} + +export default MovieList; diff --git a/src/components/NowPlayingScreen.css b/src/components/NowPlayingScreen.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/NowPlayingScreen.jsx b/src/components/NowPlayingScreen.jsx new file mode 100644 index 00000000..b9495c0a --- /dev/null +++ b/src/components/NowPlayingScreen.jsx @@ -0,0 +1,84 @@ +/* eslint-disable react/prop-types */ +import {useEffect, useState} from "react"; +import MovieList from "./MovieList"; +import SideBar from "./SideBar"; +const ACCESS_TOKEN = import.meta.env.VITE_ACCESS_TOKEN; + + +function NowPlayingScreen({criteriaFromHeader, setFave, isFave, setWatched, isWatched}){ + const [movies, setMovies]=useState([]); + const [pageNumber, setPageNumber]=useState(1); + const[url, setUrl]=useState(`https://api.themoviedb.org/3/movie/now_playing?language=en-US&page=${pageNumber}`) + + function addToFave(movieId){ + if(isFave.includes(movieId)){ + setFave(prevIds => prevIds.filter(prevId => prevId !== movieId)) + + } else{ + setFave(prevId => [...prevId, movieId]) + } + } + + function addToWatched(movieId){ + if(isWatched.includes(movieId)){ + setWatched(prevIds => prevIds.filter(prevId => prevId !== movieId)) + + } else { + setWatched(prevId => [...prevId, movieId]) + } + } + + useEffect(()=>{ + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: `Bearer ${ACCESS_TOKEN}` + } + }; + + fetch(url, options) + .then(response => response.json()) + .then(response => { + handleSortMovies(criteriaFromHeader, movies.concat(response.results)) + + }) + .catch(err => console.error(err)); + }, [url, pageNumber]); + + function loadMore(){ + setPageNumber(prevPageNumber => prevPageNumber+1) + setUrl(`https://api.themoviedb.org/3/movie/now_playing?language=en-US&page=${pageNumber + 1}`); + } + + function handleSortMovies(criteria, parmaMovies = movies){ + + const sortedMovies = [...parmaMovies].sort((a, b) => { + + if (criteria === 'title') { + return a.title.localeCompare(b.title); + } else if (criteria === 'date') { + return new Date(a.release_date) - new Date(b.release_date); + } else if (criteria === 'rating') { + return b.vote_average - a.vote_average; + } + return 0; + }); + setMovies(sortedMovies); + } + + useEffect(function() { + handleSortMovies(criteriaFromHeader) + }, [criteriaFromHeader]) + + return( +
+ + + +
+ + ) +} + +export default NowPlayingScreen; \ No newline at end of file diff --git a/src/components/SearchScreen.jsx b/src/components/SearchScreen.jsx new file mode 100644 index 00000000..8d016065 --- /dev/null +++ b/src/components/SearchScreen.jsx @@ -0,0 +1,70 @@ +/* eslint-disable react/prop-types */ +import { useState} from "react"; +import MovieList from "./MovieList"; +import SideBar from "./SideBar"; + +function SearchScreen({setFave, isFave, setWatched, isWatched}){ + const[query, setSearchQuery]= useState('') + const[movies, setMovies]= useState([]); + const[pageNumber, setPageNumber]= useState(1) + + + function addToFave(movieId){ + if(isFave.includes(movieId)){ + setFave(prevIds => prevIds.filter(prevId => prevId !== movieId)) + + } else{ + setFave(prevId => [...prevId, movieId]) + } + } + + function addToWatched(movieId){ + if(isWatched.includes(movieId)){ + setWatched(prevIds => prevIds.filter(prevId => prevId !== movieId)) + + } else { + setWatched(prevId => [...prevId, movieId]) + } + } + + + + const ACCESS_TOKEN = import.meta.env.VITE_ACCESS_TOKEN; + + function handleSearch(searchQuery){ + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: `Bearer ${ACCESS_TOKEN}` + } + }; + + fetch(`https://api.themoviedb.org/3/search/movie?query=${searchQuery}&include_adult=false&language=en-US&page=${pageNumber}`, options) + .then(response => response.json()) + .then(response => setMovies(response.results)) + .catch(err => console.error(err)); + } + + function loadMore(){ + setPageNumber(prevPageNumber => prevPageNumber+1) + } + + return( +
+ + setSearchQuery(e.target.value.toLowerCase())} + /> + + + + +
+ ) +} + +export default SearchScreen; \ No newline at end of file diff --git a/src/components/SideBar.jsx b/src/components/SideBar.jsx new file mode 100644 index 00000000..97037543 --- /dev/null +++ b/src/components/SideBar.jsx @@ -0,0 +1,65 @@ +/* eslint-disable react/prop-types */ +import "./sideBar.css"; +import FavMovies from "./FavMovies"; +import { useEffect, useState } from "react"; + +function SideBar(props) { + const [favMovieElements, setFavMovieElements] = useState([]); + const [watchedMovieElements, setWatchedMovieElements] = useState([]); + + async function getMovie(movieId) { + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkYWU5YTZiOThlMTBjZDkyZTcxN2Y4OWIzZDYxYjdjNSIsInN1YiI6IjY2NjY1MTQ1Y2M3MDc0ZDliNjFjMWM2ZiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.IDf1-04fWbMoc-zzed3BAcZLflL14UG-mdjcZobVjxA' + } + }; + + let response = await fetch(`https://api.themoviedb.org/3/movie/${movieId}`, options) + response = response.json(); + return response; + } + + async function MovieCard(movieId) { + + + const movieIndex = props.movies.findIndex((item) => item.id === movieId) + let movie; + if (movieIndex == -1) { + movie = await getMovie(movieId); + } else { + movie = props.movies[movieIndex] + } + + return movie; + } + + + useEffect(() => { + Promise.all(props.fave.map((id) => (MovieCard(id)))).then((values) => { + setFavMovieElements(values) + }); + Promise.all(props.watched.map((id) => (MovieCard(id)))).then((values) => { + setWatchedMovieElements(values) + }); + + + }, [props.watched, props.fave]) + + return ( +
+

Favorites

+ {favMovieElements.map((movie, index) => { + return () + })} +

Watched

+ {watchedMovieElements.map((movie, index) => { + return () + })} + +
+ ); +}; + +export default SideBar; \ No newline at end of file diff --git a/src/components/movieCards.css b/src/components/movieCards.css new file mode 100644 index 00000000..26cb8650 --- /dev/null +++ b/src/components/movieCards.css @@ -0,0 +1,51 @@ +.movieCard { + background-color: #ff0000; + color: white; + width: 200px; + margin: 10px; + padding: 10px; + border-radius: 10px; + text-align: center; +} +.movieCard:hover { + transform: scale(1.0); + box-shadow: 0 7px 14px black; + +} +.movieCard img { + width: 100%; + border-radius: 10px; +} + +.favoriteWatchedIcon{ + display:flex; +} +.favoriteWatchedIcon button{ + background-color:red; + cursor: pointer; + font-size: 16px; + font-weight: bold; + transition: background-color 0.3s ease; +} +.movieCard h2 { + font-size: 18px; + margin: 10px 0 5px; +} + +.movieCard p { + margin: 5px 0; +} + +.movieList { + display: flex; + flex-wrap: wrap; + justify-content: center; + padding: 20px; + margin-left: 13vw; +} + +.favoriteIcon { + cursor: pointer; + font-size: 15px; + color: #ffd700; /* Gold color for favorite star */ + } \ No newline at end of file diff --git a/src/components/sideBar.css b/src/components/sideBar.css new file mode 100644 index 00000000..f7f370fd --- /dev/null +++ b/src/components/sideBar.css @@ -0,0 +1,15 @@ +.sidebar { + height: 100vh; + width: 250px; + position: fixed; + top: 0; + left: 0; + background-color: red; + padding-top: 20px; + overflow: auto; + } + + .sidebar h2 { + color: white; + text-align: center; + } \ No newline at end of file