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