diff --git a/.env b/.env new file mode 100644 index 00000000..0ee90d1d --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_KEY=4230b01717a30e627d99da67b6a68895 diff --git a/package-lock.json b/package-lock.json index 92a683d2..e8a5bca8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "flixster", "version": "0.0.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-regular-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/react-fontawesome": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -811,6 +815,63 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz", + "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3193,7 +3254,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3457,7 +3517,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3519,8 +3578,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-refresh": { "version": "0.14.0", diff --git a/package.json b/package.json index eded5715..304d67da 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-regular-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/react-fontawesome": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/src/App.css b/src/App.css index 0bf65669..be408e53 100644 --- a/src/App.css +++ b/src/App.css @@ -26,3 +26,13 @@ flex-direction: column; } } + +.Header { + border-style: dashed; + border-color: black; +} + +.MovieCard { + border-style: dashed; + border-color: black; +} diff --git a/src/App.jsx b/src/App.jsx index 48215b3f..755a0a52 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,30 @@ -import { useState } from 'react' -import './App.css' +import React, { useState } from 'react'; + +import Header from './HomePageComponents/Header'; +import MovieCardsContainer from './HomePageComponents/MovieCardsContainer'; +import DropdownMenu from './HomePageComponents/Sort'; const App = () => { -
- -
+ const [page, setPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(''); + const [sortOption, setSortOption] = useState(''); + + + const handleSearch = (query) => { + setSearchQuery(query); + setPage(1); + }; + + const handleSortChange = (option) => { + setSortOption(option); + }; + + return ( +
+
+ +
+ ); } -export default App +export default App; diff --git a/src/HomePageComponents/Filter.jsx b/src/HomePageComponents/Filter.jsx new file mode 100644 index 00000000..0d23ebd1 --- /dev/null +++ b/src/HomePageComponents/Filter.jsx @@ -0,0 +1,33 @@ +import React, { useState, useEffect } from 'react'; + +const Filter = ({ onFilterChange }) => { + const [favorited, setFavorited] = useState(false); + const [watched, setWatched] = useState(false); + + useEffect(() => { + onFilterChange({ favorited, watched }); + }, [favorited, watched, onFilterChange]); + + return ( +
+ + +
+ ); +}; + +export default Filter; diff --git a/src/HomePageComponents/Header.jsx b/src/HomePageComponents/Header.jsx new file mode 100644 index 00000000..57c6ee6e --- /dev/null +++ b/src/HomePageComponents/Header.jsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from 'react'; + +const Header = ({ onSearch }) => { + const [searchQuery, setSearchQuery] = useState(''); + const [suggestions, setSuggestions] = useState([]); + + const handleInputChange = async (event) => { + const query = event.target.value; + setSearchQuery(query); + + if (query.length > 2) { + const apiKey = import.meta.env.VITE_API_KEY; + const url = `https://api.themoviedb.org/3/search/movie?query=${encodeURIComponent(query)}&api_key=${apiKey}`; + const response = await fetch(url); + const data = await response.json(); + setSuggestions(data.results); + } else { + setSuggestions([]); + if (query.length === 0) { + onSearch(''); // Trigger fetching "now playing" movies when search is cleared + } + } + }; + + const handleSearch = (query) => { + onSearch(query); + setSuggestions([]); // Clear suggestions after search + }; + + const handleKeyPress = (event) => { + if (event.key === 'Enter') { + handleSearch(searchQuery); + } + }; + + return ( +
+

Flixster

+ + + {suggestions.length > 0 && ( + + )} +
+ ); +} + +export default Header; diff --git a/src/HomePageComponents/LoadMoreButton.jsx b/src/HomePageComponents/LoadMoreButton.jsx new file mode 100644 index 00000000..d81c5645 --- /dev/null +++ b/src/HomePageComponents/LoadMoreButton.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const LoadMoreButton = ({ onLoadMore }) => { + return ( +
+ +
+ ); +} + +export default LoadMoreButton; diff --git a/src/HomePageComponents/MovieCard.jsx b/src/HomePageComponents/MovieCard.jsx new file mode 100644 index 00000000..5ea09a9e --- /dev/null +++ b/src/HomePageComponents/MovieCard.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faHeart as farHeart } from '@fortawesome/free-regular-svg-icons'; +import { faHeart as fasHeart } from '@fortawesome/free-solid-svg-icons'; + +const MovieCard = ({ movie, isFavorite, isWatched, onToggleFavorite, onToggleWatched }) => { + return ( +
+

{movie?.title}

+ {movie?.title} +
+ + + Rating: {movie?.vote_average} / 10 +
+
+ ); +}; + +export default MovieCard; diff --git a/src/HomePageComponents/MovieCardsContainer.jsx b/src/HomePageComponents/MovieCardsContainer.jsx new file mode 100644 index 00000000..87355b1d --- /dev/null +++ b/src/HomePageComponents/MovieCardsContainer.jsx @@ -0,0 +1,112 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import MovieCard from './MovieCard'; +import Filter from './Filter'; +import DropdownMenu from './Sort'; + +const MovieCardsContainer = ({ searchQuery }) => { + const [allMovies, setAllMovies] = useState([]); + const [displayMovies, setDisplayMovies] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [favorites, setFavorites] = useState(new Set()); + const [watched, setWatched] = useState(new Set()); + const [filterSettings, setFilterSettings] = useState({ favorited: false, watched: false }); + const [sortOption, setSortOption] = useState(''); + const itemsPerPage = 20; + + const updateDisplayMovies = useCallback((movies) => { + let filteredMovies = movies.filter(movie => + (!filterSettings.favorited || favorites.has(movie.id)) && + (!filterSettings.watched || watched.has(movie.id)) + ); + console.log("Filtered movies after applying filters:", filteredMovies); + setDisplayMovies(filteredMovies.slice(0, currentPage * itemsPerPage)); + }, [favorites, watched, filterSettings, currentPage]); + + useEffect(() => { + fetchMovies(1); + }, [searchQuery, sortOption]); + + useEffect(() => { + updateDisplayMovies(allMovies); + }, [filterSettings, allMovies, updateDisplayMovies]); + + const fetchMovies = useCallback(async (page) => { + const apiKey = import.meta.env.VITE_API_KEY; + const baseUrl = 'https://api.themoviedb.org/3'; + let url = `${baseUrl}/discover/movie?language=en-US&page=${page}&api_key=${apiKey}`; + + if (sortOption === 'alphabetical') { + url += '&sort_by=original_title.asc'; + } else if (sortOption === 'release-date') { + url += '&sort_by=release_date.desc'; + } else if (sortOption === 'rating') { + url += '&sort_by=vote_average.desc'; + } + + if (searchQuery) { + url = `${baseUrl}/search/movie?query=${encodeURIComponent(searchQuery)}&page=${page}&api_key=${apiKey}`; + } + + console.log("Fetching movies with URL:", url); + + try { + const response = await fetch(url); + const data = await response.json(); + console.log("API response data:", data); + setAllMovies(data.results); + updateDisplayMovies(data.results); + } catch (error) { + console.error("Failed to fetch movies:", error); + } + }, [searchQuery, sortOption, updateDisplayMovies]); + + const handleFilterChange = useCallback((settings) => { + console.log("Filter settings updated to:", settings); + setFilterSettings(settings); + }, []); + + const toggleSet = useCallback((setId, movieId) => { + setId(prev => { + const newSet = new Set(prev); + if (newSet.has(movieId)) { + newSet.delete(movieId); + } else { + newSet.add(movieId); + } + console.log(`Toggled ${setId === setFavorites ? 'favorite' : 'watched'} for movie ID: ${movieId}`); + return new Set(newSet); + }); + updateDisplayMovies(allMovies); + }, [allMovies, updateDisplayMovies]); + + const handleLoadMore = useCallback(() => { + const nextPage = currentPage + 1; + setCurrentPage(nextPage); + fetchMovies(nextPage); + }, [currentPage, fetchMovies]); + + const handleSortChange = useCallback((newSortOption) => { + console.log("Sort option changed to:", newSortOption); + setSortOption(newSortOption); + }, []); + + return ( +
+ + + {displayMovies.map(movie => ( + toggleSet(setFavorites, movie.id)} + onToggleWatched={() => toggleSet(setWatched, movie.id)} + /> + ))} + +
+ ); +} + +export default MovieCardsContainer; diff --git a/src/HomePageComponents/Sort.jsx b/src/HomePageComponents/Sort.jsx new file mode 100644 index 00000000..26b2a039 --- /dev/null +++ b/src/HomePageComponents/Sort.jsx @@ -0,0 +1,24 @@ +import React, { useState, useEffect } from 'react'; + +function DropdownMenu({ onSortChange }) { + const [selectedOption, setSelectedOption] = useState(''); + + const handleSelectChange = (event) => { + const newSortOption = event.target.value; + setSelectedOption(newSortOption); + onSortChange(newSortOption); // Notify the parent component of the change + }; + + return ( +
+ +
+ ); +} + +export default DropdownMenu; diff --git a/src/index.css b/src/index.css index e1faed1a..8bc39d7d 100644 --- a/src/index.css +++ b/src/index.css @@ -17,3 +17,18 @@ button:hover { background-color: #777; color: white; } + +header { + border-style: dotted; + border-color: black; +} + +.movieContainer { + border-style: dotted; + border-color: black; +} + +.movieCard { + border-style: dotted; + border-color: black; +}