diff --git a/.env b/.env
new file mode 100644
index 00000000..207eb8af
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_API_KEY=531687c8e8da8cb3eb85ddce1090cec8
diff --git a/package-lock.json b/package-lock.json
index 92a683d2..7808a87d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
+ "prettier": "^3.5.3",
"vite": "^5.2.0"
}
},
@@ -3453,6 +3454,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
diff --git a/package.json b/package.json
index eded5715..7d633af1 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
+ "prettier": "^3.5.3",
"vite": "^5.2.0"
}
}
diff --git a/src/App.css b/src/App.css
index 0bf65669..c8c459e6 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,28 +1,108 @@
.App {
text-align: center;
-}
+ min-width: 700px;
-.App-header {
+
+ }
+
+
+ .App-header {
background-color: #282c34;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-evenly;
- color: white;
padding: 20px;
-}
+ padding-bottom: 50px;
+
+ }
+
-@media (max-width: 600px) {
+ @media (max-width: 600px) {
.movie-card {
width: 100%;
}
- .search-bar {
- flex-direction: column;
- gap: 10px;
- }
- .search-bar form {
- flex-direction: column;
- }
-}
+ }
+ /* .search-bar {
+ display: inline;
+ width: 30%;
+
+
+
+
+ } */
+
+
+
+
+ /* .search-bar form {
+ flex-direction: column;
+ } */
+
+
+ /* input{
+ width: 70%;
+
+
+
+
+ } */
+
+
+ button{
+ width: 30%;
+ }
+
+
+ .movie-container{
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ padding: 10px;
+ border: 3px solid blue;
+ height: 100%;
+ /* background: linear-gradient(to right, blue, black); */
+
+ }
+
+
+ .App-footer{
+ justify-content: center;
+ display: flex;
+ border: 3px solid red;
+ height: 100%;
+ width: 100%;
+
+
+
+
+ }
+
+
+ .title{
+ color: white
+
+
+ }
+
+
+ .controls-row{
+ display: flex;
+ justify-content:space-between;
+ }
+
+
+ .search-section{
+ margin-right: 80px;
+ }
+
+
+ .load-more button{
+ width: 15%;
+ height: 7vh;
+ border-radius: 10px;
+ background-color: green;
+ }
+
+ .load-more{
+ background: linear-gradient(to right, blue, black);
+
+ }
diff --git a/src/App.jsx b/src/App.jsx
index dfa91584..3f754bf6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,12 +1,65 @@
-import { useState } from 'react'
import './App.css'
+import Search from './Search.jsx'
+import Sort from './Sort.jsx'
+import MovieList from './MovieList.jsx'
+import {useState} from 'react'
-const App = () => {
- return (
-
-
-
- )
+
+function App() {
+ const [page, setPage] = useState(1)
+
+ const [searchTerm, setSearchTerm] = useState('')
+
+ function increasePageNumber() {
+ setPage(page + 1)
+ }
+
+ function handleSearch(term) {
+ setPage(1)
+ setSearchTerm(term)
+ }
+
+ return (
+
+
+ 🎥 Flixster
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Page {page}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
}
-export default App
+export default App;
diff --git a/src/Modal.css b/src/Modal.css
new file mode 100644
index 00000000..c77611ec
--- /dev/null
+++ b/src/Modal.css
@@ -0,0 +1,112 @@
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.75);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ overflow-y: auto;
+ padding: 20px;
+ }
+
+ .modal-content {
+ background-color: #141414;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 800px;
+ max-height: 90vh;
+ overflow-y: auto;
+ position: relative;
+ color: white;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+ }
+
+ .modal-close {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: rgba(0, 0, 0, 0.5);
+ border: none;
+ color: white;
+ font-size: 24px;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 10;
+ }
+
+ .modal-backdrop {
+ height: 300px;
+ background-size: cover;
+ background-position: center top;
+ position: relative;
+ }
+
+ .modal-backdrop-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(to bottom, rgba(20, 20, 20, 0) 0%, rgba(20, 20, 20, 1) 100%);
+ }
+
+ .modal-body {
+ padding: 20px;
+ position: relative;
+ margin-top: -60px;
+ }
+
+ .modal-info {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ margin-bottom: 20px;
+ }
+
+ .modal-info p {
+ margin: 0;
+ }
+
+ .modal-overview {
+ margin-bottom: 20px;
+ }
+
+ .modal-overview h3 {
+ margin-bottom: 10px;
+ }
+
+ .trailer-container {
+ position: relative;
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
+ height: 0;
+ overflow: hidden;
+ margin-top: 10px;
+ }
+
+ .trailer-container iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: 4px;
+ }
+
+ /* Make movie cards clickable */
+ .card-content {
+ cursor: pointer;
+ transition: transform 0.2s;
+ }
+
+ .card-content:hover {
+ transform: scale(1.05);
+ }
diff --git a/src/Modal.jsx b/src/Modal.jsx
new file mode 100644
index 00000000..5a24ec9a
--- /dev/null
+++ b/src/Modal.jsx
@@ -0,0 +1,125 @@
+import './Modal.css'
+import { useState, useEffect } from 'react'
+
+function Modal({ movie, onClose }) {
+ const [movieDetails, setMovieDetails] = useState(null);
+ const [trailer, setTrailer] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const backdropUrl = `https://image.tmdb.org/t/p/original${movie.backdrop_path}`;
+
+ useEffect(() => {
+ // Function to fetch movie details (including runtime)
+ const fetchMovieDetails = async () => {
+ try {
+ const apiKey = import.meta.env.VITE_API_KEY;
+ const response = await fetch(
+ `https://api.themoviedb.org/3/movie/${movie.id}?api_key=${apiKey}&language=en-US`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch movie details');
+ }
+
+ const data = await response.json();
+ setMovieDetails(data);
+ } catch (error) {
+ console.error('Error fetching movie details:', error);
+ }
+ };
+
+ // Function to fetch movie videos (trailers)
+ const fetchMovieTrailer = async () => {
+ try {
+ const apiKey = import.meta.env.VITE_API_KEY;
+ const response = await fetch(
+ `https://api.themoviedb.org/3/movie/${movie.id}/videos?api_key=${apiKey}&language=en-US`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch movie videos');
+ }
+
+ const data = await response.json();
+
+ // Find the official trailer
+ const officialTrailer = data.results.find(
+ video => video.type === 'Trailer' && video.site === 'YouTube'
+ );
+
+ setTrailer(officialTrailer || null);
+ setLoading(false);
+ } catch (error) {
+ console.error('Error fetching movie trailer:', error);
+ setLoading(false);
+ }
+ };
+
+ // Call both fetch functions
+ fetchMovieDetails();
+ fetchMovieTrailer();
+ }, [movie.id]);
+
+ // Format runtime from minutes to hours and minutes
+ const formatRuntime = (minutes) => {
+ if (!minutes) return 'Runtime not available';
+ const hours = Math.floor(minutes / 60);
+ const mins = minutes % 60;
+ return `${hours}h ${mins}m`;
+ };
+
+ return (
+
+
e.stopPropagation()}>
+
+
+
+
+
+
{movie.title}
+
+ {loading ? (
+
Loading details...
+ ) : (
+ <>
+
+
Release Date: {movie.release_date}
+ {movieDetails && (
+
Runtime: {formatRuntime(movieDetails.runtime)}
+ )}
+
Rating: ⭐ {movie.vote_average.toFixed(1)}/10
+ {movieDetails && movieDetails.genres && (
+
Genres: {movieDetails.genres.map(g => g.name).join(', ')}
+ )}
+
+
+
+
Overview
+
{movie.overview}
+
+
+ {trailer && (
+
+ )}
+ >
+ )}
+
+
+
+ );
+}
+
+export default Modal;
diff --git a/src/MovieCard.css b/src/MovieCard.css
new file mode 100644
index 00000000..e1c9ddc2
--- /dev/null
+++ b/src/MovieCard.css
@@ -0,0 +1,28 @@
+.card-content{
+ display: block;
+
+
+ border: 2px solid black;
+ height: 400px;
+ width: 280px;
+ text-align: center;
+ border-radius: 20px;
+ }
+
+
+ .movie-card img{
+ width: 100%;
+ height: 70%;
+ border-top-right-radius: 18px;
+ border-top-left-radius: 18px;
+ }
+
+
+ .movie-card p{
+ font-size: 10px;
+ color: white;
+ }
+
+ .movie-card{
+ box-shadow: 10px black;
+ }
diff --git a/src/MovieCard.jsx b/src/MovieCard.jsx
new file mode 100644
index 00000000..97386b86
--- /dev/null
+++ b/src/MovieCard.jsx
@@ -0,0 +1,45 @@
+import './MovieCard.css'
+import Modal from './Modal.jsx'
+import {useState} from 'react'
+
+
+
+
+function MovieCard(props){
+ const [showModal, setShowModal] = useState(false);
+ const posterUrl = `https://image.tmdb.org/t/p/w500${props.movie.poster_path}`;
+
+ const openModal = () => {
+ setShowModal(true);
+ };
+
+ const closeModal = () => {
+ setShowModal(false);
+ };
+
+ return (
+
+
+

+
{props.movie.title}
+
⭐{props.movie.vote_average}
+
{props.movie.release_date.substring(0,4)}
+
+
+ {showModal &&(
+
+ )}
+
+
+
+
+ );
+}
+
+
+
+
+export default MovieCard;
diff --git a/src/MovieList.css b/src/MovieList.css
new file mode 100644
index 00000000..bcdb038f
--- /dev/null
+++ b/src/MovieList.css
@@ -0,0 +1,9 @@
+.movie-list{
+ display: flex;
+ height: 100%;
+ width: 100%;
+ flex-wrap: wrap;
+ gap: 60px;
+ padding: 40px;
+ justify-content: space-evenly;
+ }
diff --git a/src/MovieList.jsx b/src/MovieList.jsx
new file mode 100644
index 00000000..74ab1608
--- /dev/null
+++ b/src/MovieList.jsx
@@ -0,0 +1,74 @@
+import './MovieList.css'
+import data from './data/data.js'
+import MovieCard from './MovieCard.jsx'
+import {useState, useEffect} from 'react'
+
+
+
+
+function MovieList({page = 1, searchTerm = ''}){
+ // state to stores the movies fetched from the API
+ const [movies, setMovies] = useState([])
+
+ // state to track if data is currently being loaded
+ const [isLoading, setIsLoading] = useState(true)
+
+ // state to store any error that might occur during the API fetch
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ const fetchMovies = async () => {
+ try {
+ const apiKey = import.meta.env.VITE_API_KEY;
+ const apiURL = searchTerm
+ ? `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${encodeURIComponent(searchTerm)}&page=${page}`
+ : `https://api.themoviedb.org/3/movie/popular?api_key=${apiKey}&language=en-US&page=${page}`;
+
+ const response = await fetch(apiURL);
+
+ if (!response.ok){
+ throw new Error('Failed to fetch movies');
+ }
+ const data = await response.json();
+
+ if (page === 1){
+ setMovies(data.results);
+ }else{
+ setMovies(prevMovies => [...prevMovies, ...data.results]);
+ }
+
+ setIsLoading(false);
+ } catch (err) {
+ setError(err.message);
+ setIsLoading(false);
+ }
+ };
+ fetchMovies();
+ }, [page, searchTerm]); // re-reun when page or searchTerm changes
+
+ if (isLoading && page === 1){
+ return Loading...
;
+ }
+
+ if (error){
+ return An error occurred: {error}
;
+ }
+
+ return (
+
+ {movies.map((movie) =>(
+
+ ))
+
+
+ }
+
+
+
+ );
+}
+
+
+
+
+export default MovieList;
diff --git a/src/Search.css b/src/Search.css
new file mode 100644
index 00000000..a22a8727
--- /dev/null
+++ b/src/Search.css
@@ -0,0 +1,83 @@
+.search-section{
+ width: 40%;
+ display: flex;
+
+
+
+
+ }
+
+ .search-bar-container {
+ display: flex;
+ gap: 2px;
+ }
+
+
+
+ input {
+ width: 20vw;
+ height: 100%;
+ margin-left: 40px;
+ padding-right: 30px;
+ }
+
+ .search-button{
+ width: 55px;
+ margin-left: 1vw;
+ border-radius: 50%;
+ height: 40px
+ }
+
+
+
+
+ .clear-search {
+ position: absolute;
+ right: 5px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ height: 20px;
+ width: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ margin-left: 0;
+ }
+
+
+.close{
+ color: green;
+ font-size: 18px;
+ margin-right: 60px;
+}
+
+.search-bar-container {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ }
+
+ .search-bar {
+ position: relative;
+ display: flex;
+ align-items: center;
+ height: 30px
+
+ }
+
+ .now-playing-button {
+ background-color: #0d253f;
+
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 12px;
+ margin-left: 10px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.2s;
+ height: 40px;
+ white-space: nowrap;
+ }
diff --git a/src/Search.jsx b/src/Search.jsx
new file mode 100644
index 00000000..a464d700
--- /dev/null
+++ b/src/Search.jsx
@@ -0,0 +1,64 @@
+import "./Search.css";
+import React, { useState } from "react";
+
+
+
+function Search({ onSearch }) {
+ const [searchTerm, setSearchTerm] = useState("");
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ onSearch(searchTerm);
+ };
+
+ const handleClear = () => {
+ setSearchTerm("");
+ onSearch("");
+ };
+
+ const handleNowPlaying = (event) => {
+ event.preventDefault();
+ setSearchTerm("");
+ onSearch(""); // Reset search results to default (popular movies)
+ };
+ return (
+
+ );
+}
+
+export default Search;
diff --git a/src/Sort.css b/src/Sort.css
new file mode 100644
index 00000000..9c4d81ee
--- /dev/null
+++ b/src/Sort.css
@@ -0,0 +1,9 @@
+label{
+ color: aliceblue;
+ }
+
+
+ select{
+ margin-left: 10px;
+ margin-right: 120px;
+ }
diff --git a/src/Sort.jsx b/src/Sort.jsx
new file mode 100644
index 00000000..2ce1966f
--- /dev/null
+++ b/src/Sort.jsx
@@ -0,0 +1,29 @@
+import './Sort.css'
+function Sort() {
+ return (
+
+ )
+
+
+
+
+}
+
+
+export default Sort;
diff --git a/src/index.css b/src/index.css
index e1faed1a..3ac430cc 100644
--- a/src/index.css
+++ b/src/index.css
@@ -2,18 +2,23 @@ body {
margin: 0;
font-family: Arial, sans-serif;
background-color: #f4f4f4;
-}
-button {
+
+ }
+
+
+
+ button {
background-color: #282c34;
color: white;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background-color 0.3s ease;
-}
+ }
+
-button:hover {
+ button:hover {
background-color: #777;
color: white;
-}
+ }