Lots of changes

This commit is contained in:
“Henry-Hiles” 2022-10-27 13:48:50 -04:00
parent 7b9f81dd64
commit c66526241c
16 changed files with 286 additions and 114 deletions

View file

@ -5,7 +5,6 @@
"styles/*": ["styles/*"], "styles/*": ["styles/*"],
"components/*": ["components/*"], "components/*": ["components/*"],
"hooks/*": ["hooks/*"], "hooks/*": ["hooks/*"],
"contexts/*": ["contexts/*"],
"pages/*": ["pages/*"] "pages/*": ["pages/*"]
} }
} }

View file

@ -1,4 +1,3 @@
#root,
.App { .App {
width: 100%; width: 100%;
height: 100%; height: 100%;

View file

@ -10,7 +10,9 @@ const Card = ({ movie }) => (
className={styles.Image} className={styles.Image}
/> />
<div className={styles.Bottom}> <div className={styles.Bottom}>
<p className={styles.Title}>{movie.title}</p> <p className={styles.Title}>
{movie.title} - {movie.year}
</p>
<p className={styles.Average}> <p className={styles.Average}>
{movie.averageVote}{" "} {movie.averageVote}{" "}
<svg <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" /> <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> </svg>
</p> </p>
<p className={styles.Overview}>{movie.overview}</p>
</div> </div>
</div> </div>
</Link> </Link>

View file

@ -1,9 +1,14 @@
import styles from "styles/TopBar.module.css"
const TopBar = ({ search, setSearch }) => ( const TopBar = ({ search, setSearch }) => (
<div id="top-bar"> <div className={styles.Container}>
<h1>React Movie Finder</h1> <h1 className={styles.Title}>React Movie Finder</h1>
<input <input
type="text" type="text"
className={styles.Search}
value={search} value={search}
autoFocus
placeholder="Search for movies..."
onChange={({ target }) => setSearch(target.value)} onChange={({ target }) => setSearch(target.value)}
/> />
</div> </div>

21
src/constants.js Normal file
View 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" },
]

View file

@ -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
View 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
}

View file

@ -10,14 +10,12 @@
} }
body { body {
background-color: var(--secondary);
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif; "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased; margin: 0;
-moz-osx-font-smoothing: grayscale; background-color: var(--secondary);
height: 100%;
} }
code { code {
@ -28,3 +26,13 @@ code {
button { button {
cursor: pointer; cursor: pointer;
} }
hr {
width: 97%;
color: white;
}
#root,
body {
height: 100%;
}

View file

@ -2,15 +2,12 @@ import React from "react"
import ReactDOM from "react-dom/client" import ReactDOM from "react-dom/client"
import App from "./App" import App from "./App"
import "./index.css" import "./index.css"
import { MoviesProvider } from "contexts/MoviesContext"
import { BrowserRouter } from "react-router-dom" import { BrowserRouter } from "react-router-dom"
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>
<MoviesProvider> <BrowserRouter>
<BrowserRouter> <App />
<App /> </BrowserRouter>
</BrowserRouter>
</MoviesProvider>
</React.StrictMode> </React.StrictMode>
) )

View file

@ -1,10 +1,49 @@
import { useMovies } from "contexts/MoviesContext"
import CardList from "components/CardList" import CardList from "components/CardList"
import TopBar from "components/TopBar" import TopBar from "components/TopBar"
import useDebounce from "hooks/useDebounce"
import { useEffect, useState } from "react"
import styles from "styles/Home.module.css" import styles from "styles/Home.module.css"
import config from "config"
import { GENRES } from "../constants"
const Home = () => { 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 ( return (
<div className={styles.Container}> <div className={styles.Container}>

View file

@ -1,37 +1,107 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useParams } from "react-router-dom" 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 = () => {
const [movie, setMovie] = useState()
const [movies] = useMovies()
const { movieId } = useParams() const { movieId } = useParams()
const [movie, setMovie] = useState()
const [cast, setCast] = useState()
useEffect( useEffect(() => {
() => setMovie(movies.find((movie) => movie.id === parseInt(movieId))), const run = async () => {
[movieId, movies] 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 ( useEffect(() => {
<div className="movie-jumbo"> const run = async () => {
<img src={movie?.backdropUrl} alt={movie?.title} className="top" /> const response = await fetch(
<div className="bottom"> `https://api.themoviedb.org/3/movie/${movieId}/credits?api_key=${config.apiKey}`
<p className="title">{movie?.title}</p> )
<p className="average"> const data = await response.json()
{movie?.averageVote}{" "} setCast(data.cast)
<svg }
xmlns="http://www.w3.org/2000/svg" run()
width="16" }, [movieId])
height="16"
fill="currentColor" return movie ? (
className="bi bi-star-fill" <div className={styles.Container}>
viewBox="0 0 16 16" <div
> className={styles.Top}
<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" /> style={{
</svg> backgroundImage: `url(${movie.backdropUrl})`,
</p> }}
>
<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>
) : (
<div></div>
) )
} }

View file

@ -21,6 +21,15 @@
flex-direction: column; 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, .Title,
.Average { .Average {
margin: 0; margin: 0;

View file

@ -2,4 +2,5 @@
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
gap: 1.5em;
} }

View 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;
}

View 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;
}

View file

@ -10,7 +10,6 @@ export default defineConfig({
styles: path.resolve(__dirname, "/src/styles"), styles: path.resolve(__dirname, "/src/styles"),
components: path.resolve(__dirname, "/src/components"), components: path.resolve(__dirname, "/src/components"),
hooks: path.resolve(__dirname, "/src/hooks"), hooks: path.resolve(__dirname, "/src/hooks"),
contexts: path.resolve(__dirname, "/src/contexts"),
pages: path.resolve(__dirname, "/src/pages"), pages: path.resolve(__dirname, "/src/pages"),
}, },
}, },