updated branch

This commit is contained in:
andres alcocer
2021-10-09 16:02:41 -04:00
50 changed files with 25685 additions and 12022 deletions

View File

@@ -1,2 +0,0 @@
client/dist/
node_modules/

View File

@@ -1,25 +0,0 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"plugins": [
"react",
"react-hooks"
],
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2018
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"linebreak-style": ["error", "unix"]
}
}

View File

@@ -1,13 +1,16 @@
index.html,1596823315374,294686b5824777966e74b788a74f613225bd2a74f6e7eca9b7b98b3bd6e4ae60
main.css,1596823315373,1fe11d275cdaeefa9ac00b547cc17a7559f641d94302b061941c58441778dc90
static/images/add.svg,1596823315374,ef7cf8e9df96200815d767e6b3b767ef092dd338b4851d2f35966911dab09e4c
static/images/bell-logo.svg,1596823315374,5ef07d3b72a99ccebfff2fb9bc1a80513d59376038055b606deff8133c3e3b56
static/images/bell.svg,1596823315375,6766dfa1ef7da3acfab70cdb2bbee5e6861fc0cdf8afbaf1cf7c387e4df415a4
static/images/cancel-music.svg,1596823315375,1bbd7eef0b56c5d4cec9993f3762a384447ca99392778a87292333b82f8a2f39
static/images/drop-down-arrow.svg,1596823315374,7ebe79b34f9fa2590b4beb29e6e6e8d0b0fff722ff0e3071ad36d3467c045743
static/images/play-button.svg,1596823315375,d43cf31799ae2e480227ae79d858b997dfb0b54f67b1d3a3a1c18ca1dd927251
static/images/search-icon.svg,1596823315375,fcad2b6afe31083f5b452f601b1ca8669f6f768deecadf07e9d647269296012a
8562b6565f5ae1db5e4af40d85b4ed2d.png,1596823315373,ddacf16aa0704b6ff5ed27a0ff5416eff337a05915beb9c10504639f2f105ec1
static/images/Netflix_Logo_RGB.png,1596823315375,ddacf16aa0704b6ff5ed27a0ff5416eff337a05915beb9c10504639f2f105ec1
bundle.js,1596823315373,033e3afff30e48c0cc6b812ab7b48ad066cbc65e6442c1f10109e6a55a0e103b
static/images/header-bg.jpg,1596823315376,f4cfcb4caa304a777e03d623c68bf00d22f9c21a55d0a6fb088889e3c2867d40
bundle.js.LICENSE.txt,1633809037651,e6867f9fa01d54ddc1cf8eac25d56d3ae909da438be77aa5454f85532e879796
index.html,1633809037654,b96adf5936968f349bea11263a42d495b541d0bdac04424944a25ffd5253c4c4
static/images/add.svg,1633809037652,ef7cf8e9df96200815d767e6b3b767ef092dd338b4851d2f35966911dab09e4c
static/images/bell.svg,1633809037652,6766dfa1ef7da3acfab70cdb2bbee5e6861fc0cdf8afbaf1cf7c387e4df415a4
static/images/bell-logo.svg,1633809037652,5ef07d3b72a99ccebfff2fb9bc1a80513d59376038055b606deff8133c3e3b56
static/images/cancel-music.svg,1633809037652,1bbd7eef0b56c5d4cec9993f3762a384447ca99392778a87292333b82f8a2f39
main.css,1633809037650,d5d7ec1073fd2f17b3fed17b0deaa04cb7678f4b8ea9b089a7cc336886753973
static/images/mute.svg,1633809037652,dbf382f4fa81759d125e5a526fab4464ba293c4a0dedfba395bcb64e5191ffbb
static/images/play-button.svg,1633809037652,d43cf31799ae2e480227ae79d858b997dfb0b54f67b1d3a3a1c18ca1dd927251
static/images/search-icon.svg,1633809037652,fcad2b6afe31083f5b452f601b1ca8669f6f768deecadf07e9d647269296012a
static/images/unmute.svg,1633809037652,f571ee9569a235ed5102856c1a1485ce0d1f22a739f291fe02f5d5c893ddd840
static/images/drop-down-arrow.svg,1633809037652,7ebe79b34f9fa2590b4beb29e6e6e8d0b0fff722ff0e3071ad36d3467c045743
f4070143e1f521da82a119eb78b434d0.png,1633809037650,ddacf16aa0704b6ff5ed27a0ff5416eff337a05915beb9c10504639f2f105ec1
static/images/Netflix_Logo_RGB.png,1633809037654,ddacf16aa0704b6ff5ed27a0ff5416eff337a05915beb9c10504639f2f105ec1
bundle.js,1633809037651,1cbe274a30fafac6296c7ee44e3807e810c050dbd67f8a35da802bde26f80c4e
static/images/header-bg.jpg,1633809037653,f4cfcb4caa304a777e03d623c68bf00d22f9c21a55d0a6fb088889e3c2867d40

5
.gitignore vendored
View File

@@ -12,4 +12,7 @@ logs
*.log
npm-debug.log*
.firebase
# firebase
.firebase
.firebaserc
.firebase.json

View File

@@ -1,3 +0,0 @@
package-lock.json
.next
node_modules/

View File

@@ -1,3 +0,0 @@
{
"singleQuote": true
}

View File

@@ -1,11 +1,12 @@
# Netflix Clone
- Demo: http://netflix-clone-react.surge.sh/
- Demo: <https://netflix-clone-9a0b9.firebaseapp.com/>
This project is a simplified front end clone of Netflix. It was created with React and CSS (Grid and Flexbox). It uses [The MovieDB Api](https://www.themoviedb.org/documentation/api) to search for movies and display details. Feel free to contribute!
### Tools used:
- Webpack
### Tools used
- Webpack
- Axios
- Redux & React
- Sass (grid & flexbox)
@@ -13,24 +14,22 @@ This project is a simplified front end clone of Netflix. It was created with Rea
- Swiper JS
### Runing Project Locally
- Install dependencies: run `npm install` in root project
- Get API key from [here](https://www.themoviedb.org/documentation/api)
- Create .env file in root project and add: `API_KEY=YOUR_API_KEY_HERE`
- Run project: `npm run dev`
### User Stories:
### User Stories
- User can search for movies and TV shows on TMDb
- User can the see upcoming and trending movies. Data updates weekly
- User can click on a movie and a modal should pop up. It should display the title, release date, overview, and runtime.
- The webpage adapts to any screen size.
- User can the see upcoming and trending movies. Data updates weekly
- User can click on a movie and a modal should pop up. It should display the title, release date, overview, and runtime.
- The webpage adapts to any screen size.
### Video Walktrough
### Video Walktrough
![](https://github.com/AndresXI/Netflix-Clone/blob/master/netflix-demo.gif?raw=true)
Please feel free to create a pull request and submit any issues!
Currently looking for backend developers. If you would to contribute to support a backend, reach out, all ideas are welcomed!
Currently looking for backend developers. If you would to contribute to support a backend, reach out, all ideas are welcomed!

34501
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,70 +5,71 @@
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --mode development --config webpack.config.js --open",
"build": "webpack -p",
"build": "webpack --mode production",
"lint": "eslint --fix . && echo 'Lint complete.'"
},
"engines": {
"node": "10.1.0",
"npm": "6.4.1",
"node": "v14.15.4",
"npm": "7.24.0",
"watch": "watch 'clear && npm run -s test | tap-nirvana && npm run -s lint' src"
},
"author": "Andres Alcocer",
"license": "ISC",
"dependencies": {
"axios": "^0.18.1",
"lodash": "^4.17.19",
"prop-types": "^15.6.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^6.0.1",
"react-router-dom": "^4.3.1",
"redux": "^4.0.1",
"axios": "^0.22.0",
"firebase": "^9.1.2",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-player": "^2.9.0",
"react-redux": "^7.2.5",
"react-router-dom": "^5.3.0",
"redux": "^4.1.1",
"redux-promise": "^0.6.0",
"redux-thunk": "^2.3.0",
"swiper": "^6.1.1"
"swiper": "^7.0.8"
},
"devDependencies": {
"@babel/core": "^7.1.5",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-export-namespace-from": "^7.14.5",
"@babel/plugin-proposal-throw-expressions": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/polyfill": "^7.0.0-beta.51",
"@babel/preset-env": "^7.1.5",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.4",
"clean-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^1.0.1",
"dotenv": "^6.2.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.1.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^2.5.1",
"@babel/preset-env": "^7.15.6",
"@babel/preset-react": "^7.14.5",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.3.0",
"dotenv": "^10.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"image-webpack-loader": "^4.5.0",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.14.1",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"prettier": "^2.0.5",
"file-loader": "^6.2.0",
"html-loader": "^2.1.2",
"html-webpack-plugin": "^5.3.2",
"image-webpack-loader": "^8.0.1",
"mini-css-extract-plugin": "^2.4.1",
"node-sass": "^6.0.1",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"prettier": "^2.4.1",
"react-owl-carousel2": "^0.3.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"svg-inline-loader": "^0.8.0",
"sass-loader": "^12.1.0",
"style-loader": "^3.3.0",
"svg-inline-loader": "^0.8.2",
"svg-react-loader": "^0.4.6",
"tap-nirvana": "^1.1.0",
"uglifyjs-webpack-plugin": "^2.0.1",
"uglifyjs-webpack-plugin": "^2.2.0",
"watch": "^1.0.2",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
"webpack": "^5.57.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.1"
}
}

23
src/AppRouter.js Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react'
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'
import Home from './pages/Home'
import NotFound from './pages/NotFound'
import Search from './pages/Search'
import Navbar from './components/Navbar'
import Footer from './components/Footer'
const AppRouter = () => (
<BrowserRouter>
<Navbar />
<Switch>
<Route path='/' exact render={() => <Redirect to='/browse' />} />
<Route path='/browse' component={Home} />
<Route path='/search' component={Search} />
<Route component={NotFound} />
</Switch>
<Footer />
</BrowserRouter>
)
export default AppRouter

View File

@@ -0,0 +1,79 @@
import React, { Component } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper'
import { useViewport } from '../hooks/useViewport'
// install Swiper components
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y])
const DisplayMovieRow = ({ title, isNetflixMovies, movies, selectMovieHandler }) => {
const [windowDimensions] = useViewport()
const { width } = windowDimensions
return (
<>
<h1 className='movieShowcase__heading'>{title}</h1>
<Swiper
className='movieShowcase__container'
navigation={true}
grabCursor={false}
draggable={false}
loop={true}
loopAdditionalSlides={
width >= 1378 ? 4 : width >= 998 ? 3 : width >= 625 ? 2 : 2
}
breakpoints={{
1378: {
slidesPerView: 5,
slidesPerGroup: 5,
},
998: {
slidesPerView: 4,
slidesPerGroup: 4,
},
625: {
slidesPerView: 3,
slidesPerGroup: 3,
},
0: {
slidesPerView: 2,
slidesPerGroup: 2,
},
}}
preventClicksPropagation={true}
preventClicks={true}
scrollbar={{ draggable: false, hide: true }}
slideToClickedSlide={false}
pagination={{ clickable: true }}
>
{movies &&
movies.map((movie, idx) => {
let movieImageUrl = isNetflixMovies
? `https://image.tmdb.org/t/p/original/${movie.poster_path}`
: `https://image.tmdb.org/t/p/w500/${movie.backdrop_path}`
if (movie.poster_path && movie.backdrop_path !== null) {
return (
<SwiperSlide
onClick={() => selectMovieHandler(movie)}
key={idx}
className={
'movieShowcase__container--movie' +
(isNetflixMovies ? '__netflix' : '')
}
>
<img
src={movieImageUrl}
className='movieShowcase__container--movie-image'
/>
</SwiperSlide>
)
}
})}
</Swiper>
</>
)
}
export default DisplayMovieRow

View File

@@ -1,15 +1,15 @@
import React from 'react';
import React from 'react'
const footer = () => (
<footer className="footer">
<div className="footer__copyright">
&copy; 2018 Made with by{' '}
<a className="footer__copyright--link" href="http://andresio.com">
<footer className='footer'>
<div className='footer__copyright'>
&copy; 2021 Made with by{' '}
<a className='footer__copyright--link' href='https://github.com/AndresXI'>
{' '}
Andres Alcocer
</a>
</div>
</footer>
);
)
export default footer;
export default footer

View File

@@ -1,34 +1,54 @@
import React from 'react';
import React, { useState } from 'react'
import PlayLogo from '../static/images/play-button.svg';
import AddLogo from '../static/images/add.svg';
import PlayLogo from '../static/images/play-button.svg'
import AddLogo from '../static/images/add.svg'
import MuteIcon from '../static/images/mute.svg'
import UnmuteIcon from '../static/images/unmute.svg'
import ReactPlayer from 'react-player'
export default function Header(props) {
const backgroundStyle = {
backgroundSize: 'cover',
backgroundImage: `url(https://image.tmdb.org/t/p/original/${props.movie.backdrop_path})`,
backgroundPosition: 'center',
};
const Header = ({ movie: { name, overview } }) => {
const [isMuted, setIsMuted] = useState(true)
return (
<header style={backgroundStyle} className="header">
<div className="header__container">
<h1 className="header__container-heading">{props.movie.name}</h1>
<button
onClick={() => alert('not a movie!')}
className="header__container-btnPlay"
>
<PlayLogo className="header__container-btnMyList-play" />
Play
</button>
<header className='header'>
<ReactPlayer
playing={true}
loop={true}
width='100%'
height='100%'
volume={1}
muted={isMuted}
className='header__video'
url='https://vimeo.com/384025132'
/>
<h1 className='header__container-heading'>{name}</h1>
<button
onClick={() => alert('not a movie!')}
className='header__container-btnPlay'
>
<PlayLogo className='header__container-btnMyList-play' />
Play
</button>
<button className='header__container-btnMyList'>
<AddLogo className='header__container-btnMyList-add' />
My List
</button>
<button className="header__container-btnMyList">
<AddLogo className="header__container-btnMyList-add" />
My List
</button>
<p className="header__container-overview">{props.movie.overview}</p>
</div>
<div className="header--fadeBottom"></div>
{isMuted ? (
<MuteIcon
onClick={() => setIsMuted(false)}
className='header__container-btnVolume'
/>
) : (
<UnmuteIcon
onClick={() => setIsMuted(true)}
className='header__container-btnVolume'
/>
)}
<p className='header__container-overview'>{overview}</p>
<div className='header__container--fadeBottom'></div>
</header>
);
)
}
export default Header

View File

@@ -0,0 +1,83 @@
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import * as movieActions from '../store/actions'
import Header from './Header'
import DisplayMovieRow from './DisplayMovieRow'
const MainContent = ({ selectMovieHandler }) => {
const { movieDetails } = useSelector((state) => state.movieDetails)
const netflixOriginals = useSelector((state) => state.netflixOriginals)
const trending = useSelector((state) => state.trending)
const topRated = useSelector((state) => state.topRated)
const actionMovies = useSelector((state) => state.action)
const comedyMovies = useSelector((state) => state.comedy)
const horrorMovies = useSelector((state) => state.horror)
const romanceMovies = useSelector((state) => state.romance)
const documentaries = useSelector((state) => state.documentary)
const dispatch = useDispatch()
useEffect(() => {
dispatch(movieActions.fetchMovieDetails('tv', '63351'))
dispatch(movieActions.fetchNetflixOriginals())
dispatch(movieActions.fetchTrending())
dispatch(movieActions.fetchTopRated())
dispatch(movieActions.fetchActionMovies())
dispatch(movieActions.fetchComedyMovies())
dispatch(movieActions.fetchHorrorMovies())
dispatch(movieActions.fetchRomanceMovies())
dispatch(movieActions.fetchDocumentaries())
}, [dispatch])
return (
<div className='container'>
<Header movie={movieDetails} />
<div className='movieShowcase'>
<DisplayMovieRow
isNetflixMovies={true}
title='Netflix Originals'
selectMovieHandler={selectMovieHandler}
movies={netflixOriginals.data}
/>
<DisplayMovieRow
title='Trending'
selectMovieHandler={selectMovieHandler}
movies={trending.data}
/>
<DisplayMovieRow
title='Top Rated'
selectMovieHandler={selectMovieHandler}
movies={topRated.data}
/>
<DisplayMovieRow
title='Action Movies'
selectMovieHandler={selectMovieHandler}
movies={actionMovies.data}
/>
<DisplayMovieRow
title='Comedy'
selectMovieHandler={selectMovieHandler}
movies={comedyMovies.data}
/>
<DisplayMovieRow
title='Horror Movies'
selectMovieHandler={selectMovieHandler}
movies={horrorMovies.data}
/>
<DisplayMovieRow
title='Romance'
selectMovieHandler={selectMovieHandler}
movies={romanceMovies.data}
/>
<DisplayMovieRow
title='Documentaries'
selectMovieHandler={selectMovieHandler}
movies={documentaries.data}
/>
</div>
</div>
)
}
export default MainContent

View File

@@ -1,37 +1,45 @@
import React from 'react';
import Aux from '../hoc/Aux';
import AddIcon from '../static/images/add.svg';
import PlayIcon from '../static/images/play-button.svg';
import React from 'react'
export default function ModalMovieDetails(props) {
import AddIcon from '../static/images/add.svg'
import PlayIcon from '../static/images/play-button.svg'
const MovieDetails = ({
movie: {
title,
name,
vote_average,
release_date,
first_air_date,
runtime,
episode_run_time,
number_of_episodes,
number_of_seasons,
overview,
},
}) => {
return (
<Aux>
<div className="modal__container">
<h1 className="modal__title">
{props.movie.title || props.movie.name}
</h1>
<p className="modal__info">
<span className="modal__rating">
Rating: {props.movie.vote_average * 10}%{" "}
</span>
Release date: {props.movie.release_date || props.movie.first_air_date} Runtime: {props.movie.runtime || props.movie.episode_run_time}m
</p>
<p className="modal__episode">
{props.movie.number_of_episodes ? " Episodes: " + props.movie.number_of_episodes : ""}
{props.movie.number_of_seasons ? " Seasons: " + props.movie.number_of_seasons : ""}
</p>
<p className="modal__overview">{props.movie.overview}</p>
<button className="modal__btn modal__btn--red">
<PlayIcon className="modal__btn--icon" />
Play
</button>
<button className="modal__btn">
<AddIcon className="modal__btn--icon" />
My List
</button>
</div>
</Aux>
);
}
<div className='modal__container'>
<h1 className='modal__title'>{title || name}</h1>
<p className='modal__info'>
<span className='modal__rating'>Rating: {vote_average * 10}% </span>
Release date: {release_date || first_air_date} Runtime:{' '}
{runtime || episode_run_time}m
</p>
<p className='modal__episode'>
{number_of_episodes ? ' Episodes: ' + number_of_episodes : ''}
{number_of_seasons ? ' Seasons: ' + number_of_seasons : ''}
</p>
<p className='modal__overview'>{overview}</p>
<button className='modal__btn modal__btn--red'>
<PlayIcon className='modal__btn--icon' />
Play
</button>
<button className='modal__btn'>
<AddIcon className='modal__btn--icon' />
My List
</button>
</div>
)
}
export default MovieDetails

View File

@@ -1,11 +0,0 @@
import React from 'react';
const Movie = (props) => (
<div className="movie">
<div onClick={props.movieDetails} className="movie__column-poster">
<img src={props.movieImage} alt="" className="movie__poster" />
</div>
</div>
);
export default Movie;

View File

@@ -1,41 +0,0 @@
import React from 'react';
import Aux from '../../hoc/Aux';
import AddIcon from '../../static/images/add.svg';
import PlayIcon from '../../static/images/play-button.svg';
export default function MovieDetails(props) {
return (
<Aux>
<div className="modal__container">
<h1 className="modal__title">
{props.movie.title || props.movie.name}
</h1>
<p className="modal__info">
<span className="modal__rating">
Rating: {props.movie.vote_average * 10}%{' '}
</span>
Release date: {props.movie.release_date || props.movie.first_air_date}{' '}
Runtime: {props.movie.runtime || props.movie.episode_run_time}m
</p>
<p className="modal__episode">
{props.movie.number_of_episodes
? ' Episodes: ' + props.movie.number_of_episodes
: ''}
{props.movie.number_of_seasons
? ' Seasons: ' + props.movie.number_of_seasons
: ''}
</p>
<p className="modal__overview">{props.movie.overview}</p>
<button className="modal__btn modal__btn--red">
<PlayIcon className="modal__btn--icon" />
Play
</button>
<button className="modal__btn">
<AddIcon className="modal__btn--icon" />
My List
</button>
</div>
</Aux>
);
}

78
src/components/Navbar.js Normal file
View File

@@ -0,0 +1,78 @@
import React, { useState, useEffect, useRef } from 'react'
import { NavLink } from 'react-router-dom'
import { withRouter } from 'react-router-dom'
import { useScroll } from '../hooks/useScroll'
import SearchLogo from '../static/images/search-icon.svg'
import NetflixLogo from '../static/images/Netflix_Logo_RGB.png'
import BellLogo from '../static/images/bell-logo.svg'
import DropdownArrow from '../static/images/drop-down-arrow.svg'
import DropdownContent from '../components/DropdownContent'
const Navbar = ({ history }) => {
const searchInput = React.useRef(null)
const [userInput, setUserInput] = useState('')
const [scrollDimensions] = useScroll()
const { scrollY } = scrollDimensions
const onChange = async (event) => {
setUserInput(event.target.value)
}
useEffect(() => {
if (
document.activeElement === searchInput.current &&
userInput.length === 0
) {
history.push('/browse')
}
if (userInput.length > 0) history.push(`/search?q=${userInput}`)
}, [userInput, searchInput])
const onLogoClick = () => {
setUserInput('')
}
return (
<nav className={'navigation ' + (scrollY > 50 ? 'black' : '')}>
<ul className='navigation__container'>
<NavLink to='/' onClick={() => onLogoClick()}>
<img
className='navigation__container--logo'
src={NetflixLogo}
alt=''
/>
</NavLink>
<DropdownArrow className='navigation__container--downArrow-2'></DropdownArrow>
<div className='navigation__container-link pseudo-link'>Home</div>
<div className='navigation__container-link pseudo-link'>TV Shows</div>
<div className='navigation__container-link pseudo-link'>Movies</div>
<div className='navigation__container-link pseudo-link'>
Recently Added
</div>
<div className='navigation__container-link pseudo-link'>My List</div>
<div className='navigation__container--left'>
<SearchLogo className='logo' />
<input
ref={searchInput}
value={userInput}
onChange={(event) => onChange(event)}
className='navigation__container--left__input'
type='text'
placeholder='Title, genres, people'
/>
</div>
<div className='navigation__container-link pseudo-link'>KIDS</div>
<div className='navigation__container-link pseudo-link'>DVD</div>
<BellLogo className='navigation__container--bellLogo' />
<DropdownContent />
<DropdownArrow className='navigation__container--downArrow' />
</ul>
</nav>
)
}
export default withRouter(Navbar)

View File

@@ -1,8 +1,11 @@
import React from 'react';
import React from 'react'
const backdrop = (props) =>
props.show ? (
<div onClick={props.toggleBackdrop} className="backdrop"></div>
) : null;
const backdrop = ({ toggleBackdrop, show }) =>
show ? (
<div
onClick={toggleBackdrop}
className={`backdrop ${show ? 'hideB' : ''}`}
></div>
) : null
export default backdrop;
export default backdrop

View File

@@ -1,25 +1,24 @@
import React from 'react';
import React from 'react'
import Aux from '../../hoc/Aux';
import Backdrop from './Backdrop';
import Backdrop from './Backdrop'
export default function Modal(props) {
const Modal = ({ show, modalClosed, children, backgroundImage }) => {
const backgroundStyle = {
backgroundSize: 'cover',
backgroundImage: `url(https://image.tmdb.org/t/p/original/${
props.movie.backdrop_path || props.movie.poster_path
})`,
};
backgroundImage: `url(https://image.tmdb.org/t/p/original/${backgroundImage})`,
}
return (
<Aux>
<Backdrop show={props.show} toggleBackdrop={props.modalClosed} />
<div>
<Backdrop show={show} toggleBackdrop={modalClosed} />
<div
style={backgroundStyle}
className={props.show ? 'modal show' : 'modal hide'}
className={show ? 'modal show' : 'modal hide'}
>
{props.children}
{children}
</div>
</Aux>
);
</div>
)
}
export default Modal

View File

@@ -1,5 +0,0 @@
import React from 'react';
import AppRouter from './AppRouter';
const App = () => <AppRouter />;
export default App;

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from './Home';
import NotFound from './NotFound';
import Search from './Search';
import Navbar from './Navbar';
const AppRouter = () => (
<BrowserRouter>
<>
<Navbar />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/search" component={Search} />
<Route component={NotFound} />
</Switch>
</>
</BrowserRouter>
);
export default AppRouter;

View File

@@ -1,107 +0,0 @@
import React, { Component } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
// install Swiper components
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);
export default class DisplayMovieRow extends Component {
constructor(props) {
super(props);
this.state = {
width: window.innerWidth,
};
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
}
componentWillUnMount() {
window.addEventListener("resize", this.handleResize);
}
handleResize = (e) => {
this.setState({ width: window.innerWidth });
};
render() {
const { width } = this.state;
let netflixUrl = false;
if (
this.props.url ===
`/discover/tv?api_key=${process.env.API_KEY}&with_networks=213`
) {
netflixUrl = true;
}
return (
<>
<h1 className="movieShowcase__heading">{this.props.title}</h1>
<Swiper
className="movieShowcase__container"
navigation={true}
grabCursor={false}
draggable={false}
loop={true}
loopAdditionalSlides={
width >= 1378 ? 4 :
width >= 998 ? 3 :
width >= 625 ? 2 : 2
}
breakpoints={{
1378: {
slidesPerView: 5,
slidesPerGroup: 5,
},
998: {
slidesPerView: 4,
slidesPerGroup: 4,
},
625: {
slidesPerView: 3,
slidesPerGroup: 3,
},
0: {
slidesPerView: 2,
slidesPerGroup: 2,
},
}}
preventClicksPropagation={true}
preventClicks={true}
scrollbar={{ draggable: false, hide: true }}
slideToClickedSlide={false}
pagination={{ clickable: true }}
>
{this.props.movies.map((movie, idx) => {
let movieImageUrl =
'https://image.tmdb.org/t/p/w500/' + movie.backdrop_path;
if (
this.props.url ===
`/discover/tv?api_key=${process.env.API_KEY}&with_networks=213`
) {
movieImageUrl =
'https://image.tmdb.org/t/p/original/' + movie.poster_path;
}
if (movie.poster_path && movie.backdrop_path !== null) {
return (
<SwiperSlide
onClick={() => this.props.selectMovieHandler(movie)}
key={idx}
className={
'movieShowcase__container--movie' +
(netflixUrl ? '__netflix' : '')
}
>
<img
src={movieImageUrl}
className="movieShowcase__container--movie-image"
/>
</SwiperSlide>
);
}
})}
</Swiper>
</>
);
}
}

View File

@@ -1,42 +0,0 @@
import React, { Component } from 'react';
import MainContent from './MainContent';
import Modal from '../components/UI/Modal';
import MovieDetails from '../components/Movie/MovieDetails';
class Home extends Component {
state = {
/** Toggles the modal when a movie is clicked. */
toggleModal: false,
/** Holds the movie information for a single movie. */
movieOverview: {},
}
/* Get the appropriate details for a specific movie that was clicked */
selectMovieHandler = async (movie) => {
this.setState({ toggleModal: true });
await this.setState({ movieOverview: movie });
}
closeModal = () => {
this.setState({ toggleModal: false });
}
render() {
return (
<>
<div className="main-content">
<MainContent selectMovieHandler={this.selectMovieHandler} />
</div>
<Modal show={this.state.toggleModal}
modalClosed={this.closeModal}
movie={this.state.movieOverview}>
<MovieDetails movie={this.state.movieOverview} />
</Modal>
</>
);
}
}
export default Home;

View File

@@ -1,170 +0,0 @@
import React, { Component } from 'react';
import axios from 'axios';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Header from '../components/Header';
import Footer from '../components/Footer';
import {
fetchNetflixOriginals,
fetchTrending,
fetchTopRated,
fetchActionMovies,
fetchComedyMovies,
fetchDocumentaries,
fetchHorrorMovies
} from '../store/actions/index';
import DisplayMovieRow from './DisplayMovieRow';
class MainContent extends Component {
state = {
/** Will hold our chosen movie to display on the header */
selectedMovie: {},
movieInfo: [
{
title: 'Netflix Originals',
url: `/discover/tv?api_key=${process.env.API_KEY}&with_networks=213`,
movies: [],
},
{
title: 'Trending Now',
url: `/trending/all/week?api_key=${process.env.API_KEY}&language=en-US`,
movies: [],
},
{
title: 'Top Rated',
url: `/movie/top_rated?api_key=${process.env.API_KEY}&language=en-US`,
movies: [],
},
{
title: 'Action Movies',
url: `/discover/movie?api_key=${process.env.API_KEY}&with_genres=28`,
movies: [],
},
{
title: 'Comedy Movies',
url: `/discover/tv?api_key=${process.env.API_KEY}&with_genres=35`,
movies: [],
},
{
title: 'Horror Movies',
url: `/discover/tv?api_key=${process.env.API_KEY}&with_genres=27`,
movies: [],
},
{
title: 'Documentaries',
url: `/discover/tv?api_key=${process.env.API_KEY}&with_genres=99`,
movies: [],
},
],
};
componentDidMount = async () => {
await this.getMovie();
await this.props.fetchNetflixOriginals();
await this.props.fetchTrending();
await this.props.fetchTopRated();
await this.props.fetchActionMovies();
await this.props.fetchComedyMovies();
await this.props.fetchDocumentaries();
await this.props.fetchHorrorMovies();
const newMoviesArray = this.state.movieInfo.map((movie) => {
if (movie.title === 'Netflix Originals') {
movie.movies.push(...this.props.netflixOriginals.data)
}
if (movie.title === 'Trending Now') {
movie.movies.push(...this.props.trending.data)
}
if (movie.title === 'Top Rated') {
movie.movies.push(...this.props.topRated.data)
}
if (movie.title === 'Action Movies') {
movie.movies.push(...this.props.actionMovies.data)
}
if (movie.title === 'Comedy Movies') {
movie.movies.push(...this.props.comedyMovies.data)
}
if (movie.title === 'Documentaries') {
movie.movies.push(...this.props.documentaries.data)
}
if (movie.title === 'Horror Movies') {
movie.movies.push(...this.props.horrorMovies.data)
}
return movie
})
await this.setState({ movieInfo: newMoviesArray })
};
getMovie = () => {
/** Movie Id for the Narcos series */
const movieId = 63351;
/** Make Api call to retrieve the details for a single movie */
const url = `https://api.themoviedb.org/3/tv/${movieId}?api_key=${process.env.API_KEY}`;
axios
.get(url)
.then(res => {
const movieData = res.data;
this.setState({ selectedMovie: movieData });
})
.catch(error => {
console.log(error);
});
};
render() {
return (
<div className="container">
<Header movie={this.state.selectedMovie} />
<div className="movieShowcase">
{
this.state.movieInfo.map((info) => {
if (info.movies.length > 0) {
return (
<DisplayMovieRow
selectMovieHandler={this.props.selectMovieHandler}
key={info.title}
title={info.title}
url={info.url}
movies={info.movies}
/>)
}
})
}
</div>
<Footer />
</div>
);
}
}
const mapStateToProps = (state) => {
return {
netflixOriginals: state.netflixOriginals,
trending: state.trending,
topRated: state.topRated,
actionMovies: state.action,
comedyMovies: state.comedy,
documentaries: state.documentary,
horrorMovies: state.horror
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
fetchNetflixOriginals,
fetchTrending,
fetchTopRated,
fetchActionMovies,
fetchComedyMovies,
fetchDocumentaries,
fetchHorrorMovies
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(MainContent);

View File

@@ -1,108 +0,0 @@
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import _ from 'lodash';
import { withRouter } from 'react-router-dom';
import axios from '../axios-movies';
import SearchLogo from '../static/images/search-icon.svg';
import NetflixLogo from '../static/images/Netflix_Logo_RGB.png';
import BellLogo from '../static/images/bell-logo.svg';
import DropdownArrow from '../static/images/drop-down-arrow.svg';
import DropdownContent from "../components/DropdownContent";
class Navbar extends Component {
constructor(props) {
super(props)
this.state = {
scrolling: false,
userInput: ''
}
// use to debounce api call
this.makeAipCall = _.debounce(this.makeAipCall, 1000)
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
/** changes the scrolling state depending on the Y-position */
handleScroll = () => {
if (window.scrollY === 0) {
this.setState({ scrolling: false });
}
else if (window.scrollY > 50) {
this.setState({ scrolling: true });
}
}
onChange = async (event) => {
await this.setState({ userInput: event.target.value })
const { userInput } = this.state
await this.makeAipCall(userInput);
}
/** Make API call as soon as the user starts typing. */
makeAipCall = async (searchItem) => {
if (searchItem.length === 0) {
this.props.history.push('/')
return
}
const url = `/search/multi?api_key=${process.env.API_KEY}&language=en-US&include_adult=false&query=${searchItem}`;
const response = await axios.get(url);
const results = response.data.results;
this.props.history.push({
pathname: '/search',
movieRows: results,
userInput: searchItem
});
}
onLogoClick = () => {
// reset input state
this.setState({ userInput: '' })
}
render() {
const { scrolling } = this.state;
return (
<nav className={"navigation " + (scrolling ? "black" : "")} >
<ul className="navigation__container">
<NavLink to="/" onClick={() => this.onLogoClick()}>
<img className="navigation__container--logo" src={NetflixLogo} alt="" />
</NavLink>
<DropdownArrow className="navigation__container--downArrow-2"></DropdownArrow>
<div className="navigation__container-link pseudo-link">Home</div>
<div className="navigation__container-link pseudo-link">TV Shows</div>
<div className="navigation__container-link pseudo-link">Movies</div>
<div className="navigation__container-link pseudo-link">Recently Added</div>
<div className="navigation__container-link pseudo-link">My List</div>
<div className="navigation__container--left">
<SearchLogo className="logo" />
<input
value={this.state.userInput}
onChange={() => this.onChange(event)}
className="navigation__container--left__input"
type="text"
placeholder="Title, genres, people" />
</div>
<div className="navigation__container-link pseudo-link">KIDS</div>
<div className="navigation__container-link pseudo-link">DVD</div>
<BellLogo className="navigation__container--bellLogo" />
<DropdownContent />
<DropdownArrow className="navigation__container--downArrow" />
</ul>
</nav>
)
}
}
export default withRouter(Navbar);

View File

@@ -1,115 +0,0 @@
import React, { Component } from 'react';
import Modal from '../components/UI/Modal';
import MovieDetails from '../components/Movie/MovieDetails';
import Movie from '../components/Movie/Movie';
import axios from '../axios-movies';
export default class Search extends Component {
constructor(props) {
super(props)
this.state = {
movies: [],
toggleModal: false,
/** Holds the movie information for a single movie. */
movieOverview: {},
}
}
componentDidMount = async () => {
const { movieRows } = this.props.history.location;
if (movieRows)
await this.setState({ movies: movieRows });
}
componentDidUpdate = async (prevProps) => {
if (
prevProps.location.movieRows.length !==
this.props.location.movieRows.length
) {
await this.setState({ movies: this.props.location.movieRows });
}
}
closeModal = () => {
this.setState({ toggleModal: false });
}
/* Get the appropriate details for a specific movie that was clicked */
selectMovieHandler = (movie) => {
this.setState({ toggleModal: true });
let url;
/** Make the appropriate API call to get the details for a single movie or tv show. */
if (movie.media_type === "movie") {
const movieId = movie.id;
url = `https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.API_KEY}`;
} else if (movie.media_type === "tv") {
const tvId = movie.id
url = `https://api.themoviedb.org/3/tv/${tvId}?api_key=${process.env.API_KEY}`;
}
axios.get(url)
.then(res => {
const movieData = res.data;
this.setState({ movieOverview: movieData });
}).catch(error => {
console.log(error);
});
}
render() {
const { movies } = this.state
const { userInput } = this.props.location
return (
<>
{
movies.length > 0 ? (
<div className="search-container">
{
movies.map((movie) => {
let movieRows = []
let movieImageUrl;
if (movie.poster_path !== null && movie.media_type !== "person") {
movieImageUrl = "https://image.tmdb.org/t/p/w500" + movie.poster_path;
/** Set the movie object to our Movie component */
const movieComponent = <Movie
movieDetails={() => this.selectMovieHandler(movie)}
key={movie.id}
movieImage={movieImageUrl}
movie={movie} />
/** Push our movie component to our movieRows array */
movieRows.push(movieComponent);
}
return movieRows
})
}
</div>
) : (
<div className="no-results">
<div className="no-results__text">
<p>Your search for "{userInput}" did not have any matches.</p>
<p>Suggestions:</p>
<ul>
<li>Try different keywords</li>
<li>Looking for a movie or TV show?</li>
<li>Try using a movie, TV show title, an actor or director</li>
<li>Try a genre, like comedy, romance, sports, or drama</li>
</ul>
</div>
</div>
)
}
<Modal Modal show={this.state.toggleModal}
modalClosed={this.closeModal}
movie={this.state.movieOverview} >
<MovieDetails movie={this.state.movieOverview} />
</Modal>
</>
);
}
}

View File

@@ -1,3 +0,0 @@
const aux = (props) => props.children;
export default aux;

25
src/hooks/useDebounce.js Normal file
View File

@@ -0,0 +1,25 @@
import { useState, useEffect } from 'react'
export const useDebounce = (value, delay) => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler)
}
},
[value, delay] // Only re-call effect if value or delay changes
)
return debouncedValue
}

26
src/hooks/useScroll.js Normal file
View File

@@ -0,0 +1,26 @@
import { useState, useEffect } from 'react'
const scrollY = window.scrollY || document.documentElement.scrollTop
const scrollX = window.scrollY || document.documentElement.scrollWidth
export const useScroll = () => {
const [scrollDimensions, setScrollDimensions] = useState({ scrollY, scrollX })
const deriveScrollDimensions = () => {
const scrollY = window.scrollY || document.documentElement.scrollTop
const scrollX = window.scrollY || document.documentElement.scrollWidth
setScrollDimensions({ scrollY, scrollX })
}
useEffect(() => {
deriveScrollDimensions()
window.addEventListener('scroll', deriveScrollDimensions)
return () => {
window.removeEventListener('resize', deriveScrollDimensions)
}
}, [])
return [scrollDimensions]
}

39
src/hooks/useViewport.js Normal file
View File

@@ -0,0 +1,39 @@
import { useState, useEffect } from 'react'
const height =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight
const width =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
export const useViewport = () => {
const [windowDimensions, setWindowDimensions] = useState({ height, width })
const deriveWindowDimensions = () => {
const height =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight
const width =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
setWindowDimensions({ height, width })
}
useEffect(() => {
deriveWindowDimensions()
window.addEventListener('resize', deriveWindowDimensions)
return () => {
window.removeEventListener('resize', deriveWindowDimensions)
}
}, [])
return [windowDimensions]
}

View File

@@ -1,25 +1,26 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reducers from './store/reducers';
import promise from 'redux-promise';
import '@babel/polyfill';
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'
import '@babel/polyfill'
import App from './containers/App';
import 'swiper/swiper-bundle.min.css';
// import 'swiper/components/navigation/navigation.scss';
// import 'swiper/components/pagination/pagination.scss';
// import 'swiper/components/scrollbar/scrollbar.scss';
import reducers from './store/reducers'
import AppRouter from './AppRouter'
// Import Swiper styles
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
// Import main sass file to apply global styles
import './static/sass/style.scss';
import './static/sass/style.scss'
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
const store = createStore(reducers, applyMiddleware(ReduxThunk))
const app = (
<Provider store={createStoreWithMiddleware(reducers)}>
<App />
<Provider store={store}>
<AppRouter />
</Provider>
);
)
ReactDOM.render(app, document.getElementById('app'));
ReactDOM.render(app, document.getElementById('app'))

36
src/pages/Home.js Normal file
View File

@@ -0,0 +1,36 @@
import React, { useState } from 'react'
import MainContent from '../components/MainContent'
import Modal from '../components/UI/Modal'
import ModalMovieDetails from '../components/ModalMovieDetails'
const Home = () => {
const [toggleModal, setToggleModal] = useState(false)
const [movieDetails, setMovieDetails] = useState({})
const selectMovieHandler = async (movie) => {
setToggleModal(true)
setMovieDetails(movie)
}
const closeModal = () => {
setToggleModal(false)
}
return (
<>
<div className='main-content'>
<MainContent selectMovieHandler={selectMovieHandler} />
</div>
<Modal
show={toggleModal}
modalClosed={closeModal}
backgroundImage={movieDetails.backdrop_path || movieDetails.poster_path}
>
<ModalMovieDetails movie={movieDetails} />
</Modal>
</>
)
}
export default Home

View File

@@ -1,9 +1,11 @@
import React from 'react';
export default function NotFound() {
const NotFound = () => {
return (
<div>
<h1>NOT FOUND</h1>
</div>
);
}
export default NotFound

91
src/pages/Search.js Normal file
View File

@@ -0,0 +1,91 @@
import React, { useState, useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { useDebounce } from '../hooks/useDebounce'
import * as movieActions from '../store/actions'
import Modal from '../components/UI/Modal'
import ModalMovieDetails from '../components/ModalMovieDetails'
// A custom hook that builds on useLocation to parse
// the query string for you.
const useQuery = () => {
return new URLSearchParams(useLocation().search)
}
const Search = () => {
let query = useQuery()
const debouncedSearchTerm = useDebounce(query.get('q'), 500)
const [isToggleModal, setIsToggleModal] = useState(false)
const { searchResults, isLoading } = useSelector((state) => state.searchMovie)
const { movieDetails } = useSelector((state) => state.movieDetails)
const dispatch = useDispatch()
useEffect(() => {
if (debouncedSearchTerm) {
dispatch(movieActions.fetchSearchMovie(debouncedSearchTerm))
}
}, [debouncedSearchTerm])
const onCloseModalHandler = () => {
setIsToggleModal(false)
}
const onSelectMovieHandler = (movie) => {
dispatch(movieActions.fetchMovieDetails(movie.media_type, movie.id))
setIsToggleModal(true)
}
const renderSearchResults = () => {
return searchResults.length > 0 ? (
<>
<div className='search-container'>
{searchResults.map((movie) => {
if (movie.backdrop_path !== null && movie.media_type !== 'person') {
const movieImageUrl =
'https://image.tmdb.org/t/p/w500' + movie.backdrop_path
return (
<div className='movie'>
<div
onClick={() => onSelectMovieHandler(movie)}
className='movie__column-poster'
>
<img src={movieImageUrl} alt='' className='movie__poster' />
</div>
</div>
)
}
})}
</div>
<Modal
show={isToggleModal}
modalClosed={onCloseModalHandler}
backgroundImage={
movieDetails.backdrop_path || movieDetails.poster_path
}
>
<ModalMovieDetails movie={movieDetails} />
</Modal>
</>
) : (
<div className='no-results'>
<div className='no-results__text'>
<p>
Your search for "{debouncedSearchTerm}" did not have any matches.
</p>
<p>Suggestions:</p>
<ul>
<li>Try different keywords</li>
<li>Looking for a movie or TV show?</li>
<li>Try using a movie, TV show title, an actor or director</li>
<li>Try a genre, like comedy, romance, sports, or drama</li>
</ul>
</div>
</div>
)
}
return !isLoading ? renderSearchResults() : <h1>Loading...</h1>
}
export default Search

View File

@@ -0,0 +1,3 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="#000" stroke-width="2" d="M1,8 L1,16 L6.09901951,16 L12,21 L12,3 L6,8 L1,8 Z M15,9 L21,15 M21,9 L15,15"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@@ -0,0 +1,3 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="#000" stroke-width="2" d="M15,16 C17.209,16 19,14.209 19,12 C19,9.791 17.209,8 15,8 M15,20 C20,20 23,16.411 23,12 C23,7.589 19.411,4 15,4 M1,12 L1,8 L6,8 L12,3 L12,21 L6,16 L1,16 L1,12"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -1,67 +1,147 @@
/* GLOBAL RESET */
:global(html), :global(body), :global(div), :global(span), :global(applet), :global(object), :global(iframe),
:global(h1), :global(h2), :global(h3), :global(h4), :global(h5), :global(h6), :global(p), :global(blockquote), :global(pre),
:global(a), :global(abbr), :global(acronym), :global(address), :global(big), :global(cite), :global(code),
:global(del), :global(dfn), :global(em), :global(img), :global(ins), :global(kbd), :global(q), :global(s), :global(samp),
:global(small), :global(strike), :global(strong), :global(sub), :global(sup), :global(tt), :global(var),
:global(b), :global(u), :global(i), :global(center),
:global(dl), :global(dt), :global(dd), :global(ol), :global(ul), :global(li),
:global(fieldset), :global(form), :global(label), :global(legend),
:global(table), :global(caption), :global(tbody), :global(tfoot), :global(thead), :global(tr), :global(th), :global(td),
:global(article), :global(aside), :global(canvas), :global(details), :global(embed),
:global(figure), :global(figcaption), :global(footer), :global(header), :global(hgroup),
:global(menu), :global(nav), :global(output), :global(ruby), :global(section), :global(summary),
:global(time), :global(mark), :global(audio), :global(video) {
margin: 0;
padding: 0;
border: 0;
vertical-align: baseline;
box-sizing: inherit;
:global(html),
:global(body),
:global(div),
:global(span),
:global(applet),
:global(object),
:global(iframe),
:global(h1),
:global(h2),
:global(h3),
:global(h4),
:global(h5),
:global(h6),
:global(p),
:global(blockquote),
:global(pre),
:global(a),
:global(abbr),
:global(acronym),
:global(address),
:global(big),
:global(cite),
:global(code),
:global(del),
:global(dfn),
:global(em),
:global(img),
:global(ins),
:global(kbd),
:global(q),
:global(s),
:global(samp),
:global(small),
:global(strike),
:global(strong),
:global(sub),
:global(sup),
:global(tt),
:global(var),
:global(b),
:global(u),
:global(i),
:global(center),
:global(dl),
:global(dt),
:global(dd),
:global(ol),
:global(ul),
:global(li),
:global(fieldset),
:global(form),
:global(label),
:global(legend),
:global(table),
:global(caption),
:global(tbody),
:global(tfoot),
:global(thead),
:global(tr),
:global(th),
:global(td),
:global(article),
:global(aside),
:global(canvas),
:global(details),
:global(embed),
:global(figure),
:global(figcaption),
:global(footer),
:global(header),
:global(hgroup),
:global(menu),
:global(nav),
:global(output),
:global(ruby),
:global(section),
:global(summary),
:global(time),
:global(mark),
:global(audio),
:global(video) {
margin: 0;
padding: 0;
border: 0;
vertical-align: baseline;
box-sizing: inherit;
}
/* HTML5 display-role reset for older browsers */
:global(article), :global(aside), :global(details), :global(figcaption), :global(figure),
:global(footer), :global(header), :global(hgroup), :global(menu), :global(nav), :global(section) {
display: block;
:global(article),
:global(aside),
:global(details),
:global(figcaption),
:global(figure),
:global(footer),
:global(header),
:global(hgroup),
:global(menu),
:global(nav),
:global(section) {
display: block;
}
:global(body) {
line-height: 1;
line-height: 1;
}
:global(ol), :global(ul) {
list-style: none;
:global(ol),
:global(ul) {
list-style: none;
}
:global(blockquote), :global(q) {
quotes: none;
:global(blockquote),
:global(q) {
quotes: none;
}
:global(blockquote:before),
:global(blockquote:after),
:global(q:before),
:global(q:after) {
content: '';
content: none;
}
:global(blockquote:before), :global(blockquote:after),
:global(q:before), :global(q:after) {
content: '';
content: none;
}
:global(table) {
border-collapse: collapse;
border-spacing: 0;
border-collapse: collapse;
border-spacing: 0;
}
body {
font-family: 'Hind', sans-serif;
box-sizing: border-box;
background-color: $color-background;
font-family: 'Hind', sans-serif;
box-sizing: border-box;
background-color: $color-background;
}
html {
// this defines what 1rem is --> font root size
font-size: 62.5%; // 10/16, 1rem = 10px;
@include responsive(tab_port) {
font-size: 50%;
}
// this defines what 1rem is --> font root size
font-size: 62.5%; // 10/16, 1rem = 10px;
@include responsive(tab_port) {
font-size: 50%;
}
}
// GRID AREA
// GRID AREA
.container {
display: grid;
grid-template-rows: min-content min-content min-content min-content;
grid-template-columns: 4% repeat(10, 1fr) 4%;
// display: grid;
// grid-template-rows: min-content min-content min-content min-content;
// grid-template-columns: 0 repeat(10, 1fr);
}

View File

@@ -1,10 +1,19 @@
.backdrop {
width: 120%;
height: 100%;
position: fixed;
z-index: 100;
left: 0;
top: 0;
background-color: rgba($color-background, .7);
transition: all .3s;
}
width: 120%;
height: 100%;
position: fixed;
z-index: 100;
left: 0;
top: 0;
animation: fadeIn 1s cubic-bezier(0.165, 0.84, 0.44, 1) forwards;
background-color: rgba($color-background, 0.7);
}
@keyframes fadeIn {
0% {
background: rgba(0, 0, 0, 0);
}
100% {
background: rgba(0, 0, 0, 0.7);
}
}

View File

@@ -1,142 +1,147 @@
.modal {
position: fixed;
z-index: 500;
width: 100%;
top: 0;
left: 0;
color: #fff;
height: 52rem;
opacity: 0;
box-shadow: 0 1.5rem 4rem rgba($color-dark, .15);
transition: all .3s;
position: fixed;
z-index: 500;
width: 100%;
top: 0;
left: 0;
color: #fff;
height: 52rem;
opacity: 0;
box-shadow: 0 1.5rem 4rem rgba($color-dark, 0.15);
transition: all 0.3s;
@include responsive(tab_port) {
height: 38rem;
}
@include responsive(tab_port) {
height: 38rem;
}
@include responsive(phone) {
height: 50rem;
}
@include responsive(phone) {
height: 50rem;
}
&__container {
background: linear-gradient(90deg, #000 50%, transparent);
width: 70%;
padding-top: 3rem;
height: 100%;
padding-left: 5rem;
&__container {
background: linear-gradient(90deg, #000 50%, transparent);
width: 70%;
padding-top: 3rem;
height: 100%;
padding-left: 5rem;
@include responsive(tab_port) {
background: linear-gradient(
90deg,
rgb(0, 0, 0) 55%,
rgba(0, 0, 0, 0.733),
transparent
);
width: 88%;
}
@include responsive(tab_port) {
background: linear-gradient(90deg, rgb(0, 0, 0) 55%, rgba(0, 0, 0, 0.733), transparent);
width: 88%;
}
@include responsive(tab_medium) {
background: linear-gradient(90deg, rgba(0, 0, 0, 0.966) 65%, transparent);
width: 100%;
}
@include responsive(tab_medium) {
background: linear-gradient(90deg, rgba(0, 0, 0, 0.966) 65%, transparent);
width: 100%;
}
@include responsive(phone) {
padding-top: 1rem;
padding-left: 1rem;
}
}
@include responsive(phone) {
padding-top: 1rem;
padding-left: 1rem;
}
}
&__title {
font-size: 4rem;
}
&__title {
font-size: 4rem;
}
&__rating {
font-size: 2rem;
color: $color-green-modal;
}
&__rating {
font-size: 2rem;
color: $color-green-modal;
}
&__info {
padding-top: 1.6rem;
font-size: 2rem;
&__info {
padding-top: 1.6rem;
font-size: 2rem;
@include responsive(phone) {
padding-top: 1.2rem;
}
}
@include responsive(phone) {
padding-top: 1.2rem;
}
}
&__episode {
padding-top: 0.5rem;
font-size: 2rem;
}
&__episode {
padding-top: .5rem;
font-size: 2rem;
}
&__overview {
color: $color-modal-grey-2;
padding-top: 2rem;
font-size: 2rem;
hyphens: auto;
width: 60%;
line-height: 1.2;
&__overview {
color: $color-modal-grey-2;
padding-top: 2rem;
font-size: 2rem;
hyphens: auto;
@include responsive(lg_desktop) {
width: 60%;
line-height: 1.2;
font-size: 1.8rem;
}
@include responsive(lg_desktop) {
width: 60%;
font-size: 1.8rem;
}
@include responsive(tab_port) {
width: 80%;
}
@include responsive(tab_port) {
width: 80%;
}
@include responsive(tab_medium) {
width: 95%;
color: rgba(255, 255, 255, 0.877);
}
@include responsive(tab_medium) {
width: 95%;
color: rgba(255, 255, 255, 0.877);
}
@include responsive(phone) {
padding-top: 1rem;
font-size: 1.7rem;
}
}
@include responsive(phone) {
padding-top: 1rem;
font-size: 1.7rem;
}
}
&__btn {
text-transform: uppercase;
font-weight: 500;
cursor: pointer;
background-color: transparent;
color: #fff;
border: 0.5px solid $color-modal-grey-2;
border-radius: 2px;
font-size: 1.5rem;
margin-top: 2rem;
margin-right: 1rem;
padding: 1rem 2rem 1rem 2rem;
transition: all 0.2s;
&__btn {
text-transform: uppercase;
font-weight: 500;
cursor: pointer;
background-color: transparent;
color: #fff;
border: .5px solid $color-modal-grey-2;
border-radius: 2px;
font-size: 1.5rem;
margin-top: 2rem;
margin-right: 1rem;
padding: 1rem 2rem 1rem 2rem;
transition: all .2s;
@include responsive(phone) {
margin-top: 1rem;
}
@include responsive(phone) {
margin-top: 1rem;
}
&:hover {
transform: scale(1.09);
}
&:hover {
transform: scale(1.09);
}
&--red {
background-color: $color-red-2;
border: none;
}
&--red {
background-color: $color-red-2;
border: none;
}
&--icon {
fill: #fff;
padding-right: 1rem;
height: 1.4rem;
width: 1.4rem;
}
}
&--icon {
fill: #fff;
padding-right: 1rem;
height: 1.4rem;
width: 1.4rem;
}
}
}
.show {
transition: .5s .3s ease-out;
top: 25%;
left: 0;
visibility: visible;
opacity: 1;
transition: 0.3s 0.3s ease-out;
top: 25%;
left: 0;
visibility: visible;
opacity: 1;
}
.hide {
visibility: hidden;
}
visibility: hidden;
transition: 0.3s ease-out;
}

View File

@@ -1,38 +1,34 @@
.movie {
flex: 1 1 auto;
padding-top: 6rem;
display: inline-block;
flex: 1 1 auto;
display: inline-block;
padding-right: 0.5rem;
padding-bottom: 7rem;
@include responsive(tab_port) {
padding-top: 4rem;
}
@include responsive(phone) {
padding-bottom: 4rem;
}
&__column-poster {
height: 25rem;
width: 17.5rem;
cursor: pointer;
transition: transform .3s;
-webkit-transition: transform .3s;
&__column-poster {
height: 15rem;
cursor: pointer;
transition: transform 0.3s;
-webkit-transition: transform 0.3s;
&:hover {
transform: scale(1.25);
@include responsive(tab_port) {
height: 20rem;
width: 14rem;
transform: scale(1.15);
}
&:hover {
transform: scale(1.25);
@include responsive(tab_port) {
transform: scale(1.15);
}
}
}
&__poster {
height: 100%;
}
}
&__poster {
height: 100%;
border-radius: 5px;
}
}
div.movie:first-child {
padding-top: 15rem;
padding-top: 15rem;
}

View File

@@ -1,125 +1,135 @@
// Override swiper styles
.swiper-pagination {
top: 0 !important;
height: 2rem !important;
text-align: right !important;
padding-right: 4rem !important;
top: 0 !important;
height: 1rem !important;
text-align: right !important;
}
.swiper-pagination-bullet {
background-color: rgb(255, 255, 255) !important;
background-color: rgb(255, 255, 255) !important;
}
.swiper-container-horizontal>.swiper-pagination-bullets {
bottom: 0 !important;
left: 0 !important;
width: 100% !important;
.swiper-horizontal > .swiper-pagination-bullets,
.swiper-pagination-bullets.swiper-pagination-horizontal {
width: 97.5% !important;
height: 1px !important;
}
.swiper-container-horizontal > .swiper-pagination-bullets {
bottom: 0 !important;
left: 0 !important;
}
div.swiper-button-prev,
div.swiper-button-next {
transition: all 450ms !important;
color: rgb(255, 255, 255);
transition: all 450ms !important;
color: rgb(255, 255, 255);
&:hover {
transform: scale(1.2) !important;
transition: all 450ms !important;
}
&:hover {
transform: scale(1.2) !important;
transition: all 450ms !important;
}
}
.swiper-slide {
transition: all 450ms !important;
transition: all 450ms !important;
}
.swiper-wrapper:hover .swiper-slide {
opacity: .3 !important;
z-index: 10000;
opacity: 0.3 !important;
&:hover {
transform: scale(1.3) !important;
opacity: 1 !important;
&:hover {
transform: scale(1.3) !important;
opacity: 1 !important;
@include responsive(tab_port) {
transform: scale(1.2) !important;
}
}
@include responsive(tab_port) {
transform: scale(1.2) !important;
}
}
}
.movieShowcase {
background-color: $color-background;
grid-column: 2 / 13;
color: #fff;
background-color: transparent;
grid-column: 2 / 13;
color: #fff;
&__heading {
padding-top: 2rem;
}
&__heading {
position: relative;
top: 3rem;
// margin: 0;
padding-left: 4rem;
font-weight: 500;
}
&__container::-webkit-scrollbar {
display: none;
}
&__container::-webkit-scrollbar {
display: none;
}
&__container {
@include responsive(phone) {
width: 98vw;
}
&--movie:hover ~ &--movie {
transform: translate3d(6.5rem, 0, 0);
@include responsive(tab_port) {
transform: translate3d(3rem, 0, 0);
}
&__container {
@include responsive(phone) {
width: 98vw;
transform: translate3d(2.8rem, 0, 0);
}
}
&--movie__netflix:hover ~ &--movie__netflix {
transform: translate3d(1rem, 0, 0);
}
&:hover &--movie__netflix {
&:hover {
transform: scale(1.1) !important;
@include responsive(tab_port) {
transform: scale(1.05) !important;
}
@include responsive(phone) {
transform: scale(1.05) !important;
}
}
}
&--movie {
cursor: pointer;
transition: all 450ms;
transform: center left;
padding-top: 3.5rem;
padding-bottom: 4rem;
min-height: 0;
object-fit: contain;
&:not(:last-child) {
padding-right: 0.5rem;
}
&--movie:hover~&--movie {
transform: translate3d(5rem, 0, 0);
@include responsive(tab_port) {
transform: translate3d(3rem, 0, 0);
}
@include responsive(phone) {
transform: translate3d(2.8rem, 0, 0);
}
&-image {
height: 100%;
width: 100%;
object-fit: contain;
border-radius: 5px;
}
&--movie__netflix:hover~&--movie__netflix {
transform: translate3d(1rem, 0, 0);
&__netflix {
min-height: 0;
padding-top: 3rem;
padding-bottom: 4rem;
padding-right: 1rem;
cursor: pointer;
transition: all 450ms;
transform: center left;
}
&:hover &--movie__netflix {
&:hover {
transform: scale(1.1) !important;
@include responsive(tab_port) {
transform: scale(1.05) !important;
}
@include responsive(phone) {
transform: scale(1.05) !important;
}
}
}
&--movie {
cursor: pointer;
transition: all 450ms;
transform: center left;
padding-top: 3.5rem;
padding-bottom: 4rem;
min-height: 0;
object-fit: contain;
&:not(:last-child) {
padding-right: .5rem;
}
&-image {
height: 100%;
width: 100%;
object-fit: contain;
}
&__netflix {
min-height: 0;
padding-top: 3rem;
padding-bottom: 4rem;
padding-right: 1rem;
cursor: pointer;
transition: all 450ms;
transform: center left;
}
}
}
}
}
}
}

View File

@@ -1,85 +1,165 @@
.header {
background-color: $color-background;
grid-column: 1 / 13;
height: 65rem;
display: inline-block;
background-color: $color-background;
// grid-column: 1 / 13;
// height: 82vh;
// display: grid;
position: relative;
padding-top: 56.25%;
@include responsive(phone) {
height: 50rem;
}
&__container {
padding-top: 20rem;
padding-left: 4.5rem;
@include responsive(phone) {
padding-top: 0;
height: 32rem;
}
&__video {
position: absolute;
top: 0;
left: 0;
}
&__container {
color: #fff;
@include responsive(phone) {
padding-left: 1rem;
}
&-heading {
font-size: 6rem;
position: absolute;
color: #fff;
top: 12rem;
left: 3.5rem;
@include responsive(phone) {
padding-left: 1rem;
top: 7rem;
font-size: 3rem;
}
}
&-btnPlay,
&-btnMyList {
top: 40rem;
cursor: pointer;
font-size: 1.6rem;
color: #fff;
outline: none;
border: none;
font-weight: 700;
border-radius: 5px;
padding-left: 3.5rem;
padding-right: 3.5rem;
margin-right: 1rem;
padding-top: 1rem;
background-color: rgba(112, 111, 111, 0.5);
padding-bottom: 1rem;
@include responsive(phone) {
top: 24rem;
}
&-heading {
font-size: 6rem;
padding-bottom: 1rem;
&-add {
width: 15px;
height: 15px;
fill: #fff;
margin-right: 1rem;
}
&-btnPlay,
&-btnMyList {
cursor: pointer;
font-size: 1.6rem;
color: #fff;
outline: none;
border: none;
font-weight: 700;
border-radius: .2vw;
padding-left: 3.5rem;
padding-right: 3.5rem;
margin-right: 1rem;
padding-top: 1rem;
background-color: rgba(51,51,51,.5);
padding-bottom: 1rem;
&-play {
width: 15px;
height: 15px;
fill: #fff;
margin-right: 1rem;
}
}
&-add {
width: 15px;
height: 15px;
fill: #fff;
margin-right: 1rem;
}
&-btnPlay {
color: #000;
background-color: #e6e6e6;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
&-play {
width: 15px;
height: 15px;
fill: #fff;
margin-right: 1rem;
}
&:hover {
color: #000;
background-color: #e6e6e6;
transition: all .2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, .3);
& > * {
fill: #000;
}
left: 4rem;
position: absolute;
}
& > * {
fill: #000;
}
}
&-btnMyList {
left: 18.5rem;
position: absolute;
&:hover {
background-color: rgb(37, 37, 37);
}
}
&-btnVolume {
position: absolute;
height: 3rem;
width: 3rem;
top: 45rem;
right: 14rem;
cursor: pointer;
border-radius: 50%;
padding: 1rem;
border: #fff solid 1px;
transition: all 0.3s;
@include responsive(tab_medium) {
top: 30rem;
}
@include responsive(phone) {
top: 20rem;
right: 8rem;
}
&-overview {
width: 45rem;
line-height: 1.3;
padding-top: 2.5rem;
font-size: 1.8rem;
@include responsive(phone) {
width: 36rem;
}
& > * {
stroke: #fff;
stroke-width: 1;
}
}
&--fadeBottom {
height: 28.5rem;
background-image: linear-gradient(180deg, transparent,rgba(37, 37, 37, 0.61), rgb(17, 17, 17));
}
&:hover {
background-color: rgba(211, 211, 211, 0.178);
transition: all 0.3s;
}
}
&-overview {
top: 28rem;
left: 4rem;
position: absolute;
color: #fff;
width: 55rem;
line-height: 1.3;
padding-top: 2.5rem;
font-size: 1.8rem;
@include responsive(phone) {
top: 10rem;
width: 20rem;
}
}
&--fadeBottom {
// z-index: 100000000000;
position: absolute;
bottom: 0;
height: 10rem;
width: 100%;
background-image: linear-gradient(
180deg,
transparent,
rgba(15, 15, 15, 0.61),
rgb(17, 17, 17),
rgb(17, 17, 17)
);
@include responsive(phone) {
height: 4rem;
}
}
}
}

View File

@@ -1,264 +1,252 @@
.navigation {
background-color: transparent;
z-index: 100;
grid-column: 1 / 13;
position: fixed;
width: 100%;
height: 6.7rem;
transition-timing-function: ease-in;
transition: all 1s;
background-color: transparent;
z-index: 100;
grid-column: 1 / 13;
position: fixed;
width: 100%;
height: 6.7rem;
transition-timing-function: ease-in;
transition: all 1s;
top: 0;
padding-bottom: 1.5rem;
&.black {
background-color: $color-background;
}
&.black {
background-color: $color-background;
}
&__container {
background-color: transparent;
display: flex;
height: 6.7rem;
justify-content: flex-start;
align-items: center;
padding-left: 2rem;
&__container {
background-color: transparent;
margin-left: 3.8rem;
@include responsive(phone) {
margin-left: 1rem;
}
a:first-child {
padding-left: 0.2%;
color: $color-red;
font-size: 4rem;
}
&--bellLogo {
cursor: pointer;
height: 2.2rem;
fill: red;
width: 2.2rem;
padding-right: 2.5rem;
@include responsive(phone) {
display: none;
}
}
&--userLogo {
margin-top: 2rem;
border-radius: 5px;
cursor: pointer;
background-color: rgb(201, 199, 78);
height: 3rem;
width: 3rem;
margin-right: 0.5rem;
@include responsive(tab_port) {
margin-right: 3rem;
}
@include responsive(phone) {
display: none;
}
&:hover > .dropdownContent {
transition: all 0.4s;
opacity: 1;
visibility: visible;
}
}
&--downArrow {
cursor: pointer;
fill: #fff;
height: 1rem;
width: 1rem;
margin-right: 5.8rem;
@include responsive(tab_port) {
display: none;
}
}
&--downArrow-2 {
display: none;
cursor: pointer;
fill: #fff;
height: 1rem;
width: 1rem;
@include responsive(tab_port) {
height: 1.5rem;
width: 1.5rem;
display: inline-block;
}
@include responsive(phone) {
display: none;
}
}
&--logo {
margin-top: 2rem;
height: 6rem;
padding-right: 1rem;
@include responsive(tab_port) {
padding-right: 0.1rem;
}
}
&-link {
font-weight: 500;
font-size: 1.4rem;
color: rgb(221, 221, 221);
text-decoration: none;
margin-right: 2rem;
transition: all 0.2s;
&:last-child {
padding-right: 2.6rem;
}
&:hover {
color: rgb(167, 167, 167);
}
}
&--left {
flex-grow: 1;
color: #fff;
padding-right: 2rem;
display: flex;
height: 6.7rem;
justify-content: flex-start;
align-items: center;
justify-content: flex-end;
@include responsive(phone) {
margin-left: 1rem;
}
&__input {
font-size: 1.4rem;
border: none;
color: #fff;
outline: none;
width: 0px;
padding: 10px;
padding-right: 2rem;
background: $color-background;
border: 1px solid #fff;
transition: width 0.5s;
cursor: pointer;
opacity: 0;
a:first-child {
padding-left: .2%;
color: $color-red;
font-size: 4rem;
&:focus {
padding-left: 4rem;
width: 22rem;
cursor: text;
opacity: 1;
@include responsive(phone) {
width: 16rem;
}
}
}
&--bellLogo {
cursor: pointer;
height: 2.2rem;
fill: red;
width: 2.2rem;
padding-right: 2.5rem;
@include responsive(phone) {
display: none;
}
}
&--userLogo {
margin-top: 2rem;
border-radius: 5px;
cursor: pointer;
background-color: rgb(201, 199, 78);
height: 3rem;
width: 3rem;
margin-right: .5rem;
@include responsive(tab_port) {
margin-right: 3rem;
}
@include responsive(phone) {
display: none;
}
&:hover > .dropdownContent {
transition: all .4s;
opacity: 1;
visibility: visible;
}
}
&--downArrow {
cursor: pointer;
fill: #fff;
height: 1rem;
width: 1rem;
margin-right: 5.8rem;
@include responsive(tab_port) {
display: none;
}
}
&--downArrow-2 {
display: none;
cursor: pointer;
fill: #fff;
height: 1rem;
width: 1rem;
@include responsive(tab_port) {
height: 1.5rem;
width: 1.5rem;
display: inline-block;
}
@include responsive(phone) {
display: none;
}
}
&--logo {
margin-top: 1rem;
height: 5rem;
padding-right: 1rem;
@include responsive(tab_port) {
padding-right: .1rem;
}
}
&-link {
font-weight: 500;
font-size: 1.5rem;
color: rgb(221, 221, 221);
text-decoration: none;
margin-right: 2rem;
transition: all .2s;
&:last-child {
padding-right: 2.6rem;
}
&:hover {
color: rgb(167, 167, 167);
}
}
&--left {
flex-grow: 1;
color: #fff;
padding-right: 2rem;
display: flex;
justify-content: flex-end;
&__input {
font-size: 1.4rem;
border: none;
color: #fff;
outline: none;
width: 0px;
padding: 10px;
padding-right: 2rem;
background: $color-background;
border: 1px solid #fff;
transition: width .5s;
cursor: pointer;
opacity: 0;
&:focus {
padding-left: 4rem;
width: 22rem;
cursor: text;
opacity: 1;
@include responsive(phone) {
width: 16rem;
}
}
}
}
}
}
}
}
.logo {
width: 1.8rem;
height: 1.8rem;
transform: translateX(2.4rem) translateY(1rem);
cursor: pointer;
width: 1.8rem;
height: 1.8rem;
transform: translateX(2.4rem) translateY(1rem);
cursor: pointer;
}
.dropdownContent {
display: flex;
flex-direction: column;
// align-items: center;
opacity: 0;
color: #FFF;
transition-delay: .5s;
padding-left: 1rem;
padding-top: 1.8rem;
visibility: hidden;
height: 19rem;
border: 1px solid rgb(73, 73, 73);
width: 15rem;
background-color: rgba(#000, .9);
transform: translateY(4.6rem) translateX(-11rem);
display: flex;
flex-direction: column;
// align-items: center;
opacity: 0;
&--2 {
height: 10rem;
}
color: #fff;
transition-delay: 0.5s;
padding-left: 1rem;
padding-top: 1.8rem;
visibility: hidden;
height: 19rem;
border: 1px solid rgb(73, 73, 73);
width: 15rem;
background-color: rgba(#000, 0.9);
transform: translateY(4.6rem) translateX(-11rem);
&-text {
width: 60%;
padding-top: 1.2rem;
font-size: 1.2rem;
&:hover {
border-bottom: 1px solid #fff;
// border-width: 2%;
}
}
&--2 {
height: 10rem;
}
&-textOutside {
font-size: 1.2rem;
padding-bottom: 1rem;
}
&-text {
width: 60%;
padding-top: 1.2rem;
font-size: 1.2rem;
&--user {
height: 3rem;
&:hover {
border-bottom: 1px solid #fff;
// border-width: 2%;
}
}
&-textOutside {
font-size: 1.2rem;
padding-bottom: 1rem;
}
&--user {
height: 3rem;
width: 3rem;
border-radius: 5px;
background-color: rgb(64, 168, 228);
&-2 {
background-color: rgb(230, 145, 48);
}
&-3 {
background-color: rgb(123, 230, 96);
}
&-text {
width: 3rem;
border-radius: 5px;
background-color: rgb(64, 168, 228);
font-size: 1.2rem;
transform: translateX(4rem) translateY(-2rem);
&-2 {
background-color: rgb(230, 145, 48);
&:hover {
border-bottom: 1px solid #fff;
border-width: 100%;
}
&-3 {
background-color: rgb(123, 230, 96);
}
&-text {
width: 3rem;
font-size: 1.2rem;
transform: translateX(4rem) translateY(-2rem);
&:hover {
border-bottom: 1px solid #fff;
border-width: 100%;
}
}
}
}
}
}
.dropdownContainer {
height: 100%;
justify-content: center;
align-content: center;
height: 100%;
justify-content: center;
align-content: center;
&:hover .dropdownContent {
transition: all .4s;
opacity: 1;
visibility: visible;
}
&:hover .dropdownContent {
transition: all 0.4s;
opacity: 1;
visibility: visible;
}
}
.pseudo-link {
cursor: pointer;
@include responsive(tab_port) {
display: none;
}
}
cursor: pointer;
@include responsive(tab_port) {
display: none;
}
}

View File

@@ -1,98 +1,150 @@
import axios from '../../axios-movies';
import axios from '../../axios-movies'
export const FETCH_TRENDING = 'FETCH_TRENDING';
export const FETCH_NETFLIX_ORIGINALS = 'FETCH_NETFLIX_ORIGINALS';
export const FETCH_TOP_RATED = 'FETCH_TOP_RATED';
export const FETCH_ACTION_MOVIES = 'FETCH_ACTION_MOVIES';
export const FETCH_COMEDY_MOVIES = 'FETCH_COMEDY_MOVIES';
export const FETCH_HORROR_MOVIES = 'FETCH_HORROR_MOVIES';
export const FETCH_ROMANCE_MOVIES = 'FETCH_ROMANCE_MOVIES';
export const FETCH_DOCUMENTARIES = 'FETCH_DOCUMENTARIES';
export const FETCH_TRENDING = 'FETCH_TRENDING'
export const FETCH_NETFLIX_ORIGINALS = 'FETCH_NETFLIX_ORIGINALS'
export const FETCH_TOP_RATED = 'FETCH_TOP_RATED'
export const FETCH_ACTION_MOVIES = 'FETCH_ACTION_MOVIES'
export const FETCH_COMEDY_MOVIES = 'FETCH_COMEDY_MOVIES'
export const FETCH_HORROR_MOVIES = 'FETCH_HORROR_MOVIES'
export const FETCH_ROMANCE_MOVIES = 'FETCH_ROMANCE_MOVIES'
export const FETCH_DOCUMENTARIES = 'FETCH_DOCUMENTARIES'
// movie details
export const FETCH_MOVIE_DETAILS = 'FETCH_MOVIE_DETAILS'
export const FETCH_MOVIE_DETAILS_SUCCESS = 'FETCH_MOVIE_DETAILS_SUCCESS'
export const FETCH_MOVIE_DETAILS_FAIL = 'FETCH_MOVIE_DETAILS_FAIL'
// search
export const FETCH_SEARCH_MOVIE = 'FETCH_SEARCH_MOVIE'
export const FETCH_SEARCH_MOVIE_FAIL = 'FETCH_SEARCH_MOVIE_FAIL'
export const FETCH_SEARCH_MOVIE_SUCCESS = 'FETCH_SEARCH_MOVIE_SUCCESS'
export function fetchTrending() {
const request = axios.get(
`/trending/all/week?api_key=${process.env.API_KEY}&language=en-US`
);
return {
type: FETCH_TRENDING,
payload: request,
};
const media_type = {
tv: 'tv',
movie: 'movie',
}
export function fetchNetflixOriginals() {
const request = axios.get(
`/discover/tv?api_key=${process.env.API_KEY}&with_networks=213`
);
export const fetchMovieDetails = (mediaType, mediaId) => {
return async (dispatch) => {
try {
dispatch({ type: FETCH_MOVIE_DETAILS })
let urlPath
if (mediaType === media_type.movie)
urlPath = `/movie/${mediaId}?api_key=${process.env.API_KEY}`
if (mediaType === media_type.tv)
urlPath = `/tv/${mediaId}?api_key=${process.env.API_KEY}`
return {
type: FETCH_NETFLIX_ORIGINALS,
payload: request,
};
const request = await axios.get(urlPath)
dispatch({ type: FETCH_MOVIE_DETAILS_SUCCESS, payload: request })
} catch (error) {
console.log('error', error)
dispatch({ type: FETCH_MOVIE_DETAILS_FAIL })
}
}
}
export function fetchTopRated() {
const request = axios.get(
`/movie/top_rated?api_key=${process.env.API_KEY}&language=en-US`
);
return {
type: FETCH_TOP_RATED,
payload: request,
};
export const fetchSearchMovie = (searchTerm) => {
return async (dispatch) => {
try {
dispatch({ type: FETCH_SEARCH_MOVIE })
const request = await axios.get(
`/search/multi?api_key=${process.env.API_KEY}&language=en-US&include_adult=false&query=${searchTerm}`
)
dispatch({ type: FETCH_SEARCH_MOVIE_SUCCESS, payload: request })
} catch (error) {
dispatch({ type: FETCH_SEARCH_MOVIE_FAIL })
console.log('error', error)
}
}
}
export function fetchActionMovies() {
const request = axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=28`
);
export const fetchNetflixOriginals = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/tv?api_key=${process.env.API_KEY}&with_networks=213`
)
return {
type: FETCH_ACTION_MOVIES,
payload: request,
};
dispatch({ type: FETCH_NETFLIX_ORIGINALS, payload: request })
} catch (error) {
console.log('error', error)
}
}
}
export function fetchComedyMovies() {
const request = axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=35`
);
return {
type: FETCH_COMEDY_MOVIES,
payload: request,
};
export const fetchTrending = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/trending/all/week?api_key=${process.env.API_KEY}&language=en-US`
)
dispatch({ type: FETCH_TRENDING, payload: request })
} catch (error) {}
}
}
export function fetchHorrorMovies() {
const request = axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=27`
);
return {
type: FETCH_HORROR_MOVIES,
payload: request,
};
export const fetchTopRated = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/movie/top_rated?api_key=${process.env.API_KEY}&language=en-US`
)
dispatch({ type: FETCH_TOP_RATED, payload: request })
} catch (error) {}
}
}
export function fetchRomanceMovies() {
const request = axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=10749`
);
export const fetchActionMovies = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=28`
)
return {
type: FETCH_ROMANCE_MOVIES,
payload: request,
};
dispatch({ type: FETCH_ACTION_MOVIES, payload: request })
} catch (error) {}
}
}
export function fetchDocumentaries() {
const request = axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=99`
);
export const fetchComedyMovies = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=35`
)
return {
type: FETCH_DOCUMENTARIES,
payload: request,
};
dispatch({ type: FETCH_COMEDY_MOVIES, payload: request })
} catch (error) {}
}
}
export const fetchHorrorMovies = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=27`
)
dispatch({ type: FETCH_HORROR_MOVIES, payload: request })
} catch (error) {}
}
}
export const fetchRomanceMovies = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=10749`
)
dispatch({ type: FETCH_ROMANCE_MOVIES, payload: request })
} catch (error) {}
}
}
export const fetchDocumentaries = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=99`
)
dispatch({ type: FETCH_DOCUMENTARIES, payload: request })
} catch (error) {}
}
}

View File

@@ -1,12 +1,14 @@
import { combineReducers } from 'redux';
import TrendingReducer from './reducerTrending';
import NetflixOriginalsReducer from './reducerNetflixOriginals';
import TopRatedReducer from './reducerTopRated';
import ActionMoviesReducer from './reducerActionMovies';
import ComedyMoviesReducer from './reducerComedyMovies';
import HorrorMoviesReducer from './reducerHorrorMovies';
import RomanceMoviesReducer from './reducerRomanceMovies';
import DocumentaryReducer from './reducerDocumentary';
import { combineReducers } from 'redux'
import TrendingReducer from './reducerTrending'
import NetflixOriginalsReducer from './reducerNetflixOriginals'
import TopRatedReducer from './reducerTopRated'
import ActionMoviesReducer from './reducerActionMovies'
import ComedyMoviesReducer from './reducerComedyMovies'
import HorrorMoviesReducer from './reducerHorrorMovies'
import RomanceMoviesReducer from './reducerRomanceMovies'
import DocumentaryReducer from './reducerDocumentary'
import SearchMovieReducer from './reducerSearchMovie'
import MovieDetailsReducer from './reducerMovieDetails'
const rootReducer = combineReducers({
trending: TrendingReducer,
@@ -17,6 +19,8 @@ const rootReducer = combineReducers({
horror: HorrorMoviesReducer,
romance: RomanceMoviesReducer,
documentary: DocumentaryReducer,
});
searchMovie: SearchMovieReducer,
movieDetails: MovieDetailsReducer,
})
export default rootReducer;
export default rootReducer

View File

@@ -0,0 +1,24 @@
import {
FETCH_MOVIE_DETAILS,
FETCH_MOVIE_DETAILS_SUCCESS,
FETCH_MOVIE_DETAILS_FAIL,
} from '../actions/index'
const initialState = {
isLoading: false,
movieDetails: [],
}
export default function (state = initialState, action) {
switch (action.type) {
case FETCH_MOVIE_DETAILS:
return { ...state, isLoading: true }
case FETCH_MOVIE_DETAILS_FAIL:
return { ...state, isLoading: false }
case FETCH_MOVIE_DETAILS_SUCCESS:
const movieDetails = action.payload.data
return { ...state, movieDetails, isLoading: false }
default:
return state
}
}

View File

@@ -1,11 +1,11 @@
import { FETCH_NETFLIX_ORIGINALS } from '../actions/index';
import { FETCH_NETFLIX_ORIGINALS } from '../actions/index'
export default function (state = {}, action) {
switch (action.type) {
case FETCH_NETFLIX_ORIGINALS:
const data = action.payload.data.results;
return { ...state, data };
const data = action.payload.data.results
return { ...state, data }
default:
return state;
return state
}
}

View File

@@ -0,0 +1,24 @@
import {
FETCH_SEARCH_MOVIE,
FETCH_SEARCH_MOVIE_FAIL,
FETCH_SEARCH_MOVIE_SUCCESS,
} from '../actions/index'
const initialState = {
isLoading: false,
searchResults: [],
}
export default function (state = initialState, action) {
switch (action.type) {
case FETCH_SEARCH_MOVIE:
return { ...state, isLoading: true }
case FETCH_SEARCH_MOVIE_FAIL:
return { ...state, isLoading: false }
case FETCH_SEARCH_MOVIE_SUCCESS:
const searchResults = action.payload.data.results
return { ...state, searchResults, isLoading: false }
default:
return state
}
}

View File

@@ -1,7 +1,7 @@
const HtmlWebPackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const dotenv = require('dotenv');
const webpack = require('webpack');
var path = require('path');
@@ -42,7 +42,7 @@ module.exports = () => {
{
test: /\.css$/,
include: /node_modules/,
loaders: ['style-loader', 'css-loader'],
use: ['style-loader', 'css-loader'],
},
{
test: /\.scss$/,
@@ -76,7 +76,7 @@ module.exports = () => {
historyApiFallback: true,
},
node: {
fs: 'empty',
global: true,
},
plugins: [
new webpack.DefinePlugin(envKeys),
@@ -84,15 +84,23 @@ module.exports = () => {
template: './src/index.html',
filename: './index.html',
}),
new CopyWebpackPlugin([
{ from: 'src/static/images', to: 'static/images' },
]),
// new CopyWebpackPlugin({
// patterns: [[ { from: 'src/static/images', to: 'static/images' }],
// }),
new CopyWebpackPlugin({
patterns: [
{
from: 'src/static/images',
to: 'static/images',
},
],
}),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'main.css',
}),
new CleanWebpackPlugin(['dist']),
new CleanWebpackPlugin(),
],
};
};