diff --git a/.env b/.env new file mode 100644 index 00000000..3d6431d5 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_KEY=25269b1222c2d59b0848b099daeb7d9b \ No newline at end of file diff --git a/README.md b/README.md index f768e33f..151fce6a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,67 @@ -# React + Vite +Submitted by: Marvin Howell-Aguirre -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Estimated time spent: 23.5 hours spent in total -Currently, two official plugins are available: +Deployed Application (optional): [Flixster Deployed Site](https://flixster-project.onrender.com/) -- [@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 +### 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. +- [] **Sidebar** + - [ ] Users can open a sidebar + - [ ] The sidebar displays the user's favorited and watched movies + +### Walkthrough Video + + +https://github.com/Mar1789/flixster-starter/assets/116681148/254312c2-9aff-453e-922d-ba618c04d158 + +### 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 topics discussed in my labs did prepare me to complete the assignment, but it only prepared me with useState and useEffect. I wish the labs went over passing variables to sibling components. + +* 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. + +I think I would not have change my code differently since my project runs well and completes the core features and almost all of the stretch features. I configured it so anything to modify the movie cards would reuse the same useEffect property. + +* 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 believe what went well was showing the movie cards and adding a footer and header. What did not go as planned was setting up the modal since I originally thought it would be the same process as the week 1 project, but I had to learn about passing variables between sibling components to fix the issue. + +### Open-source libraries used + +N/A + +### Shout out + +I would like to give a shout out to George for helping me debug my code and taught me how I can improve my skills on fixing my errors. I also would like to shout out Theo for helping me fix my like button stretch feature error I had when I imported my project into render. diff --git a/package.json b/package.json index eded5715..668c0ff5 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "start": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" diff --git a/public/heart1.png b/public/heart1.png new file mode 100644 index 00000000..c4a739f8 Binary files /dev/null and b/public/heart1.png differ diff --git a/public/heart2.png b/public/heart2.png new file mode 100644 index 00000000..b4e8b64e Binary files /dev/null and b/public/heart2.png differ diff --git a/src/assets/react.svg b/public/react.svg similarity index 100% rename from src/assets/react.svg rename to public/react.svg diff --git a/src/App.css b/src/App.css index 0bf65669..52ed34e2 100644 --- a/src/App.css +++ b/src/App.css @@ -1,9 +1,11 @@ .App { text-align: center; + background-color: #282c34; } + .App-header { - background-color: #282c34; + background-color: black; display: flex; flex-direction: row; align-items: center; @@ -12,17 +14,246 @@ padding: 20px; } -@media (max-width: 600px) { - .movie-card { - width: 100%; - } +.image{ + width: 265px; +} +.img{ + position: relative; + top: -280px; + right: -300px; + width: 200px; +} + +.movie-container{ + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 30px; + width: 100%; + flex-direction:row; +} + +form{ + height: 200px; + margin-bottom: -100px; +} +.load{ + border-radius: 30px; + width: 600px; + height: 300px; + margin-top: 30px; + margin-bottom: 30px; + border-radius: 30px; + width: 326px; + height: 46px; +} + +input{ + border-radius: 20px; + position: relative; + margin-left: 0px; + width: 190px; + height: 11% ; +} +h2{ + font-size: 60px; + margin: 0; + margin-bottom: 28px; + color: white; + height: 100%; +} +.search{ + position: relative; + margin-left: 14px; + border-radius: 20px; + width: 190px; + height: 19%; +} +.reset{ + position: relative; + margin-left: 14px; + top: -100px; + right: -400px; + border-radius: 20px; + width: 170px; + height: 38px; +} +.title{ + position: relative; + top: -301px; + right: -300px; + font-size: 30px; +} +.overview{ + position: relative; + top: -270px; + right: -650px; + width: 700px; +} +.genre{ + position: absolute; + top: 500px; + left:50px; + width: 550px; +} +.release-date{ + position: absolute; + top: 460px; + left: 150px ; +} +#modal { + display: block; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ +} +.modal-content { + top: -97px; + grid-auto-flow: column; + position: relative; + background-color: #404753; + text-transform: capitalize; + color: #fff; + font-size: 20px; + margin: 15% auto; /* 15% from the top and centered */ + padding: 2px; + height: 69%; + border-radius: 20.7px; + border: 2px solid white; + width: 80%; /* Could be more or less, depending on screen size */ + overflow-y: auto; + overflow-x: hidden; +} +.runtime{ + position: absolute; + top: 420px; + left: 160px; +} +.close { + top: -862px; + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} +.dropbtn { + transition: transform 250ms; + background-color: #3498DB; + color: white; + padding: 16px; + font-size: 16px; + border: none; + cursor: pointer; + +} +.dropbtn:hover, .dropbtn:focus { + background-color: #2980B9; +} + +.dropbtn:hover { + transform: translateY(-10px); + background-color: #01DF8E; +} - .search-bar { - flex-direction: column; - gap: 10px; - } +.dropdown { + text-align: center; + transition: transform 250ms; + font-size: 16px; + height: 28px; + width: 120px; + color: white; + background-color: #04AA6D; + border-radius: 20px; + position: relative; + display: inline-block; + left: -365px; + top: -95px; +} +.dropdown:hover { + transform: translateY(-10px); + background-color: #01DF8E; +} + +footer { + color: white; + background-color: #2C2C2C; + position: relative; + height: 80px; +} +footer p{ + position: relative; + top: 30px; + right: -620px; +} - .search-bar form { - flex-direction: column; - } +.dropdown-right { + text-align: center; + transition: transform 250ms; + font-size: 16px; + height: 28px; + width: 120px; + color: white; + background-color: #04AA6D; + border-radius: 20px; + position: relative; + display: inline-block; + left: 365px; + top: -97px; +} + +.dropdown-right:hover { + transform: translateY(-10px); + background-color: #01DF8E; +} + +header{ + margin-bottom: 22px; + max-height: 153px; + top: 0px; + background-color: #2C2C2C; +} + +.dropdown-content-right a{ + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} +.dropdown-content-right a:active {background-color: #f1f1f1;} +.dropdown-right:hover .dropdown-content-right {display: block;} +.dropdown-right:focus .dropbtn {background-color: #3e8e41;} + +.heart { + right: 21px; + position: absolute; + top: 0px; + width: 300px; + height: 50px; +} +.heart-image{ + position: relative; + width: 20px; + top: 419px; + left: 118px; +} +#scales{ + right: -122px; + height: 32%; + position: relative; + top: 417px; + width: 17px; } +iframe{ + position: relative; + top: 90px; + left: -400px; +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 48215b3f..6a08c8e3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,19 @@ import { useState } from 'react' import './App.css' +import React from 'react' +import MovieCard from './MovieCard' +import MovieList from './MovieList' +import Modal from './Modal' + const App = () => { + let search; + + return(
- +
+) } export default App diff --git a/src/Load.jsx b/src/Load.jsx new file mode 100644 index 00000000..aef7cf57 --- /dev/null +++ b/src/Load.jsx @@ -0,0 +1,38 @@ +import MovieCard from "./MovieCard"; +import React, { useState, useEffect } from 'react'; +import './App.css' + + +const Load = () => { + const [data, setData] = useState([]); + const [count, setCount] = useState(1); + let container = document.querySelector(".container") + + let url = 'https://api.themoviedb.org/3/movie/now_playing?language=en-US&page=' + count; + useEffect(() => { + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyNTI2OWIxMjIyYzJkNTliMDg0OGIwOTlkYWViN2Q5YiIsInN1YiI6IjY2Njc2NTkzNmI4ZGRiZDI3NGE5YmI5MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.A_LcJj_8sed0Oiyee3M9o2ZgcHKGE_jdnCA4aoAx0iY' + } + }; + fetch(url, options) + .then(response => response.json()) + .then(response => setData(response.results)) + }, [count]); + return( + + ) +} + +export default Load; \ No newline at end of file diff --git a/src/Modal.jsx b/src/Modal.jsx new file mode 100644 index 00000000..cc359041 --- /dev/null +++ b/src/Modal.jsx @@ -0,0 +1,77 @@ +import { useState, useEffect } from "react"; +const Modal = (props) =>{ + const [data, setData] = useState([]); + const [video, setVideo] = useState(""); + let id = props.query; + let url = `https://api.themoviedb.org/3/movie/${id}?language=en-US` + + useEffect(() => { + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyNTI2OWIxMjIyYzJkNTliMDg0OGIwOTlkYWViN2Q5YiIsInN1YiI6IjY2Njc2NTkzNmI4ZGRiZDI3NGE5YmI5MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.A_LcJj_8sed0Oiyee3M9o2ZgcHKGE_jdnCA4aoAx0iY' + } + }; + fetch(url, options) + .then(response => response.json()) + .then(response => { + setData(response); + }) + .catch(err => console.error(err)); + + + const info = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyNTI2OWIxMjIyYzJkNTliMDg0OGIwOTlkYWViN2Q5YiIsInN1YiI6IjY2Njc2NTkzNmI4ZGRiZDI3NGE5YmI5MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.A_LcJj_8sed0Oiyee3M9o2ZgcHKGE_jdnCA4aoAx0iY' + } + }; + fetch(`https://api.themoviedb.org/3/movie/${id}/videos`, info) + .then(response => response.json()) + .then(response => { + setVideo(response.results[1].key); + console.log(video) + }) + .catch(err => console.error(err)); + }, [id]); + + + + function handleClick() { + props.close() + } + function FilterArray(genres) { + if(genres === undefined){ + return; + } + let arr = []; + genres.map((genre) => arr.push(genre.name)); + return arr.join(", "); + } + console.log(video); + + return( + + ) +} +export default Modal; \ No newline at end of file diff --git a/src/Movie b/src/Movie new file mode 100644 index 00000000..e69de29b diff --git a/src/MovieCard.css b/src/MovieCard.css new file mode 100644 index 00000000..1652e845 --- /dev/null +++ b/src/MovieCard.css @@ -0,0 +1,228 @@ +@import 'https://fonts.googleapis.com/css?family=Do+Hyeon'; +*, +*:before, +*:after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.clear-fix:after { + display: block; + clear: both; + content: ""; +} + +.img-responsive { + max-width: 100%; + height: auto; +} + +.card { + position: relative; + display: block; + padding: 10px; + font-family: "Do Hyeon", sans-serif; + overflow: hidden; +} +.card .cards { + width: 300px; + height: 400px; + display: block; + background-size: cover; + float: left; + border-radius: 15px; + position: relative; + overflow: hidden; + background-position: center; + margin: 10px; +} +.card .cards--one { + backface-visibility: hidden; +} +.card .cards--one:hover:after { + bottom: -20px; +} +.card .cards--one:hover:before { + bottom: -10px; +} +.card .cards--one:hover .cards--one__rect { + left: 45%; +} +.card .cards--one:hover .cards--one__rect--back { + left: 50%; +} +.card .cards--one::after { + content: ""; + display: block; + position: absolute; + height: 70px; + transform: rotate(-3deg); + background: #e660e3; + position: absolute; + bottom: -80px; + left: 0; + right: -10px; + z-index: 9; + transition: all 0.2s ease-in; + transition-delay: 0.3s; +} +.card .cards--one:before { + content: ""; + display: block; + position: absolute; + height: 80px; + transform: rotate(-3deg); + bottom: -90px; + left: 0; + background: #fff; + right: -10px; + z-index: 5; + transition: all 0.2s ease-in; + transition-delay: 0.1s; +} +.card .cards--one__rect { + color: #fff; + text-transform: uppercase; + font-size: 18px; + background: #0f9bc0; + width: 126px; + height: 55px; + transform: skewY(5deg); + position: absolute; + display: block; + top: 60%; + left: -45%; + z-index: 1; + line-height: 3.3rem; + text-align: center; + transition: all 0.2s ease-in; +} +.card .cards--one__rect--back { + display: block; + background: rgba(34, 65, 154, 0.8); + width: 126px; + height: 55px; + transform: skewY(7deg); + position: absolute; + top: 65%; + left: -50%; + transition: all 0.2s ease-in; + transition-delay: 0.3s; +} +.card .cards--one__rect p { + transform: skewY(-7deg); + position: relative; +} +.card .card-movie { + position: relative; + backface-visibility: hidden; +} +.card .card-movie p { + position: absolute; + top: 83%; + left: -100%; + text-transform: capitalize; + color: #fff; + font-size: 20px; + z-index: 8; + transition: all 0.6s ease; + width: 185px; + margin-top: -5px; +} +.card .card-movie:hover p { + left: 8%; +} +.card .card-movie:hover img { + transform: translateY(-15px); +} +.card .card-movie:hover .card-movie__rect { + top: 75%; +} +.card .card-movie:hover .card-movie__rect:before { + transform: translateY(15px); +} +.card .card-movie:hover li { + transform: translateY(0); +} +.card .card-movie:hover .card-movie__tri { + right: -40%; +} +.card .card-movie:hover .card-movie__tri:before { + right: -312px; +} +.card .card-movie img { + transition: all 0.2s ease; +} +.card .card-movie__tri { + border-top: 220px solid transparent; + border-bottom: 190px solid transparent; + border-right: 288px solid #fff; + opacity: 0.9; + position: absolute; + display: block; + top: 0; + right: -100%; + transition: all 0.3s ease-in-out; +} +.card .card-movie__tri:before { + border-top: 220px solid transparent; + border-bottom: 190px solid transparent; + border-right: 288px solid #57ccfd; + position: absolute; + content: ""; + display: block; + top: -220px; + right: -612px; + transition: all 0.3s ease-in-out; + transition-delay: 0.2s; +} +.card .card-movie__rect { + width: 750px; + height: 200px; + background: #fff; + display: block; + position: absolute; + top: 175%; + left: -78%; + transform: rotate(30deg); + z-index: 5; + opacity: 0.9; + transition: all 0.3s ease-in-out; +} +.card .card-movie__rect:before { + content: ""; + display: block; + width: 100%; + position: relative; + height: 100%; + background: #f07306; + transform: translateY(200px); + z-index: 2; + transition: all 0.3s ease-in-out; + transition-delay: 0.1s; +} +.card .card-movie ul { + list-style: none; + position: absolute; + bottom: 0; + left: 10px; + z-index: 9; +} +.card .card-movie ul li { + display: inline-block; + font-size: 16px; + margin: 7px; + color: #fff; + transition: all 0.2s ease-in-out; + transform: translateY(100px); +} +.card .card-movie ul li:nth-child(2) { + transition-delay: 0.2s; +} +.card .card-movie ul li:nth-child(3) { + transition-delay: 0.3s; +} +.card .card-movie ul li:nth-child(4) { + transition-delay: 0.4s; +} \ No newline at end of file diff --git a/src/MovieCard.jsx b/src/MovieCard.jsx new file mode 100644 index 00000000..ba531f64 --- /dev/null +++ b/src/MovieCard.jsx @@ -0,0 +1,46 @@ +// import Modal from "./Modal"; +import "./MovieCard.css" +// import EmptyHeart from './heart1.png' +// import Heart from './heart2.png' +import { useState, useEffect} from "react"; + +const MovieCard = (props) => { + const[heart, setHeart] = useState(false); + function handleClick () { + props.query(props.id); + props.handleOpening() + } + function ChangeHeart(e){ + e.stopPropagation(); + if(e.target.src.substring(e.target.src.length - 10) != "heart2.png"){ + setHeart(true); + } else { + setHeart(false); + } + } + function Check(e){ + e.stopPropagation(); + } + useEffect(() => { + }, [heart]); + + return ( + +
+
+ Cards Image + + + +

{props.title}

+
+
+ + +
+
+ ) +} +export default MovieCard; \ No newline at end of file diff --git a/src/MovieList.jsx b/src/MovieList.jsx new file mode 100644 index 00000000..f87d0e74 --- /dev/null +++ b/src/MovieList.jsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect } from 'react'; +import MovieCard from './MovieCard'; +import './App.css' +import Modal from './Modal'; +const MovieList = () =>{ + const [count, setCount] = useState(1); + const [movies, setMovies] = useState([]); + const [search, setSearch] = useState(""); + const [query, setQuery] = useState(""); + const[open, setOpen] = useState(false); + const[sort, setSort] = useState(""); + const[genre, setGenre] = useState(1); + let url; + if(genre !== 1){ + url=`https://api.themoviedb.org/3/discover/movie?include_adult=false&include_video=false&language=en-US&page=${count}&with_genres=${genre}` + } else if(sort !== ""){ + url = `https://api.themoviedb.org/3/discover/movie?include_adult=false&include_video=false&language=en-US&page=${count}&sort_by=${sort}` + } else if (search === "") { + url = `https://api.themoviedb.org/3/discover/movie?include_adult=false&include_video=false&language=en-US&page=${count}&sort_by=popularity.desc`; + } else { + url =`https://api.themoviedb.org/3/search/movie?query=${search}&include_adult=false&language=en-US&page=1` + } + useEffect(() => { + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyNTI2OWIxMjIyYzJkNTliMDg0OGIwOTlkYWViN2Q5YiIsInN1YiI6IjY2Njc2NTkzNmI4ZGRiZDI3NGE5YmI5MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.A_LcJj_8sed0Oiyee3M9o2ZgcHKGE_jdnCA4aoAx0iY' + } + }; + fetch(url, options) + .then(response => response.json()) + .then(response => { + if(count === 1){ + setMovies(response.results) + } else{ + setMovies((movies)=>[...movies, ...response.results]) + } + + })}, [count, search, query, sort, genre]); + + function handleSubmit(e){ + e.preventDefault(); + + const form = e.target; + const formData = new FormData(form); + setSearch(formData.get("search")); + setSort(""); + setGenre(1) + } + + function openModal() { + setOpen(true) + } + + function closeModal() { + setOpen(false) + } + function handleSort(e){ + e.preventDefault(); + if(e.target.value === "Revenue"){ + setSort("revenue.desc") + } else if(e.target.value === "Popularity"){ + setSort("popularity.desc") + } else if(e.target.value === "Primary Release Date"){ + setSort("primary_release_date.desc") + } else if(e.target.value === "Vote Average"){ + setSort("vote_average.desc") + }else if(e.target.value === "Vote Count"){ + setSort("vote_count.desc") + } else { + setSort(""); + } + setSearch(""); + setGenre(1); + } + function handleFilter(e){ + e.preventDefault(); + if(e.target.value === "Action"){ + setGenre(28); + } else if(e.target.value === "Comedy"){ + setGenre(35); + } else if(e.target.value === "Thriller"){ + setGenre(53); + } else if(e.target.value === "War"){ + setGenre(10752); + }else if(e.target.value === "Romance"){ + setGenre(10749); + }else { + setGenre(1); + } + } + function ResetSearch(e){ + e.preventDefault(); + setSearch(""); + setSort(""); + setGenre(1); + } + + return( + <> +
+

🍿 Flixster 🎥

+
+ + +
+ + + +
+
+ {open && + } + {movies.map(movie => ( + + ))} +
+ {search === "" ? : null} + + + ) + + +} +export default MovieList; \ No newline at end of file diff --git a/src/Search.jsx b/src/Search.jsx new file mode 100644 index 00000000..8639cb76 --- /dev/null +++ b/src/Search.jsx @@ -0,0 +1,35 @@ +import { useState, useEffect } from "react"; +import MovieCard from "./MovieCard"; + +const Search = (props) => { + const [movies, setMovies] = useState([]); + let container = document.querySelector(".container") + let set; + let url = `https://api.themoviedb.org/3/search/movie?query=${props.name}&include_adult=false&language=en-US&page=1`; + useEffect(() => { + const options = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyNTI2OWIxMjIyYzJkNTliMDg0OGIwOTlkYWViN2Q5YiIsInN1YiI6IjY2Njc2NTkzNmI4ZGRiZDI3NGE5YmI5MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.A_LcJj_8sed0Oiyee3M9o2ZgcHKGE_jdnCA4aoAx0iY' + } + }; + fetch(url, options) + .then(response => response.json()) + .then(response => { + setMovies(response.results) + + })}, []); + + return( + <> +
+ {movies.map(movie => ( + + ))} +
+ + ) + +} +export default Search;