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}
+
+
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)}>
+
+
+
+
+
{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
+ <>
+
+
+
+ {data.length > 0 ? (
+ data.map((movie) => (
+
toggleModal(movie.id)}
+ seen={seenMovies.some((m) => m.id === movie.id)}
+ loved={lovedMovies.some((m) => m.id === movie.id)}
+ toggleSeen={() =>
+ toggleSeen({ id: movie.id, title: movie.title })
+ }
+ toggleLoved={() =>
+ toggleLoved({ id: movie.id, title: movie.title })
+ }
+ />
+ ))
+ ) : (
+ No movies found...
//error handle if data length == 0
+ )}
+ {data.length > 0 && (
+
+ )}
+
+ {showModal && MovieInfo && (
+ setShowModal(false)}
+ title={MovieInfo.title}
+ poster={`https://image.tmdb.org/t/p/original${MovieInfo.poster_path}`}
+ release={MovieInfo.release_date}
+ overview={MovieInfo.overview}
+ genres={genre_convert(MovieInfo.genres.map((genre) => genre.id))}
+ trailer={trailer}
+ />
+ )}
+ >
+ );
+};
+export default MovieList;
diff --git a/src/Nav.css b/src/Nav.css
new file mode 100644
index 00000000..882c6955
--- /dev/null
+++ b/src/Nav.css
@@ -0,0 +1,44 @@
+.nav-bar {
+ display: flex;
+ justify-content: space-between; /* Distributes space between and around content items */
+ align-items: center; /* Align items vertically in the center */
+ padding: 5px; /* Add some padding around the nav-bar */
+ }
+
+ .search-area {
+ flex-grow: 1; /* Allows the search area to take up any remaining space */
+ display: flex;
+ justify-content: center; /* Center the search bar when it appears */
+ }
+
+ .sort-menu {
+ width: 150px; /* Set a fixed width for the dropdown */
+ }
+
+ .button-group {
+ display: flex;
+ gap: 5px; /* Space between buttons */
+ }
+
+ .sort-filter-group {
+ display: flex;
+ gap: 10px; /* Space between sort and filter dropdowns */
+ }
+
+
+ @media (max-width: 600px) {
+ .nav-bar {
+ flex-direction: column; /* Stack elements vertically */
+ align-items: stretch; /* Stretch items to fill the width */
+ }
+ .button-group, .search-area, .sort-filter-group {
+ flex-direction: column; /* Stack buttons and inputs vertically */
+ align-items: center; /* Center-align the items */
+ width: 100%; /* Full width */
+ margin: 5px 0; /* Add some vertical spacing */
+ }
+ .sort-menu, .search-area input, .search-area button {
+ width: 100%; /* Full width for select elements and input */
+ text-align: center;
+ }
+ }
\ No newline at end of file
diff --git a/src/Nav.jsx b/src/Nav.jsx
new file mode 100644
index 00000000..a1527561
--- /dev/null
+++ b/src/Nav.jsx
@@ -0,0 +1,114 @@
+import { useState } from "react";
+import "./Nav.css";
+
+//Purpose: create nav bar to sort,search,filter info
+const Nav = ({
+ setShowSearch,
+ setData,
+ updateSearchText,
+ sortBy,
+ updatePage,
+ updateLastAction,
+ fetchData,
+ showSearch,
+ searchText,
+ performSearch,
+ applySort,
+ updateGenreFilter,
+}) => {
+ const [action, setAction] = useState("now_showing"); // update last used action
+
+ const handleSortChange = (newSortString) => {
+ applySort(newSortString);
+ };
+
+ const handleGenreChange = (newGenre) => {
+ setData([]);
+ updateGenreFilter(newGenre);
+ };
+
+ const clearInfo = (newAction) => {
+ if (newAction === action) {
+ return;
+ }
+ setShowSearch(!showSearch);
+ setData([]);
+ setAction(newAction);
+ };
+
+ const resetVariables = () => {
+ updateSearchText("");
+ sortBy("");
+ updatePage(1);
+ };
+
+ const activateNowShowing = () => {
+ updateLastAction("now_showing");
+ fetchData();
+ };
+
+ return (
+
+ );
+};
+
+export default Nav;
diff --git a/src/SideBar.css b/src/SideBar.css
new file mode 100644
index 00000000..eeceab1a
--- /dev/null
+++ b/src/SideBar.css
@@ -0,0 +1,60 @@
+/* SideBar.css */
+.w3-sidebar {
+ height: 100%;
+ width: 12%; /* Fixed width for the sidebar */
+ position: fixed;
+ z-index: 1;
+ top: 0;
+ left: -250px;
+ background-color: lightseagreen;
+ overflow-x: hidden;
+ transition: left 0.3s ease;
+ padding-top: 60px;
+}
+.w3-sidebar.open {
+ left: 0; /* Bring sidebar on-screen */
+}
+
+#side-bar-close {
+ margin-bottom: 10px;
+}
+
+ .w3-bar-item {
+ padding: 8px 16px;
+ font-size: 25px;
+ color: white;
+ display: block;
+ transition: 0.3s;
+ }
+
+ .w3-bar-item:hover {
+ color: #f1f1f1;
+ }
+
+ .dropdown {
+ padding: 8px;
+ margin-bottom: 10px;
+ cursor: pointer;
+ }
+ .dropdown-content {
+ display: block;
+ margin-top: 2px;
+ padding: 5px;
+ background-color: #CBC3E3;
+ }
+ .dropdown-content a {
+ display: block;
+ padding: 5px;
+ border: solid black;
+ text-decoration: none;
+ color: black;
+ }
+ .dropdown-content a:hover {
+ background-color: #ddd;
+ }
+
+ @media (max-width: 600px) {
+ .w3-sidebar {
+ width: 16%;
+ }
+ }
\ No newline at end of file
diff --git a/src/SideBar.jsx b/src/SideBar.jsx
new file mode 100644
index 00000000..0947461a
--- /dev/null
+++ b/src/SideBar.jsx
@@ -0,0 +1,57 @@
+import React, { useState } from "react";
+import "./SideBar.css";
+//purpose: add sidebar to hold liked and seen movies
+const SideBar = ({
+ lovedMovies,
+ seenMovies,
+ onMovieSelect,
+ toggleSidebar,
+ isSidebarOpen,
+}) => {
+ const [isDropdownOneOpen, setDropdownOneOpen] = useState(false);
+ const [isDropdownTwoOpen, setDropdownTwoOpen] = useState(false);
+ const toggleDropdownOne = () => {
+ setDropdownOneOpen(!isDropdownOneOpen);
+ };
+ const toggleDropdownTwo = () => {
+ setDropdownTwoOpen(!isDropdownTwoOpen);
+ };
+
+ return (
+
+
+
+
+ {isDropdownOneOpen && (
+
+ )}
+
+
+
+ {isDropdownTwoOpen && (
+
+ )}
+
+
+ );
+};
+
+export default SideBar;
diff --git a/src/index.css b/src/index.css
index e1faed1a..6a4ed65a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,7 +1,7 @@
body {
margin: 0;
font-family: Arial, sans-serif;
- background-color: #f4f4f4;
+ background-color: #CBC3E3;
}
button {
diff --git a/src/main.jsx b/src/main.jsx
index 54b39dd1..b91620d3 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,10 +1,10 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App.jsx'
-import './index.css'
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App.jsx";
+import "./index.css";
-ReactDOM.createRoot(document.getElementById('root')).render(
+ReactDOM.createRoot(document.getElementById("root")).render(
- ,
-)
+
+);