Initial commit
This commit is contained in:
commit
7b9f81dd64
20 changed files with 2789 additions and 0 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
config.json
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
13
index.html
Normal file
13
index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>React movie viewer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
jsconfig.json
Normal file
12
jsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
"paths": {
|
||||||
|
"styles/*": ["styles/*"],
|
||||||
|
"components/*": ["components/*"],
|
||||||
|
"hooks/*": ["hooks/*"],
|
||||||
|
"contexts/*": ["contexts/*"],
|
||||||
|
"pages/*": ["pages/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2406
package-lock.json
generated
Normal file
2406
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "movie-react-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.17",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
|
"vite": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
5
src/App.css
Normal file
5
src/App.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#root,
|
||||||
|
.App {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
16
src/App.jsx
Normal file
16
src/App.jsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Routes, Route } from "react-router-dom"
|
||||||
|
import "./App.css"
|
||||||
|
import Home from "pages/Home"
|
||||||
|
import Movie from "pages/Movie"
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<div className="App">
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Home />} />
|
||||||
|
<Route path="/movie/:movieId" element={<Movie />} />
|
||||||
|
<Route path="*" element={<h1>404 - Not Found</h1>} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default App
|
31
src/components/Card.jsx
Normal file
31
src/components/Card.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import styles from "styles/Card.module.css"
|
||||||
|
|
||||||
|
const Card = ({ movie }) => (
|
||||||
|
<Link to={`/movie/${movie.id}`} className={styles.Link}>
|
||||||
|
<div className={styles.Card}>
|
||||||
|
<img
|
||||||
|
src={movie.posterUrl}
|
||||||
|
alt={movie.title}
|
||||||
|
className={styles.Image}
|
||||||
|
/>
|
||||||
|
<div className={styles.Bottom}>
|
||||||
|
<p className={styles.Title}>{movie.title}</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>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Card
|
12
src/components/CardList.jsx
Normal file
12
src/components/CardList.jsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import Card from "components/Card"
|
||||||
|
import styles from "styles/CardList.module.css"
|
||||||
|
|
||||||
|
const CardList = ({ movies }) => (
|
||||||
|
<div className={styles.CardList}>
|
||||||
|
{movies.map((movie) => (
|
||||||
|
<Card key={movie.id} movie={movie} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default CardList
|
5
src/components/Jumbotron.jsx
Normal file
5
src/components/Jumbotron.jsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// const Jumbotron = ({ movie }) => (
|
||||||
|
|
||||||
|
// )
|
||||||
|
|
||||||
|
// export default Jumbotron
|
12
src/components/TopBar.jsx
Normal file
12
src/components/TopBar.jsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const TopBar = ({ search, setSearch }) => (
|
||||||
|
<div id="top-bar">
|
||||||
|
<h1>React Movie Finder</h1>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={({ target }) => setSearch(target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TopBar
|
71
src/contexts/MoviesContext.jsx
Normal file
71
src/contexts/MoviesContext.jsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
30
src/index.css
Normal file
30
src/index.css
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
:root {
|
||||||
|
--primary: #14bbaa;
|
||||||
|
--secondary: #2c3c4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
16
src/main.jsx
Normal file
16
src/main.jsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
17
src/pages/Home.jsx
Normal file
17
src/pages/Home.jsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useMovies } from "contexts/MoviesContext"
|
||||||
|
import CardList from "components/CardList"
|
||||||
|
import TopBar from "components/TopBar"
|
||||||
|
import styles from "styles/Home.module.css"
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const [movies, page, search, { nextPage, setSearch }] = useMovies()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Container}>
|
||||||
|
<TopBar search={search} setSearch={setSearch} />
|
||||||
|
<CardList movies={movies} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
38
src/pages/Movie.jsx
Normal file
38
src/pages/Movie.jsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useMovies } from "contexts/MoviesContext"
|
||||||
|
|
||||||
|
const Movie = () => {
|
||||||
|
const [movie, setMovie] = useState()
|
||||||
|
const [movies] = useMovies()
|
||||||
|
const { movieId } = useParams()
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => setMovie(movies.find((movie) => movie.id === parseInt(movieId))),
|
||||||
|
[movieId, movies]
|
||||||
|
)
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Movie
|
27
src/styles/Card.module.css
Normal file
27
src/styles/Card.module.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.Link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Card {
|
||||||
|
background-color: var(--primary);
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 7px;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Title,
|
||||||
|
.Average {
|
||||||
|
margin: 0;
|
||||||
|
}
|
9
src/styles/CardList.module.css
Normal file
9
src/styles/CardList.module.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.CardList {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
width: 85%;
|
||||||
|
margin: 0 100px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
5
src/styles/Home.module.css
Normal file
5
src/styles/Home.module.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.Container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
17
vite.config.js
Normal file
17
vite.config.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
config: path.resolve(__dirname, "/src/config.json"),
|
||||||
|
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