From 1edff9eb9b3cc2186992286616e1206210e7414a Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Tue, 10 Jun 2025 12:53:52 -0700 Subject: [PATCH 01/19] populate page with real movie data --- .env | 1 + src/App.css | 37 ++++++++++++++++++++++++++ src/App.jsx | 49 +++++++++++++++++++++++++++++++++-- src/components/LoadMore.jsx | 5 ++++ src/components/MovieCard.jsx | 15 +++++++++++ src/components/MovieList.jsx | 19 ++++++++++++++ src/components/SearchForm.jsx | 20 ++++++++++++++ src/components/SortMenu.jsx | 12 +++++++++ src/utils/utils.js | 0 9 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 .env create mode 100644 src/components/LoadMore.jsx create mode 100644 src/components/MovieCard.jsx create mode 100644 src/components/MovieList.jsx create mode 100644 src/components/SearchForm.jsx create mode 100644 src/components/SortMenu.jsx create mode 100644 src/utils/utils.js diff --git a/.env b/.env new file mode 100644 index 00000000..07ea420a --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_APP_API_KEY=68ba6609d837e9f977dc8e0e897b19e6 diff --git a/src/App.css b/src/App.css index 0bf65669..1890c29b 100644 --- a/src/App.css +++ b/src/App.css @@ -1,3 +1,8 @@ +* { + margin: 0; + padding: 0; +} + .App { text-align: center; } @@ -26,3 +31,35 @@ flex-direction: column; } } + +#toolbar { + display: flex; + justify-content: space-around; + /* gap: 50px; */ +} + +#movie-list { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 30px; + padding: 40px 300px; +} + +.movie-card { + display: flex; + flex-direction: column; + flex-grow: 1; + width: 18%; + gap: 5px; + box-sizing: border-box; + border-radius: 10px; + background-color: white; + border: 1px solid gray; +} + +.movie-card * { + /* word-wrap: break-word; */ + border-radius: 10px; + /* background-color: white; */ +} diff --git a/src/App.jsx b/src/App.jsx index dfa91584..517e9335 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,55 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import './App.css' +import SearchForm from './components/SearchForm' +import SortMenu from './components/SortMenu' +import MovieList from './components/MovieList' +import LoadMore from './components/LoadMore' const App = () => { + const [query, setQuery] = useState(''); + const [page, setPage] = useState(0); + const [movieData, setMovieData] = useState([]); + const apiKey = import.meta.env.VITE_APP_API_KEY; + + const fetchData = async () => { + try { + const response = await fetch(`https://api.themoviedb.org/3/movie/now_playing?api_key=${apiKey}&language=en-US&page=1`); + if (!response.ok) { + throw new Error('Failed to fetch movie data.'); + } + return await response.json(); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + const fetchMovieData = async () => { + const data = await fetchData(query); + const movieInfoData = data.results; + setMovieData(movieInfoData); + }; + fetchMovieData(); + }, [query]); + + const handleQueryChange = async (query) => { + setQuery(query); + } + return (
- +
+

Flixster

+
+ + +
+
+
+ + +
+
2025 Flixter
) } diff --git a/src/components/LoadMore.jsx b/src/components/LoadMore.jsx new file mode 100644 index 00000000..40feffd3 --- /dev/null +++ b/src/components/LoadMore.jsx @@ -0,0 +1,5 @@ +function LoadMore() { + +} + +export default LoadMore \ No newline at end of file diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx new file mode 100644 index 00000000..c113df82 --- /dev/null +++ b/src/components/MovieCard.jsx @@ -0,0 +1,15 @@ +function MovieCard( {image, title, rating} ) { + return ( +
+ {`Poster +

{title}

+

{rating}

+ {/*
+ + +
*/} +
+ ) +}; + +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..298fbae3 --- /dev/null +++ b/src/components/MovieList.jsx @@ -0,0 +1,19 @@ +import MovieCard from "./MovieCard" + +function MovieList({ movieData }) { + + return ( + <> +
+ { + movieData.map(movie => { + let image = `https://image.tmdb.org/t/p/original/${movie.poster_path}`; + return + }) + } +
+ + ) +} + +export default MovieList \ No newline at end of file diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx new file mode 100644 index 00000000..dc2f6196 --- /dev/null +++ b/src/components/SearchForm.jsx @@ -0,0 +1,20 @@ +function SearchForm( {onQueryChange} ) { +// const handleSubmit = (event) => { +// event.preventDefault(); +// const query = event.target[0].value; +// event.target.reset(); +// onQueryChange(query); +// } + +// return ( +// <> +//
+// +// +// +//
+// +// ) +} + +export default SearchForm \ No newline at end of file diff --git a/src/components/SortMenu.jsx b/src/components/SortMenu.jsx new file mode 100644 index 00000000..646dd1c4 --- /dev/null +++ b/src/components/SortMenu.jsx @@ -0,0 +1,12 @@ +function SortMenu() { +// return ( +// +// ) +} + +export default SortMenu \ No newline at end of file diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 00000000..e69de29b From d3afcaabdeed11cb7bf236f2370d07055515b14b Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Tue, 10 Jun 2025 16:10:33 -0700 Subject: [PATCH 02/19] add buttons | no functionality yet --- src/App.css | 95 +++++++++++++++++++++++++---------- src/App.jsx | 2 +- src/components/LoadMore.jsx | 3 ++ src/components/MovieCard.jsx | 14 +++--- src/components/SearchForm.jsx | 32 ++++++------ src/components/SortMenu.jsx | 16 +++--- 6 files changed, 106 insertions(+), 56 deletions(-) diff --git a/src/App.css b/src/App.css index 1890c29b..2f0332af 100644 --- a/src/App.css +++ b/src/App.css @@ -1,49 +1,53 @@ * { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .App { text-align: center; } -.App-header { - background-color: #282c34; +#title { + margin: 2vh 0vw; +} + +#toolbar { display: flex; - flex-direction: row; - align-items: center; - justify-content: space-evenly; - color: white; - padding: 20px; + justify-content: space-around; +} + +form * { + border-radius: 5px; + padding: .1vh .2vw; } -@media (max-width: 600px) { - .movie-card { - width: 100%; - } +form { + display: flex; + gap: 1vw; +} - .search-bar { - flex-direction: column; - gap: 10px; - } +form button { + margin: 0px .1vw; + background-color: #f4f4f4; + color: black; +} - .search-bar form { - flex-direction: column; - } +#sort-options { + border-radius: 5px; + padding: .1vh .2vw; } -#toolbar { - display: flex; - justify-content: space-around; - /* gap: 50px; */ +form button:hover { + background-color: black; + color: white; } #movie-list { display: flex; flex-wrap: wrap; justify-content: center; - gap: 30px; - padding: 40px 300px; + gap: 2vw; + margin: 2vh 10vw; } .movie-card { @@ -63,3 +67,42 @@ border-radius: 10px; /* background-color: white; */ } + +.movie-info { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; +} + +.features { + display: flex; + justify-content: space-around; +} + +.features button { + border-radius: 0px; + background-color: white; + color: black; + border: none; + font-weight: lighter; +} + +.features button:hover { + background-color: white; + color: black; +} + +#load-more { + margin: 1vh 0px 3vh; + border-radius: 5px; + background-color: #f4f4f4; + color: black; + border: none; + font-size: 20px; +} + +footer { + background-color: lightgray; + padding: 1vh; +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 517e9335..d0c0e6a4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -39,7 +39,7 @@ const App = () => { return (
-

Flixster

+

Flixster

diff --git a/src/components/LoadMore.jsx b/src/components/LoadMore.jsx index 40feffd3..20e1cdc4 100644 --- a/src/components/LoadMore.jsx +++ b/src/components/LoadMore.jsx @@ -1,5 +1,8 @@ function LoadMore() { + return ( + + ) } export default LoadMore \ No newline at end of file diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index c113df82..e062eae4 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -2,12 +2,14 @@ function MovieCard( {image, title, rating} ) { return (
{`Poster -

{title}

-

{rating}

- {/*
- - -
*/} +
+

{title}

+

Rating: {rating}

+
+ + +
+
) }; diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx index dc2f6196..9ac5b4b3 100644 --- a/src/components/SearchForm.jsx +++ b/src/components/SearchForm.jsx @@ -1,20 +1,22 @@ function SearchForm( {onQueryChange} ) { -// const handleSubmit = (event) => { -// event.preventDefault(); -// const query = event.target[0].value; -// event.target.reset(); -// onQueryChange(query); -// } + const handleSubmit = (event) => { + event.preventDefault(); + const query = event.target[0].value; + event.target.reset(); + onQueryChange(query); + } -// return ( -// <> -//
-// -// -// -//
-// -// ) + return ( + <> +
+ +
+ + +
+
+ + ) } export default SearchForm \ No newline at end of file diff --git a/src/components/SortMenu.jsx b/src/components/SortMenu.jsx index 646dd1c4..7f55b1cb 100644 --- a/src/components/SortMenu.jsx +++ b/src/components/SortMenu.jsx @@ -1,12 +1,12 @@ function SortMenu() { -// return ( -// -// ) + return ( + + ) } export default SortMenu \ No newline at end of file From 81bd9f2a579933b75a814c36aca8a851ab38c955 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Tue, 10 Jun 2025 19:30:00 -0700 Subject: [PATCH 03/19] add search feature --- src/App.css | 3 ++- src/App.jsx | 33 +++++++++++++++++++++++++++------ src/components/LoadMore.jsx | 7 +++---- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/App.css b/src/App.css index 2f0332af..412c2887 100644 --- a/src/App.css +++ b/src/App.css @@ -53,7 +53,7 @@ form button:hover { .movie-card { display: flex; flex-direction: column; - flex-grow: 1; + /* flex-grow: 1; */ width: 18%; gap: 5px; box-sizing: border-box; @@ -105,4 +105,5 @@ form button:hover { footer { background-color: lightgray; padding: 1vh; + margin-top: auto; } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index d0c0e6a4..f336b564 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,13 +7,20 @@ import LoadMore from './components/LoadMore' const App = () => { const [query, setQuery] = useState(''); - const [page, setPage] = useState(0); + // const [isSearch, setIsSearch] = useState(false); + const [page, setPage] = useState(1); const [movieData, setMovieData] = useState([]); const apiKey = import.meta.env.VITE_APP_API_KEY; - const fetchData = async () => { + const fetchData = async (query) => { try { - const response = await fetch(`https://api.themoviedb.org/3/movie/now_playing?api_key=${apiKey}&language=en-US&page=1`); + let response = null; + if (query) { + const keywords = query.split(' ').join('%20'); + response = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${query}&include_adult=false&language=en-US&page=1`); + } else { + response = await fetch(`https://api.themoviedb.org/3/movie/now_playing?api_key=${apiKey}&language=en-US&page=1`); + } if (!response.ok) { throw new Error('Failed to fetch movie data.'); } @@ -24,18 +31,32 @@ const App = () => { } useEffect(() => { - const fetchMovieData = async () => { - const data = await fetchData(query); + const fetchMovieData = async (query) => { + setPage(1); + const data = await fetchData(query, page); const movieInfoData = data.results; setMovieData(movieInfoData); }; - fetchMovieData(); + fetchMovieData(query); }, [query]); const handleQueryChange = async (query) => { setQuery(query); } + // const handlePageChange = async (page) => { + // setPage(page + 1); + // } + + // useEffect(() => { + // const fetchMoreMovieData = async () => { + // const data = await fetchData(query, page); + // const movieInfoData = data.results; + // setMovieData((movieData.push(movieInfoData))); + // }; + // fetchMoreMovieData(); + // }, [page]) + return (
diff --git a/src/components/LoadMore.jsx b/src/components/LoadMore.jsx index 20e1cdc4..c73c7865 100644 --- a/src/components/LoadMore.jsx +++ b/src/components/LoadMore.jsx @@ -1,8 +1,7 @@ function LoadMore() { - - return ( - - ) + return ( + + ) } export default LoadMore \ No newline at end of file From 3a20c3c8ee721480d2c8781b545fa89928967e64 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Wed, 11 Jun 2025 07:51:49 -0700 Subject: [PATCH 04/19] add sorting feature --- README.md | 131 ++++++++++++++++++++++++++++++++-- src/App.css | 8 ++- src/App.jsx | 55 +++++++++----- src/components/LoadMore.jsx | 4 +- src/components/SearchForm.jsx | 7 +- src/components/SortMenu.jsx | 9 ++- 6 files changed, 184 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index f768e33f..ed282871 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,129 @@ -# React + Vite +📝 `NOTE` Use this template to initialize the contents of a README.md file for your application. As you work on your assignment over the course of the week, update the required or stretch features lists to indicate which features you have completed by changing `[ ]` to `[x]`. (🚫 Remove this paragraph before submitting your assignment.) -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +## Unit Assignment: Flixster -Currently, two official plugins are available: +Submitted by: **Carlos Escobar** -- [@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 +Estimated time spent: **#** hours spent in total + +Deployed Application (**required**): [Flixster Deployed Site](ADD_LINK_HERE) + +### Application Features + +#### REQUIRED FEATURES + +- [x] **Display Movies** + - [x] Users can view a list of current movies from The Movie Database API in a grid view. + - [x] Movie tiles should be reasonably sized (at least 6 playlists on your laptop when full screen; large enough that the playlist components detailed in the next feature are legible). + - [x] For each movie displayed, users can see the movie's: + - [x] Title + - [x] Poster image + - [x] Vote average + - [x] Users can load more current movies by clicking a button which adds more movies to the grid without reloading the entire page. +- [x] **Search Functionality** + - [x] Users can use a search bar to search for movies by title. + - [ ] The search bar should include: + - [x] Text input field + - [x] Submit/Search button + - [x] Clear button + - [x] Movies with a title containing the search query in the text input field are displayed in a grid view when the user either: + - [x] Presses the Enter key + - [x] Clicks the Submit/Search button + - [x] Users can click the Clear button. When clicked: + - [x] Most recent search results are cleared from the text input field and the grid view and all current movies are displayed in a grid view +- [ ] **Design Features** + - [ ] Website implements all of the following accessibility features: + - [ ] Semantic HTML + - [ ] [Color contrast](https://webaim.org/resources/contrastchecker/) + - [ ] Alt text for images + - [ ] Website implements responsive web design. + - [x] Uses CSS Flexbox or CSS Grid + - [ ] Movie tiles and images shrink/grow in response to window size + - [ ] Users can click on a movie tile to view more details about a movie in a pop-up modal. + - [ ] The pop-up window is centered in the screen and does not occupy the entire screen. + - [ ] The pop-up window has a shadow to show that it is a pop-up and appears floating on the screen. + - [ ] The backdrop of the pop-up appears darker or in a different shade than before. including: + - [ ] The pop-up displays additional details about the moving including: + - [ ] Runtime in minutes + - [ ] Backdrop poster + - [ ] Release date + - [ ] Genres + - [ ] An overview + - [x] Users can use a drop-down menu to sort movies. + - [x] Drop-down allows movies to be sorted by: + - [x] Title (alphabetic, A-Z) + - [x] Release date (chronologically, most recent to oldest) + - [x] Vote average (descending, highest to lowest) + - [x] When a sort option is clicked, movies display in a grid according to selected criterion. + - [ ] Website displays: + - [ ] Header section + - [ ] Banner section + - [x] Search bar + - [x] Movie grid + - [x] Footer section + - [ ] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: To ease the grading process, please use the [color contrast checker](https://webaim.org/resources/contrastchecker/) to demonstrate to the grading team that text and background colors on your website have appropriate contrast. The Contrast Ratio should be above 4.5:1 and should have a green box surrounding it. + - [ ] **Deployment** + - [ ] Website is deployed via Render. + - [ ] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: For ease of grading, please use the deployed version of your website when creating your walkthrough. + +#### STRETCH FEATURES + + +- [ ] **Embedded Movie Trailers** + - [ ] Within the pop-up modal displaying a movie's details, the movie trailer is viewable. + - [ ] When the trailer is clicked, users can play the movie trailer. +- [ ] **Favorite Button** + - [ ] For each movie displayed, users can favorite the movie. + - [ ] There should be visual element (such as a heart icon) on each movie's tile to show whether or not the movie has been favorited. + - [ ] If the movie is not favorited: + - [ ] Clicking on the visual element should mark the movie as favorited + - [ ] There should be visual feedback (such as the heart turning a different color) to show that the movie has been favorited by the user. + - [ ] If the movie is already favorited: + - [ ] Clicking on the visual element should mark the movie as *not* favorited. + - [ ] There should be visual feedback (such as the heart turning a different color) to show that the movie has been unfavorited. +- [ ] **Watched Checkbox** + - [ ] For each movie displayed, users can mark the movie as watched. + - [ ] There should be visual element (such as an eye icon) on each movie's tile to show whether or not the movie has been watched. + - [ ] If the movie has not been watched: + - [ ] Clicking on the visual element should mark the movie as watched + - [ ] There should be visual feedback (such as the eye turning a different color) to show that the movie has been watched by the user. + - [ ] If the movie is already watched: + - [ ] Clicking on the visual element should mark the movie as *not* watched. + - [ ] There should be visual feedback (such as the eye turning a different color) to show that the movie has not been watched. +- [ ] **Sidebar** + - [ ] The website includes a side navigation bar. + - [ ] The sidebar has three pages: + - [ ] Home + - [ ] Favorites + - [ ] Watched + - [ ] The Home page displays all current movies in a grid view, the search bar, and the sort movies drop-down. + - [ ] The Favorites page displays all favorited movies in a grid view. + - [ ] The Watched page displays all watched movies in a grid view. + +### Walkthrough Video + +`TODO://` Add the embedded URL code to your animated app walkthrough below, `ADD_EMBEDDED_CODE_HERE`. Make sure the video or gif actually renders and animates when viewing this README. Ensure your walkthrough showcases the presence and/or functionality of all features you implemented above (check them off as you film!). Pay attention to any **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS** checkboxes listed above to ensure graders see the full functionality of your website! (🚫 Remove this paragraph after adding walkthrough video) + +`ADD_EMBEDDED_CODE_HERE` + +### 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? + +Add your response here + +* 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. + +Add your response here + +* 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? + +Add your response here + +### Open-source libraries used + +- Add any links to open-source libraries used in your project. + +### Shout out + +Give a shout out to somebody from your cohort that especially helped you during your project. This can be a fellow peer, instructor, TA, mentor, etc. \ No newline at end of file diff --git a/src/App.css b/src/App.css index 412c2887..0dd0c5dd 100644 --- a/src/App.css +++ b/src/App.css @@ -54,6 +54,7 @@ form button:hover { display: flex; flex-direction: column; /* flex-grow: 1; */ + min-width: 150px; width: 18%; gap: 5px; box-sizing: border-box; @@ -77,7 +78,7 @@ form button:hover { .features { display: flex; - justify-content: space-around; + justify-content: space-between; } .features button { @@ -91,6 +92,7 @@ form button:hover { .features button:hover { background-color: white; color: black; + cursor: pointer; } #load-more { @@ -102,6 +104,10 @@ form button:hover { font-size: 20px; } +#load-more:hover { + cursor: pointer; +} + footer { background-color: lightgray; padding: 1vh; diff --git a/src/App.jsx b/src/App.jsx index f336b564..868ad4b3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,19 +7,18 @@ import LoadMore from './components/LoadMore' const App = () => { const [query, setQuery] = useState(''); - // const [isSearch, setIsSearch] = useState(false); const [page, setPage] = useState(1); const [movieData, setMovieData] = useState([]); const apiKey = import.meta.env.VITE_APP_API_KEY; - const fetchData = async (query) => { + const fetchData = async (query, page) => { try { let response = null; if (query) { - const keywords = query.split(' ').join('%20'); - response = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${query}&include_adult=false&language=en-US&page=1`); + const formattedQuery = query.split(' ').join('%20'); + response = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${formattedQuery}&include_adult=false&language=en-US&page=${page}`); } else { - response = await fetch(`https://api.themoviedb.org/3/movie/now_playing?api_key=${apiKey}&language=en-US&page=1`); + response = await fetch(`https://api.themoviedb.org/3/movie/now_playing?api_key=${apiKey}&language=en-US&page=${page}`); } if (!response.ok) { throw new Error('Failed to fetch movie data.'); @@ -44,18 +43,38 @@ const App = () => { setQuery(query); } - // const handlePageChange = async (page) => { - // setPage(page + 1); - // } + const handlePageChange = async () => { + setPage(page + 1); + } - // useEffect(() => { - // const fetchMoreMovieData = async () => { - // const data = await fetchData(query, page); - // const movieInfoData = data.results; - // setMovieData((movieData.push(movieInfoData))); - // }; - // fetchMoreMovieData(); - // }, [page]) + useEffect(() => { + const fetchMoreMovieData = async () => { + const data = await fetchData(query, page); + const movieInfoData = data.results; + const allMovieData = movieData.concat(movieInfoData); + setMovieData(allMovieData); + }; + + fetchMoreMovieData(); + }, [page]); + + const handleSortOptionSelected = async (sortOption) => { + let sortedMovieData = [...movieData]; + switch (sortOption) { + case 'alphabetical': + sortedMovieData.sort((a, b) => a.title.localeCompare(b.title)); + break; + case 'chronological': + sortedMovieData.sort((a, b) => b.release_date.localeCompare(a.release_date)); + break; + case 'vote-average': + sortedMovieData.sort((a, b) => b.vote_average - a.vote_average); + break; + default: + console.error("Invalid sort option selected.") + } + setMovieData(sortedMovieData); + } return (
@@ -63,12 +82,12 @@ const App = () => {

Flixster

- +
- +
2025 Flixter
diff --git a/src/components/LoadMore.jsx b/src/components/LoadMore.jsx index c73c7865..1a1e93f3 100644 --- a/src/components/LoadMore.jsx +++ b/src/components/LoadMore.jsx @@ -1,6 +1,6 @@ -function LoadMore() { +function LoadMore({ onPageChange }) { return ( - + ) } diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx index 9ac5b4b3..b2c2be18 100644 --- a/src/components/SearchForm.jsx +++ b/src/components/SearchForm.jsx @@ -6,13 +6,18 @@ function SearchForm( {onQueryChange} ) { onQueryChange(query); } + const handleReset = (event) => { + event.preventDefault(); + onQueryChange(''); + }; + return ( <>
- +
diff --git a/src/components/SortMenu.jsx b/src/components/SortMenu.jsx index 7f55b1cb..8533bd56 100644 --- a/src/components/SortMenu.jsx +++ b/src/components/SortMenu.jsx @@ -1,9 +1,12 @@ -function SortMenu() { +function SortMenu( {sort} ) { + const handleChange = (event) => { + sort(event.target.value); + } return ( - - + ) From ee82767d11ae08ca1a70c2e8617528ad1d3dbff2 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Wed, 11 Jun 2025 09:50:43 -0700 Subject: [PATCH 05/19] change API key and add .env to .gitignore --- .env | 1 - .gitignore | 1 + src/App.jsx | 6 ++++-- 3 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 07ea420a..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -VITE_APP_API_KEY=68ba6609d837e9f977dc8e0e897b19e6 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.jsx b/src/App.jsx index 868ad4b3..1a88a8f3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -79,12 +79,14 @@ const App = () => { return (
-

Flixster

+

Flixster 🎥

+
+
+
From 74335d16751be366c14f82b6123cc711590643c3 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Wed, 11 Jun 2025 17:15:32 -0700 Subject: [PATCH 06/19] add basic modal & fix sort bug --- src/App.css | 53 ++++++++++++++++++++++++++++----- src/App.jsx | 55 ++++++++++++++++++++++++++--------- src/components/LoadMore.jsx | 7 ----- src/components/Modal.jsx | 17 +++++++++++ src/components/MovieCard.jsx | 18 ++++++++---- src/components/MovieList.jsx | 6 ++-- src/components/SearchForm.jsx | 9 ++---- 7 files changed, 121 insertions(+), 44 deletions(-) delete mode 100644 src/components/LoadMore.jsx create mode 100644 src/components/Modal.jsx diff --git a/src/App.css b/src/App.css index 0dd0c5dd..563efc70 100644 --- a/src/App.css +++ b/src/App.css @@ -5,10 +5,16 @@ .App { text-align: center; + background-color: #8AAAE5; + display: flex; + flex-direction: column; + min-height: 100vh; } #title { - margin: 2vh 0vw; + padding: .5vh 0vw; + background-color: #6795e5; + margin-bottom: 1.5vh; } #toolbar { @@ -53,32 +59,38 @@ form button:hover { .movie-card { display: flex; flex-direction: column; - /* flex-grow: 1; */ min-width: 150px; width: 18%; gap: 5px; box-sizing: border-box; border-radius: 10px; background-color: white; - border: 1px solid gray; } .movie-card * { - /* word-wrap: break-word; */ border-radius: 10px; - /* background-color: white; */ +} + +.movie-card img { + height: 65%; + object-fit: cover; } .movie-info { display: flex; flex-direction: column; - gap: 10px; padding: 10px; + height: 35%; +} + +.movie-info * { + margin: auto; } .features { display: flex; justify-content: space-between; + gap: 1vw; } .features button { @@ -98,18 +110,43 @@ form button:hover { #load-more { margin: 1vh 0px 3vh; border-radius: 5px; - background-color: #f4f4f4; color: black; border: none; font-size: 20px; + background-color: #8AAAE5; } #load-more:hover { cursor: pointer; } +.modal-overlay { + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.4); + backdrop-filter: blur(5px); +} + +.modal-content { + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 60%; + display: flex; + flex-direction: column; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + border-radius: 10px; +} + footer { - background-color: lightgray; padding: 1vh; margin-top: auto; + width: 100%; + background-color: #6795e5; } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 1a88a8f3..fb431a8e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,15 +3,17 @@ import './App.css' import SearchForm from './components/SearchForm' import SortMenu from './components/SortMenu' import MovieList from './components/MovieList' -import LoadMore from './components/LoadMore' +import Modal from './components/Modal' const App = () => { + const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(''); const [page, setPage] = useState(1); const [movieData, setMovieData] = useState([]); + const [selectedMovieData, setSelectedMovieData] = useState({}); const apiKey = import.meta.env.VITE_APP_API_KEY; - const fetchData = async (query, page) => { + const fetchData = async () => { try { let response = null; if (query) { @@ -30,26 +32,39 @@ const App = () => { } useEffect(() => { - const fetchMovieData = async (query) => { - setPage(1); - const data = await fetchData(query, page); + setPage(1); + }, [query]); + + useEffect(() => { + const fetchMovieData = async () => { + const data = await fetchData(); const movieInfoData = data.results; - setMovieData(movieInfoData); + if (page == 1) { + setMovieData(movieInfoData); + } else { + setMovieData(prev => [...prev, ...data.results]); + } }; - fetchMovieData(query); - }, [query]); + fetchMovieData(); + }, [page, query]); const handleQueryChange = async (query) => { setQuery(query); } + const handleClear = async () => { + setQuery(''); + setPage(1); + } + const handlePageChange = async () => { - setPage(page + 1); + setPage(prev => prev + 1); } useEffect(() => { const fetchMoreMovieData = async () => { - const data = await fetchData(query, page); + if (page == 1) return; + const data = await fetchData(); const movieInfoData = data.results; const allMovieData = movieData.concat(movieInfoData); setMovieData(allMovieData); @@ -58,6 +73,19 @@ const App = () => { fetchMoreMovieData(); }, [page]); + const updateSelectedMovieData = async (id) => { + const selectedMovie = movieData.filter(movie => movie.id == id)[0]; + let image = `https://image.tmdb.org/t/p/original/${selectedMovie.poster_path}`; + // TODO: fetch genres here + const selectedMovieDataObj = { + title: selectedMovie.title, + image, + release_date: selectedMovie.release_date, + overview: selectedMovie.overview, + } + setSelectedMovieData(selectedMovieDataObj) + } + const handleSortOptionSelected = async (sortOption) => { let sortedMovieData = [...movieData]; switch (sortOption) { @@ -83,14 +111,15 @@ const App = () => {
- - + +
+
2025 Flixter
) diff --git a/src/components/LoadMore.jsx b/src/components/LoadMore.jsx deleted file mode 100644 index 1a1e93f3..00000000 --- a/src/components/LoadMore.jsx +++ /dev/null @@ -1,7 +0,0 @@ -function LoadMore({ onPageChange }) { - return ( - - ) -} - -export default LoadMore \ No newline at end of file diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx new file mode 100644 index 00000000..bb661366 --- /dev/null +++ b/src/components/Modal.jsx @@ -0,0 +1,17 @@ +function Modal({ selectedMovieData, setIsOpen, isOpen }) { + if (!isOpen) return null; + return ( +
+
+

{selectedMovieData.title}

+ {`Poster +

Release Date: {selectedMovieData.release_date}

+

Overview: {selectedMovieData.description}

+

Genres: N/A

+ {setIsOpen(false)}}>× +
+
+ ) +}; + +export default Modal \ No newline at end of file diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index e062eae4..bdae7eed 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,14 +1,20 @@ -function MovieCard( {image, title, rating} ) { +function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpen }) { + + const handleModalClick = () => { + updateSelectedMovieData(id); + setIsOpen(true); + } return ( -
+
{`Poster

{title}

Rating: {rating}

-
- - -
+ {/*
+ // Button Component Here (reusable) + + +
*/}
) diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx index 298fbae3..105108ed 100644 --- a/src/components/MovieList.jsx +++ b/src/components/MovieList.jsx @@ -1,14 +1,14 @@ import MovieCard from "./MovieCard" -function MovieList({ movieData }) { - +function MovieList({ movieData, onMovieClick }) { + const { updateSelectedMovieData, setIsOpen } = onMovieClick; return ( <>
{ movieData.map(movie => { let image = `https://image.tmdb.org/t/p/original/${movie.poster_path}`; - return + return }) }
diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx index b2c2be18..c949f2e1 100644 --- a/src/components/SearchForm.jsx +++ b/src/components/SearchForm.jsx @@ -1,4 +1,4 @@ -function SearchForm( {onQueryChange} ) { +function SearchForm({ onQueryChange, onClear }) { const handleSubmit = (event) => { event.preventDefault(); const query = event.target[0].value; @@ -6,18 +6,13 @@ function SearchForm( {onQueryChange} ) { onQueryChange(query); } - const handleReset = (event) => { - event.preventDefault(); - onQueryChange(''); - }; - return ( <>
- +
From 2ad416ed5b3301af20524cf3badb829c29768b3d Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Thu, 12 Jun 2025 12:27:34 -0700 Subject: [PATCH 07/19] refine modal & sort data as it is loaded --- README.md | 2 +- src/App.css | 51 +++++++++++++++++++++---- src/App.jsx | 71 ++++++++++++++++++++++++++--------- src/components/Modal.jsx | 21 ++++++++--- src/components/MovieCard.jsx | 18 ++++++--- src/components/MovieList.jsx | 5 ++- src/components/SearchForm.jsx | 2 +- 7 files changed, 130 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index ed282871..827bf92f 100644 --- a/README.md +++ b/README.md @@ -126,4 +126,4 @@ Add your response here ### Shout out -Give a shout out to somebody from your cohort that especially helped you during your project. This can be a fellow peer, instructor, TA, mentor, etc. \ No newline at end of file +Shout out Noah Pyrzanowski \ No newline at end of file diff --git a/src/App.css b/src/App.css index 563efc70..b58c2e0a 100644 --- a/src/App.css +++ b/src/App.css @@ -22,7 +22,12 @@ justify-content: space-around; } +#search-helper-buttons { + display: flex; +} + form * { + border: none; border-radius: 5px; padding: .1vh .2vw; } @@ -41,6 +46,7 @@ form button { #sort-options { border-radius: 5px; padding: .1vh .2vw; + max-width: 30%; } form button:hover { @@ -72,15 +78,15 @@ form button:hover { } .movie-card img { - height: 65%; object-fit: cover; + height: fit-content; } .movie-info { display: flex; flex-direction: column; padding: 10px; - height: 35%; + justify-content: space-between; } .movie-info * { @@ -120,28 +126,57 @@ form button:hover { cursor: pointer; } -.modal-overlay { +#modal-overlay { position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; - overflow: auto; + overflow-y: auto; background-color: rgba(0,0,0,0.4); backdrop-filter: blur(5px); } -.modal-content { +#modal-content { background-color: #fefefe; - margin: 15% auto; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); padding: 20px; border: 1px solid #888; - width: 60%; + max-width: 50vw; + max-height: 50vh; + overflow-y: auto; display: flex; flex-direction: column; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); - border-radius: 10px; + align-items: center; + border-radius: 25px; +} + +#modal-content img { + height: 30vh; + width: auto; +} + +#modal-content * { + margin-top: 2vh; +} + +#close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + display: flex; + cursor: pointer; +} + +#close { + margin-left: auto; + margin-top: 0; } footer { diff --git a/src/App.jsx b/src/App.jsx index fb431a8e..4c96f84f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,10 +11,15 @@ const App = () => { const [page, setPage] = useState(1); const [movieData, setMovieData] = useState([]); const [selectedMovieData, setSelectedMovieData] = useState({}); + const [sortOption, setSortOption] = useState(''); + const [genreMap, setGenreMap] = useState({}); + const [favoritedMovies, setFavoritedMovies] = useState([]); + const [watchedMovies, setWatchedMovies] = useState([]); const apiKey = import.meta.env.VITE_APP_API_KEY; const fetchData = async () => { try { + fetchGenreData(); let response = null; if (query) { const formattedQuery = query.split(' ').join('%20'); @@ -31,6 +36,20 @@ const App = () => { } } + const fetchGenreData = async () => { + let response = await fetch(`https://api.themoviedb.org/3/genre/movie/list?api_key=${apiKey}&language=en`); + let genreData = await response.json(); + const map = {}; + genreData.genres.forEach(info => { + map[info.id] = info.name; + }); + setGenreMap(map); + } + + useEffect(() => { + fetchGenreData(); + }, []); + useEffect(() => { setPage(1); }, [query]); @@ -42,7 +61,14 @@ const App = () => { if (page == 1) { setMovieData(movieInfoData); } else { - setMovieData(prev => [...prev, ...data.results]); + if (sortOption != '') { + setMovieData(prev => { + let combinedData = [...prev, ...data.results]; + return sortMovieData(combinedData, sortOption); + }) + } else { + setMovieData(prev => [...prev, ...data.results]); + } } }; fetchMovieData(); @@ -61,32 +87,37 @@ const App = () => { setPage(prev => prev + 1); } - useEffect(() => { - const fetchMoreMovieData = async () => { - if (page == 1) return; - const data = await fetchData(); - const movieInfoData = data.results; - const allMovieData = movieData.concat(movieInfoData); - setMovieData(allMovieData); - }; - - fetchMoreMovieData(); - }, [page]); - const updateSelectedMovieData = async (id) => { - const selectedMovie = movieData.filter(movie => movie.id == id)[0]; + const selectedMovie = movieData.find(movie => movie.id == id); let image = `https://image.tmdb.org/t/p/original/${selectedMovie.poster_path}`; + let genres = selectedMovie.genre_ids.map(id => genreMap[id]).join(', '); // TODO: fetch genres here const selectedMovieDataObj = { title: selectedMovie.title, image, release_date: selectedMovie.release_date, overview: selectedMovie.overview, + genres } - setSelectedMovieData(selectedMovieDataObj) + setSelectedMovieData(selectedMovieDataObj); } - const handleSortOptionSelected = async (sortOption) => { + // TODO: refactor + const updateFavoritedMovies = async (id) => { + const favoritedMovie = movieData.find(movie => movie.id == id); + setFavoritedMovies(prev => { + return [...prev, ...favoritedMovie]; + }); + }; + + const updateWatchedMovies = async (id) => { + const watchedMovie = movieData.find(movie => movie.id == id); + setWatchedMovies(prev => { + return [...prev, ...watchedMovie]; + }); + }; + + const sortMovieData = (movieData, sortOption) => { let sortedMovieData = [...movieData]; switch (sortOption) { case 'alphabetical': @@ -101,6 +132,12 @@ const App = () => { default: console.error("Invalid sort option selected.") } + return sortedMovieData; + } + + const handleSortOptionSelected = async (sortOption) => { + setSortOption(sortOption); + let sortedMovieData = sortMovieData(movieData, sortOption); setMovieData(sortedMovieData); } @@ -116,7 +153,7 @@ const App = () => {
- +
diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx index bb661366..49c0bfe2 100644 --- a/src/components/Modal.jsx +++ b/src/components/Modal.jsx @@ -1,14 +1,23 @@ function Modal({ selectedMovieData, setIsOpen, isOpen }) { if (!isOpen) return null; + const handleClickOutside = (event) => { + if (event.target.id === 'modal-overlay') { + setIsOpen(false); + } + }; + const handleClickClose = (event) => { + event.stopPropagation(); + setIsOpen(false); + } return ( -
-
+ ) diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index bdae7eed..26f30e20 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,20 +1,28 @@ -function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpen }) { +function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpen, updateFavoritedMovies, updateWatchedMovies }) { const handleModalClick = () => { updateSelectedMovieData(id); setIsOpen(true); } + + const onFavorite = (event) => { + setFavoritedMovies() + } + + const onWatched = (event) => { + setWatchedMovies() + } return (
{`Poster

{title}

-

Rating: {rating}

- {/*
- // Button Component Here (reusable) +

Rating: {rating.toFixed(2)}

+
+ {/* Button Component Here (reusable) */} -
*/} +
) diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx index 105108ed..b0b28d79 100644 --- a/src/components/MovieList.jsx +++ b/src/components/MovieList.jsx @@ -1,14 +1,15 @@ import MovieCard from "./MovieCard" -function MovieList({ movieData, onMovieClick }) { +function MovieList({ movieData, onMovieClick, onButtonClick }) { const { updateSelectedMovieData, setIsOpen } = onMovieClick; + const { updateFavoritedMovies, updateWatchedMovies } = onButtonClick; return ( <>
{ movieData.map(movie => { let image = `https://image.tmdb.org/t/p/original/${movie.poster_path}`; - return + return }) }
diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx index c949f2e1..f49c77d7 100644 --- a/src/components/SearchForm.jsx +++ b/src/components/SearchForm.jsx @@ -10,7 +10,7 @@ function SearchForm({ onQueryChange, onClear }) { <>
-
+
From 2dabad6646682692267becc7c0bc61c833fe795a Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Thu, 12 Jun 2025 17:26:56 -0700 Subject: [PATCH 08/19] added sidebar & styling with no functionality --- src/App.css | 41 ++++++++++++++++++++---- src/App.jsx | 61 +++++++++++++++++++++++------------- src/components/MovieCard.jsx | 7 +++-- src/components/MovieList.jsx | 1 + src/components/Sidebar.jsx | 11 +++++++ 5 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 src/components/Sidebar.jsx diff --git a/src/App.css b/src/App.css index b58c2e0a..33bd974e 100644 --- a/src/App.css +++ b/src/App.css @@ -7,13 +7,13 @@ text-align: center; background-color: #8AAAE5; display: flex; - flex-direction: column; + flex-direction: row; min-height: 100vh; } #title { padding: .5vh 0vw; - background-color: #6795e5; + /* background-color: #6795e5; */ margin-bottom: 1.5vh; } @@ -54,12 +54,42 @@ form button:hover { color: white; } +.content-container { + display: flex; + flex-direction: column; + max-width: 85vw; +} + + +.sidebar { + background-color: #6795e5; + display: flex; + flex-direction: column; + width: 15vw; + height: auto; + gap: 2vh; + flex-shrink: 0; + /* position: sticky; */ +} + +.sidebar button { + background-color: transparent; + border: none; + text-align: center; +} + +.sidebar button:hover { + color: #2c5db2; + background-color: inherit; +} + #movie-list { + max-width: 100vw; display: flex; flex-wrap: wrap; justify-content: center; gap: 2vw; - margin: 2vh 10vw; + margin: 2vh 5vw; } .movie-card { @@ -114,7 +144,7 @@ form button:hover { } #load-more { - margin: 1vh 0px 3vh; + margin: 1vh auto 3vh; border-radius: 5px; color: black; border: none; @@ -180,8 +210,7 @@ form button:hover { } footer { + text-align: center; padding: 1vh; margin-top: auto; - width: 100%; - background-color: #6795e5; } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 4c96f84f..ab374009 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import SearchForm from './components/SearchForm' import SortMenu from './components/SortMenu' import MovieList from './components/MovieList' import Modal from './components/Modal' +import Sidebar from './components/Sidebar' const App = () => { const [isOpen, setIsOpen] = useState(false); @@ -91,7 +92,6 @@ const App = () => { const selectedMovie = movieData.find(movie => movie.id == id); let image = `https://image.tmdb.org/t/p/original/${selectedMovie.poster_path}`; let genres = selectedMovie.genre_ids.map(id => genreMap[id]).join(', '); - // TODO: fetch genres here const selectedMovieDataObj = { title: selectedMovie.title, image, @@ -105,16 +105,30 @@ const App = () => { // TODO: refactor const updateFavoritedMovies = async (id) => { const favoritedMovie = movieData.find(movie => movie.id == id); - setFavoritedMovies(prev => { - return [...prev, ...favoritedMovie]; - }); + if (!favoritedMovies.includes(favoritedMovie)) { + let allFavoritedMovies = [...favoritedMovies]; + allFavoritedMovies.push(favoritedMovie); + setFavoritedMovies(allFavoritedMovies); + } else { + let allFavoritedMovies = [...favoritedMovies]; + const indexOfMovie = movieData.indexOf(favoritedMovie); + allFavoritedMovies.splice(indexOfMovie, 1); + setFavoritedMovies(allFavoritedMovies); + } }; const updateWatchedMovies = async (id) => { const watchedMovie = movieData.find(movie => movie.id == id); - setWatchedMovies(prev => { - return [...prev, ...watchedMovie]; - }); + if (!watchedMovies.includes(watchedMovie)) { + let allWatchedMovies = [...watchedMovies]; + allWatchedMovies.push(watchedMovie); + setWatchedMovies(allWatchedMovies); + } else { + let allWatchedMovies = [...watchedMovies]; + const indexOfMovie = movieData.indexOf(watchedMovie); + allWatchedMovies.splice(indexOfMovie, 1); + setWatchedMovies(allWatchedMovies); + } }; const sortMovieData = (movieData, sortOption) => { @@ -143,21 +157,24 @@ const App = () => { return (
-
-

Flixster 🎥

-
- -
- - -
- -
2025 Flixter
+ +
+
+

Flixster 🎥

+
+ +
+ + +
+ +
2025 Flixter
+
) } diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 26f30e20..3b6a9395 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -6,12 +6,15 @@ function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpe } const onFavorite = (event) => { - setFavoritedMovies() + event.stopPropagation(); + updateFavoritedMovies(id); } const onWatched = (event) => { - setWatchedMovies() + event.stopPropagation(); + updateWatchedMovies(id); } + return (
{`Poster diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx index b0b28d79..abc39929 100644 --- a/src/components/MovieList.jsx +++ b/src/components/MovieList.jsx @@ -8,6 +8,7 @@ function MovieList({ movieData, onMovieClick, onButtonClick }) {
{ movieData.map(movie => { + let image = `https://image.tmdb.org/t/p/original/${movie.poster_path}`; return }) diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx new file mode 100644 index 00000000..f50d8355 --- /dev/null +++ b/src/components/Sidebar.jsx @@ -0,0 +1,11 @@ +function Sidebar() { + return ( +
+ + + +
+ ) +} + +export default Sidebar \ No newline at end of file From fb591843faa0a047bd6b3bae4603876b32a35248 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Fri, 13 Jun 2025 07:32:53 -0700 Subject: [PATCH 09/19] added marking as favorite/watched functionality & made sidebar fixed in position --- README.md | 100 +++++++++++++++++------------------ src/App.css | 35 +++++++----- src/App.jsx | 69 +++++++++++++++--------- src/components/MovieCard.jsx | 6 +-- src/components/MovieList.jsx | 4 +- src/components/Sidebar.jsx | 13 +++-- 6 files changed, 130 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 827bf92f..2668cfb1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ -📝 `NOTE` Use this template to initialize the contents of a README.md file for your application. As you work on your assignment over the course of the week, update the required or stretch features lists to indicate which features you have completed by changing `[ ]` to `[x]`. (🚫 Remove this paragraph before submitting your assignment.) - ## Unit Assignment: Flixster Submitted by: **Carlos Escobar** Estimated time spent: **#** hours spent in total -Deployed Application (**required**): [Flixster Deployed Site](ADD_LINK_HERE) +Deployed Application (**required**): [Flixster Deployed Site](https://flixster-cr7f.onrender.com) ### Application Features @@ -31,39 +29,39 @@ Deployed Application (**required**): [Flixster Deployed Site](ADD_LINK_HERE) - [x] Clicks the Submit/Search button - [x] Users can click the Clear button. When clicked: - [x] Most recent search results are cleared from the text input field and the grid view and all current movies are displayed in a grid view -- [ ] **Design Features** - - [ ] Website implements all of the following accessibility features: - - [ ] Semantic HTML - - [ ] [Color contrast](https://webaim.org/resources/contrastchecker/) - - [ ] Alt text for images - - [ ] Website implements responsive web design. +- [x] **Design Features** + - [x] Website implements all of the following accessibility features: + - [x] Semantic HTML + - [x] [Color contrast](https://webaim.org/resources/contrastchecker/) + - [x] Alt text for images + - [x] Website implements responsive web design. - [x] Uses CSS Flexbox or CSS Grid - - [ ] Movie tiles and images shrink/grow in response to window size - - [ ] Users can click on a movie tile to view more details about a movie in a pop-up modal. - - [ ] The pop-up window is centered in the screen and does not occupy the entire screen. - - [ ] The pop-up window has a shadow to show that it is a pop-up and appears floating on the screen. - - [ ] The backdrop of the pop-up appears darker or in a different shade than before. including: - - [ ] The pop-up displays additional details about the moving including: + - [x] Movie tiles and images shrink/grow in response to window size + - [x] Users can click on a movie tile to view more details about a movie in a pop-up modal. + - [x] The pop-up window is centered in the screen and does not occupy the entire screen. + - [x] The pop-up window has a shadow to show that it is a pop-up and appears floating on the screen. + - [x] The backdrop of the pop-up appears darker or in a different shade than before. including: + - [x] The pop-up displays additional details about the moving including: - [ ] Runtime in minutes - - [ ] Backdrop poster - - [ ] Release date - - [ ] Genres - - [ ] An overview + - [x] Backdrop poster + - [x] Release date + - [x] Genres + - [x] An overview - [x] Users can use a drop-down menu to sort movies. - [x] Drop-down allows movies to be sorted by: - [x] Title (alphabetic, A-Z) - [x] Release date (chronologically, most recent to oldest) - [x] Vote average (descending, highest to lowest) - [x] When a sort option is clicked, movies display in a grid according to selected criterion. - - [ ] Website displays: - - [ ] Header section - - [ ] Banner section + - [x] Website displays: + - [x] Header section + - [x] Banner section - [x] Search bar - [x] Movie grid - [x] Footer section - [ ] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: To ease the grading process, please use the [color contrast checker](https://webaim.org/resources/contrastchecker/) to demonstrate to the grading team that text and background colors on your website have appropriate contrast. The Contrast Ratio should be above 4.5:1 and should have a green box surrounding it. - - [ ] **Deployment** - - [ ] Website is deployed via Render. + - [x] **Deployment** + - [x] Website is deployed via Render. - [ ] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: For ease of grading, please use the deployed version of your website when creating your walkthrough. #### STRETCH FEATURES @@ -72,33 +70,33 @@ Deployed Application (**required**): [Flixster Deployed Site](ADD_LINK_HERE) - [ ] **Embedded Movie Trailers** - [ ] Within the pop-up modal displaying a movie's details, the movie trailer is viewable. - [ ] When the trailer is clicked, users can play the movie trailer. -- [ ] **Favorite Button** - - [ ] For each movie displayed, users can favorite the movie. - - [ ] There should be visual element (such as a heart icon) on each movie's tile to show whether or not the movie has been favorited. - - [ ] If the movie is not favorited: - - [ ] Clicking on the visual element should mark the movie as favorited - - [ ] There should be visual feedback (such as the heart turning a different color) to show that the movie has been favorited by the user. - - [ ] If the movie is already favorited: - - [ ] Clicking on the visual element should mark the movie as *not* favorited. - - [ ] There should be visual feedback (such as the heart turning a different color) to show that the movie has been unfavorited. -- [ ] **Watched Checkbox** - - [ ] For each movie displayed, users can mark the movie as watched. - - [ ] There should be visual element (such as an eye icon) on each movie's tile to show whether or not the movie has been watched. - - [ ] If the movie has not been watched: - - [ ] Clicking on the visual element should mark the movie as watched - - [ ] There should be visual feedback (such as the eye turning a different color) to show that the movie has been watched by the user. - - [ ] If the movie is already watched: - - [ ] Clicking on the visual element should mark the movie as *not* watched. - - [ ] There should be visual feedback (such as the eye turning a different color) to show that the movie has not been watched. -- [ ] **Sidebar** - - [ ] The website includes a side navigation bar. - - [ ] The sidebar has three pages: - - [ ] Home - - [ ] Favorites - - [ ] Watched - - [ ] The Home page displays all current movies in a grid view, the search bar, and the sort movies drop-down. - - [ ] The Favorites page displays all favorited movies in a grid view. - - [ ] The Watched page displays all watched movies in a grid view. +- [x] **Favorite Button** + - [x] For each movie displayed, users can favorite the movie. + - [x] There should be visual element (such as a heart icon) on each movie's tile to show whether or not the movie has been favorited. + - [x] If the movie is not favorited: + - [x] Clicking on the visual element should mark the movie as favorited + - [x] There should be visual feedback (such as the heart turning a different color) to show that the movie has been favorited by the user. + - [x] If the movie is already favorited: + - [x] Clicking on the visual element should mark the movie as *not* favorited. + - [x] There should be visual feedback (such as the heart turning a different color) to show that the movie has been unfavorited. +- [x] **Watched Checkbox** + - [x] For each movie displayed, users can mark the movie as watched. + - [x] There should be visual element (such as an eye icon) on each movie's tile to show whether or not the movie has been watched. + - [x] If the movie has not been watched: + - [x] Clicking on the visual element should mark the movie as watched + - [x] There should be visual feedback (such as the eye turning a different color) to show that the movie has been watched by the user. + - [x] If the movie is already watched: + - [x] Clicking on the visual element should mark the movie as *not* watched. + - [x] There should be visual feedback (such as the eye turning a different color) to show that the movie has not been watched. +- [x] **Sidebar** + - [x] The website includes a side navigation bar. + - [x] The sidebar has three pages: + - [x] Home + - [x] Favorites + - [x] Watched + - [x] The Home page displays all current movies in a grid view, the search bar, and the sort movies drop-down. + - [x] The Favorites page displays all favorited movies in a grid view. + - [x] The Watched page displays all watched movies in a grid view. ### Walkthrough Video diff --git a/src/App.css b/src/App.css index 33bd974e..334c9c46 100644 --- a/src/App.css +++ b/src/App.css @@ -5,7 +5,7 @@ .App { text-align: center; - background-color: #8AAAE5; + background-color: #2755AA; display: flex; flex-direction: row; min-height: 100vh; @@ -13,7 +13,6 @@ #title { padding: .5vh 0vw; - /* background-color: #6795e5; */ margin-bottom: 1.5vh; } @@ -58,28 +57,34 @@ form button:hover { display: flex; flex-direction: column; max-width: 85vw; + margin-left: 15vw; } .sidebar { - background-color: #6795e5; + position: fixed; + top: 50%; + left: 0; + transform: translateY(-50%); + background-color: #10254C; display: flex; flex-direction: column; width: 15vw; - height: auto; gap: 2vh; - flex-shrink: 0; - /* position: sticky; */ + justify-content: center; + height: 100vh; } .sidebar button { background-color: transparent; border: none; text-align: center; + /* font-size: xx-large; */ + text-decoration: underline; } .sidebar button:hover { - color: #2c5db2; + color: #8AAAE5; background-color: inherit; } @@ -87,6 +92,7 @@ form button:hover { max-width: 100vw; display: flex; flex-wrap: wrap; + align-items: stretch; justify-content: center; gap: 2vw; margin: 2vh 5vw; @@ -97,7 +103,8 @@ form button:hover { flex-direction: column; min-width: 150px; width: 18%; - gap: 5px; + /* flex: 1 1 18%; */ + /* gap: 5px; */ box-sizing: border-box; border-radius: 10px; background-color: white; @@ -109,19 +116,22 @@ form button:hover { .movie-card img { object-fit: cover; - height: fit-content; + /* height: fit-content; */ + height: auto; } .movie-info { display: flex; + flex: 1; flex-direction: column; padding: 10px; + gap: 2vh; justify-content: space-between; } -.movie-info * { +/* .movie-info * { margin: auto; -} +} */ .features { display: flex; @@ -149,7 +159,8 @@ form button:hover { color: black; border: none; font-size: 20px; - background-color: #8AAAE5; + background-color: #2755AA; + text-decoration: underline; } #load-more:hover { diff --git a/src/App.jsx b/src/App.jsx index ab374009..d54a380a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -16,6 +16,7 @@ const App = () => { const [genreMap, setGenreMap] = useState({}); const [favoritedMovies, setFavoritedMovies] = useState([]); const [watchedMovies, setWatchedMovies] = useState([]); + const [activeView, setActiveView] = useState('home'); const apiKey = import.meta.env.VITE_APP_API_KEY; const fetchData = async () => { @@ -102,32 +103,21 @@ const App = () => { setSelectedMovieData(selectedMovieDataObj); } - // TODO: refactor const updateFavoritedMovies = async (id) => { const favoritedMovie = movieData.find(movie => movie.id == id); - if (!favoritedMovies.includes(favoritedMovie)) { - let allFavoritedMovies = [...favoritedMovies]; - allFavoritedMovies.push(favoritedMovie); - setFavoritedMovies(allFavoritedMovies); + if (favoritedMovies.includes(favoritedMovie)) { + setFavoritedMovies(prev => prev.filter(movie => movie != favoritedMovie)); } else { - let allFavoritedMovies = [...favoritedMovies]; - const indexOfMovie = movieData.indexOf(favoritedMovie); - allFavoritedMovies.splice(indexOfMovie, 1); - setFavoritedMovies(allFavoritedMovies); + setFavoritedMovies(prev => [...prev, favoritedMovie]); } }; const updateWatchedMovies = async (id) => { const watchedMovie = movieData.find(movie => movie.id == id); - if (!watchedMovies.includes(watchedMovie)) { - let allWatchedMovies = [...watchedMovies]; - allWatchedMovies.push(watchedMovie); - setWatchedMovies(allWatchedMovies); + if (watchedMovies.includes(watchedMovie)) { + setWatchedMovies(prev => prev.filter(movie => movie != watchedMovie)); } else { - let allWatchedMovies = [...watchedMovies]; - const indexOfMovie = movieData.indexOf(watchedMovie); - allWatchedMovies.splice(indexOfMovie, 1); - setWatchedMovies(allWatchedMovies); + setWatchedMovies(prev => [...prev, watchedMovie]); } }; @@ -152,25 +142,54 @@ const App = () => { const handleSortOptionSelected = async (sortOption) => { setSortOption(sortOption); let sortedMovieData = sortMovieData(movieData, sortOption); - setMovieData(sortedMovieData); + setMovieData(sortedMovieData) } + const onSidebarClick = async (label) => { + switch (label) { + case 'favorites': + setActiveView('favorites'); + break; + case 'watched': + setActiveView('watched'); + break; + case 'home': + setActiveView('home') + break; + default: + console.error("Invalid option was selected.") + } + } + + const moviesToDisplay = activeView == 'favorites' ? favoritedMovies : activeView == 'watched' ? watchedMovies : movieData; + return (
- +

Flixster 🎥

- - + + {activeView == 'home' && ( + + )}
2025 Flixter
diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 3b6a9395..0e79e779 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,4 +1,4 @@ -function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpen, updateFavoritedMovies, updateWatchedMovies }) { +function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpen, updateFavoritedMovies, updateWatchedMovies, isFavorited, isWatched }) { const handleModalClick = () => { updateSelectedMovieData(id); @@ -23,8 +23,8 @@ function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpe

Rating: {rating.toFixed(2)}

{/* Button Component Here (reusable) */} - - + +
diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx index abc39929..f932fe5f 100644 --- a/src/components/MovieList.jsx +++ b/src/components/MovieList.jsx @@ -1,6 +1,6 @@ import MovieCard from "./MovieCard" -function MovieList({ movieData, onMovieClick, onButtonClick }) { +function MovieList({ movieData, favoritedMovies, watchedMovies, onMovieClick, onButtonClick }) { const { updateSelectedMovieData, setIsOpen } = onMovieClick; const { updateFavoritedMovies, updateWatchedMovies } = onButtonClick; return ( @@ -10,7 +10,7 @@ function MovieList({ movieData, onMovieClick, onButtonClick }) { movieData.map(movie => { let image = `https://image.tmdb.org/t/p/original/${movie.poster_path}`; - return + return }) }
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index f50d8355..08af8a45 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,9 +1,14 @@ -function Sidebar() { +function Sidebar({ onClick }) { + + const handleClick = (event) => { + onClick(event.target.textContent.toLowerCase()); + } + return (
- - - + + +
) } From 69be5a8fe2d0bbd3d8f612199812b4a2dcd6312f Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Fri, 13 Jun 2025 11:15:34 -0700 Subject: [PATCH 10/19] add trailer to modal & separate CSS files --- README.md | 14 +-- src/App.css | 179 +--------------------------------- src/App.jsx | 33 +++---- src/components/Modal.css | 49 ++++++++++ src/components/Modal.jsx | 6 ++ src/components/MovieCard.css | 46 +++++++++ src/components/MovieCard.jsx | 2 + src/components/MovieList.css | 9 ++ src/components/MovieList.jsx | 1 + src/components/SearchForm.css | 23 +++++ src/components/SearchForm.jsx | 4 +- src/components/Sidebar.css | 23 +++++ src/components/Sidebar.jsx | 2 + src/components/SortMenu.css | 5 + src/components/SortMenu.jsx | 2 + 15 files changed, 195 insertions(+), 203 deletions(-) create mode 100644 src/components/Modal.css create mode 100644 src/components/MovieCard.css create mode 100644 src/components/MovieList.css create mode 100644 src/components/SearchForm.css create mode 100644 src/components/Sidebar.css create mode 100644 src/components/SortMenu.css diff --git a/README.md b/README.md index 2668cfb1..6d6a2ff7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Deployed Application (**required**): [Flixster Deployed Site](https://flixster-c - [x] Users can load more current movies by clicking a button which adds more movies to the grid without reloading the entire page. - [x] **Search Functionality** - [x] Users can use a search bar to search for movies by title. - - [ ] The search bar should include: + - [x] The search bar should include: - [x] Text input field - [x] Submit/Search button - [x] Clear button @@ -42,7 +42,7 @@ Deployed Application (**required**): [Flixster Deployed Site](https://flixster-c - [x] The pop-up window has a shadow to show that it is a pop-up and appears floating on the screen. - [x] The backdrop of the pop-up appears darker or in a different shade than before. including: - [x] The pop-up displays additional details about the moving including: - - [ ] Runtime in minutes + - [x] Runtime in minutes - [x] Backdrop poster - [x] Release date - [x] Genres @@ -59,17 +59,17 @@ Deployed Application (**required**): [Flixster Deployed Site](https://flixster-c - [x] Search bar - [x] Movie grid - [x] Footer section - - [ ] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: To ease the grading process, please use the [color contrast checker](https://webaim.org/resources/contrastchecker/) to demonstrate to the grading team that text and background colors on your website have appropriate contrast. The Contrast Ratio should be above 4.5:1 and should have a green box surrounding it. + - [x] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: To ease the grading process, please use the [color contrast checker](https://webaim.org/resources/contrastchecker/) to demonstrate to the grading team that text and background colors on your website have appropriate contrast. The Contrast Ratio should be above 4.5:1 and should have a green box surrounding it. - [x] **Deployment** - [x] Website is deployed via Render. - - [ ] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: For ease of grading, please use the deployed version of your website when creating your walkthrough. + - [x] **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS**: For ease of grading, please use the deployed version of your website when creating your walkthrough. #### STRETCH FEATURES -- [ ] **Embedded Movie Trailers** - - [ ] Within the pop-up modal displaying a movie's details, the movie trailer is viewable. - - [ ] When the trailer is clicked, users can play the movie trailer. +- [x] **Embedded Movie Trailers** + - [x] Within the pop-up modal displaying a movie's details, the movie trailer is viewable. + - [x] When the trailer is clicked, users can play the movie trailer. - [x] **Favorite Button** - [x] For each movie displayed, users can favorite the movie. - [x] There should be visual element (such as a heart icon) on each movie's tile to show whether or not the movie has been favorited. diff --git a/src/App.css b/src/App.css index 334c9c46..a0796633 100644 --- a/src/App.css +++ b/src/App.css @@ -14,10 +14,12 @@ #title { padding: .5vh 0vw; margin-bottom: 1.5vh; + background-color: #8AAAE5; } #toolbar { display: flex; + flex-wrap: wrap; justify-content: space-around; } @@ -25,134 +27,13 @@ display: flex; } -form * { - border: none; - border-radius: 5px; - padding: .1vh .2vw; -} - -form { - display: flex; - gap: 1vw; -} - -form button { - margin: 0px .1vw; - background-color: #f4f4f4; - color: black; -} - -#sort-options { - border-radius: 5px; - padding: .1vh .2vw; - max-width: 30%; -} - -form button:hover { - background-color: black; - color: white; -} - .content-container { display: flex; flex-direction: column; - max-width: 85vw; + width: 85vw; margin-left: 15vw; } - -.sidebar { - position: fixed; - top: 50%; - left: 0; - transform: translateY(-50%); - background-color: #10254C; - display: flex; - flex-direction: column; - width: 15vw; - gap: 2vh; - justify-content: center; - height: 100vh; -} - -.sidebar button { - background-color: transparent; - border: none; - text-align: center; - /* font-size: xx-large; */ - text-decoration: underline; -} - -.sidebar button:hover { - color: #8AAAE5; - background-color: inherit; -} - -#movie-list { - max-width: 100vw; - display: flex; - flex-wrap: wrap; - align-items: stretch; - justify-content: center; - gap: 2vw; - margin: 2vh 5vw; -} - -.movie-card { - display: flex; - flex-direction: column; - min-width: 150px; - width: 18%; - /* flex: 1 1 18%; */ - /* gap: 5px; */ - box-sizing: border-box; - border-radius: 10px; - background-color: white; -} - -.movie-card * { - border-radius: 10px; -} - -.movie-card img { - object-fit: cover; - /* height: fit-content; */ - height: auto; -} - -.movie-info { - display: flex; - flex: 1; - flex-direction: column; - padding: 10px; - gap: 2vh; - justify-content: space-between; -} - -/* .movie-info * { - margin: auto; -} */ - -.features { - display: flex; - justify-content: space-between; - gap: 1vw; -} - -.features button { - border-radius: 0px; - background-color: white; - color: black; - border: none; - font-weight: lighter; -} - -.features button:hover { - background-color: white; - color: black; - cursor: pointer; -} - #load-more { margin: 1vh auto 3vh; border-radius: 5px; @@ -167,61 +48,9 @@ form button:hover { cursor: pointer; } -#modal-overlay { - position: fixed; - z-index: 1; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow-y: auto; - background-color: rgba(0,0,0,0.4); - backdrop-filter: blur(5px); -} - -#modal-content { - background-color: #fefefe; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - padding: 20px; - border: 1px solid #888; - max-width: 50vw; - max-height: 50vh; - overflow-y: auto; - display: flex; - flex-direction: column; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); - align-items: center; - border-radius: 25px; -} - -#modal-content img { - height: 30vh; - width: auto; -} - -#modal-content * { - margin-top: 2vh; -} - -#close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - display: flex; - cursor: pointer; -} - -#close { - margin-left: auto; - margin-top: 0; -} - footer { text-align: center; padding: 1vh; margin-top: auto; + background-color: #8AAAE5; } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index d54a380a..a43907e3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,7 +13,6 @@ const App = () => { const [movieData, setMovieData] = useState([]); const [selectedMovieData, setSelectedMovieData] = useState({}); const [sortOption, setSortOption] = useState(''); - const [genreMap, setGenreMap] = useState({}); const [favoritedMovies, setFavoritedMovies] = useState([]); const [watchedMovies, setWatchedMovies] = useState([]); const [activeView, setActiveView] = useState('home'); @@ -21,7 +20,6 @@ const App = () => { const fetchData = async () => { try { - fetchGenreData(); let response = null; if (query) { const formattedQuery = query.split(' ').join('%20'); @@ -38,20 +36,6 @@ const App = () => { } } - const fetchGenreData = async () => { - let response = await fetch(`https://api.themoviedb.org/3/genre/movie/list?api_key=${apiKey}&language=en`); - let genreData = await response.json(); - const map = {}; - genreData.genres.forEach(info => { - map[info.id] = info.name; - }); - setGenreMap(map); - } - - useEffect(() => { - fetchGenreData(); - }, []); - useEffect(() => { setPage(1); }, [query]); @@ -90,15 +74,24 @@ const App = () => { } const updateSelectedMovieData = async (id) => { - const selectedMovie = movieData.find(movie => movie.id == id); - let image = `https://image.tmdb.org/t/p/original/${selectedMovie.poster_path}`; - let genres = selectedMovie.genre_ids.map(id => genreMap[id]).join(', '); + let selectedMovieData = await fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`); + let selectedMovie = await selectedMovieData.json(); + let image = `https://image.tmdb.org/t/p/original/${selectedMovie.backdrop_path}`; + let genres = selectedMovie.genres.map(genre => genre.name).join(', '); + let movieVideosData = await fetch(`https://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US`); + let movieVideos = await movieVideosData.json(); + + let movieTrailer = movieVideos.results.find(trailer => trailer.type == 'Trailer'); + let movieTrailerLink = `https://www.youtube.com/embed/${movieTrailer.key}`; + const selectedMovieDataObj = { title: selectedMovie.title, image, + runtime: selectedMovie.runtime, release_date: selectedMovie.release_date, overview: selectedMovie.overview, - genres + genres, + trailer: movieTrailerLink } setSelectedMovieData(selectedMovieDataObj); } diff --git a/src/components/Modal.css b/src/components/Modal.css new file mode 100644 index 00000000..7045b7bf --- /dev/null +++ b/src/components/Modal.css @@ -0,0 +1,49 @@ +#modal-overlay { + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow-y: auto; + background-color: rgba(0,0,0,0.4); + backdrop-filter: blur(5px); +} + +#modal-content { + background-color: #fefefe; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border: 1px solid #888; + max-width: 50vw; + max-height: 50vh; + overflow-y: auto; + display: flex; + flex-direction: column; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + align-items: center; + border-radius: 25px; + padding: 3vh 3vw; +} + +#modal-content img { + height: 30vh; + width: auto; +} + +#modal-content * { + margin-top: 2vh; +} + +#close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + display: flex; + cursor: pointer; + margin-left: auto; + margin-top: 0; +} \ No newline at end of file diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx index 49c0bfe2..aab62f9c 100644 --- a/src/components/Modal.jsx +++ b/src/components/Modal.jsx @@ -1,3 +1,5 @@ +import './Modal.css' + function Modal({ selectedMovieData, setIsOpen, isOpen }) { if (!isOpen) return null; const handleClickOutside = (event) => { @@ -15,9 +17,13 @@ function Modal({ selectedMovieData, setIsOpen, isOpen }) { ×

{selectedMovieData.title}

{`Poster +

Runtime: {selectedMovieData.runtime} minutes

Release Date: {selectedMovieData.release_date}

Overview: {selectedMovieData.overview}

Genres: {selectedMovieData.genres}

+ { + selectedMovieData.trailer && + }
) diff --git a/src/components/MovieCard.css b/src/components/MovieCard.css new file mode 100644 index 00000000..294cfbea --- /dev/null +++ b/src/components/MovieCard.css @@ -0,0 +1,46 @@ +.movie-card { + display: flex; + flex-direction: column; + min-width: 150px; + width: 14%; + box-sizing: border-box; + border-radius: 10px; + background-color: white; +} + +.movie-card * { + border-radius: 10px; +} + +.movie-card img { + object-fit: cover; +} + +.movie-info { + display: flex; + flex: 1; + flex-direction: column; + padding: 10px; + gap: 2vh; + justify-content: space-between; +} + +.features { + display: flex; + justify-content: space-between; + gap: 1vw; +} + +.features button { + border-radius: 0px; + background-color: white; + color: black; + border: none; + font-weight: lighter; +} + +.features button:hover { + background-color: white; + color: black; + cursor: pointer; +} \ No newline at end of file diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 0e79e779..831694e9 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,3 +1,5 @@ +import './MovieCard.css' + function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpen, updateFavoritedMovies, updateWatchedMovies, isFavorited, isWatched }) { const handleModalClick = () => { diff --git a/src/components/MovieList.css b/src/components/MovieList.css new file mode 100644 index 00000000..3c9d1464 --- /dev/null +++ b/src/components/MovieList.css @@ -0,0 +1,9 @@ +#movie-list { + max-width: 100vw; + display: flex; + flex-wrap: wrap; + align-items: stretch; + justify-content: center; + gap: 2vw; + margin: 2vh 5vw; +} \ No newline at end of file diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx index f932fe5f..3e7339e5 100644 --- a/src/components/MovieList.jsx +++ b/src/components/MovieList.jsx @@ -1,4 +1,5 @@ import MovieCard from "./MovieCard" +import './MovieList.css' function MovieList({ movieData, favoritedMovies, watchedMovies, onMovieClick, onButtonClick }) { const { updateSelectedMovieData, setIsOpen } = onMovieClick; diff --git a/src/components/SearchForm.css b/src/components/SearchForm.css new file mode 100644 index 00000000..f284d630 --- /dev/null +++ b/src/components/SearchForm.css @@ -0,0 +1,23 @@ +.search-form { + display: flex; + gap: 1vw; + flex-wrap: wrap; + justify-content: center; +} + +.search-form * { + border: none; + border-radius: 5px; + padding: .1vh .2vw; +} + +.search-form button { + margin: 0px .1vw; + background-color: #f4f4f4; + color: black; +} + +.search-form button:hover { + background-color: black; + color: white; +} \ No newline at end of file diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx index f49c77d7..97141c57 100644 --- a/src/components/SearchForm.jsx +++ b/src/components/SearchForm.jsx @@ -1,3 +1,5 @@ +import './SearchForm.css' + function SearchForm({ onQueryChange, onClear }) { const handleSubmit = (event) => { event.preventDefault(); @@ -8,7 +10,7 @@ function SearchForm({ onQueryChange, onClear }) { return ( <> - +
diff --git a/src/components/Sidebar.css b/src/components/Sidebar.css new file mode 100644 index 00000000..d2a18dba --- /dev/null +++ b/src/components/Sidebar.css @@ -0,0 +1,23 @@ +.sidebar { + position: fixed; + left: 0; + background-color: #10254C; + display: flex; + flex-direction: column; + width: 15vw; + gap: 2vh; + justify-content: center; + height: 100vh; +} + +.sidebar button { + background-color: transparent; + border: none; + text-align: center; + text-decoration: underline; +} + +.sidebar button:hover { + color: #8AAAE5; + background-color: inherit; +} \ No newline at end of file diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 08af8a45..2cd3fab8 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,3 +1,5 @@ +import './Sidebar.css' + function Sidebar({ onClick }) { const handleClick = (event) => { diff --git a/src/components/SortMenu.css b/src/components/SortMenu.css new file mode 100644 index 00000000..dde40b92 --- /dev/null +++ b/src/components/SortMenu.css @@ -0,0 +1,5 @@ +#sort-options { + border-radius: 5px; + padding: .1vh .2vw; + max-width: 30%; +} \ No newline at end of file diff --git a/src/components/SortMenu.jsx b/src/components/SortMenu.jsx index 8533bd56..a48101ea 100644 --- a/src/components/SortMenu.jsx +++ b/src/components/SortMenu.jsx @@ -1,3 +1,5 @@ +import './SortMenu.css' + function SortMenu( {sort} ) { const handleChange = (event) => { sort(event.target.value); From c09f30b784311dc2317f564586379cb05edd5cb6 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Fri, 13 Jun 2025 11:54:40 -0700 Subject: [PATCH 11/19] add demo video to README --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6d6a2ff7..a886cdea 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Submitted by: **Carlos Escobar** -Estimated time spent: **#** hours spent in total +Estimated time spent: 16 hours spent in total -Deployed Application (**required**): [Flixster Deployed Site](https://flixster-cr7f.onrender.com) +Deployed Application: [Flixster Deployed Site](https://flixster-cr7f.onrender.com) ### Application Features @@ -100,27 +100,29 @@ Deployed Application (**required**): [Flixster Deployed Site](https://flixster-c ### Walkthrough Video -`TODO://` Add the embedded URL code to your animated app walkthrough below, `ADD_EMBEDDED_CODE_HERE`. Make sure the video or gif actually renders and animates when viewing this README. Ensure your walkthrough showcases the presence and/or functionality of all features you implemented above (check them off as you film!). Pay attention to any **VIDEO WALKTHROUGH SPECIAL INSTRUCTIONS** checkboxes listed above to ensure graders see the full functionality of your website! (🚫 Remove this paragraph after adding walkthrough video) - -`ADD_EMBEDDED_CODE_HERE` +
+ + + + +
### 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? -Add your response here +The labs did prepare us for this project. An instance I can think of where it really helped was for the weather project where we had to display the different days in a card format. This was similar to the movie list and the card 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. -Add your response here +I am writing this with 6 hours till the deadline and am hoping I could better the code. I want to handle errors more gracefully, try to reuse the favorite/watched button as they share similar functionality, and add a cool hover effect when the cards are hovered over. -* 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? -Add your response here +* 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? -### Open-source libraries used +The demo went fine. I got to highlight how I sort the incoming data whenever the 'Load More' button is pressed, which was a stretch feature of my own. Nothing really went bad, which is a good thing. -- Add any links to open-source libraries used in your project. ### Shout out From 20cd4560d21c81356a09331878da95725b495eea Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Fri, 13 Jun 2025 14:57:44 -0700 Subject: [PATCH 12/19] remove empty utils folder --- src/utils/utils.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/utils/utils.js diff --git a/src/utils/utils.js b/src/utils/utils.js deleted file mode 100644 index e69de29b..00000000 From 14b07b0115c4c820c549efff10a80da3b3ab3d2d Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Fri, 13 Jun 2025 15:36:18 -0700 Subject: [PATCH 13/19] add new demo video to README showcasing responsiveness --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a886cdea..a865d402 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,11 @@ Deployed Application: [Flixster Deployed Site](https://flixster-cr7f.onrender.co ### Walkthrough Video
- - - - -
+ + + + +
### Reflection From 92e3359ffe124e24d77d3ff44a46fa5f74148d0a Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Sat, 14 Jun 2025 14:55:20 -0700 Subject: [PATCH 14/19] move functionality from App to their respective components --- src/App.jsx | 116 ++++++++--------------------------- src/components/Modal.jsx | 18 +++--- src/components/MovieCard.jsx | 31 ++++++++-- src/components/MovieList.jsx | 5 +- src/components/Sidebar.jsx | 25 ++++++-- src/components/SortMenu.jsx | 29 +++++++-- 6 files changed, 105 insertions(+), 119 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index a43907e3..3347cd54 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,8 +11,7 @@ const App = () => { const [query, setQuery] = useState(''); const [page, setPage] = useState(1); const [movieData, setMovieData] = useState([]); - const [selectedMovieData, setSelectedMovieData] = useState({}); - const [sortOption, setSortOption] = useState(''); + const [modalData, setModalData] = useState({}); const [favoritedMovies, setFavoritedMovies] = useState([]); const [watchedMovies, setWatchedMovies] = useState([]); const [activeView, setActiveView] = useState('home'); @@ -34,7 +33,7 @@ const App = () => { } catch (error) { console.error(error); } - } + }; useEffect(() => { setPage(1); @@ -47,14 +46,7 @@ const App = () => { if (page == 1) { setMovieData(movieInfoData); } else { - if (sortOption != '') { - setMovieData(prev => { - let combinedData = [...prev, ...data.results]; - return sortMovieData(combinedData, sortOption); - }) - } else { - setMovieData(prev => [...prev, ...data.results]); - } + setMovieData(prev => [...prev, ...data.results]); } }; fetchMovieData(); @@ -62,103 +54,46 @@ const App = () => { const handleQueryChange = async (query) => { setQuery(query); - } + }; const handleClear = async () => { setQuery(''); setPage(1); - } + }; const handlePageChange = async () => { setPage(prev => prev + 1); - } - - const updateSelectedMovieData = async (id) => { - let selectedMovieData = await fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`); - let selectedMovie = await selectedMovieData.json(); - let image = `https://image.tmdb.org/t/p/original/${selectedMovie.backdrop_path}`; - let genres = selectedMovie.genres.map(genre => genre.name).join(', '); - let movieVideosData = await fetch(`https://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US`); - let movieVideos = await movieVideosData.json(); + }; - let movieTrailer = movieVideos.results.find(trailer => trailer.type == 'Trailer'); - let movieTrailerLink = `https://www.youtube.com/embed/${movieTrailer.key}`; + const updateModalData = async (modalMovieInfo) => { + await setModalData(modalMovieInfo); + setIsOpen(true); + }; - const selectedMovieDataObj = { - title: selectedMovie.title, - image, - runtime: selectedMovie.runtime, - release_date: selectedMovie.release_date, - overview: selectedMovie.overview, - genres, - trailer: movieTrailerLink - } - setSelectedMovieData(selectedMovieDataObj); - } + const updateMovies = (id, setMovies) => { + const movie = movieData.find(m => m.id == id); + setMovies(prev => { + return prev.includes(movie) ? prev.filter(m => m.id != id) : [...prev, movie]; + }) + }; const updateFavoritedMovies = async (id) => { - const favoritedMovie = movieData.find(movie => movie.id == id); - if (favoritedMovies.includes(favoritedMovie)) { - setFavoritedMovies(prev => prev.filter(movie => movie != favoritedMovie)); - } else { - setFavoritedMovies(prev => [...prev, favoritedMovie]); - } + updateMovies(id, setFavoritedMovies); }; const updateWatchedMovies = async (id) => { - const watchedMovie = movieData.find(movie => movie.id == id); - if (watchedMovies.includes(watchedMovie)) { - setWatchedMovies(prev => prev.filter(movie => movie != watchedMovie)); - } else { - setWatchedMovies(prev => [...prev, watchedMovie]); - } + updateMovies(id, setWatchedMovies); }; - const sortMovieData = (movieData, sortOption) => { - let sortedMovieData = [...movieData]; - switch (sortOption) { - case 'alphabetical': - sortedMovieData.sort((a, b) => a.title.localeCompare(b.title)); - break; - case 'chronological': - sortedMovieData.sort((a, b) => b.release_date.localeCompare(a.release_date)); - break; - case 'vote-average': - sortedMovieData.sort((a, b) => b.vote_average - a.vote_average); - break; - default: - console.error("Invalid sort option selected.") - } - return sortedMovieData; - } - - const handleSortOptionSelected = async (sortOption) => { - setSortOption(sortOption); - let sortedMovieData = sortMovieData(movieData, sortOption); - setMovieData(sortedMovieData) - } - - const onSidebarClick = async (label) => { - switch (label) { - case 'favorites': - setActiveView('favorites'); - break; - case 'watched': - setActiveView('watched'); - break; - case 'home': - setActiveView('home') - break; - default: - console.error("Invalid option was selected.") - } - } + const handleSort = async (sortedMovieData) => { + setMovieData(sortedMovieData); + }; const moviesToDisplay = activeView == 'favorites' ? favoritedMovies : activeView == 'watched' ? watchedMovies : movieData; return (
- +

Flixster 🎥

@@ -167,7 +102,7 @@ const App = () => { {activeView == 'home' && (
- +
)} @@ -176,15 +111,14 @@ const App = () => { movieData={moviesToDisplay} favoritedMovies={favoritedMovies} watchedMovies={watchedMovies} - - onMovieClick={{updateSelectedMovieData, setIsOpen}} + updateModalData={updateModalData} onButtonClick={{updateFavoritedMovies, updateWatchedMovies}} /> {activeView == 'home' && ( )} - +
2025 Flixter
diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx index aab62f9c..aeb5cf5a 100644 --- a/src/components/Modal.jsx +++ b/src/components/Modal.jsx @@ -1,6 +1,6 @@ import './Modal.css' -function Modal({ selectedMovieData, setIsOpen, isOpen }) { +function Modal({ modalData, setIsOpen, isOpen }) { if (!isOpen) return null; const handleClickOutside = (event) => { if (event.target.id === 'modal-overlay') { @@ -15,15 +15,13 @@ function Modal({ selectedMovieData, setIsOpen, isOpen }) { ) diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 831694e9..06b6a6f1 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,10 +1,31 @@ import './MovieCard.css' -function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpen, updateFavoritedMovies, updateWatchedMovies, isFavorited, isWatched }) { +function MovieCard({ image, title, rating, id, updateModalData, updateFavoritedMovies, updateWatchedMovies, isFavorited, isWatched }) { - const handleModalClick = () => { - updateSelectedMovieData(id); - setIsOpen(true); + const apiKey = import.meta.env.VITE_APP_API_KEY; + + const handleCardClick = async () => { + let modalMovieData = await fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`); + let modalMovie = await modalMovieData.json(); + let backdrop = `https://image.tmdb.org/t/p/original/${modalMovie.backdrop_path}`; + let genres = modalMovie.genres.map(genre => genre.name).join(', '); + let movieVideosData = await fetch(`https://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US`); + let movieVideos = await movieVideosData.json(); + + let movieTrailer = movieVideos.results.find(trailer => trailer.type == 'Trailer'); + let movieTrailerLink = `https://www.youtube.com/embed/${movieTrailer.key}`; + + const modalMovieInfo = { + title: modalMovie.title, + backdrop, + runtime: modalMovie.runtime, + release_date: modalMovie.release_date, + overview: modalMovie.overview, + genres, + trailer: movieTrailerLink + } + + updateModalData(modalMovieInfo); } const onFavorite = (event) => { @@ -18,7 +39,7 @@ function MovieCard({ image, title, rating, id, updateSelectedMovieData, setIsOpe } return ( -
+
{`Poster

{title}

diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx index 3e7339e5..c7c4eaf8 100644 --- a/src/components/MovieList.jsx +++ b/src/components/MovieList.jsx @@ -1,8 +1,7 @@ import MovieCard from "./MovieCard" import './MovieList.css' -function MovieList({ movieData, favoritedMovies, watchedMovies, onMovieClick, onButtonClick }) { - const { updateSelectedMovieData, setIsOpen } = onMovieClick; +function MovieList({ movieData, favoritedMovies, watchedMovies, updateModalData, onButtonClick }) { const { updateFavoritedMovies, updateWatchedMovies } = onButtonClick; return ( <> @@ -11,7 +10,7 @@ function MovieList({ movieData, favoritedMovies, watchedMovies, onMovieClick, on movieData.map(movie => { let image = `https://image.tmdb.org/t/p/original/${movie.poster_path}`; - return + return }) }
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 2cd3fab8..d5e0ef52 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,16 +1,29 @@ import './Sidebar.css' -function Sidebar({ onClick }) { +function Sidebar({ setActiveView }) { - const handleClick = (event) => { - onClick(event.target.textContent.toLowerCase()); + const updateActiveView = async (event) => { + const label = event.target.textContent.toLowerCase(); + switch (label) { + case 'favorites': + setActiveView('favorites'); + break; + case 'watched': + setActiveView('watched'); + break; + case 'home': + setActiveView('home') + break; + default: + console.error("Invalid option was selected.") + } } return (
- - - + + +
) } diff --git a/src/components/SortMenu.jsx b/src/components/SortMenu.jsx index a48101ea..dd5f21af 100644 --- a/src/components/SortMenu.jsx +++ b/src/components/SortMenu.jsx @@ -1,11 +1,32 @@ import './SortMenu.css' -function SortMenu( {sort} ) { - const handleChange = (event) => { - sort(event.target.value); +function SortMenu({ movieData, onSort }) { + + const sortMovieData = (movieData, sortOption) => { + let sortedMovieData = [...movieData]; + switch (sortOption) { + case 'alphabetical': + sortedMovieData.sort((a, b) => a.title.localeCompare(b.title)); + break; + case 'chronological': + sortedMovieData.sort((a, b) => b.release_date.localeCompare(a.release_date)); + break; + case 'vote-average': + sortedMovieData.sort((a, b) => b.vote_average - a.vote_average); + break; + default: + console.error("Invalid sort option selected.") + } + return sortedMovieData; + }; + + const handleSortOptionChange = (sortOption) => { + const sortedData = sortMovieData(movieData, sortOption); + onSort(sortedData); } + return ( - handleSortOptionChange(event.target.value)}> From ec40023eae6e79edd27dc7fd74831b6ccc4c7f07 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Sat, 14 Jun 2025 16:05:11 -0700 Subject: [PATCH 15/19] improve compatibility with lesser-known titles --- src/App.jsx | 16 ++++++++-------- src/components/MovieCard.jsx | 2 +- src/components/Sidebar.jsx | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 3347cd54..b33e216c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -52,21 +52,21 @@ const App = () => { fetchMovieData(); }, [page, query]); - const handleQueryChange = async (query) => { + const handleQueryChange = (query) => { setQuery(query); }; - const handleClear = async () => { + const handleClear = () => { setQuery(''); setPage(1); }; - const handlePageChange = async () => { + const handlePageChange = () => { setPage(prev => prev + 1); }; - const updateModalData = async (modalMovieInfo) => { - await setModalData(modalMovieInfo); + const updateModalData = (modalMovieInfo) => { + setModalData(modalMovieInfo); setIsOpen(true); }; @@ -77,15 +77,15 @@ const App = () => { }) }; - const updateFavoritedMovies = async (id) => { + const updateFavoritedMovies = (id) => { updateMovies(id, setFavoritedMovies); }; - const updateWatchedMovies = async (id) => { + const updateWatchedMovies = (id) => { updateMovies(id, setWatchedMovies); }; - const handleSort = async (sortedMovieData) => { + const handleSort = (sortedMovieData) => { setMovieData(sortedMovieData); }; diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 06b6a6f1..0de19912 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -13,7 +13,7 @@ function MovieCard({ image, title, rating, id, updateModalData, updateFavoritedM let movieVideos = await movieVideosData.json(); let movieTrailer = movieVideos.results.find(trailer => trailer.type == 'Trailer'); - let movieTrailerLink = `https://www.youtube.com/embed/${movieTrailer.key}`; + let movieTrailerLink = movieTrailer ? `https://www.youtube.com/embed/${movieTrailer.key}` : null; const modalMovieInfo = { title: modalMovie.title, diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index d5e0ef52..43f6a0eb 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -2,7 +2,7 @@ import './Sidebar.css' function Sidebar({ setActiveView }) { - const updateActiveView = async (event) => { + const updateActiveView = (event) => { const label = event.target.textContent.toLowerCase(); switch (label) { case 'favorites': From 061cde1c6cd8ec41e6e3de63787a321a3a9c2846 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Sat, 14 Jun 2025 16:11:55 -0700 Subject: [PATCH 16/19] remove load more button when there are no more results --- src/App.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index b33e216c..ea06bc6f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,6 +11,7 @@ const App = () => { const [query, setQuery] = useState(''); const [page, setPage] = useState(1); const [movieData, setMovieData] = useState([]); + const [totalPages, setTotalPages] = useState(0); const [modalData, setModalData] = useState({}); const [favoritedMovies, setFavoritedMovies] = useState([]); const [watchedMovies, setWatchedMovies] = useState([]); @@ -43,6 +44,8 @@ const App = () => { const fetchMovieData = async () => { const data = await fetchData(); const movieInfoData = data.results; + setTotalPages(data.total_pages); + if (page == 1) { setMovieData(movieInfoData); } else { @@ -114,7 +117,7 @@ const App = () => { updateModalData={updateModalData} onButtonClick={{updateFavoritedMovies, updateWatchedMovies}} /> - {activeView == 'home' && ( + {activeView == 'home' && page < totalPages && ( )} From 2bbf1d72f756d7cb544591a9ba666a96155a208c Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Sun, 15 Jun 2025 10:56:44 -0700 Subject: [PATCH 17/19] add comments & clean up code --- src/App.jsx | 24 +++++++++++++--- src/components/Modal.jsx | 11 ++++++-- src/components/MovieCard.css | 5 ++++ src/components/MovieCard.jsx | 53 ++++++++++++++++++++--------------- src/components/MovieList.jsx | 7 ++--- src/components/SearchForm.jsx | 22 +++++++-------- src/components/Sidebar.css | 9 ++++-- src/components/Sidebar.jsx | 13 +++++---- src/components/SortMenu.jsx | 36 +++++++++++++----------- 9 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index ea06bc6f..1710d44f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import Modal from './components/Modal' import Sidebar from './components/Sidebar' const App = () => { + const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(''); const [page, setPage] = useState(1); @@ -16,36 +17,44 @@ const App = () => { const [favoritedMovies, setFavoritedMovies] = useState([]); const [watchedMovies, setWatchedMovies] = useState([]); const [activeView, setActiveView] = useState('home'); + + // Load API key from environment variable const apiKey = import.meta.env.VITE_APP_API_KEY; const fetchData = async () => { try { let response = null; if (query) { + // Handle a query with spaces const formattedQuery = query.split(' ').join('%20'); + // Fetch data based on what user searched for response = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${formattedQuery}&include_adult=false&language=en-US&page=${page}`); } else { + // Fetch from 'Now Playing' response = await fetch(`https://api.themoviedb.org/3/movie/now_playing?api_key=${apiKey}&language=en-US&page=${page}`); } if (!response.ok) { - throw new Error('Failed to fetch movie data.'); + throw new Error(`Failed to fetch movie data: ${response.status}`); } return await response.json(); } catch (error) { - console.error(error); + console.error('Error fetching data: ', error); } }; + // When query changes, fetch data starting at page 1 useEffect(() => { setPage(1); }, [query]); + // When user presses 'load more' or searches for a movie useEffect(() => { const fetchMovieData = async () => { const data = await fetchData(); const movieInfoData = data.results; setTotalPages(data.total_pages); - + + // Handle whether user pressed 'load more' or a new search was entered if (page == 1) { setMovieData(movieInfoData); } else { @@ -59,6 +68,7 @@ const App = () => { setQuery(query); }; + // Clears the movie catalog back to top 20 now playing const handleClear = () => { setQuery(''); setPage(1); @@ -68,11 +78,13 @@ const App = () => { setPage(prev => prev + 1); }; + // Callback function that updates modal data and opens modal const updateModalData = (modalMovieInfo) => { setModalData(modalMovieInfo); setIsOpen(true); }; + // Handles whether to add or remove movie from Favorited/Watched lists const updateMovies = (id, setMovies) => { const movie = movieData.find(m => m.id == id); setMovies(prev => { @@ -92,6 +104,7 @@ const App = () => { setMovieData(sortedMovieData); }; + // Handles what movies to display as cards based on the active view const moviesToDisplay = activeView == 'favorites' ? favoritedMovies : activeView == 'watched' ? watchedMovies : movieData; return ( @@ -102,6 +115,7 @@ const App = () => {

Flixster 🎥

) -} +}; export default App diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx index aeb5cf5a..1440674a 100644 --- a/src/components/Modal.jsx +++ b/src/components/Modal.jsx @@ -1,16 +1,23 @@ import './Modal.css' function Modal({ modalData, setIsOpen, isOpen }) { + + // If modal is not supposed to be open, don't display it if (!isOpen) return null; + + // Closes the modal when clicked on the outside of it const handleClickOutside = (event) => { if (event.target.id === 'modal-overlay') { setIsOpen(false); } }; + + // Closes the modal when 'close' icon is clicked const handleClickClose = (event) => { event.stopPropagation(); setIsOpen(false); - } + }; + return ( ) diff --git a/src/components/MovieCard.css b/src/components/MovieCard.css index 294cfbea..3685e6fb 100644 --- a/src/components/MovieCard.css +++ b/src/components/MovieCard.css @@ -6,6 +6,11 @@ box-sizing: border-box; border-radius: 10px; background-color: white; + transition: transform 0.2s ease; +} + +.movie-card:hover { + transform: scale(1.05); } .movie-card * { diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 0de19912..ee7466c8 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -2,41 +2,50 @@ import './MovieCard.css' function MovieCard({ image, title, rating, id, updateModalData, updateFavoritedMovies, updateWatchedMovies, isFavorited, isWatched }) { + // Load API key from environment variable const apiKey = import.meta.env.VITE_APP_API_KEY; const handleCardClick = async () => { - let modalMovieData = await fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`); - let modalMovie = await modalMovieData.json(); - let backdrop = `https://image.tmdb.org/t/p/original/${modalMovie.backdrop_path}`; - let genres = modalMovie.genres.map(genre => genre.name).join(', '); - let movieVideosData = await fetch(`https://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US`); - let movieVideos = await movieVideosData.json(); - let movieTrailer = movieVideos.results.find(trailer => trailer.type == 'Trailer'); - let movieTrailerLink = movieTrailer ? `https://www.youtube.com/embed/${movieTrailer.key}` : null; - - const modalMovieInfo = { - title: modalMovie.title, - backdrop, - runtime: modalMovie.runtime, - release_date: modalMovie.release_date, - overview: modalMovie.overview, - genres, - trailer: movieTrailerLink + try { + // Get data about movie that was clicked + let modalMovieData = await fetch(`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`); + let modalMovie = await modalMovieData.json(); + // Get backdrop image + let backdrop = `https://image.tmdb.org/t/p/original/${modalMovie.backdrop_path}`; + // Get genres in readable format + let genres = modalMovie.genres.map(genre => genre.name).join(', '); + // Get trailer + let movieVideosData = await fetch(`https://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US`); + let movieVideos = await movieVideosData.json(); + let movieTrailer = movieVideos.results.find(trailer => trailer.type == 'Trailer'); + let movieTrailerLink = movieTrailer ? `https://www.youtube.com/embed/${movieTrailer.key}` : null; + + const modalMovieInfo = { + title: modalMovie.title, + backdrop, + runtime: modalMovie.runtime, + release_date: modalMovie.release_date, + overview: modalMovie.overview, + genres, + trailer: movieTrailerLink + } + + updateModalData(modalMovieInfo); + } catch (error) { + console.error('Error loading movie data: ', error); } - - updateModalData(modalMovieInfo); - } + }; const onFavorite = (event) => { event.stopPropagation(); updateFavoritedMovies(id); - } + }; const onWatched = (event) => { event.stopPropagation(); updateWatchedMovies(id); - } + }; return (
diff --git a/src/components/MovieList.jsx b/src/components/MovieList.jsx index c7c4eaf8..f52b4292 100644 --- a/src/components/MovieList.jsx +++ b/src/components/MovieList.jsx @@ -2,20 +2,19 @@ import MovieCard from "./MovieCard" import './MovieList.css' function MovieList({ movieData, favoritedMovies, watchedMovies, updateModalData, onButtonClick }) { + const { updateFavoritedMovies, updateWatchedMovies } = onButtonClick; + return ( - <>
{ movieData.map(movie => { - let image = `https://image.tmdb.org/t/p/original/${movie.poster_path}`; return }) }
- ) -} +}; export default MovieList \ No newline at end of file diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx index 97141c57..27ada613 100644 --- a/src/components/SearchForm.jsx +++ b/src/components/SearchForm.jsx @@ -1,24 +1,24 @@ import './SearchForm.css' function SearchForm({ onQueryChange, onClear }) { + + // Handles user search const handleSubmit = (event) => { event.preventDefault(); const query = event.target[0].value; event.target.reset(); onQueryChange(query); - } + }; return ( - <> - - -
- - -
- - +
+ +
+ + +
+
) -} +}; export default SearchForm \ No newline at end of file diff --git a/src/components/Sidebar.css b/src/components/Sidebar.css index d2a18dba..4b33d3d1 100644 --- a/src/components/Sidebar.css +++ b/src/components/Sidebar.css @@ -2,12 +2,17 @@ position: fixed; left: 0; background-color: #10254C; + height: 100vh; display: flex; flex-direction: column; width: 15vw; - gap: 2vh; justify-content: center; - height: 100vh; +} + +.sidebar ul { + display: flex; + flex-direction: column; + gap: 2vh; } .sidebar button { diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 43f6a0eb..b406476d 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -2,6 +2,7 @@ import './Sidebar.css' function Sidebar({ setActiveView }) { + // Updates what should be displayed on screen based on button clicked on sidebar const updateActiveView = (event) => { const label = event.target.textContent.toLowerCase(); switch (label) { @@ -17,15 +18,17 @@ function Sidebar({ setActiveView }) { default: console.error("Invalid option was selected.") } - } + }; return (
- - - +
    +
  • +
  • +
  • +
) -} +}; export default Sidebar \ No newline at end of file diff --git a/src/components/SortMenu.jsx b/src/components/SortMenu.jsx index dd5f21af..7cceb9e3 100644 --- a/src/components/SortMenu.jsx +++ b/src/components/SortMenu.jsx @@ -2,28 +2,30 @@ import './SortMenu.css' function SortMenu({ movieData, onSort }) { + // Sorts movie data based on user selection const sortMovieData = (movieData, sortOption) => { - let sortedMovieData = [...movieData]; - switch (sortOption) { - case 'alphabetical': - sortedMovieData.sort((a, b) => a.title.localeCompare(b.title)); - break; - case 'chronological': - sortedMovieData.sort((a, b) => b.release_date.localeCompare(a.release_date)); - break; - case 'vote-average': - sortedMovieData.sort((a, b) => b.vote_average - a.vote_average); - break; - default: - console.error("Invalid sort option selected.") - } - return sortedMovieData; + let sortedMovieData = [...movieData]; + switch (sortOption) { + case 'alphabetical': + sortedMovieData.sort((a, b) => a.title.localeCompare(b.title)); + break; + case 'chronological': + sortedMovieData.sort((a, b) => b.release_date.localeCompare(a.release_date)); + break; + case 'vote-average': + sortedMovieData.sort((a, b) => b.vote_average - a.vote_average); + break; + default: + console.error("Invalid sort option was selected.") + } + return sortedMovieData; }; + // Handles user selecting a sorting option by sorting data and then updating movieData back in App const handleSortOptionChange = (sortOption) => { const sortedData = sortMovieData(movieData, sortOption); onSort(sortedData); - } + }; return ( ) -} +}; export default SortMenu \ No newline at end of file From 6555e742ee296d59c810c3daa23bccdc64c4b6f2 Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Sun, 15 Jun 2025 12:19:07 -0700 Subject: [PATCH 18/19] add new demo video & reflection responses to README --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a865d402..db4caf5c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Submitted by: **Carlos Escobar** -Estimated time spent: 16 hours spent in total +Estimated time spent: 18 hours spent in total Deployed Application: [Flixster Deployed Site](https://flixster-cr7f.onrender.com) @@ -101,11 +101,10 @@ Deployed Application: [Flixster Deployed Site](https://flixster-cr7f.onrender.co ### Walkthrough Video
- - - - -
+ + + +
### Reflection @@ -116,12 +115,12 @@ The labs did prepare us for this project. An instance I can think of where it re * 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 am writing this with 6 hours till the deadline and am hoping I could better the code. I want to handle errors more gracefully, try to reuse the favorite/watched button as they share similar functionality, and add a cool hover effect when the cards are hovered over. +If I had more time, I would improve the responsiveness of the page and make the UI nicer. After moving the sorting functionality to the sortMenu component, I lost the ability to sort the incoming movies whenever the 'load more' button was pressed, so that is also something I would love to bring back. * 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? -The demo went fine. I got to highlight how I sort the incoming data whenever the 'Load More' button is pressed, which was a stretch feature of my own. Nothing really went bad, which is a good thing. +The demo went great. I got to highlight how whenever one goes to the 'Favorites' or 'Watched' tab, the toolbar and load more button goes away. A peer of mine had the background of the modal set to the backdrop image of the movie which was really nice visually and something I would love to experiment with on the next project. ### Shout out From 4a80bd774c63ec3f4e5901cd38580736cadd446e Mon Sep 17 00:00:00 2001 From: Carlos Escobar Date: Mon, 16 Jun 2025 08:12:01 -0700 Subject: [PATCH 19/19] make favorite/watched buttons into components for reusability --- src/components/Button.css | 13 +++++++++++++ src/components/Button.jsx | 17 +++++++++++++++++ src/components/MovieCard.css | 14 -------------- src/components/MovieCard.jsx | 16 +++------------- 4 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 src/components/Button.css create mode 100644 src/components/Button.jsx diff --git a/src/components/Button.css b/src/components/Button.css new file mode 100644 index 00000000..caf23de5 --- /dev/null +++ b/src/components/Button.css @@ -0,0 +1,13 @@ +.feature-button { + border-radius: 0px; + background-color: white; + color: black; + border: none; + font-weight: lighter; +} + +.feature-button:hover { + background-color: white; + color: black; + cursor: pointer; +} \ No newline at end of file diff --git a/src/components/Button.jsx b/src/components/Button.jsx new file mode 100644 index 00000000..f9cfd382 --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,17 @@ +import './Button.css' + +function Button({ id, updateData, isActive, text, icons }) { + + const handleClick = (event) => { + event.stopPropagation(); + updateData(id); + }; + + return ( + + ) +}; + +export default Button \ No newline at end of file diff --git a/src/components/MovieCard.css b/src/components/MovieCard.css index 3685e6fb..a0152d85 100644 --- a/src/components/MovieCard.css +++ b/src/components/MovieCard.css @@ -34,18 +34,4 @@ display: flex; justify-content: space-between; gap: 1vw; -} - -.features button { - border-radius: 0px; - background-color: white; - color: black; - border: none; - font-weight: lighter; -} - -.features button:hover { - background-color: white; - color: black; - cursor: pointer; } \ No newline at end of file diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index ee7466c8..956c096d 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,3 +1,4 @@ +import Button from './Button'; import './MovieCard.css' function MovieCard({ image, title, rating, id, updateModalData, updateFavoritedMovies, updateWatchedMovies, isFavorited, isWatched }) { @@ -36,16 +37,6 @@ function MovieCard({ image, title, rating, id, updateModalData, updateFavoritedM console.error('Error loading movie data: ', error); } }; - - const onFavorite = (event) => { - event.stopPropagation(); - updateFavoritedMovies(id); - }; - - const onWatched = (event) => { - event.stopPropagation(); - updateWatchedMovies(id); - }; return (
@@ -54,9 +45,8 @@ function MovieCard({ image, title, rating, id, updateModalData, updateFavoritedM

{title}

Rating: {rating.toFixed(2)}

- {/* Button Component Here (reusable) */} - - +