Lots of changes
This commit is contained in:
parent
7b9f81dd64
commit
c66526241c
16 changed files with 286 additions and 114 deletions
|
@ -5,7 +5,6 @@
|
|||
"styles/*": ["styles/*"],
|
||||
"components/*": ["components/*"],
|
||||
"hooks/*": ["hooks/*"],
|
||||
"contexts/*": ["contexts/*"],
|
||||
"pages/*": ["pages/*"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#root,
|
||||
.App {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -10,7 +10,9 @@ const Card = ({ movie }) => (
|
|||
className={styles.Image}
|
||||
/>
|
||||
<div className={styles.Bottom}>
|
||||
<p className={styles.Title}>{movie.title}</p>
|
||||
<p className={styles.Title}>
|
||||
{movie.title} - {movie.year}
|
||||
</p>
|
||||
<p className={styles.Average}>
|
||||
{movie.averageVote}{" "}
|
||||
<svg
|
||||
|
@ -23,6 +25,7 @@ const Card = ({ movie }) => (
|
|||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z" />
|
||||
</svg>
|
||||
</p>
|
||||
<p className={styles.Overview}>{movie.overview}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import styles from "styles/TopBar.module.css"
|
||||
|
||||
const TopBar = ({ search, setSearch }) => (
|
||||
<div id="top-bar">
|
||||
<h1>React Movie Finder</h1>
|
||||
<div className={styles.Container}>
|
||||
<h1 className={styles.Title}>React Movie Finder</h1>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.Search}
|
||||
value={search}
|
||||
autoFocus
|
||||
placeholder="Search for movies..."
|
||||
onChange={({ target }) => setSearch(target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
|
21
src/constants.js
Normal file
21
src/constants.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export const GENRES = [
|
||||
{ id: 28, name: "Action" },
|
||||
{ id: 12, name: "Adventure" },
|
||||
{ id: 16, name: "Animation" },
|
||||
{ id: 35, name: "Comedy" },
|
||||
{ id: 80, name: "Crime" },
|
||||
{ id: 99, name: "Documentary" },
|
||||
{ id: 18, name: "Drama" },
|
||||
{ id: 10751, name: "Family" },
|
||||
{ id: 14, name: "Fantasy" },
|
||||
{ id: 36, name: "History" },
|
||||
{ id: 27, name: "Horror" },
|
||||
{ id: 10402, name: "Music" },
|
||||
{ id: 9648, name: "Mystery" },
|
||||
{ id: 10749, name: "Romance" },
|
||||
{ id: 878, name: "Science Fiction" },
|
||||
{ id: 10770, name: "TV Movie" },
|
||||
{ id: 53, name: "Thriller" },
|
||||
{ id: 10752, name: "War" },
|
||||
{ id: 37, name: "Western" },
|
||||
]
|
|
@ -1,71 +0,0 @@
|
|||
import { createContext, useContext, useEffect, useState } from "react"
|
||||
import config from "config"
|
||||
|
||||
const MoviesContext = createContext()
|
||||
|
||||
export const useMovies = () => useContext(MoviesContext)
|
||||
|
||||
export const MoviesProvider = ({ children }) => {
|
||||
const [movies, setMovies] = useState([])
|
||||
const [genres, setGenres] = useState({})
|
||||
const [page, setPage] = useState(1)
|
||||
const [search, setSearch] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const genresResponse = await fetch(
|
||||
`https://api.themoviedb.org/3/genre/movie/list?api_key=${config.apiKey}`
|
||||
)
|
||||
const genresData = await genresResponse.json()
|
||||
genresData.genres.forEach((genre) =>
|
||||
setGenres((current) => {
|
||||
const copy = {}
|
||||
Object.assign(copy, current)
|
||||
copy[genre.id] = genre.name
|
||||
return copy
|
||||
})
|
||||
)
|
||||
}
|
||||
run()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const response = await fetch(
|
||||
`https://api.themoviedb.org/3/${
|
||||
search ? "search" : "discover"
|
||||
}/movie?api_key=${
|
||||
config.apiKey
|
||||
}&page=${page}&query=${encodeURIComponent(search)}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setMovies(
|
||||
data.results.map((movie) => ({
|
||||
id: movie.id,
|
||||
overview: movie.overview,
|
||||
adult: movie.adult,
|
||||
posterUrl: `https://image.tmdb.org/t/p/w342/${movie.poster_path}`,
|
||||
backdropUrl: `https://image.tmdb.org/t/p/original/${movie.backdrop_path}`,
|
||||
genres: movie.genre_ids.map((genreId) => genres[genreId]),
|
||||
title: movie.title,
|
||||
releaseDate: movie.release_date,
|
||||
averageVote: movie.vote_average,
|
||||
voteCount: movie.vote_count,
|
||||
popularity: movie.popularity,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
run()
|
||||
}, [page, genres])
|
||||
|
||||
const nextPage = () => setPage((page) => page + 1)
|
||||
|
||||
return (
|
||||
<MoviesContext.Provider
|
||||
value={[movies, page, search, { nextPage, setSearch }]}
|
||||
>
|
||||
{children}
|
||||
</MoviesContext.Provider>
|
||||
)
|
||||
}
|
14
src/hooks/useDebounce.jsx
Normal file
14
src/hooks/useDebounce.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { useState, useEffect } from "react"
|
||||
|
||||
export default function useDebounce(value, delay = 500) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
|
@ -10,14 +10,12 @@
|
|||
}
|
||||
|
||||
body {
|
||||
background-color: var(--secondary);
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
|
||||
code {
|
||||
|
@ -28,3 +26,13 @@ code {
|
|||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 97%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#root,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -2,15 +2,12 @@ import React from "react"
|
|||
import ReactDOM from "react-dom/client"
|
||||
import App from "./App"
|
||||
import "./index.css"
|
||||
import { MoviesProvider } from "contexts/MoviesContext"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<MoviesProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</MoviesProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
|
|
@ -1,10 +1,49 @@
|
|||
import { useMovies } from "contexts/MoviesContext"
|
||||
import CardList from "components/CardList"
|
||||
import TopBar from "components/TopBar"
|
||||
import useDebounce from "hooks/useDebounce"
|
||||
import { useEffect, useState } from "react"
|
||||
import styles from "styles/Home.module.css"
|
||||
import config from "config"
|
||||
import { GENRES } from "../constants"
|
||||
|
||||
const Home = () => {
|
||||
const [movies, page, search, { nextPage, setSearch }] = useMovies()
|
||||
const [movies, setMovies] = useState([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [search, setSearch] = useState("")
|
||||
const debouncedSearch = useDebounce(search)
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const response = await fetch(
|
||||
`https://api.themoviedb.org/3/${
|
||||
debouncedSearch ? "search" : "discover"
|
||||
}/movie?api_key=${
|
||||
config.apiKey
|
||||
}&page=${page}&query=${encodeURIComponent(debouncedSearch)}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setMovies(
|
||||
data.results.map((movie) => ({
|
||||
id: movie.id,
|
||||
overview: movie.overview,
|
||||
adult: movie.adult,
|
||||
posterUrl: `https://image.tmdb.org/t/p/w342${movie.poster_path}`,
|
||||
backdropUrl: `https://image.tmdb.org/t/p/original${movie.backdrop_path}`,
|
||||
genres: movie.genre_ids.map((genreId) =>
|
||||
GENRES.find((genre) => genre.id == genreId)
|
||||
),
|
||||
title: movie.title,
|
||||
releaseDate: movie.release_date,
|
||||
year: movie.release_date.split("-")[0],
|
||||
averageVote: movie.vote_average,
|
||||
voteCount: movie.vote_count,
|
||||
popularity: movie.popularity,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
run()
|
||||
}, [page, debouncedSearch])
|
||||
|
||||
return (
|
||||
<div className={styles.Container}>
|
||||
|
|
|
@ -1,37 +1,107 @@
|
|||
import { useEffect, useState } from "react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useMovies } from "contexts/MoviesContext"
|
||||
import styles from "styles/Movie.module.css"
|
||||
import config from "config"
|
||||
|
||||
const Movie = () => {
|
||||
const [movie, setMovie] = useState()
|
||||
const [movies] = useMovies()
|
||||
const { movieId } = useParams()
|
||||
const [movie, setMovie] = useState()
|
||||
const [cast, setCast] = useState()
|
||||
|
||||
useEffect(
|
||||
() => setMovie(movies.find((movie) => movie.id === parseInt(movieId))),
|
||||
[movieId, movies]
|
||||
)
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const response = await fetch(
|
||||
`https://api.themoviedb.org/3/movie/${movieId}?api_key=${config.apiKey}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setMovie({
|
||||
id: data.id,
|
||||
overview: data.overview,
|
||||
adult: data.adult,
|
||||
posterUrl: `https://image.tmdb.org/t/p/w342${data.poster_path}`,
|
||||
backdropUrl: `https://image.tmdb.org/t/p/original${data.backdrop_path}`,
|
||||
genres: data.genres.map((genre) => genre.name),
|
||||
title: data.title,
|
||||
releaseDate: data.release_date,
|
||||
year: data.release_date.split("-")[0],
|
||||
averageVote: data.vote_average,
|
||||
voteCount: data.vote_count,
|
||||
popularity: data.popularity,
|
||||
tagline: data.tagline,
|
||||
})
|
||||
}
|
||||
run()
|
||||
}, [movieId])
|
||||
|
||||
return (
|
||||
<div className="movie-jumbo">
|
||||
<img src={movie?.backdropUrl} alt={movie?.title} className="top" />
|
||||
<div className="bottom">
|
||||
<p className="title">{movie?.title}</p>
|
||||
<p className="average">
|
||||
{movie?.averageVote}{" "}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
className="bi bi-star-fill"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z" />
|
||||
</svg>
|
||||
</p>
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const response = await fetch(
|
||||
`https://api.themoviedb.org/3/movie/${movieId}/credits?api_key=${config.apiKey}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setCast(data.cast)
|
||||
}
|
||||
run()
|
||||
}, [movieId])
|
||||
|
||||
return movie ? (
|
||||
<div className={styles.Container}>
|
||||
<div
|
||||
className={styles.Top}
|
||||
style={{
|
||||
backgroundImage: `url(${movie.backdropUrl})`,
|
||||
}}
|
||||
>
|
||||
<div className={styles.Summary}>
|
||||
<h2 className={styles.Title}>
|
||||
{movie.title} - {movie.year}
|
||||
</h2>
|
||||
<p>{movie.tagline}</p>
|
||||
<p className={styles.Overview}>{movie.overview}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.Bottom}>
|
||||
<div className={styles.Tags}>
|
||||
{movie.genres.map((genre) => (
|
||||
<p>{genre}</p>
|
||||
))}
|
||||
<p className={styles.Average}>
|
||||
{movie.averageVote}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z" />
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.Section}></div>
|
||||
<div className={styles.Section}>
|
||||
<h1 className={styles.Header}>Actors</h1>
|
||||
<div className={styles.Actors}>
|
||||
{cast?.map((actor) => (
|
||||
<div key={actor.id}>
|
||||
{actor.profile_path ? (
|
||||
<img
|
||||
src={`https://image.tmdb.org/t/p/w200${actor.profile_path}`}
|
||||
alt={actor.name}
|
||||
/>
|
||||
) : (
|
||||
<p>Image Unavailable</p>
|
||||
)}
|
||||
|
||||
<h3>{actor.name}</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,15 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Overview {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: -webkit-box !important;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.Title,
|
||||
.Average {
|
||||
margin: 0;
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
|
58
src/styles/Movie.module.css
Normal file
58
src/styles/Movie.module.css
Normal file
|
@ -0,0 +1,58 @@
|
|||
.Container {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.Top {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.Summary {
|
||||
background: linear-gradient(#00000035, black);
|
||||
height: 15%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Bottom {
|
||||
background-color: black;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
padding: 20px 40px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Section div {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.Actors {
|
||||
display: flex;
|
||||
overflow: scroll;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.Actors > * {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Tags {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.Tags > * {
|
||||
background-color: var(--primary);
|
||||
padding: 5px 7px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.Average {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
21
src/styles/TopBar.module.css
Normal file
21
src/styles/TopBar.module.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.Container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Search {
|
||||
padding: 0.5em;
|
||||
border: 2px solid white;
|
||||
color: white;
|
||||
font: inherit;
|
||||
width: 20em;
|
||||
height: 3em;
|
||||
border-radius: 10px;
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
|
||||
.Title {
|
||||
color: white;
|
||||
}
|
|
@ -10,7 +10,6 @@ export default defineConfig({
|
|||
styles: path.resolve(__dirname, "/src/styles"),
|
||||
components: path.resolve(__dirname, "/src/components"),
|
||||
hooks: path.resolve(__dirname, "/src/hooks"),
|
||||
contexts: path.resolve(__dirname, "/src/contexts"),
|
||||
pages: path.resolve(__dirname, "/src/pages"),
|
||||
},
|
||||
},
|
||||
|
|
Reference in a new issue