Merge pull request #55 from devandres-tech/migrate-to-typescript

Migrate to typescript
This commit is contained in:
Andres Alcocer
2022-11-26 16:27:08 -05:00
committed by GitHub
52 changed files with 2471 additions and 3246 deletions

9
custom.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare module '*.svg' {
const content: any
export default content
}
declare module '*.png' {
const value: any
export = value
}

4402
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,18 +6,19 @@
"scripts": {
"build:dev": "webpack --mode development --config webpack.config.js",
"build:prod": "webpack --mode development",
"run:dev": "webpack serve --mode development --config webpack.config.js --open",
"run:prod": "webpack serve --mode production --open",
"start:dev": "webpack serve --mode development --config webpack.config.js --open",
"start:prod": "webpack serve --mode production --open",
"lint": "eslint --fix . && echo 'Lint complete.'"
},
"engines": {
"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": {
"@reduxjs/toolkit": "^1.9.0",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/react-redux": "^7.1.24",
"axios": "^1.2.0",
"dotenv": "^16.0.3",
"firebase": "^9.14.0",
@@ -31,7 +32,9 @@
"redux": "^4.2.0",
"redux-promise": "^0.6.0",
"redux-thunk": "^2.4.2",
"swiper": "^8.4.5"
"swiper": "^8.4.5",
"ts-loader": "^9.4.1",
"typescript": "^4.9.3"
},
"devDependencies": {
"@babel/core": "^7.20.2",
@@ -46,6 +49,7 @@
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.2",
"css-minimizer-webpack-plugin": "^4.2.2",
"eslint": "^8.28.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
@@ -54,22 +58,19 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^6.2.0",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"image-webpack-loader": "^8.1.0",
"mini-css-extract-plugin": "^2.7.0",
"node-sass": "^8.0.0",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"prettier": "^2.8.0",
"react-owl-carousel2": "^0.3.0",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"svg-inline-loader": "^0.8.2",
"svg-react-loader": "^0.4.6",
"tap-nirvana": "^1.1.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"terser-webpack-plugin": "^5.3.6",
"watch": "^1.0.2",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",

View File

@@ -1,8 +0,0 @@
import axios from 'axios';
/** base url to make requests to the the movie database */
const instance = axios.create({
baseURL: 'https://api.themoviedb.org/3',
});
export default instance;

8
src/axios-movies.ts Normal file
View File

@@ -0,0 +1,8 @@
import axios from 'axios'
/** base url to make requests to the movie database */
const instance = axios.create({
baseURL: 'https://api.themoviedb.org/3',
})
export default instance

View File

@@ -1,13 +1,27 @@
import React, { Component } from 'react'
import React from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper'
import { useViewport } from '../hooks/useViewport'
import { IMovieDetails } from '../store/slices/movieDetailsSlice'
interface IDisplayMovie {
title: string
isNetflixMovies?: boolean
movies: IMovieDetails[]
selectMovieHandler?: (movie: IMovieDetails) => void
}
// install Swiper components
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y])
const DisplayMovieRow = ({ title, isNetflixMovies, movies, selectMovieHandler }) => {
const DisplayMovieRow = ({
title,
isNetflixMovies,
movies,
selectMovieHandler,
}: IDisplayMovie) => {
const [windowDimensions] = useViewport()
const { width } = windowDimensions

View File

@@ -6,7 +6,12 @@ import MuteIcon from '../static/images/mute.svg'
import UnmuteIcon from '../static/images/unmute.svg'
import ReactPlayer from 'react-player'
const Header = ({ movie: { name, overview } }) => {
interface IHeader {
name: string
overview: string
}
const Header = ({ name, overview }: IHeader) => {
const [isMuted, setIsMuted] = useState(true)
return (
@@ -33,7 +38,6 @@ const Header = ({ movie: { name, overview } }) => {
<AddLogo className='header__container-btnMyList-add' />
My List
</button>
{isMuted ? (
<MuteIcon
onClick={() => setIsMuted(false)}

View File

@@ -1,83 +0,0 @@
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

@@ -0,0 +1,96 @@
import React, { useEffect } from 'react'
import * as actionMoviesSlice from '../store/slices/actionMovieSlice'
import * as movieDetailsSlice from '../store/slices/movieDetailsSlice'
import * as netflixOriginalsSlice from '../store/slices/netflixOriginalsSlice'
import * as trendingSlice from '../store/slices/trendingSlice'
import * as topRatedSlice from '../store/slices/topRatedSlice'
import * as comedySlice from '../store/slices/comedyMoviesSlice'
import * as documentarySlice from '../store/slices/documentarySlice'
import * as horrorMoviesSlice from '../store/slices/horrorMoviesSlice'
import * as romanceMoviesSlice from '../store/slices/romanceMoviesSlice'
import { useAppSelector, useAppDispatch } from '../store'
import Header from './Header'
import DisplayMovieRow from './DisplayMovieRow'
const MainContent = ({ selectMovieHandler }: { selectMovieHandler: any }) => {
const { movieDetails } = useAppSelector((state) => state.movieDetails)
const netflixOriginals = useAppSelector((state) => state.netflixOriginals)
const trending = useAppSelector((state) => state.trending)
const topRated = useAppSelector((state) => state.topRated)
const actionMoviesState = useAppSelector((state) => state.action)
const comedyMovies = useAppSelector((state) => state.comedy)
const horrorMovies = useAppSelector((state) => state.horror)
const romanceMovies = useAppSelector((state) => state.romance)
const documentaries = useAppSelector((state) => state.documentary)
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(
movieDetailsSlice.getMovieDetailsAsync({
mediaType: 'tv',
mediaId: '63351',
})
)
dispatch(netflixOriginalsSlice.getNetflixOriginalsAsync())
dispatch(trendingSlice.getTrendingAsync())
dispatch(topRatedSlice.getTopRatedAsync())
dispatch(actionMoviesSlice.getActionMoviesAsync())
dispatch(comedySlice.getComedyMoviesAsync())
dispatch(horrorMoviesSlice.getHorrorMoviesAsync())
dispatch(romanceMoviesSlice.getRomanceMoviesAsync())
dispatch(documentarySlice.getDocumentariesAsync())
}, [dispatch])
return (
<div className='container'>
<Header name={movieDetails.name} overview={movieDetails.overview} />
<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={actionMoviesState.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

35
src/components/Modal.tsx Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react'
interface IBackdrop {
toggleBackdrop?: () => void
show: boolean
}
interface IModal extends IBackdrop {
backgroundImage: string
children: JSX.Element
}
const Backdrop = ({ toggleBackdrop, show }: IBackdrop) =>
show ? <div onClick={toggleBackdrop} className='backdrop'></div> : null
const Modal = ({ show, toggleBackdrop, children, backgroundImage }: IModal) => {
const backgroundStyle = {
backgroundSize: 'cover',
backgroundImage: `url(https://image.tmdb.org/t/p/original/${backgroundImage})`,
}
return (
<div>
<Backdrop show={show} toggleBackdrop={toggleBackdrop} />
<div
style={backgroundStyle}
className={show ? 'modal show' : 'modal hide'}
>
{children}
</div>
</div>
)
}
export default Modal

View File

@@ -2,9 +2,10 @@ import React from 'react'
import AddIcon from '../static/images/add.svg'
import PlayIcon from '../static/images/play-button.svg'
import { IMovieDetails } from '../store/slices/movieDetailsSlice'
const MovieDetails = ({
movie: {
const MovieDetails = (props: IMovieDetails) => {
const {
title,
name,
vote_average,
@@ -15,8 +16,7 @@ const MovieDetails = ({
number_of_episodes,
number_of_seasons,
overview,
},
}) => {
} = props
return (
<div className='modal__container'>
<h1 className='modal__title'>{title || name}</h1>

View File

@@ -7,7 +7,7 @@ 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'
import DropdownContent from './DropdownContent'
const Navbar = () => {
const navigate = useNavigate()
@@ -16,7 +16,7 @@ const Navbar = () => {
const [scrollDimensions] = useScroll()
const { scrollY } = scrollDimensions
const onChange = async (event) => {
const onChange = async (event: any) => {
setUserInput(event.target.value)
}

View File

@@ -1,6 +0,0 @@
import React from 'react'
const backdrop = ({ toggleBackdrop, show }) =>
show ? <div onClick={toggleBackdrop} className='backdrop'></div> : null
export default backdrop

View File

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

View File

@@ -1,30 +0,0 @@
import MovieGenre from './components/MovieGenre';
import React from 'react';
export function getMovieRows(movies, url) {
const movieRow = movies.map((movie) => {
let movieImageUrl =
'https://image.tmdb.org/t/p/w500/' + movie.backdrop_path;
if (
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) {
const movieComponent = (
<MovieGenre
key={movie.id}
url={url}
posterUrl={movieImageUrl}
movie={movie}
/>
);
return movieComponent;
}
});
return movieRow;
}

View File

@@ -1,6 +1,5 @@
import { useDebounce } from './useDebounce'
import { useScroll } from './useScroll'
import { useViewport } from './useViewport'
import { useWithRouter } from './useWithRouter'
export { useDebounce, useScroll, useViewport, useWithRouter }
export { useDebounce, useScroll, useViewport }

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
export const useDebounce = (value, delay) => {
export const useDebounce = (value: string, delay: number) => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value)

View File

@@ -1,16 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css?family=Hind:400,500,700|Ramabhadra" rel="stylesheet">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://fonts.googleapis.com/css?family=Hind:400,500,700|Ramabhadra"
rel="stylesheet"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Netflix Clone</title>
</head>
</head>
<body>
<body>
<div id="app"></div>
</body>
</body>
</html>

View File

@@ -1,11 +1,9 @@
import React from 'react'
import * as ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'
import '@babel/polyfill'
import reducers from './store/reducers'
import { store } from './store'
import AppRouter from './AppRouter'
// Import Swiper styles
@@ -15,8 +13,6 @@ import 'swiper/css/pagination'
// Import main sass file to apply global styles
import './static/sass/style.scss'
const store = createStore(reducers, applyMiddleware(ReduxThunk))
const app = (
<Provider store={store}>
<AppRouter />

View File

@@ -1,14 +1,18 @@
import React, { useState } from 'react'
import MainContent from '../components/MainContent'
import Modal from '../components/UI/Modal'
import Modal from '../components/Modal'
import ModalMovieDetails from '../components/ModalMovieDetails'
import { IMovieDetails } from '../store/slices/movieDetailsSlice'
const Home = () => {
const [toggleModal, setToggleModal] = useState(false)
const [movieDetails, setMovieDetails] = useState({})
const [movieDetails, setMovieDetails] = useState<IMovieDetails>({
poster_path: '',
backdrop_path: '',
})
const selectMovieHandler = async (movie) => {
const selectMovieHandler = async (movie: IMovieDetails) => {
setToggleModal(true)
setMovieDetails(movie)
}
@@ -24,10 +28,10 @@ const Home = () => {
</div>
<Modal
show={toggleModal}
modalClosed={closeModal}
toggleBackdrop={closeModal}
backgroundImage={movieDetails.backdrop_path || movieDetails.poster_path}
>
<ModalMovieDetails movie={movieDetails} />
<ModalMovieDetails {...movieDetails} />
</Modal>
</>
)

View File

@@ -1,10 +1,18 @@
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { useAppSelector, useAppDispatch } from '../store'
import ModalMovieDetails from '../components/ModalMovieDetails'
import Modal from '../components/UI/Modal'
import Modal from '../components/Modal'
import { useDebounce } from '../hooks/useDebounce'
import * as movieActions from '../store/actions'
import * as searchSlice from '../store/slices/searchSlice'
import * as movieDetailsSlice from '../store/slices/movieDetailsSlice'
interface IMovie {
id: string
media_type?: string
backdrop_path?: string
}
// A custom hook that builds on useLocation to parse
// the query string for you.
@@ -16,13 +24,15 @@ 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()
const { searchResults, isLoading } = useAppSelector(
(state) => state.searchMovie
)
const { movieDetails } = useAppSelector((state) => state.movieDetails)
const dispatch = useAppDispatch()
useEffect(() => {
if (debouncedSearchTerm) {
dispatch(movieActions.fetchSearchMovie(debouncedSearchTerm))
dispatch(searchSlice.searchItemsAsync(debouncedSearchTerm))
}
}, [debouncedSearchTerm])
@@ -30,8 +40,13 @@ const Search = () => {
setIsToggleModal(false)
}
const onSelectMovieHandler = (movie) => {
dispatch(movieActions.fetchMovieDetails(movie.media_type, movie.id))
const onSelectMovieHandler = (movie: IMovie) => {
dispatch(
movieDetailsSlice.getMovieDetailsAsync({
mediaType: movie.media_type,
mediaId: movie.id,
})
)
setIsToggleModal(true)
}
@@ -39,7 +54,7 @@ const Search = () => {
return searchResults.length > 0 ? (
<>
<div className='search-container'>
{searchResults.map((movie) => {
{searchResults.map((movie: IMovie) => {
if (movie.backdrop_path !== null && movie.media_type !== 'person') {
const movieImageUrl =
'https://image.tmdb.org/t/p/w500' + movie.backdrop_path
@@ -58,12 +73,12 @@ const Search = () => {
</div>
<Modal
show={isToggleModal}
modalClosed={onCloseModalHandler}
toggleBackdrop={onCloseModalHandler}
backgroundImage={
movieDetails.backdrop_path || movieDetails.poster_path
}
>
<ModalMovieDetails movie={movieDetails} />
<ModalMovieDetails {...movieDetails} />
</Modal>
</>
) : (

View File

@@ -1,150 +0,0 @@
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'
// 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'
const media_type = {
tv: 'tv',
movie: 'movie',
}
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}`
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 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 const fetchNetflixOriginals = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/tv?api_key=${process.env.API_KEY}&with_networks=213`
)
dispatch({ type: FETCH_NETFLIX_ORIGINALS, payload: request })
} catch (error) {
console.log('error', error)
}
}
}
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 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 const fetchActionMovies = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=28`
)
dispatch({ type: FETCH_ACTION_MOVIES, payload: request })
} catch (error) {}
}
}
export const fetchComedyMovies = () => {
return async (dispatch) => {
try {
const request = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=35`
)
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) {}
}
}

33
src/store/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import { configureStore } from '@reduxjs/toolkit'
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import TrendingReducer from './slices/trendingSlice'
import NetflixOriginalsReducer from './slices/netflixOriginalsSlice'
import TopRatedReducer from './slices/topRatedSlice'
import ActionMoviesReducer from './slices/actionMovieSlice'
import ComedyMoviesReducer from './slices/comedyMoviesSlice'
import HorrorMoviesReducer from './slices/horrorMoviesSlice'
import RomanceMoviesReducer from './slices/romanceMoviesSlice'
import DocumentaryReducer from './slices/documentarySlice'
import SearchMovieReducer from './slices/searchSlice'
import MovieDetailsReducer from './slices/movieDetailsSlice'
export const store = configureStore({
reducer: {
trending: TrendingReducer,
netflixOriginals: NetflixOriginalsReducer,
topRated: TopRatedReducer,
action: ActionMoviesReducer,
comedy: ComedyMoviesReducer,
horror: HorrorMoviesReducer,
romance: RomanceMoviesReducer,
searchMovie: SearchMovieReducer,
documentary: DocumentaryReducer,
movieDetails: MovieDetailsReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@@ -1,26 +0,0 @@
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,
netflixOriginals: NetflixOriginalsReducer,
topRated: TopRatedReducer,
action: ActionMoviesReducer,
comedy: ComedyMoviesReducer,
horror: HorrorMoviesReducer,
romance: RomanceMoviesReducer,
documentary: DocumentaryReducer,
searchMovie: SearchMovieReducer,
movieDetails: MovieDetailsReducer,
})
export default rootReducer

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
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 +0,0 @@
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 }
default:
return state
}
}

View File

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

View File

@@ -1,24 +0,0 @@
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,11 +0,0 @@
import { FETCH_TOP_RATED } from '../actions/index';
export default function (state = {}, action) {
switch (action.type) {
case FETCH_TOP_RATED:
const data = action.payload.data.results;
return { ...state, data };
default:
return state;
}
}

View File

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

View File

@@ -0,0 +1,33 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getActionMoviesAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('action/getActionMovies', async () => {
const response = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=28`
)
return response.data.results
})
const actionMovieSlice = createSlice({
name: 'actionMovie',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getActionMoviesAsync.fulfilled, (state, { payload }) => {
state.data = payload
})
},
})
export default actionMovieSlice.reducer

View File

@@ -0,0 +1,33 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getComedyMoviesAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('comedy/getComedyMovies', async () => {
const response = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=28`
)
return response.data.results
})
const comedyMovieSlice = createSlice({
name: 'comedyMovie',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getComedyMoviesAsync.fulfilled, (state, { payload }) => {
state.data = payload
})
},
})
export default comedyMovieSlice.reducer

View File

@@ -0,0 +1,33 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getDocumentariesAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('documentary/getDocumentaries', async () => {
const response = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=99`
)
return response.data.results
})
const documentarySlice = createSlice({
name: 'actionMovie',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getDocumentariesAsync.fulfilled, (state, { payload }) => {
state.data = payload
})
},
})
export default documentarySlice.reducer

View File

@@ -0,0 +1,33 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getHorrorMoviesAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('horror/getHorrorMovies', async () => {
const response = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=27`
)
return response.data.results
})
const horrorMoviesSlice = createSlice({
name: 'horrorMovies',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getHorrorMoviesAsync.fulfilled, (state, { payload }) => {
state.data = payload
})
},
})
export default horrorMoviesSlice.reducer

View File

@@ -0,0 +1,80 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
export interface IMovieDetails {
backdrop_path?: string
poster_path?: string
title?: any
name?: any
vote_average?: any
release_date?: any
first_air_date?: any
runtime?: any
episode_run_time?: any
number_of_episodes?: any
number_of_seasons?: any
overview?: any
}
interface IInitialState {
isLoading: boolean
movieDetails: IMovieDetails
}
const media_type = {
tv: 'tv',
movie: 'movie',
}
const initialState: IInitialState = {
isLoading: true,
movieDetails: {
backdrop_path: '',
poster_path: '',
title: '',
name: '',
vote_average: '',
release_date: '',
first_air_date: '',
runtime: '',
episode_run_time: '',
number_of_episodes: '',
number_of_seasons: '',
overview: '',
},
}
export const getMovieDetailsAsync = createAsyncThunk<
any,
{ mediaType: string; mediaId: string },
{ state: RootState }
>('movieDetails/getMovieDetailsAsync', async ({ mediaType, mediaId }) => {
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}`
const response = await axios.get(urlPath)
return response.data
})
const movieDetailsSlice = createSlice({
name: 'movieDetails',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getMovieDetailsAsync.fulfilled, (state, { payload }) => {
state.isLoading = false
console.log('state ', state)
console.log('payload', payload)
state.movieDetails = payload
})
builder.addCase(getMovieDetailsAsync.pending, (state) => {
state.isLoading = true
})
},
})
export default movieDetailsSlice.reducer

View File

@@ -0,0 +1,36 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getNetflixOriginalsAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('netflixOriginals/getNetflixOriginals', async () => {
const response = await axios.get(
`/discover/tv?api_key=${process.env.API_KEY}&with_networks=213`
)
return response.data.results
})
const netflixOriginalsSlice = createSlice({
name: 'netflixOriginals',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(
getNetflixOriginalsAsync.fulfilled,
(state, { payload }) => {
state.data = payload
}
)
},
})
export default netflixOriginalsSlice.reducer

View File

@@ -0,0 +1,33 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getRomanceMoviesAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('romance/getRomanceMovies', async () => {
const response = await axios.get(
`/discover/movie?api_key=${process.env.API_KEY}&with_genres=28`
)
return response.data.results
})
const romanceMovieSlice = createSlice({
name: 'romanceMovie',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getRomanceMoviesAsync.fulfilled, (state, { payload }) => {
state.data = payload
})
},
})
export default romanceMovieSlice.reducer

View File

@@ -0,0 +1,37 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
const initialState = {
searchResults: [{}],
isLoading: true,
}
export const searchItemsAsync = createAsyncThunk<
any,
string,
{ state: RootState }
>('search/getSearchItems', async (searchTerm) => {
const response = await axios.get(
`/search/multi?api_key=${process.env.API_KEY}&language=en-US&include_adult=false&query=${searchTerm}`
)
return response.data.results
})
const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(searchItemsAsync.fulfilled, (state, { payload }) => {
state.isLoading = false
state.searchResults = payload
})
builder.addCase(searchItemsAsync.pending, (state) => {
state.isLoading = true
})
},
})
export default searchSlice.reducer

View File

@@ -0,0 +1,33 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getTopRatedAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('topRated/getTopRated', async () => {
const response = await axios.get(
`/movie/top_rated?api_key=${process.env.API_KEY}&language=en-US`
)
return response.data.results
})
const trendingSlice = createSlice({
name: 'topRated',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getTopRatedAsync.fulfilled, (state, { payload }) => {
state.data = payload
})
},
})
export default trendingSlice.reducer

View File

@@ -0,0 +1,33 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from '../../axios-movies'
import { RootState } from '../index'
import { IMovieDetails } from './movieDetailsSlice'
const initialState: { data: IMovieDetails[] } = {
data: [],
}
export const getTrendingAsync = createAsyncThunk<
any,
void,
{ state: RootState }
>('trending/getTrending', async () => {
const response = await axios.get(
`/trending/all/week?api_key=${process.env.API_KEY}&language=en-US`
)
return response.data.results
})
const trendingSlice = createSlice({
name: 'trending',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getTrendingAsync.fulfilled, (state, { payload }) => {
state.data = payload
})
},
})
export default trendingSlice.reducer

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"sourceMap": true,
"allowJs": true,
"moduleResolution": "node",
"typeRoots": ["custom.d.ts", "node_modules/@types"],
"allowSyntheticDefaultImports": true
},
"files": ["custom.d.ts"],
"include": ["src/", "custom.d.ts"]
}

View File

@@ -1,9 +1,12 @@
const HtmlWebPackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const dotenv = require('dotenv')
const webpack = require('webpack')
const prod =
(process.env.NODE_ENV ? process.env.NODE_ENV : '').trim() === 'production'
const path = require('path')
@@ -20,9 +23,13 @@ module.exports = () => {
path: path.resolve(__dirname, 'dist'),
clean: true,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
optimization: {
runtimeChunk: 'single',
moduleIds: 'deterministic',
minimizer: [`...`, new CssMinimizerPlugin()],
splitChunks: {
cacheGroups: {
vendor: {
@@ -30,61 +37,18 @@ module.exports = () => {
name: 'vendors',
chunks: 'all',
},
styles: {
name: 'styles',
type: 'css/mini-extract',
chunks: 'all',
enforce: true,
},
},
},
},
mode: prod ? 'production' : 'development',
// Enable sourcemaps for debugging webpack's output.
devtool: prod ? 'none' : 'eval-source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.svg$/,
exclude: /node_modules/,
use: {
loader: 'svg-react-loader',
},
},
{
test: /\.css$/,
include: /node_modules/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
},
},
'css-loader',
'sass-loader',
],
},
{
test: /\.(gif|png|jpe?g)$/i,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true, // webpack@1.x
disable: true, // webpack@2.x and newer
},
},
],
},
],
},
devServer: {
historyApiFallback: true,
},
@@ -108,9 +72,62 @@ module.exports = () => {
],
}),
new MiniCssExtractPlugin({
filename: 'main.css',
filename: '[name].css',
}),
new CleanWebpackPlugin(),
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.svg$/,
exclude: /node_modules/,
use: {
loader: 'svg-react-loader',
},
},
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
},
},
'css-loader',
'sass-loader',
],
},
{
test: /\.(gif|png|jpe?g)$/i,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
disable: true,
},
},
],
},
],
},
}
}