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