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/src/App.css b/src/App.css index 0bf65669..e191acb3 100644 --- a/src/App.css +++ b/src/App.css @@ -3,13 +3,32 @@ } .App-header { - background-color: #282c34; + background-color: #880ac2; display: flex; flex-direction: row; align-items: center; - justify-content: space-evenly; color: white; - padding: 20px; + padding: 10px 20px; + +} +.title { + display: flex; + align-items: center; + height: 100px; + justify-content: center; + flex-grow: 1; +} + +.title h1 { + font-size: 50px; +} +@media (max-width: 600px) { + .title h1 { + font-size: 30px; / + } +} +.title img { + height: 60px; } @media (max-width: 600px) { @@ -26,3 +45,37 @@ flex-direction: column; } } + +.line { + width: 35px; + height: 5px; + background-color: black; + margin: 3px 0; +} + +.btn-wrapper { + display: flex; + align-items: flex-start; + flex-direction: column; /* Aligns the lines vertically */ + justify-content: center; /* Center lines vertically within the btn-wrapper */ + align-items: flex-start; +} + +.btn-wrapper:hover { + cursor: pointer; +} + +.App-footer { + background-color: #880ac2; + display: flex; + flex-direction: row; + align-items: center; + color: white; + padding: 10px 20px; +} +.App-footer a { + display: flex; + justify-content: center; + flex-grow: 1; + color: white; +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 48215b3f..b3e13d47 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,36 @@ -import { useState } from 'react' -import './App.css' +// App.jsx +import React, { useState } from "react"; +import "./App.css"; +import MovieList from "./MovieList"; const App = () => { -
- -
-} + const [isSidebarOpen, setSidebarOpen] = useState(false); + const toggleSidebar = () => { + setSidebarOpen(!isSidebarOpen); + }; + return ( +
+
+ {!isSidebarOpen && ( +
+
+
+
+
+ )} +
+

Flixster

+ +
+
+ + +
+ ); +}; -export default App +export default App; diff --git a/src/Modal.css b/src/Modal.css new file mode 100644 index 00000000..eca113a3 --- /dev/null +++ b/src/Modal.css @@ -0,0 +1,45 @@ + +.modal-overlay { + box-sizing: border-box; + position:fixed; + z-index: 1; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.4); + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; +} + +.modal-content { + display: flex; + flex-direction: column; + align-items: center; + background-color: #fefefe; + margin: 10% auto; + padding: 20px; + border: 1px solid #888; + width: 50%; + height: auto; + max-height: 80%; + } + + +#close-modal { + flex-shrink: 0; + font-size: 25px; + cursor: pointer; + margin-left: auto; + justify-content: flex-end; + + } + + .modal-content img { + width: 150px; + + } \ No newline at end of file diff --git a/src/Modal.jsx b/src/Modal.jsx new file mode 100644 index 00000000..4be56a56 --- /dev/null +++ b/src/Modal.jsx @@ -0,0 +1,36 @@ +import "./Modal.css"; +const Modal = ({ + isOpen, + onClose, + title, + poster, + release, + overview, + genres, + trailer, +}) => { + const modalStyle = { display: isOpen ? "flex" : "none" }; + return ( +
+
+ + × + +

{title}

+ movie-poster +

Release date: {release}

+

Overview: {overview}

+

Genres: {genres}

+ +
+
+ ); +}; +export default Modal; diff --git a/src/MovieCard.css b/src/MovieCard.css new file mode 100644 index 00000000..7039c5e0 --- /dev/null +++ b/src/MovieCard.css @@ -0,0 +1,63 @@ +.card { + display: flex; + flex-direction: column; + width: 200px; + height: 375px; + border: solid black; + background-color: white; + margin: 10px; + box-shadow: 12px 12px 2px 1px rgba(0, 0, 0, 0.582); + position: relative; + top: 0; + transition: top ease 0.5s; +} +.card:hover { + top: -15px; + cursor: pointer; +} + +.img-container { + width: 100%; + height: 80%; + background-color: #ccc; + overflow: contain; +} + +.img-container img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.txt-container { + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 20%; +} + +.txt-container h2, .txt-container p { + font-size: 15px; + margin: 0%; +} + +.sub-txt { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.icons { + display: flex; + gap: 5px; + position: absolute; + left: 10px; /* Position icons to the left inside the card */ +} + +.rating { + flex-grow: 1; + text-align: center; +} \ No newline at end of file diff --git a/src/MovieCard.jsx b/src/MovieCard.jsx new file mode 100644 index 00000000..e5614a43 --- /dev/null +++ b/src/MovieCard.jsx @@ -0,0 +1,43 @@ +import "./MovieCard.css"; + +const MovieCard = ({ + movieId, + title, + poster, + rating, + clickHandler, + seen, + loved, + toggleSeen, + toggleLoved, +}) => { + const handleToggleSeen = (e) => { + e.stopPropagation(); + toggleSeen(movieId); + }; + + const handleToggleLoved = (e) => { + e.stopPropagation(); + toggleLoved(movieId); + }; + + return ( +
clickHandler(movieId)}> +
+ Movie Poster +
+
+

{title}

+
+
+

{loved ? "❤️" : "🤍"}

+

{seen ? "📖" : "📘"}

+
+

{rating}⭐️

+
+
+
+ ); +}; + +export default MovieCard; diff --git a/src/MovieList.css b/src/MovieList.css new file mode 100644 index 00000000..4f75a516 --- /dev/null +++ b/src/MovieList.css @@ -0,0 +1,16 @@ +.movie-row { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; /* Center cards horizontally */ + padding: 20px; + width: 75%; + margin: 0 auto; + gap: 2%; + z-index: 0; +} + +.load-btn { + width: 100%; + float: left; + margin-top: 30px; +} \ No newline at end of file diff --git a/src/MovieList.jsx b/src/MovieList.jsx new file mode 100644 index 00000000..7e219587 --- /dev/null +++ b/src/MovieList.jsx @@ -0,0 +1,201 @@ +import { useState, useEffect } from "react"; +import "./MovieList.css"; +import MovieCard from "./MovieCard"; +import Modal from "./Modal"; +import Nav from "./Nav"; +import SideBar from "./SideBar"; + +// Purpose: Create card elements from API info +export const MovieList = ({ isSidebarOpen, toggleSidebar }) => { + const [data, setData] = useState([]); + const [page, setPage] = useState(1); + const [searchText, setSearchText] = useState(""); + const [showModal, setShowModal] = useState(false); + const [MovieInfo, setMovieInfo] = useState(null); + const [sortString, setSortString] = useState(""); + const [lastAction, setLastAction] = useState(""); + const [showSearch, setShowSearch] = useState(false); + const [genre, setGenreFilter] = useState(""); + const [seenMovies, setSeenMovies] = useState([]); + const [lovedMovies, setLovedMovies] = useState([]); + const [trailer, setTrailer] = useState(""); + + const options = { + method: "GET", + headers: { + accept: "application/json", + Authorization: `Bearer ${import.meta.env.VITE_TOKEN}`, //private token used to access api + }, + }; + + const toggleSeen = (movie) => { + setSeenMovies((prev) => { + const isAlreadySeen = prev.some((m) => m.id === movie.id); + return isAlreadySeen + ? prev.filter((m) => m.id !== movie.id) + : [...prev, movie]; + }); + }; + + const toggleLoved = (movie) => { + setLovedMovies((prev) => { + const isAlreadyLoved = prev.some((m) => m.id === movie.id); + return isAlreadyLoved + ? prev.filter((m) => m.id !== movie.id) + : [...prev, movie]; + }); + }; + + const applySort = (newSortString) => { + setLastAction("sort"); + setSortString(newSortString); // Update sort state + setPage(1); // Reset to the first page + setGenreFilter(""); // Clear the genre filter + }; + + const performSearch = () => { + if (searchText === "") { + return; + } + setLastAction("search"); + setSortString(""); + setGenreFilter(""); + setPage(1); + setData([]); + fetchData(); + }; + + const changePage = () => { + setPage( + (prevPage) => prevPage + 1, + () => { + fetchData(); + } + ); + }; + + const toggleModal = async (movie_id) => { + const url = `https://api.themoviedb.org/3/movie/${movie_id}?language=en-US&append_to_response=videos`; + const movieInfoResponse = await fetch(url, options); + const movie = await movieInfoResponse.json(); + setMovieInfo(movie); + let trailerKey = ""; + for (let trailer of movie.videos.results) { + if (trailer.type === "Trailer") { + trailerKey = trailer.key; + break; + } + } + setTrailer(`https://www.youtube.com/embed/${trailerKey}`); + setShowModal((prev) => !prev); + }; + + const genre_convert = (ids) => { + if (!MovieInfo || !MovieInfo.genres || !ids) { + return ""; + } + return ids + .map((id) => MovieInfo.genres.find((genre) => genre.id === id)?.name) + .filter((name) => name) // filter out any undefined values + .join(", "); + }; + + useEffect(() => { + fetchData(); + }, [page, sortString, lastAction, genre]); // update on change + + const fetchData = async () => { + let baseUrl = "https://api.themoviedb.org/3"; + let url = `${baseUrl}/movie/now_playing?language=en-US&page=${page}`; + + if (genre) { + url = `${baseUrl}/discover/movie?include_adult=false&include_video=false&language=en-US&page=${page}&with_genres=${genre}`; + } else if (lastAction === "search" && searchText !== "") { + url = `${baseUrl}/search/movie?query=${searchText}&include_adult=false&language=en-US&page=${page}`; + } else if (lastAction === "sort" && sortString !== "") { + url = `${baseUrl}/discover/movie?include_adult=false&include_video=false&language=en-US&page=${page}&sort_by=${sortString}`; + } + + try { + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + if (page === 1) { + setData(data.results); + } else { + setData((prevData) => [...prevData, ...data.results]); // Append new data to existing data + } + } catch (error) { + console.error("Fetching data failed:", error); + } + }; + return ( + //creates movie card element + <> +