Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REACT_APP_API_KEY=8cac6dec66e09ab439c081b251304443
32 changes: 32 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:react/jsx-runtime",
"plugin:prettier/recommended",
"plugin:testing-library/react"
],
"rules": {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"curly": "error",
"prettier/prettier": [
"error",
{
"singleQuote": true,
"semi": false,
"trailingComma": "es5"
}
]
},
"parserOptions": {
"ecmaVersion": 2024,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
}
}
11 changes: 11 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"semi": false,
"trailingComma": "es5",
"singleQuote": true,
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"printWidth": 80,
"quoteProps": "as-needed"
}
1,878 changes: 1,295 additions & 583 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --coverage",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}"
},
"eslintConfig": {
"extends": [
Expand All @@ -40,5 +42,14 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-testing-library": "^6.2.2",
"prettier": "^3.3.2"
}
}
108 changes: 30 additions & 78 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,98 +1,50 @@
import { useEffect, useState } from 'react'
import { Routes, Route, createSearchParams, useSearchParams, useNavigate } from "react-router-dom"
import { Routes, Route } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import 'reactjs-popup/dist/index.css'
import { fetchMovies } from './data/moviesSlice'
import { ENDPOINT_SEARCH, ENDPOINT_DISCOVER, ENDPOINT, API_KEY } from './constants'
import Header from './components/Header'
import Movies from './components/Movies'
import Starred from './components/Starred'
import WatchLater from './components/WatchLater'
import YouTubePlayer from './components/YoutubePlayer'
import './app.scss'
import { closeModal } from './data/movieTrailerSlice'
import { createPortal } from 'react-dom'
import Modal from './components/Modal'
import Header from './components/Header'

const App = () => {

const state = useSelector((state) => state)
const { movies } = state
const { videoKey, modal: trailerModal } = useSelector(
(state) => state.movieTrailer
)
const dispatch = useDispatch()
const [searchParams, setSearchParams] = useSearchParams()
const searchQuery = searchParams.get('search')
const [videoKey, setVideoKey] = useState()
const [isOpen, setOpen] = useState(false)
const navigate = useNavigate()

const closeModal = () => setOpen(false)

const closeCard = () => {

}

const getSearchResults = (query) => {
if (query !== '') {
dispatch(fetchMovies(`${ENDPOINT_SEARCH}&query=`+query))
setSearchParams(createSearchParams({ search: query }))
} else {
dispatch(fetchMovies(ENDPOINT_DISCOVER))
setSearchParams()
}
}

const searchMovies = (query) => {
navigate('/')
getSearchResults(query)
}

const getMovies = () => {
if (searchQuery) {
dispatch(fetchMovies(`${ENDPOINT_SEARCH}&query=`+searchQuery))
} else {
dispatch(fetchMovies(ENDPOINT_DISCOVER))
}
}

const viewTrailer = (movie) => {
getMovie(movie.id)
if (!videoKey) setOpen(true)
setOpen(true)
}

const getMovie = async (id) => {
const URL = `${ENDPOINT}/movie/${id}?api_key=${API_KEY}&append_to_response=videos`

setVideoKey(null)
const videoData = await fetch(URL)
.then((response) => response.json())

if (videoData.videos && videoData.videos.results.length) {
const trailer = videoData.videos.results.find(vid => vid.type === 'Trailer')
setVideoKey(trailer ? trailer.key : videoData.videos.results[0].key)
}
}

useEffect(() => {
getMovies()
}, [])

return (
<div className="App">
<Header searchMovies={searchMovies} searchParams={searchParams} setSearchParams={setSearchParams} />
<Header />

<div className="container">
{videoKey ? (
<YouTubePlayer
videoKey={videoKey}
/>
) : (
<div style={{padding: "30px"}}><h6>no trailer available. Try another movie</h6></div>
)}

<Routes>
<Route path="/" element={<Movies movies={movies} viewTrailer={viewTrailer} closeCard={closeCard} />} />
<Route path="/starred" element={<Starred viewTrailer={viewTrailer} />} />
<Route path="/watch-later" element={<WatchLater viewTrailer={viewTrailer} />} />
<Route path="*" element={<h1 className="not-found">Page Not Found</h1>} />
<Route path="/" element={<Movies />} />
<Route path="/starred" element={<Starred />} />
<Route path="/watch-later" element={<WatchLater />} />
<Route
path="*"
element={<h1 className="not-found">Page Not Found</h1>}
/>
</Routes>

{trailerModal &&
createPortal(
<Modal isOpen={trailerModal} onClose={() => dispatch(closeModal())}>
{videoKey ? (
<YouTubePlayer videoKey={videoKey} />
) : (
<div>
<h6>no trailer available. Try another movie</h6>
</div>
)}
</Modal>,
document.getElementById('root')
)}
</div>
</div>
)
Expand Down
17 changes: 10 additions & 7 deletions src/App.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithProviders } from "./test/utils"
import { renderWithProviders } from './test/utils'
import App from './App'

it('renders watch later link', () => {
Expand All @@ -13,7 +13,9 @@ it('search for movies', async () => {
renderWithProviders(<App />)
await userEvent.type(screen.getByTestId('search-movies'), 'forrest gump')
await waitFor(() => {
expect(screen.getAllByText('Through the Eyes of Forrest Gump')[0]).toBeInTheDocument()
expect(
screen.getAllByText('Through the Eyes of Forrest Gump')[0]
).toBeInTheDocument()
})
const viewTrailerBtn = screen.getAllByText('View Trailer')[0]
await userEvent.click(viewTrailerBtn)
Expand All @@ -22,20 +24,21 @@ it('search for movies', async () => {
})
})

it('renders watch later component', async() => {
it('renders watch later component', async () => {
renderWithProviders(<App />)
const user = userEvent.setup()
await user.click(screen.getByText(/watch later/i))
expect(screen.getByText(/You have no movies saved to watch later/i)).toBeInTheDocument()
expect(
screen.getByText(/You have no movies saved to watch later/i)
).toBeInTheDocument()
})


it('renders starred component', async() => {
it('renders starred component', async () => {
renderWithProviders(<App />)
const user = userEvent.setup()
await user.click(screen.getByTestId('nav-starred'))
expect(screen.getByText(/There are no starred movies/i)).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByTestId('starred')).toBeInTheDocument()
})
})
})
12 changes: 7 additions & 5 deletions src/app.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#root {
height: 100%;
min-height: 850px;
background: linear-gradient(0deg, #1CB5E0 20%, #050b45 100%) no-repeat;
min-height: 850px;
}

:root,
body {
background: linear-gradient(0deg, #1cb5e0 20%, #050b45 100%) no-repeat fixed;
}

:root,
.App {
text-align: center;

Expand All @@ -31,7 +34,7 @@
-moz-transition: anvil 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards;
-ms-transition: anvil 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards;
-o-transition: anvil 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards;
transition: anvil 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards;
transition: anvil 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards;
-webkit-animation: anvil 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards;
animation: anvil 0.3s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards;

Expand All @@ -40,5 +43,4 @@
height: 60vw;
}
}

}
48 changes: 30 additions & 18 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { Link, NavLink } from "react-router-dom"
import { Link, NavLink } from 'react-router-dom'
import { useSelector } from 'react-redux'

import '../styles/header.scss'
import useSearchMovies from '../hooks/useSearchMovies'

const Header = ({ searchMovies }) => {

const Header = () => {
const { starredMovies } = useSelector((state) => state.starred)
const { handleSearch, setSearchValue, searchValue } = useSearchMovies()

return (
<header>
<Link to="/" data-testid="home" onClick={() => searchMovies('')}>
<Link
to="/"
data-testid="home"
onClick={() => {
handleSearch()
setSearchValue('')
}}
>
<i className="bi bi-film" />
</Link>

<nav>
<NavLink to="/starred" data-testid="nav-starred" className="nav-starred">
<NavLink
to="/starred"
data-testid="nav-starred"
className="nav-starred"
>
{starredMovies.length > 0 ? (
<>
<i className="bi bi-star-fill bi-star-fill-white" />
<sup className="star-number">{starredMovies.length}</sup>
<i className="bi bi-star-fill bi-star-fill-white" />
<sup className="star-number">{starredMovies.length}</sup>
</>
) : (
<i className="bi bi-star" />
Expand All @@ -30,16 +41,17 @@ const Header = ({ searchMovies }) => {
</nav>

<div className="input-group rounded">
<Link to="/" onClick={(e) => searchMovies('')} className="search-link" >
<input type="search" data-testid="search-movies"
onKeyUp={(e) => searchMovies(e.target.value)}
className="form-control rounded"
placeholder="Search movies..."
aria-label="Search movies"
aria-describedby="search-addon"
/>
</Link>
</div>
<input
type="text"
data-testid="search-movies"
className="form-control rounded search-link"
placeholder="Search movies..."
aria-label="Search movies"
aria-describedby="search-addon"
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
/>
</div>
</header>
)
}
Expand Down
14 changes: 14 additions & 0 deletions src/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import '../styles/modal.scss'

const Modal = ({ isOpen, onClose, children }) => {
return isOpen ? (
<div className="modal">
<button className="modal-close-btn" onClick={onClose}>
<span aria-hidden="true">&times;</span>
</button>
<div className="content">{children}</div>
</div>
) : null
}

export default Modal
Loading