Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_KEY=1a10c2de7cdd2a3c938c1010f999d241
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?

.env
432 changes: 395 additions & 37 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"focus-trap-react": "^10.2.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-select": "^5.8.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
Expand Down
59 changes: 40 additions & 19 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
.App {
text-align: center;
header {
height: 10vh;
background-color: #457462;
position: sticky;
display: flex;
align-items: center;
}

.App-header {
background-color: #282c34;
h1 {
color: #ee892f;
margin-left: 2%;
font-size: 40pt;
}

footer {
height: 10vh;
background-color: #457462;
position: sticky;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
color: white;
padding: 20px;
justify-content: center;
width: 100%;
}

#loadbtn {
justify-content: center;
align-self: center;
display: flex;
}

@media (max-width: 600px) {
.movie-card {
width: 100%;
}

.search-bar {
flex-direction: column;
gap: 10px;
}
.App {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}


.search-bar form {
flex-direction: column;
}
#emptymessage {
font-size: 16pt;
}

#hamburger {
font-size: 30pt;
color: #ee892f;
cursor: pointer;
}
128 changes: 121 additions & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,124 @@
import { useState } from 'react'
import './App.css'
import './App.css';
import MovieList from './MovieList';
import Search from './Search';
import Filter from './Filter';
import Sort from './Sort';
import Side from './Side';
import { useState, useEffect } from 'react';

const App = () => {
<div className="App">

</div>
function App() {
//a central state object map for all elements that interact with api requests allows us to ensure all fetch requests are in sync with all current relevant values
const [apiData, setApiData] = useState({
search: '',
page: 1,
filters: [],
sort: ''
});
const [movieData, setMovieData] = useState([]);
const [sideOpen, setSideOpen] = useState(false);
const [favorites, setFavorites] = useState([]);
const [watchlist, setWatchlist] = useState([]);

useEffect(() => {
fetchData();
}, [apiData]);

//fetchData fetches data by inserting sort,search,and filter info onto the base API URL as needed and sets visible movies to correspond with API response
const fetchData = async () => {
try {
let url = `https://api.themoviedb.org/3/discover/movie?language=en-US&include_adult=false&include_video=false&page=${apiData.page}&api_key=${import.meta.env.VITE_API_KEY}`;

if (apiData.search !== '') {
url = `https://api.themoviedb.org/3/search/movie?query=${encodeURIComponent(apiData.search)}&language=en-US&page=${apiData.page}&include_adult=false&api_key=${import.meta.env.VITE_API_KEY}`;
}
if (apiData.filters.length > 0) {
url += `&with_genres=${apiData.filters.join(',')}`;
}
if (apiData.sort !== '') {
url += `&sort_by=${apiData.sort}`
}

const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch movies: ' + response.statusText);
}
const data = await response.json();
// Update movieData by appending new results to existing data if not on the first page, otherwise replace existing data.
setMovieData(prevData => apiData.page > 1 ? [...prevData, ...data.results] : [...data.results]);

} catch (error) {
console.error('Fetch error:', error);
}
};

// All handlers work by updating the apiData state with new values for search, filters, sort, or page whenever relevant while maintaining unchanged fields
const handleSearch = (query) => {
setApiData({ ...apiData, search: query, page: 1 });
};

const handleFilter = (newFilters) => {
setApiData({ ...apiData, filters: newFilters, page: 1 });
};

const handleSort = (newSort) => {
setApiData({ ...apiData, sort: newSort, page: 1 });
};

const incrementPage = () => {
setApiData({ ...apiData, page: apiData.page + 1 });
};

const toggleSide = () => {
setSideOpen(!sideOpen);
};

const updateFavorites = (movie, add) => {
setFavorites(prev => add ? [...prev, movie] : prev.filter(fav => fav.id !== movie.id));
};

const updateWatchlist = (movie, add) => {
setWatchlist(prev => add ? [...prev, movie] : prev.filter(watch => watch.id !== movie.id));
};

const checkForEmptyMovie = () => {
if (movieData.length > 0) {
return (
<>
<MovieList
data={movieData}
updateFavorites={updateFavorites}
updateWatchlist={updateWatchlist}
/>
<footer>
<button id="loadbtn" onClick={incrementPage}>Load More</button>
</footer>
</>
);
} else {
return <p id="emptymessage">No movies match your search for "{apiData.search}", try searching for something else :D</p>
}
};

return (
<>
<header>
<h1>Flixster</h1>
<Search searchQuery={apiData.search} setSearchQuery={handleSearch} />
<Filter setFilters={handleFilter} />
<Sort setSort={handleSort} />
<p id="hamburger" onClick={toggleSide}>☰</p>
</header>
<div className={`App ${sideOpen ? 'open-side' : ''}`}>
{checkForEmptyMovie()}
</div>
<Side
isOpen={sideOpen}
onClose={toggleSide}
favorites={favorites}
watchlist={watchlist}
/>
</>
);
}

export default App
export default App;
4 changes: 4 additions & 0 deletions src/Filter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.filterbar {
margin-left: 4%;
z-index: 10000;
}
51 changes: 51 additions & 0 deletions src/Filter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import './Filter.css'
import Select from 'react-select'
import makeAnimated from 'react-select/animated'

function Filter({ setFilters }) {
const animatedComponents = makeAnimated();
const filterOptions = [
{ value: 28, label: 'Action' },
{ value: 12, label: 'Adventure' },
{ value: 16, label: 'Animation' },
{ value: 35, label: 'Comedy' },
{ value: 80, label: 'Crime' },
{ value: 99, label: 'Documentary' },
{ value: 18, label: 'Drama' },
{ value: 10751, label: 'Family' },
{ value: 14, label: 'Fantasy' },
{ value: 36, label: 'History' },
{ value: 27, label: 'Horror' },
{ value: 10402, label: 'Music' },
{ value: 9648, label: 'Mystery' },
{ value: 10749, label: 'Romance' },
{ value: 878, label: 'Science Fiction' },
{ value: 10770, label: 'TV Movie' },
{ value: 53, label: 'Thriller' },
{ value: 10752, label: 'War' },
{ value: 37, label: 'Western' },
];

const handleChange = (selectedOptions) => {
const selectedGenres = selectedOptions ? selectedOptions.map(option => option.value) : [];
setFilters(selectedGenres);
};

return (
<div className='filterbar' key={'filter'}>
<Select
placeholder="Filter by …"
closeMenuOnSelect={false}
components={animatedComponents}
isMulti
options={filterOptions}
onChange={handleChange}
/>
</div>
);
}

export default Filter;



35 changes: 35 additions & 0 deletions src/Modal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(220, 228, 221, 0.96);
z-index: 1000;
width: auto;
max-width: 90%;
padding: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
animation: fadeInScaleUp 0.3s ease-out forwards;
border-radius: 18px;
}

@keyframes fadeInScaleUp {
from {
opacity: 0;
transform: scale(0.95) translate(-50%, -50%);
}
to {
opacity: 1;
transform: scale(1) translate(-50%, -50%);
}
}

.modal img {
width: 100%;
height: 100%;
max-width: 20vw;
display: block;
margin: 0 auto;
float: left;
}

55 changes: 55 additions & 0 deletions src/Modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import './Modal.css';
import FocusTrap from "focus-trap-react"
import { genre_to_id, swap, timeFormat } from './utils/utils';
import { imageGuard } from './utils/utils';
function Modal({ isOpen, imgSrc, title, genres, overview, date, closeModal, runtime, trailer }) {

if (!isOpen) {
return null;
}

const formattedTime = timeFormat(runtime);
const genreKeys = swap(genre_to_id);
const genreNames = []
for (let i = 0; i < genres.length; i++) {
genreNames.push(genreKeys[genres[i]]);
}

const checkForTrailer = () => {
if (trailer != '') {
return (
<>
<img src={imageGuard(imgSrc)} alt={`Poster of ${title}`} />
<iframe src={"https://www.youtube.com/embed/" + trailer}></iframe>
</>
)
} else {
return (
<>
<img src={imageGuard(imgSrc)} alt={`Poster of ${title}`} />
<p>No Trailer Available</p>
</>
)
}
}

return (
<FocusTrap>
<div className='modal' onClick={closeModal}>
<button onClick={closeModal}>Close</button>
<h3 className='title'>{title}</h3>
<div className='movieMedia'>
{checkForTrailer()}
</div>
<p>{`Release date: ${date}`}</p>
<p>{`Overview: ${overview}`}</p>
<p>{`Genres: ${genreNames}`}</p>
<p>{`Runtime: ${formattedTime}`}</p>
</div>
</FocusTrap>
);
}

export default Modal;


Loading