├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── client
├── package-lock.json
├── package.json
├── public
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── index.html
│ ├── mstile-150x150.png
│ └── site.webmanifest
└── src
│ ├── App.css
│ ├── App.js
│ ├── Routes.js
│ ├── actions
│ ├── browseActions.js
│ ├── userActions.js
│ └── watchActions.js
│ ├── assets
│ ├── fonts
│ │ └── Montserrat
│ │ │ ├── Montserrat-Black.ttf
│ │ │ ├── Montserrat-BlackItalic.ttf
│ │ │ ├── Montserrat-Bold.ttf
│ │ │ ├── Montserrat-BoldItalic.ttf
│ │ │ ├── Montserrat-ExtraBold.ttf
│ │ │ ├── Montserrat-ExtraBoldItalic.ttf
│ │ │ ├── Montserrat-ExtraLight.ttf
│ │ │ ├── Montserrat-ExtraLightItalic.ttf
│ │ │ ├── Montserrat-Italic.ttf
│ │ │ ├── Montserrat-Light.ttf
│ │ │ ├── Montserrat-LightItalic.ttf
│ │ │ ├── Montserrat-Medium.ttf
│ │ │ ├── Montserrat-MediumItalic.ttf
│ │ │ ├── Montserrat-Regular.ttf
│ │ │ ├── Montserrat-SemiBold.ttf
│ │ │ ├── Montserrat-SemiBoldItalic.ttf
│ │ │ ├── Montserrat-Thin.ttf
│ │ │ ├── Montserrat-ThinItalic.ttf
│ │ │ └── OFL.txt
│ ├── images
│ │ ├── LOGO.psd
│ │ ├── google.svg
│ │ ├── imdb.png
│ │ ├── logo-min.png
│ │ ├── logo.png
│ │ └── pdp.png
│ └── theme.js
│ ├── components
│ ├── Banner
│ │ ├── Banner.js
│ │ └── index.js
│ ├── Footer
│ │ ├── Footer.js
│ │ └── index.js
│ ├── ImdbRating
│ │ ├── index.js
│ │ └── style.css
│ ├── Loader
│ │ ├── Loader.js
│ │ └── index.js
│ ├── Modal
│ │ ├── Modal.js
│ │ └── index.js
│ ├── MovieCard
│ │ ├── index.js
│ │ └── style.css
│ ├── Navbar
│ │ ├── Logo.js
│ │ ├── Menu.js
│ │ ├── Navbar.js
│ │ ├── NotificationsMenu.js
│ │ ├── ProfileMenu.js
│ │ └── index.js
│ ├── PrivateRoute
│ │ ├── PrivateRoute.js
│ │ └── index.js
│ ├── Row
│ │ ├── index.js
│ │ └── style.css
│ ├── ScrollToTop.js
│ ├── SearchBar
│ │ ├── SearchBar.js
│ │ └── index.js
│ ├── Slider
│ │ ├── Slider.js
│ │ ├── index.js
│ │ └── style.css
│ └── VideoPlayer
│ │ ├── VideoPlayer.js
│ │ ├── index.js
│ │ ├── spinner.scss
│ │ └── style.css
│ ├── constants
│ ├── actionTypes.js
│ ├── api.js
│ └── routes.js
│ ├── containers
│ └── MainContainer.js
│ ├── index.js
│ ├── pages
│ ├── Browse
│ │ ├── Browse.js
│ │ └── index.js
│ ├── BrowseMovies
│ │ ├── BrowseMovies.js
│ │ └── index.js
│ ├── BrowseTVShows
│ │ ├── BrowseTVShows.js
│ │ ├── index.js
│ │ └── style.css
│ ├── Home
│ │ ├── Home.js
│ │ └── index.js
│ ├── Movie
│ │ ├── Cast.js
│ │ ├── Movie.js
│ │ ├── MovieDetails.js
│ │ ├── Trailers.js
│ │ └── index.js
│ ├── MyList
│ │ ├── MyList.js
│ │ └── index.js
│ ├── NotFound
│ │ ├── NotFound.js
│ │ └── index.js
│ ├── SearchContent
│ │ ├── index.js
│ │ └── style.css
│ ├── SignIn
│ │ ├── SignIn.js
│ │ └── index.js
│ ├── SignUp
│ │ ├── Signup.js
│ │ └── index.js
│ └── TVShow
│ │ ├── Cast.js
│ │ ├── Episodes.js
│ │ ├── Modal.js
│ │ ├── TVShow.js
│ │ ├── TVShowDetails.js
│ │ ├── Trailers.js
│ │ └── index.js
│ ├── reducers
│ ├── browseReducer.js
│ ├── index.js
│ ├── userReducer.js
│ └── watchReducer.js
│ ├── requests.js
│ └── store
│ └── index.js
├── package-lock.json
├── package.json
├── screenshots
├── browse.png
├── connexion.png
├── inscription.png
├── movie.png
├── player.png
└── tvshow.png
└── server
├── assets
└── captions
│ ├── 508943Arabic.vtt
│ ├── 508943English.vtt
│ ├── 508943French.vtt
│ ├── 51876Arabic.vtt
│ ├── 51876English.vtt
│ ├── 51876French.vtt
│ ├── 588228Arabic.vtt
│ ├── 588228English.vtt
│ └── 588228French.vtt
├── config
├── connectDB.js
├── env.js
├── index.js
└── passport.js
├── controllers
├── auth.controller.js
├── browse.controller.js
├── movie.controller.js
└── tvshow.controller.js
├── index.js
├── middlewares
├── auth.js
└── validator.js
├── models
├── movie.model.js
└── user.model.js
├── routes
├── auth.routes.js
├── browse.routes.js
├── movie.routes.js
└── tvshow.routes.js
└── utils
├── captions.utils.js
├── movie.utils.js
├── requests.js
└── tvshow.utils.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | **/.env
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Mehdi Ben Hadj Ali
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # X-Netflix
2 | X-Netflix is a streaming platform based on Netflix UI: built with ReactJS in frontend and nodeJS in backend.
3 |
4 | ## Built with
5 |
6 | FrontEnd: React.JS, Redux Library, Material UI, CSS
7 | Backend: Node.JS, Express.JS, Passportjs
8 | Database:MongoDB, Mongoose
9 |
10 |
11 | ## UI
12 |
13 | ### Home
14 |
15 | 
16 |
17 | ### Movie Page
18 |
19 | 
20 |
21 | ### TV Show Page
22 |
23 | 
24 |
25 | ### Player
26 |
27 | 
28 |
29 | ### Sign in
30 |
31 | 
32 |
33 | ### Sign up
34 |
35 | 
36 |
37 |
38 |
Installation
39 |
40 | Use the package manager [npm](https://www.npmjs.com/) to install X-Netflix.
41 | Setup the project and install the packages by running:
42 | ```bash
43 | npm run setup
44 | ```
45 | Run project with command
46 |
47 | ```bash
48 | npm run dev
49 | ```
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tunflixv2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:4444",
6 | "dependencies": {
7 | "@material-ui/core": "^4.11.4",
8 | "@material-ui/icons": "^4.11.2",
9 | "@react-spring/web": "^9.2.3",
10 | "@silvermine/videojs-quality-selector": "^1.2.5",
11 | "@testing-library/jest-dom": "^5.13.0",
12 | "@testing-library/react": "^11.2.7",
13 | "@testing-library/user-event": "^12.8.3",
14 | "@videojs/themes": "^1.0.1",
15 | "axios": "^0.21.1",
16 | "formik": "^2.2.9",
17 | "js-cookie": "^2.2.1",
18 | "node-sass": "^6.0.1",
19 | "normalize.css": "^8.0.1",
20 | "plyr": "^3.6.8",
21 | "react": "^17.0.2",
22 | "react-dom": "^17.0.2",
23 | "react-redux": "^7.2.4",
24 | "react-router-dom": "^5.2.0",
25 | "react-scripts": "4.0.3",
26 | "react-select": "^4.3.1",
27 | "redux": "^4.1.0",
28 | "redux-thunk": "^2.3.0",
29 | "styled-components": "^5.3.0",
30 | "swiper": "^6.7.5",
31 | "video.js": "^7.13.3",
32 | "videojs-contrib-quality-levels": "^2.1.0",
33 | "videojs-hls-quality-selector": "^1.1.4",
34 | "web-vitals": "^1.1.2",
35 | "yup": "^0.32.9"
36 | },
37 | "scripts": {
38 | "start": "react-scripts start",
39 | "build": "react-scripts build",
40 | "test": "react-scripts test",
41 | "eject": "react-scripts eject"
42 | },
43 | "eslintConfig": {
44 | "extends": [
45 | "react-app",
46 | "react-app/jest"
47 | ]
48 | },
49 | "browserslist": {
50 | "production": [
51 | ">0.2%",
52 | "not dead",
53 | "not op_mini all"
54 | ],
55 | "development": [
56 | "last 1 chrome version",
57 | "last 1 firefox version",
58 | "last 1 safari version"
59 | ]
60 | },
61 | "devDependencies": {
62 | "eslint": "^7.28.0",
63 | "prettier": "^2.3.1"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/client/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/client/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/client/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/public/favicon-16x16.png
--------------------------------------------------------------------------------
/client/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/public/favicon-32x32.png
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 | X-Netflix - Regardez vos films et séries préférés
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/client/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/public/mstile-150x150.png
--------------------------------------------------------------------------------
/client/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400&display=swap');
2 | @import url('https://fonts.googleapis.com/css2?family=Fjalla+One&display=swap');
3 |
4 | a {
5 | text-decoration: none;
6 | cursor:pointer;
7 | }
8 | a:hover{
9 | text-decoration: underline;
10 | }
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import "./App.css";
4 | import { ThemeProvider } from "@material-ui/styles";
5 | import theme from "./assets/theme";
6 | import CssBaseline from "@material-ui/core/CssBaseline";
7 | import ScrollToTop from "./components/ScrollToTop";
8 | import { BrowserRouter as Router, Route } from "react-router-dom";
9 | import { getAuthUser } from "./actions/userActions";
10 | import Cookies from "js-cookie";
11 | import Routes from "./Routes";
12 | import Loader from "./components/Loader";
13 |
14 | function App() {
15 | const isLoading = useSelector((state) => state.userReducer.isLoading);
16 | const darkMode = useSelector((state) => state.userReducer.darkMode);
17 | const dispatch = useDispatch();
18 | useEffect(() => {
19 | if (window.location.hash === "#_=_") window.location.hash = "";
20 | const cookieJwt = Cookies.get("auth-cookie");
21 | if (cookieJwt) {
22 | Cookies.remove("auth-cookie");
23 | localStorage.setItem("token", cookieJwt);
24 | }
25 | dispatch(getAuthUser());
26 | }, [dispatch]);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | {isLoading ? : }
34 |
35 |
36 | );
37 | }
38 |
39 | export default App;
40 |
--------------------------------------------------------------------------------
/client/src/Routes.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy } from "react";
2 | import Navbar from "./components/Navbar";
3 | import PrivateRoute from "./components/PrivateRoute";
4 | import { Switch, Route, useLocation } from "react-router-dom";
5 | import { HOME, BROWSE, SIGN_IN, SIGN_UP, MOVIES, TV_SHOWS, MY_LIST } from "./constants/routes";
6 | import { useSelector } from "react-redux";
7 | import Footer from './components/Footer'
8 | import Home from './pages/Home';
9 | import Browse from './pages/Browse';
10 | import SignIn from './pages/SignIn';
11 | import SignUp from './pages/SignUp';
12 | import NotFound from './pages/NotFound';
13 | import BrowseMovies from './pages/BrowseMovies';
14 | import BrowseTVShows from './pages/BrowseTVShows';
15 | import MyList from './pages/MyList';
16 | import SearchContent from './pages/SearchContent';
17 | import Movie from './pages/Movie';
18 | import TVShow from './pages/TVShow';
19 |
20 |
21 | const Routes = () => {
22 | const location = useLocation();
23 | const isAuth = useSelector((state) => state.userReducer.isAuth);
24 | return (
25 | <>
26 | {(location.pathname.slice(0, 7) === "/browse" || location.pathname.slice(0, 7) === "/search" || location.pathname.slice(0, 6) === "/movie" || location.pathname.slice(0,3) === "/tv") && isAuth && (
27 |
28 | )}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | >
44 | );
45 | };
46 |
47 | export default Routes;
48 |
--------------------------------------------------------------------------------
/client/src/actions/browseActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {
3 | BROWSE_HOME_LOAD,
4 | BROWSE_MOVIES_LOAD,
5 | BROWSE_TVSHOWS_LOAD,
6 | BROWSE_HOME_SUCCESS,
7 | BROWSE_MOVIES_SUCCESS,
8 | BROWSE_TVSHOWS_SUCCESS,
9 | BROWSE_HOME_FAIL,
10 | BROWSE_MOVIES_FAIL,
11 | BROWSE_TVSHOWS_FAIL
12 | } from "../constants/actionTypes";
13 | import { BROWSE_HOME, BROWSE_MOVIES, BROWSE_TVSHOWS } from "../constants/api";
14 |
15 | export const getBrowseHome = () => async (dispatch) => {
16 | try {
17 | dispatch({ type: BROWSE_HOME_LOAD });
18 | const results = await axios.get(BROWSE_HOME);
19 | dispatch({
20 | type: BROWSE_HOME_SUCCESS,
21 | payload: results.data,
22 | });
23 | } catch (error) {
24 | dispatch({
25 | type:BROWSE_HOME_FAIL,
26 | payload:error
27 | })
28 | }
29 | };
30 |
31 | export const getBrowseMovies = () => async (dispatch) => {
32 | try {
33 | dispatch({ type: BROWSE_MOVIES_LOAD });
34 | const results = await axios.get(BROWSE_MOVIES);
35 | dispatch({
36 | type: BROWSE_MOVIES_SUCCESS,
37 | payload: results.data,
38 | });
39 | } catch (error) {
40 | dispatch({
41 | type:BROWSE_MOVIES_FAIL,
42 | payload:error
43 | })
44 | }
45 | };
46 |
47 | export const getBrowseTVShows = () => async (dispatch) => {
48 | try {
49 | dispatch({ type: BROWSE_TVSHOWS_LOAD });
50 | const results = await axios.get(BROWSE_TVSHOWS);
51 | dispatch({
52 | type: BROWSE_TVSHOWS_SUCCESS,
53 | payload: results.data,
54 | });
55 | } catch (error) {
56 | dispatch({
57 | type:BROWSE_TVSHOWS_FAIL,
58 | payload:error
59 | })
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/client/src/actions/userActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {
3 | SIGN_IN_USER_SUCCESS,
4 | SIGN_UP_USER_SUCCESS,
5 | AUTH_FAIL,
6 | SET_LOADING,
7 | GET_AUTH_USER,
8 | SET_LOADING_LOCAL,
9 | SIGN_OUT_USER_SUCCESS,
10 | SET_DARK_MODE,
11 | } from "../constants/actionTypes";
12 | import { CURRENT_USER, SIGN_IN, SIGN_UP } from "../constants/api";
13 |
14 | export const getAuthUser = () => async (dispatch) => {
15 | axios.defaults.headers.common["Authorization"] = localStorage.getItem("token");
16 | dispatch({ type: SET_LOADING });
17 | try {
18 | const { data } = await axios.get(CURRENT_USER);
19 | dispatch({
20 | type: GET_AUTH_USER,
21 | payload: data,
22 | });
23 | const darkMode = localStorage.getItem("darkMode");
24 | if (darkMode !== null) {
25 | dispatch({
26 | type: SET_DARK_MODE,
27 | payload: darkMode === "true",
28 | });
29 | }
30 | } catch (error) {
31 | dispatch({ type: AUTH_FAIL, payload: null });
32 | }
33 | };
34 |
35 | export const signIn = (formData) => async (dispatch) => {
36 | try {
37 | dispatch({ type: SET_LOADING });
38 | dispatch({ type: SET_LOADING_LOCAL });
39 | const { data } = await axios.post(SIGN_IN, formData);
40 | localStorage.setItem("token", data.token);
41 | dispatch({ type: SIGN_IN_USER_SUCCESS, payload: data });
42 | } catch (error) {
43 | const res = error.response.data;
44 | dispatch({ type: AUTH_FAIL, payload: res });
45 | }
46 | };
47 |
48 | export const signUp = (formData) => async (dispatch) => {
49 | try {
50 | dispatch({ type: SET_LOADING });
51 | const { data } = await axios.post(SIGN_UP, formData);
52 | localStorage.setItem("token", data.token);
53 | dispatch({ type: SIGN_UP_USER_SUCCESS, payload: data });
54 | } catch (error) {
55 | const res = error.response.data;
56 | dispatch({ type: AUTH_FAIL, payload: res });
57 | }
58 | };
59 |
60 | export const signOut = () => async (dispatch) => {
61 | try {
62 | localStorage.removeItem("token");
63 | dispatch({ type: SIGN_OUT_USER_SUCCESS });
64 | } catch (error) {
65 | const res = error.response.data;
66 | dispatch({ type: AUTH_FAIL, payload: res });
67 | }
68 | };
69 |
70 | export const setDarkMode = () => async (dispatch, getState) => {
71 | const { darkMode } = getState().userReducer;
72 | localStorage.setItem("darkMode", !darkMode);
73 | dispatch({ type: SET_DARK_MODE, payload: !darkMode });
74 | };
75 |
--------------------------------------------------------------------------------
/client/src/actions/watchActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { GET_MEDIA_LOAD, GET_MEDIA_SUCCESS, GET_MEDIA_FAIL, HANDLE_MODAL_IS_OPEN } from "../constants/actionTypes";
3 | import { BASE_MOVIE, BASE_TVSHOW } from "../constants/api";
4 |
5 | export const getMedia = (id, mediaType) => async (dispatch) => {
6 | try {
7 | dispatch({ type: GET_MEDIA_LOAD });
8 | const results = await axios.get(`${mediaType === 'tv' ? BASE_TVSHOW : BASE_MOVIE}/${id}`);
9 | dispatch({
10 | type: GET_MEDIA_SUCCESS,
11 | payload: { media: results.data, mediaType },
12 | });
13 | } catch (error) {
14 | dispatch({
15 | type: GET_MEDIA_FAIL,
16 | payload: error,
17 | });
18 | }
19 | };
20 |
21 | export const handleModal = (bool) => (dispatch) => {
22 | dispatch({ type: HANDLE_MODAL_IS_OPEN, payload: bool });
23 | };
24 |
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-Black.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-BlackItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-Bold.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-BoldItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-ExtraBold.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-ExtraLight.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-Italic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-Light.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-LightItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-Medium.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-MediumItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-Regular.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-SemiBold.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-Thin.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/Montserrat-ThinItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/fonts/Montserrat/Montserrat-ThinItalic.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Montserrat/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/client/src/assets/images/LOGO.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/images/LOGO.psd
--------------------------------------------------------------------------------
/client/src/assets/images/google.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
51 |
--------------------------------------------------------------------------------
/client/src/assets/images/imdb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/images/imdb.png
--------------------------------------------------------------------------------
/client/src/assets/images/logo-min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/images/logo-min.png
--------------------------------------------------------------------------------
/client/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/images/logo.png
--------------------------------------------------------------------------------
/client/src/assets/images/pdp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/assets/images/pdp.png
--------------------------------------------------------------------------------
/client/src/assets/theme.js:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from "@material-ui/core/styles";
2 | import red from "@material-ui/core/colors/red";
3 | import grey from "@material-ui/core/colors/grey";
4 |
5 | const theme = (darkMode) => {
6 | return createMuiTheme({
7 | palette: {
8 | type: darkMode ? 'dark' : "light",
9 | primary: red,
10 | secondary: grey,
11 | background: {
12 | default: darkMode ? "#141414" : "white",
13 | },
14 | error: { main: "#e87c03" },
15 | text: { primary: "#fff", secondary: "rgb(150,150,150)", disabled: "#fff" },
16 | },
17 | typography: {
18 | fontFamily: ["Montserrat", "sans-serif"].join(","),
19 | },
20 | });
21 | };
22 |
23 | export default theme;
24 |
--------------------------------------------------------------------------------
/client/src/components/Banner/Banner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/styles";
3 | import ImdbRating from "../../components/ImdbRating";
4 | import Paper from "@material-ui/core/Paper";
5 | import Typography from "@material-ui/core/Typography";
6 | import Button from "@material-ui/core/Button";
7 | import PlayArrowIcon from "@material-ui/icons/PlayArrow";
8 | import AddIcon from "@material-ui/icons/Add";
9 | import { Link } from "react-router-dom";
10 |
11 | const Banner = ({ data, mediaType }) => {
12 | const classes = useStyles();
13 | return (
14 |
21 |
22 |
23 |
{mediaType === "movie" ? data.title : data.name}
24 |
25 |
26 | {data.genres.slice(0, 3).map((elem) => (
27 |
{elem.name}
28 | ))}
29 | {mediaType === 'tv' &&
{`${data.number_of_seasons} saison${data.number_of_seasons>1 ? 's' : ''}`}}
30 |
{data.year.slice(0,4)}
31 | {data.runtime &&
32 | {`${Math.floor(data.runtime / 60)} h ${data.runtime - Math.floor(data.runtime / 60) * 60} min`}{" "}
33 | }
34 |
35 |
36 | {data.overview.length > 150 ? `${data.overview.slice(0, 150)}...` : data.overview}
37 |
38 |
39 | }
44 | size="large"
45 | to={`/${mediaType}/${data.tmdb_id}`}
46 | >
47 | Lecture
48 |
49 | } size="large">
50 | Ma liste
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | const useStyles = makeStyles((theme) => ({
61 | paper: {
62 | position: "relative",
63 | height: "650px",
64 | backgroundSize: "cover",
65 | "&::after": {
66 | backgroundColor: "black",
67 | content: "",
68 | },
69 | },
70 | bannerContent: {
71 | position: "absolute",
72 | width: "100%",
73 | bottom: 0,
74 | },
75 | bannerDetails: {
76 | position: "absolute",
77 | bottom: "50px",
78 | margin: theme.spacing(10, 5),
79 | },
80 | title: {
81 | fontSize: "40px",
82 | letterSpacing: "2px",
83 | textShadow: "2px 2px 4px rgb(0 0 0 / 45%)",
84 | flexGrow: 1,
85 | marginBottom: theme.spacing(2),
86 | },
87 | desc: {
88 | display: "flex",
89 | alignItems: "center",
90 | marginBottom: theme.spacing(2),
91 | textShadow: "1px 1px 2px rgb(0 0 0 / 100%)",
92 | },
93 | infos: {
94 | padding: "5px 10px",
95 | marginRight: theme.spacing(1),
96 | },
97 | playBtn: {
98 | fontWeight: 700,
99 | marginRight: theme.spacing(2),
100 | "&:hover": {
101 | backgroundColor: "rgba(255, 255, 255, 0.75)",
102 | },
103 | textTransform: "capitalize",
104 | },
105 | listBtn: {
106 | backgroundColor: "rgba(109, 109, 110, 0.7)",
107 | fontWeight: 700,
108 | color: "#fff",
109 | "&:hover": {
110 | backgroundColor: "rgba(109, 109, 110, 0.4)",
111 | },
112 | textTransform: "capitalize",
113 | },
114 | overview: {
115 | textShadow: "1px 1px 2px rgb(0 0 0 / 100%)",
116 | fontSize: "18px",
117 | marginBottom: theme.spacing(2),
118 | maxWidth: "80%",
119 | },
120 | vignette: {
121 | background: "linear-gradient(180deg,transparent 10%,rgb(20, 20, 20))",
122 | width: "100%",
123 | height: "200px",
124 | },
125 | }));
126 |
127 | export default Banner;
128 |
--------------------------------------------------------------------------------
/client/src/components/Banner/index.js:
--------------------------------------------------------------------------------
1 | import Banner from './Banner'
2 |
3 | export default Banner
--------------------------------------------------------------------------------
/client/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FacebookIcon from "@material-ui/icons/Facebook";
3 | import InstagramIcon from "@material-ui/icons/Instagram";
4 | import Logo from "../../assets/images/logo.png";
5 | import TwitterIcon from "@material-ui/icons/Twitter";
6 | import Container from "@material-ui/core/Container";
7 | import Grid from "@material-ui/core/Grid";
8 | import Typography from "@material-ui/core/Typography";
9 | import IconButton from "@material-ui/core/iconButton";
10 | import { makeStyles } from "@material-ui/core/styles";
11 | import { Box, Link } from "@material-ui/core";
12 |
13 |
14 | const Footer = () => {
15 | const classes = useStyles();
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Mentions légales
36 | Qui sommes nous?
37 | Comment nous contacter?
38 | Notre vision
39 | Blog
40 |
41 |
42 |
43 | Mentions légales
44 | Mentions légales
45 | Centre d'aide
46 | Préférences de cookies
47 |
48 |
49 |
50 | Mentions légales
51 | Nous rejoindre
52 | Devenir développeur
53 |
54 |
55 |
56 |
57 |
58 | © 2021 Tunflix, Inc.
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | const useStyles = makeStyles((theme) => ({
66 | footer: {
67 | paddingTop: theme.spacing(8),
68 | },
69 | socialIcon: {
70 | color: "grey",
71 | cursor: "pointer",
72 | "&:hover": {
73 | color: "#fff",
74 | },
75 | transition: ".1s",
76 | marginRight: theme.spacing(1),
77 | },
78 | }));
79 |
80 | export default Footer;
81 |
--------------------------------------------------------------------------------
/client/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import Footer from './Footer'
2 | export default Footer
3 |
--------------------------------------------------------------------------------
/client/src/components/ImdbRating/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.css'
3 | import ImdbBackground from '../../assets/images/imdb.png'
4 |
5 | const ImdbRating = ({rating, ...rest}) => {
6 | return (
7 |
8 |
9 | {rating}
10 |
11 | )
12 | }
13 |
14 | export default ImdbRating
15 |
--------------------------------------------------------------------------------
/client/src/components/ImdbRating/style.css:
--------------------------------------------------------------------------------
1 | .imdb{
2 | position:relative;
3 | display: flex;
4 | align-items: center;
5 | }
6 | .imdb-img{
7 | width: 80px;
8 | }
9 |
10 | .imdb-rating{
11 | position:absolute;
12 | top:50%;
13 | transform:translateY(-50%);
14 | left:45px;
15 | width:40px;
16 | text-align: center;
17 | color:black;
18 | font-size: 17px;
19 | font-weight: 900;
20 | z-index: 10;
21 | }
--------------------------------------------------------------------------------
/client/src/components/Loader/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import CircularProgress from "@material-ui/core/CircularProgress";
4 | import { Helmet } from "react-helmet";
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | loaderContainer: {
8 | position: "relative",
9 | width: "100%",
10 | height: "100vh",
11 | },
12 | loader: {
13 | position: "absolute",
14 | top: "50%",
15 | left: "50%",
16 | transform: "translate(-50%,-50%)",
17 | },
18 | }));
19 |
20 | const Loader = () => {
21 | const classes = useStyles();
22 |
23 | return (
24 | <>
25 |
26 | Chargement...
27 |
28 |
33 | >
34 | );
35 | };
36 |
37 | export default Loader;
38 |
--------------------------------------------------------------------------------
/client/src/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | import Loader from "./Loader";
2 |
3 | export default Loader;
4 |
--------------------------------------------------------------------------------
/client/src/components/Modal/Modal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Backdrop from "@material-ui/core/Backdrop";
4 | import VideoPlayer from "../../components/VideoPlayer";
5 | import { useSpring, animated } from "@react-spring/web";
6 | import { Dialog } from "@material-ui/core";
7 |
8 | const Modal = ({ open, handleClose, videoLinks, tmdbId, title }) => {
9 | const classes = useStyles();
10 | const videoJsOptions = (urls) => {
11 | return {
12 | controlBar: {
13 | volumePanel: {
14 | inline: false,
15 | },
16 | },
17 | aspectRatio: "30:9",
18 | autoplay: true,
19 | controls: true,
20 | sources: [
21 | { label: 'fr', src: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", type: "video/mp4" },
22 | { label: 'en', src: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", type: "video/mp4" },
23 | ],
24 | tracks: [
25 | {
26 | kind: "captions",
27 | src: `/api/movie/subs/test/ar`,
28 | srclang: "ar",
29 | label: "Arabic",
30 | },
31 | {
32 | kind: "captions",
33 | src: `/api/movie/subs/test/fr`,
34 | srclang: "fr",
35 | label: "Français",
36 | },
37 | {
38 | kind: "captions",
39 | src: `/api/movie/subs/test/en`,
40 | srclang: "ar",
41 | label: "English",
42 | },
43 | ],
44 | };
45 | };
46 |
47 | return (
48 |
65 | );
66 | };
67 |
68 | const useStyles = makeStyles((theme) => ({
69 | modal: { maxHeight: "100vh" },
70 | }));
71 |
72 | const Fade = React.forwardRef(function Fade(props, ref) {
73 | const { in: open, children, onEnter, onExited, ...other } = props;
74 | const style = useSpring({
75 | from: { opacity: 0 },
76 | to: { opacity: open ? 1 : 0 },
77 | onStart: () => {
78 | if (open && onEnter) {
79 | onEnter();
80 | }
81 | },
82 | onRest: () => {
83 | if (!open && onExited) {
84 | onExited();
85 | }
86 | },
87 | });
88 |
89 | return (
90 |
91 | {children}
92 |
93 | );
94 | });
95 |
96 | export default Modal;
97 |
--------------------------------------------------------------------------------
/client/src/components/Modal/index.js:
--------------------------------------------------------------------------------
1 | import Modal from "./Modal"
2 |
3 | export default Modal
--------------------------------------------------------------------------------
/client/src/components/MovieCard/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./style.css";
3 | import { Link } from "react-router-dom";
4 |
5 | function MovieCard({ id, rating, img, title, mediaType }) {
6 | return (
7 |
8 |
9 | {/*
10 |
{title}
11 |

12 |
{title}
13 |
*/}
14 |

15 |
16 |
17 | );
18 | }
19 |
20 | export default MovieCard;
21 |
--------------------------------------------------------------------------------
/client/src/components/MovieCard/style.css:
--------------------------------------------------------------------------------
1 | .movie-card {
2 | background-color: #222;
3 | background-image : linear-gradient(rgba(0,0,0,0) ,#000);
4 | display: flex;
5 | transition-duration: .2s;
6 | height: 250px;
7 | border-radius: 4px !important;
8 | margin-right: 10px;
9 | }
10 |
11 | .movie-card-img{
12 | height:100%;
13 | }
14 |
15 | .movie-card:hover{
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/Logo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import LogoTunflix from "../../assets/images/logo.png";
3 | import {Link} from 'react-router-dom'
4 |
5 | const Logo = (props) => (
6 |
7 |
8 |
9 | );
10 |
11 | export default Logo;
12 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/Menu.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import { NavLink } from 'react-router-dom'
4 | import Button from "@material-ui/core/Button";
5 | import ButtonGroup from "@material-ui/core/ButtonGroup";
6 |
7 | const Menu = () => {
8 | const classes = useStyles();
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | const useStyles = makeStyles((theme) => ({
21 | navMenu: {
22 | flexGrow: 1,
23 | },
24 | navMenuItem :{
25 | '&:hover':{
26 | color: 'rgb(220,220,220)'
27 | }
28 | },
29 | active:{
30 | fontWeight: 700,
31 | '&:hover':{
32 | color: '#fff'
33 | }
34 | }
35 | }));
36 |
37 | export default Menu;
38 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/Navbar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useLocation } from "react-router-dom";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import SearchBar from "../SearchBar";
5 | import Logo from "./Logo";
6 | import AppBar from "@material-ui/core/AppBar";
7 | import Toolbar from "@material-ui/core/Toolbar";
8 | import Menu from "./Menu";
9 | import ProfileMenu from "./ProfileMenu";
10 | import NotificationsMenu from "./NotificationsMenu";
11 |
12 | const Navbar = () => {
13 | const classes = useStyles();
14 | const [navBackground, setNavBackground] = useState(false);
15 |
16 | const changeBackground = () => setNavBackground(window.scrollY > 20);
17 | window.addEventListener("scroll", changeBackground);
18 |
19 | return (
20 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | const useStyles = makeStyles((theme) => ({
41 | navbar: (props) => ({
42 | padding: theme.spacing(0, 3),
43 | backgroundImage: "linear-gradient(to bottom,rgba(0,0,0,.7) 10%,rgba(0,0,0,0))",
44 | transition: ".4s",
45 | }),
46 | logo: {
47 | width: "90px",
48 | marginRight: theme.spacing(2),
49 | },
50 | }));
51 |
52 | export default Navbar;
53 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/NotificationsMenu.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import NotificationsIcon from "@material-ui/icons/Notifications";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Menu from "@material-ui/core/Menu";
5 | import MenuItem from "@material-ui/core/MenuItem";
6 | import IconButton from "@material-ui/core/IconButton";
7 | import Badge from "@material-ui/core/Badge";
8 |
9 | const NotificationsMenu = () => {
10 | const notifications = [];
11 | const classes = useStyles();
12 | const [anchorEl, setAnchorEl] = useState(null);
13 | const handleClose = () => {
14 | setAnchorEl(null);
15 | };
16 | return (
17 | <>
18 | setAnchorEl(e.currentTarget)}>
19 |
20 |
21 |
22 |
23 |
47 | >
48 | );
49 | };
50 |
51 | const useStyles = makeStyles((theme) => ({
52 | profileMenu: {
53 | "& .MuiPaper-root": {
54 | backgroundColor: "rgba(0,0,0,.8)",
55 | border: "1px solid rgb(20,20,20)",
56 | },
57 | },
58 | menuItem: {
59 | "&:hover": {
60 | backgroundColor: "rgb(25,25,25)",
61 | },
62 | borderBottom: "1px solid rgb(40,40,40)",
63 | },
64 | }));
65 |
66 | export default NotificationsMenu;
67 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/ProfileMenu.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { useHistory } from "react-router-dom";
4 | import { signOut as signOutUser } from "../../actions/userActions";
5 |
6 | import { makeStyles } from "@material-ui/core/styles";
7 | import FormControlLabel from "@material-ui/core/FormControlLabel";
8 | import Switch from "@material-ui/core/Switch";
9 | import Menu from "@material-ui/core/Menu";
10 | import MenuItem from "@material-ui/core/MenuItem";
11 | import Button from "@material-ui/core/Button";
12 | import KeyboardArrowDownSharpIcon from "@material-ui/icons/KeyboardArrowDownSharp";
13 | import { setDarkMode } from "../../actions/userActions";
14 |
15 | const ProfileMenu = () => {
16 | const dispatch = useDispatch();
17 | const classes = useStyles();
18 | const history = useHistory();
19 | const darkMode = useSelector((state) => state.userReducer.darkMode);
20 | const avatar = useSelector((state) => state.userReducer.user.avatar);
21 | const [anchorEl, setAnchorEl] = useState(null);
22 | const handleClose = () => {
23 | setAnchorEl(null);
24 | };
25 | const signOut = () => {
26 | handleClose();
27 | dispatch(signOutUser());
28 | history.push("/signin");
29 | };
30 | return (
31 |
32 |
45 |
86 |
87 | );
88 | };
89 |
90 | const useStyles = makeStyles((theme) => ({
91 | profileMenu: {
92 | "& .MuiPaper-root": {
93 | backgroundColor: "rgba(0,0,0,.9)",
94 | border: "1px solid rgb(20,20,20)",
95 | },
96 | },
97 | menuItem: {
98 | "&:hover": {
99 | textDecoration: "underline",
100 | },
101 | },
102 | track: {
103 | backgroundColor: "grey",
104 | opacity: 1,
105 | },
106 | }));
107 |
108 | export default ProfileMenu;
109 |
--------------------------------------------------------------------------------
/client/src/components/Navbar/index.js:
--------------------------------------------------------------------------------
1 | import Navbar from './Navbar'
2 | export default Navbar
--------------------------------------------------------------------------------
/client/src/components/PrivateRoute/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import { Route, Redirect } from "react-router-dom";
4 |
5 | const PrivateRoute = ({ path, component: Component, ...rest }) => {
6 | const isAuth = useSelector((state) => state.userReducer.isAuth);
7 | const isLoading = useSelector((state) => state.userReducer.isLoading);
8 | if (isLoading) {
9 | return loading...
;
10 | } else {
11 | if (!isAuth) {
12 | return ;
13 | } else {
14 | return ;
15 | }
16 | }
17 | };
18 |
19 | export default PrivateRoute;
20 |
--------------------------------------------------------------------------------
/client/src/components/PrivateRoute/index.js:
--------------------------------------------------------------------------------
1 | import PrivateRoute from './PrivateRoute'
2 |
3 | export default PrivateRoute
--------------------------------------------------------------------------------
/client/src/components/Row/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import MovieCard from "../MovieCard";
3 | import "./style.css";
4 |
5 | const Row = ({ title, data, wrap }) => {
6 | const wrapOptions = {flexWrap:"wrap",margin:"0 20px",justifyContent:'space-between'}
7 | return (
8 |
9 |
{title}
10 |
11 | {data
12 | .filter((elem) => elem.poster_path != null)
13 | .map((movie) => (
14 |
22 | ))}
23 |
24 |
25 | );
26 | };
27 |
28 | export default Row;
29 |
--------------------------------------------------------------------------------
/client/src/components/Row/style.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300&display=swap");
2 |
3 | .row-title{
4 | color: white;
5 | font-size: 30px;
6 | margin-bottom: 20px;
7 | font-family: "Noto Sans JP", sans-serif;
8 | margin: 0 30px;
9 | letter-spacing: 8px;
10 | }
11 |
12 | .row-posters {
13 | padding-top: 30px;
14 | padding-bottom: 100px;
15 | display: flex;
16 | overflow-x: scroll;
17 | flex-direction: row;
18 | justify-content: flex-start;
19 | align-items: center;
20 | }
21 |
22 | .row-posters::-webkit-scrollbar {
23 | display: none;
24 | }
25 |
26 |
27 | @media screen and (max-width:500px) {
28 | .row-title {
29 | font-size: 25px;
30 | }
31 | }
32 |
33 | @media screen and (max-width:420px) {
34 | .row-title {
35 | margin: 0 20px;
36 | font-size: 23px;
37 | }
38 | }
--------------------------------------------------------------------------------
/client/src/components/ScrollToTop.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useLocation } from "react-router-dom";
3 |
4 | const ScrollToTop = () => {
5 | const { pathname } = useLocation();
6 |
7 | useEffect(() => {
8 | window.scrollTo(0, 0);
9 | }, [pathname]);
10 |
11 | return null;
12 | }
13 |
14 | export default ScrollToTop
--------------------------------------------------------------------------------
/client/src/components/SearchBar/SearchBar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { useHistory, useLocation } from "react-router-dom";
3 | import InputBase from "@material-ui/core/InputBase";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import SearchIcon from "@material-ui/icons/Search";
6 |
7 |
8 | const SearchBar = (props) => {
9 | const [searchInput, setSearchInput] = useState("");
10 | const classes = useStyles({searchInput});
11 | const history = useHistory();
12 |
13 | useEffect(()=>{
14 | if (searchInput !== "") {
15 | history.push(`/search/${searchInput}`);
16 | }
17 | },[searchInput])
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
setSearchInput(e.target.value)}
33 | />
34 |
35 | );
36 | };
37 |
38 |
39 | const useStyles = makeStyles((theme) => ({
40 | search: {
41 | position: "relative",
42 | borderRadius: 0,
43 | backgroundColor: theme.palette.background,
44 | "&:focus-within": {
45 | backgroundColor: "rgba(0,0,0,1)",
46 | border: '1px solid white'
47 | },
48 | marginLeft: 0,
49 | width: "100%",
50 | [theme.breakpoints.up("sm")]: {
51 | marginLeft: theme.spacing(1),
52 | width: "auto",
53 | },
54 | transition: theme.transitions.create("background-color"),
55 | },
56 | searchIcon: {
57 | padding: theme.spacing(0, 2),
58 | height: "100%",
59 | position: "absolute",
60 | display: "flex",
61 | alignItems: "center",
62 | justifyContent: "center",
63 | color:"white",
64 | },
65 | inputRoot: {
66 | color: "white",
67 |
68 | },
69 | inputInput: props => ({
70 | cursor:'pointer',
71 | padding: theme.spacing(1, 1, 1, 0),
72 | fontSize:"14px",
73 | paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
74 | transition: theme.transitions.create("width"),
75 | width: "100%",
76 | [theme.breakpoints.up("sm")]: {
77 | width: props.searchInput.length > 0 ? "150px" : "0px",
78 | "&:focus": {
79 | width: "150px",
80 | },
81 | },
82 | "&:focus-within": {
83 | cursor : 'text'
84 | }
85 | }),
86 | }));
87 |
88 | export default SearchBar;
89 |
--------------------------------------------------------------------------------
/client/src/components/SearchBar/index.js:
--------------------------------------------------------------------------------
1 | import SearchBar from './SearchBar'
2 |
3 | export default SearchBar
--------------------------------------------------------------------------------
/client/src/components/Slider/Slider.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SwiperCore, { Navigation } from "swiper";
3 | import { Swiper, SwiperSlide } from "swiper/react";
4 |
5 | import "./style.css";
6 | import "swiper/swiper.scss";
7 | import "swiper/components/navigation/navigation.scss";
8 |
9 | SwiperCore.use([Navigation]);
10 |
11 | const Slider = ({ slides }) => {
12 | return (
13 |
14 |
47 | {slides.map((elem) => (
48 | {elem}
49 | ))}
50 |
51 |
52 | );
53 | };
54 |
55 | export default Slider;
56 |
--------------------------------------------------------------------------------
/client/src/components/Slider/index.js:
--------------------------------------------------------------------------------
1 | import Slider from './Slider'
2 |
3 | export default Slider
--------------------------------------------------------------------------------
/client/src/components/Slider/style.css:
--------------------------------------------------------------------------------
1 | .swiper-container{
2 | padding:50px 50px !important;
3 | }
4 |
5 | .swiper-slide{
6 | height:auto !important;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/components/VideoPlayer/VideoPlayer.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import videojs from "video.js";
3 | import qualitySelector from "@silvermine/videojs-quality-selector";
4 | import "@silvermine/videojs-quality-selector/dist/css/quality-selector.css";
5 | import "video.js/dist/video-js.css"
6 | import "./style.css";
7 | import './spinner.scss'
8 |
9 | export const VideoPlayer = (props) => {
10 |
11 | qualitySelector(videojs);
12 | const videoRef = React.useRef(null);
13 | const { options, handleClose, title } = props;
14 | const VideoHtml = (props) => (
15 |
16 |
17 |
18 | );
19 |
20 | useEffect(() => {
21 | const videoElement = videoRef.current;
22 | let player;
23 | if (videoElement) {
24 | player = videojs(videoElement, options, () => {
25 | player.controlBar.addChild("QualitySelector");
26 | const backButton = player.addChild("Component");
27 | const arrowLeft = backButton.addChild("Component");
28 | const backButtonText = backButton.addChild("Component");
29 | const logo = player.controlBar.addChild("Component");
30 | const titleComponent = player.controlBar.addChild("Component");
31 |
32 | const titleDom = titleComponent.el();
33 | titleDom.innerHTML = title;
34 |
35 | logo.addClass("video-player-logo");
36 | titleComponent.addClass("video-player-title");
37 | arrowLeft.addClass("fas");
38 | arrowLeft.addClass("fa-arrow-left");
39 | arrowLeft.addClass("back-btn-icon");
40 | backButton.addClass("back-btn");
41 | backButtonText.addClass("back-btn-text");
42 |
43 | const backButtonTextDom = backButtonText.el();
44 | backButtonTextDom.innerHTML = "Retour à la navigation";
45 |
46 | const myButtonDom = backButton.el();
47 | myButtonDom.onclick = function () {
48 | handleClose();
49 | };
50 | });
51 | }
52 | return () => {
53 | console.log(player)
54 | if (player) {
55 | player.dispose();
56 | }
57 | };
58 | }, [options, handleClose]);
59 |
60 | return ;
61 | };
62 | export default VideoPlayer;
63 |
--------------------------------------------------------------------------------
/client/src/components/VideoPlayer/index.js:
--------------------------------------------------------------------------------
1 | import VideoPlayer from "./VideoPlayer";
2 |
3 | export default VideoPlayer
--------------------------------------------------------------------------------
/client/src/components/VideoPlayer/spinner.scss:
--------------------------------------------------------------------------------
1 | .vjs-loading-spinner {
2 | border: none;
3 | opacity: 0;
4 | visibility: hidden;
5 | animation: vjs-spinner-fade-out 2s linear 1;
6 | animation-delay: 2s;
7 | &:before,
8 | &:after {
9 | border: none;
10 | }
11 | &:after {
12 | background-image: url(https://assets.nflxext.com/en_us/pages/wiplayer/site-spinner.png);
13 | background-repeat: no-repeat;
14 | background-position-x: 50%;
15 | background-position-y: 50%;
16 | -moz-background-size: 100%;
17 | -o-background-size: 100%;
18 | background-size: 100%;
19 | }
20 | }
21 |
22 | .vjs-seeking .vjs-loading-spinner:after,
23 | .vjs-waiting .vjs-loading-spinner:after {
24 | animation: vjs-spinner-spin 1.1s linear infinite, vjs-spinner-fade 1.1s linear 1 !important;
25 | animation-delay: 2s;
26 | }
27 |
28 | .vjs-seeking .vjs-loading-spinner,
29 | .vjs-waiting .vjs-loading-spinner {
30 | opacity: 1;
31 | visibility: visible;
32 | animation: vjs-spinner-fade-in 2s linear 1;
33 | animation-delay: 2s;
34 | }
35 |
36 | @keyframes vjs-spinner-fade-in {
37 | 0% {
38 | opacity: 0;
39 | visibility: visible;
40 | }
41 | 100% {
42 | opacity: 1;
43 | visibility: visible;
44 | }
45 | }
46 |
47 | @keyframes vjs-spinner-fade-out {
48 | 0% {
49 | opacity: 1;
50 | visibility: visible;
51 | }
52 | 100% {
53 | opacity: 0;
54 | visibility: visible;
55 | }
56 | }
--------------------------------------------------------------------------------
/client/src/components/VideoPlayer/style.css:
--------------------------------------------------------------------------------
1 | /* video player to fit container */
2 |
3 | .video-js.vjs-fluid,
4 | .video-js.vjs-16-9,
5 | .video-js.vjs-4-3,
6 | .video-js.vjs-9-16,
7 | .video-js.vjs-1-1 {
8 | max-width: 100% !important;
9 | height: 100vh !important;
10 | max-height: 100vh !important;
11 | }
12 |
13 |
14 | /*** LOGO ***/
15 |
16 | .video-player-logo {
17 | background: url("https://i.ibb.co/cbTxkP0/logo.png") center center no-repeat;
18 | width: 80px;
19 | height: 100%;
20 | margin-right: 20px;
21 | background-size: contain;
22 | }
23 |
24 | @media screen and (max-width: 650px) {
25 | .video-player-logo {
26 | display: none;
27 | }
28 | }
29 |
30 | /***** TITLE ******/
31 | .vjs-theme-tunflix .video-player-title {
32 | text-align: center;
33 | }
34 |
35 | @media screen and (max-width: 500px) {
36 | .vjs-theme-tunflix .video-player-title {
37 | display: none;
38 | }
39 | .vjs-theme-tunflix .vjs-volume-panel {
40 | flex: 1;
41 | }
42 | }
43 |
44 | .back-btn {
45 | display: flex;
46 | align-items: center;
47 | font-size: 25px;
48 | cursor: pointer;
49 | position: absolute;
50 | top: 40px;
51 | left: 40px;
52 | transition: 0.3s;
53 | }
54 |
55 | .back-btn-icon {
56 | margin-right: 10px;
57 | }
58 |
59 | .back-btn-text {
60 | font-size: 15px;
61 | visibility: hidden;
62 | opacity: 0;
63 | transform: translate(-4px);
64 | transition: 0.3s;
65 | }
66 |
67 | .back-btn:hover .back-btn-text {
68 | visibility: visible;
69 | opacity: 1;
70 | transform: translate(0);
71 | }
72 |
73 | .back-btn:hover {
74 | transform: scale(1.1);
75 | }
76 |
77 | .vjs-user-inactive .back-btn {
78 | visibility: hidden;
79 | opacity: 0;
80 | }
81 |
82 | .vjs-theme-tunflix {
83 | --vjs-theme-tunflix--primary: red;
84 | --vjs-theme-tunflix--secondary: #fff;
85 | }
86 |
87 | .vjs-theme-tunflix .vjs-control-bar {
88 | height: 60px;
89 | padding: 10px 10px 0px 10px;
90 |
91 | background: linear-gradient(to top,rgba(0,0,0,.3) 0,rgba(0,0,0,0) 100%);
92 | font-size: 15px;
93 |
94 | align-items: center;
95 | }
96 |
97 | .vjs-theme-tunflix .vjs-button > .vjs-icon-placeholder::before {
98 | line-height: 50px;
99 | }
100 |
101 | .vjs-theme-tunflix .vjs-play-progress::before {
102 | display: none;
103 | }
104 |
105 | .vjs-theme-tunflix .vjs-progress-control {
106 | position: absolute;
107 | top: 0;
108 | right: 0;
109 | left: 0;
110 | width: 100%;
111 | height: 20px;
112 | }
113 |
114 | .vjs-theme-tunflix .vjs-big-play-button {
115 | display: none;
116 | width: 70px;
117 | height: 70px;
118 | background: none;
119 | line-height: 70px;
120 | font-size: 80px;
121 | border: none;
122 | top: 50%;
123 | left: 50%;
124 | margin-top: -35px;
125 | margin-left: -35px;
126 | color: white;
127 | transition: 0.3s !important;
128 | }
129 |
130 | .vjs-theme-tunflix .vjs-big-play-button:hover {
131 | transform: scale(1.2) !important;
132 | }
133 |
134 | .vjs-theme-tunflix:hover .vjs-big-play-button,
135 | .vjs-theme-tunflix.vjs-big-play-button:focus {
136 | background-color: transparent;
137 | color: #fff;
138 | }
139 |
140 | .vjs-theme-tunflix .vjs-remaining-time,
141 | .vjs-theme-tunflix .vjs-picture-in-picture-control {
142 | display: none;
143 | }
144 |
145 | /* Ordering the buttons */
146 |
147 | .vjs-theme-tunflix .vjs-play-control,
148 | .vjs-theme-tunflix .vjs-volume-panel {
149 | order: 1;
150 | }
151 |
152 | .vjs-theme-tunflix .video-player-title {
153 | flex: 1;
154 | order: 2;
155 | }
156 |
157 | .vjs-theme-tunflix .video-player-logo {
158 | order: 3;
159 | }
160 |
161 | .vjs-theme-tunflix .vjs-quality-selector {
162 | order: 4;
163 | }
164 |
165 | .vjs-theme-tunflix .vjs-subs-caps-button,
166 | .vjs-theme-tunflix .vjs-fullscreen-control {
167 | order: 5;
168 | }
169 |
170 | /* to not display menu on hover (letting it just on click) */
171 |
172 | .vjs-workinghover .vjs-menu-button-popup.vjs-hover .vjs-menu,
173 | .vjs-menu-button-popup .vjs-menu.vjs-lock-showing:hover{
174 | display: none ;
175 | }
176 | .vjs-play-progress.vjs-slider-bar .vjs-time-tooltip {
177 | display: none !important;
178 | }
179 |
180 | .vjs-theme-tunflix .vjs-play-progress {
181 | background-color: red;
182 | }
183 |
184 | .vjs-theme-tunflix .vjs-menu-content {
185 | background-color: rgba(20, 20, 20, 0.8) !important;
186 | border-radius: 6px;
187 | }
188 |
189 | .vjs-theme-tunflix .vjs-volume-level {
190 | background-color: red;
191 | }
192 |
193 | .vjs-theme-tunflix .vjs-progress-holder{
194 | background-color: rgba(80,80,80,.8);
195 | }
196 |
197 | .vjs-theme-tunflix .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical {
198 | width: 1.5em !important;
199 | height: 7em;
200 | transform: translateX(0.8em);
201 | border-radius: 7px;
202 | }
203 |
204 | .vjs-theme-tunflix .vjs-play-control,
205 | .vjs-theme-tunflix .vjs-mute-control,
206 | .vjs-theme-tunflix .vjs-quality-selector .vjs-menu-button,
207 | .vjs-theme-tunflix .vjs-subs-caps-button .vjs-menu-button,
208 | .vjs-theme-tunflix .vjs-fullscreen-control {
209 | transition: 0.3s;
210 | }
211 |
212 | .vjs-theme-tunflix .vjs-play-control:hover,
213 | .vjs-theme-tunflix .vjs-mute-control:hover,
214 | .vjs-theme-tunflix .vjs-quality-selector .vjs-menu-button:hover,
215 | .vjs-theme-tunflix .vjs-subs-caps-button .vjs-menu-button:hover,
216 | .vjs-theme-tunflix .vjs-fullscreen-control:hover
217 | {
218 | transform: scale(1.2);
219 | }
220 |
221 |
222 |
223 | /*
224 |
225 |
226 |
227 | .vjs-theme-tunflix .vjs-remaining-time {
228 | order: 1;
229 | line-height: 50px;
230 | flex: 3;
231 | text-align: left;
232 | }
233 |
234 |
235 |
236 | .vjs-theme-tunflix .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal {
237 | height: 100%;
238 | }
239 |
240 | .vjs-theme-tunflix .vjs-mute-control {
241 | display: none;
242 | }
243 |
244 | .vjs-theme-tunflix .vjs-volume-panel {
245 | margin-left: 0.5em;
246 | margin-right: 0.5em;
247 | padding-top: 1.5em;
248 | }
249 | .vjs-theme-tunflix .vjs-volume-panel,
250 | .vjs-theme-tunflix .vjs-volume-panel:hover,
251 | .vjs-theme-tunflix .vjs-volume-panel.vjs-volume-panel-horizontal:hover,
252 | .vjs-theme-tunflix .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal,
253 | .vjs-theme-tunflix .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal,
254 | .vjs-theme-tunflix .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal,
255 | .vjs-theme-tunflix .vjs-volume-panel.vjs-volume-panel-horizontal:hover,
256 | .vjs-theme-tunflix .vjs-volume-bar.vjs-slider-horizontal {
257 | width: 3em;
258 | }
259 |
260 | .vjs-theme-tunflix .vjs-volume-level::before {
261 | font-size: 1em;
262 | }
263 |
264 | .vjs-theme-tunflix .vjs-volume-panel .vjs-volume-control {
265 | opacity: 1;
266 | width: 100%;
267 | height: 100%;
268 | }
269 |
270 | .vjs-theme-tunflix .vjs-volume-bar {
271 | background-color: transparent;
272 | margin: 0;
273 | }
274 |
275 | .vjs-theme-tunflix .vjs-slider-horizontal .vjs-volume-level {
276 | height: 100%;
277 | }
278 |
279 | .vjs-theme-tunflix .vjs-volume-bar.vjs-slider-horizontal {
280 | margin-top: 0;
281 | margin-bottom: 0;
282 | height: 100%;
283 | }
284 |
285 | .vjs-theme-tunflix .vjs-volume-bar::before {
286 | content: '';
287 | z-index: 0;
288 | width: 0;
289 | height: 0;
290 | position: absolute;
291 | top: 0px;
292 | left: 0;
293 |
294 | border-style: solid;
295 | border-width: 0 0 1.75em 3em;
296 | border-color: transparent transparent rgba(255, 255, 255, 0.25) transparent;
297 | }
298 |
299 | .vjs-theme-tunflix .vjs-volume-level {
300 | overflow: hidden;
301 | background-color: transparent;
302 | }
303 |
304 | .vjs-theme-tunflix .vjs-volume-level::before {
305 | content: '';
306 | z-index: 1;
307 | width: 0;
308 | height: 0;
309 | position: absolute;
310 | top: 0;
311 | left: 0;
312 |
313 | border-style: solid;
314 | border-width: 0 0 1.75em 3em;
315 | border-color: transparent transparent var(--vjs-theme-tunflix--secondary) transparent;
316 | }
317 | */
318 |
--------------------------------------------------------------------------------
/client/src/constants/actionTypes.js:
--------------------------------------------------------------------------------
1 | // User
2 | export const SIGN_UP_USER_SUCCESS = "SIGN_UP_USER_SUCCESS";
3 | export const SIGN_IN_USER_SUCCESS = "SIGN_IN_USER_SUCCESS";
4 | export const SIGN_OUT_USER_SUCCESS = "SIGN_OUT_USER_SUCCESS";
5 | export const AUTH_FAIL = "AUTH_FAIL";
6 | export const GET_AUTH_USER = "GET_AUTH_USER";
7 | export const SET_LOADING = "SET_LOADING";
8 | export const SET_LOADING_LOCAL = "SET_LOADING_LOCAL";
9 | export const SET_LOADING_GOOGLE = "SET_LOADING_GOOGLE";
10 | // Dark mode
11 | export const SET_DARK_MODE = "SET_DARK_MODE";
12 | // Browse
13 | export const BROWSE_HOME_LOAD = 'BROWSE_HOME_LOAD'
14 | export const BROWSE_HOME_SUCCESS = 'BROWSE_HOME_SUCCESS'
15 | export const BROWSE_HOME_FAIL = 'BROWSE_HOME_FAIL'
16 | export const BROWSE_MOVIES_LOAD = 'BROWSE_MOVIES_LOAD'
17 | export const BROWSE_MOVIES_SUCCESS = 'BROWSE_MOVIES_SUCCESS'
18 | export const BROWSE_MOVIES_FAIL = 'BROWSE_MOVIES_FAIL'
19 | export const BROWSE_TVSHOWS_LOAD = 'BROWSE_TVSHOWS_LOAD'
20 | export const BROWSE_TVSHOWS_SUCCESS = 'BROWSE_TVSHOWS_SUCCESS'
21 | export const BROWSE_TVSHOWS_FAIL = 'BROWSE_TVSHOWS_FAIL'
22 | // Watch
23 | export const GET_MEDIA_LOAD = 'GET_MEDIA_LOAD'
24 | export const GET_MEDIA_SUCCESS = 'GET_MEDIA_SUCCESS'
25 | export const GET_MEDIA_FAIL = 'GET_MEDIA_FAIL'
26 | export const HANDLE_MODAL_IS_OPEN = 'HANDLE_MODAL_IS_OPEN'
27 |
--------------------------------------------------------------------------------
/client/src/constants/api.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = '/api'
2 | const BASE_AUTH='/auth'
3 | const BASE_BROWSE='/browse'
4 |
5 |
6 | export const CURRENT_USER = `${BASE_URL}${BASE_AUTH}/current`
7 | export const SIGN_IN = `${BASE_URL}${BASE_AUTH}/signin`
8 | export const SIGN_UP = `${BASE_URL}${BASE_AUTH}/signup`
9 |
10 | export const BROWSE_HOME = `${BASE_URL}${BASE_BROWSE}/home`
11 | export const BROWSE_MOVIES = `${BASE_URL}${BASE_BROWSE}/movies`
12 | export const BROWSE_TVSHOWS = `${BASE_URL}${BASE_BROWSE}/tvshows`
13 |
14 | export const BASE_MOVIE = "/api/movie"
15 | export const BASE_TVSHOW = "/api/tv"
16 |
--------------------------------------------------------------------------------
/client/src/constants/routes.js:
--------------------------------------------------------------------------------
1 | export const HOME = '/'
2 | export const BROWSE = '/browse'
3 | export const SIGN_IN = '/signin'
4 | export const SIGN_UP = '/signup'
5 | export const MOVIES = '/browse/movies'
6 | export const TV_SHOWS = '/browse/tvshows'
7 | export const MY_LIST = '/browse/mylist'
8 | export const NOT_FOUND = '/404'
--------------------------------------------------------------------------------
/client/src/containers/MainContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {makeStyles} from '@material-ui/core/styles'
3 |
4 | const MainContainer = ({children,...rest}) => {
5 | const classes = useStyles()
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
13 | const useStyles = makeStyles((theme)=>({
14 | container:{
15 | minHeight: '100vh'
16 | }
17 | }))
18 |
19 | export default MainContainer
20 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import { Provider } from "react-redux";
5 | import store from "./store";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/client/src/pages/Browse/Browse.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { getBrowseHome } from "../../actions/browseActions";
4 | import Box from "@material-ui/core/Box";
5 | import Banner from "../../components/Banner/Banner";
6 | import Loader from "../../components/Loader";
7 | import MainContainer from "../../containers/MainContainer";
8 | import Row from "../../components/Row";
9 | import { Helmet } from "react-helmet";
10 |
11 | const Browse = () => {
12 | const dispatch = useDispatch();
13 | const isLoading = useSelector((state) => state.browseReducer.isLoadingHome);
14 | const banner = useSelector((state) => state.browseReducer.bannerHome);
15 | const rows = useSelector((state) => state.browseReducer.rowsHome);
16 | useEffect(() => {
17 | if (!banner && !rows) {
18 | dispatch(getBrowseHome());
19 | }
20 | }, [banner, dispatch, rows]);
21 | return !isLoading && banner && rows ? (
22 | <>
23 |
24 | X-Netflix - Accueil
25 |
26 |
27 |
28 |
29 | {rows && rows.slice(0,4).map((elem, k) => (
30 |
31 | ))}
32 |
33 |
34 | >
35 | ) : (
36 |
37 | );
38 | };
39 |
40 | export default Browse;
41 |
--------------------------------------------------------------------------------
/client/src/pages/Browse/index.js:
--------------------------------------------------------------------------------
1 |
2 | import Browse from './Browse'
3 |
4 | export default Browse;
5 |
--------------------------------------------------------------------------------
/client/src/pages/BrowseMovies/BrowseMovies.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { getBrowseMovies } from "../../actions/browseActions";
4 | import MainContainer from "../../containers/MainContainer";
5 | import Banner from "../../components/Banner/Banner";
6 | import Loader from "../../components/Loader";
7 | import Row from "../../components/Row";
8 | import { Helmet } from "react-helmet";
9 |
10 | const Browse = () => {
11 | const dispatch = useDispatch();
12 | const isLoading = useSelector((state) => state.browseReducer.isLoadingMovies);
13 | const banner = useSelector((state) => state.browseReducer.bannerMovies);
14 | const rows = useSelector((state) => state.browseReducer.rowsMovies);
15 | const error = useSelector((state) => state.browseReducer.errorMovies);
16 | useEffect(() => {
17 | if (!banner && !rows && !error) {
18 | dispatch(getBrowseMovies());
19 | }
20 | }, [dispatch,banner,rows,error]);
21 | return !isLoading && banner && rows ? (
22 | <>
23 |
24 | X-Netflix - Films
25 |
26 |
27 |
28 |
29 | {rows.map((elem, k) => (
30 |
31 | ))}
32 |
33 |
34 | >
35 | ) : (
36 |
37 | );
38 | };
39 |
40 | export default Browse;
41 |
--------------------------------------------------------------------------------
/client/src/pages/BrowseMovies/index.js:
--------------------------------------------------------------------------------
1 | import BrowseMovies from './BrowseMovies'
2 |
3 | export default BrowseMovies
--------------------------------------------------------------------------------
/client/src/pages/BrowseTVShows/BrowseTVShows.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { getBrowseTVShows } from "../../actions/browseActions";
4 | import Box from "@material-ui/core/Box";
5 | import Banner from "../../components/Banner/Banner";
6 | import Loader from "../../components/Loader";
7 | import Row from "../../components/Row";
8 | import { Helmet } from "react-helmet";
9 | import MainContainer from "../../containers/MainContainer";
10 |
11 | const Browse = () => {
12 | const dispatch = useDispatch();
13 | const isLoading = useSelector((state) => state.browseReducer.isLoadingTVShows);
14 | const banner = useSelector((state) => state.browseReducer.bannerTVShows);
15 | const rows = useSelector((state) => state.browseReducer.rowsTVShows);
16 | const error = useSelector((state) => state.browseReducer.errorTVShows);
17 | useEffect(() => {
18 | if (!banner && !rows & !error) {
19 | dispatch(getBrowseTVShows());
20 | }
21 | }, [dispatch,banner,rows,error]);
22 | return !isLoading && banner && rows ? (
23 | <>
24 |
25 | X-Netflix - Séries TV
26 |
27 |
28 |
29 |
30 | {rows.map((elem, k) => (
31 |
32 | ))}
33 |
34 |
35 | >
36 | ) : (
37 |
38 | );
39 | };
40 |
41 | export default Browse;
42 |
--------------------------------------------------------------------------------
/client/src/pages/BrowseTVShows/index.js:
--------------------------------------------------------------------------------
1 | import BrowseTVShows from './BrowseTVShows'
2 | export default BrowseTVShows
--------------------------------------------------------------------------------
/client/src/pages/BrowseTVShows/style.css:
--------------------------------------------------------------------------------
1 | .tvshows-container{
2 | padding-top: 60px;
3 | }
4 |
5 | .tvshows-container h1{
6 | color:white;
7 | font-size: 50px;
8 | font-family: 'Fjalla One', sans-serif;
9 | font-weight: 400;
10 | text-transform: uppercase;
11 | margin:0 30px;
12 | }
13 |
14 | .tvshows-title{
15 | display: flex;
16 | align-items: center;
17 | margin:30px 0;
18 | }
19 |
20 | .selector{
21 | min-width: 150px;
22 | cursor: pointer;
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/pages/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import { Redirect } from "react-router-dom";
4 |
5 | const Home = () => {
6 | const isAuth = useSelector((state) => state.userReducer.isAuth);
7 | if (isAuth) {
8 | return ;
9 | } else {
10 | return ;
11 | }
12 | };
13 |
14 | export default Home;
15 |
--------------------------------------------------------------------------------
/client/src/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | import Home from './Home'
2 |
3 | export default Home
--------------------------------------------------------------------------------
/client/src/pages/Movie/Cast.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Paper, Typography } from "@material-ui/core";
3 | import {makeStyles} from '@material-ui/core/styles'
4 | import Container from '@material-ui/core/Container'
5 |
6 | const Cast = ({ cast }) => {
7 | const classes = useStyles()
8 | return (
9 |
10 | {cast.slice(0,8).map((elem,k) => (
11 |
12 |
13 | {elem.original_name}
14 | {elem.character.split('/')[0].trim()}
15 |
16 | ))}
17 |
18 | );
19 | };
20 |
21 | const useStyles = makeStyles((theme) => ({
22 | container:{
23 | display:'flex',
24 | justifyContent :'center',
25 | overflow:'hidden',
26 | padding: theme.spacing(0,1),
27 | },
28 | actor:{
29 | backgroundColor:'rgba(0,0,0,0)',
30 | padding: theme.spacing(1),
31 | cursor:'pointer',
32 | borderRadius: '4px',
33 | transition:'.2s',
34 | '&:hover': {
35 | backgroundColor:'rgba(0,0,0,1)',
36 | },
37 |
38 |
39 | },
40 | actorImage:{
41 | borderRadius: '4px',
42 | width:'100%'
43 | }
44 | }))
45 |
46 | export default Cast;
47 |
--------------------------------------------------------------------------------
/client/src/pages/Movie/Movie.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import { Helmet } from "react-helmet";
5 | import { getMedia } from "../../actions/watchActions";
6 | import Loader from "../../components/Loader";
7 | import Grid from "@material-ui/core/Grid";
8 | import { makeStyles } from "@material-ui/core/styles";
9 | import MovieDetails from "./MovieDetails";
10 | import Trailers from "./Trailers";
11 | import Modal from "../../components/Modal/Modal";
12 | import { handleModal } from "../../actions/watchActions";
13 | import { Container } from "@material-ui/core";
14 | import MainContainer from "../../containers/MainContainer";
15 | import Cast from "./Cast";
16 |
17 | const Movie = () => {
18 | const { movie_id } = useParams();
19 | const dispatch = useDispatch();
20 | const isLoading = useSelector((state) => state.watchReducer.isLoading);
21 | const modalIsOpen = useSelector((state) => state.watchReducer.modalIsOpen);
22 | const media = useSelector((state) => state.watchReducer.media);
23 | const mediaType = useSelector((state) => state.watchReducer.mediaType);
24 | const classes = useStyles();
25 |
26 | useEffect(() => {
27 | dispatch(getMedia(movie_id, "movie"));
28 | }, [dispatch, movie_id]);
29 |
30 | return !isLoading && media && (mediaType === 'movie') ? (
31 | <>
32 |
33 | {`X-Netflix - ${media.title}`}
34 |
35 |
36 |
37 |
38 |

39 |
40 |
41 |
42 |
43 |
44 |
45 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | dispatch(handleModal(false))}
65 | videoLinks={media.videoLinks}
66 | title={media.title}
67 | />
68 |
69 | >
70 | ) : (
71 |
72 | );
73 | };
74 |
75 | const useStyles = makeStyles((theme) => ({
76 |
77 | backgroundContainer: {
78 | position: "absolute",
79 | top: 0,
80 | left: 0,
81 | zIndex: -1,
82 | },
83 | fillContainer: {
84 | padding: theme.spacing(30, 10, 0, 10),
85 | },
86 | background: {
87 | position: "relative",
88 | width: "100%",
89 | },
90 | backdropPath: {
91 | width: "100%",
92 | filter: "brightness(50%) blur(4px)",
93 | },
94 | vignette: {
95 | position: "absolute",
96 | bottom: 0,
97 | transform:'translateY(10px)',
98 | background: "linear-gradient(180deg,transparent 20%,rgb(20, 20, 20) 80%)",
99 | width: "100%",
100 | height: "500px",
101 | },
102 | }));
103 |
104 | export default Movie;
105 |
--------------------------------------------------------------------------------
/client/src/pages/Movie/MovieDetails.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Grid from "@material-ui/core/Grid";
3 | import Paper from "@material-ui/core/Paper";
4 | import Box from "@material-ui/core/Box";
5 | import { makeStyles } from "@material-ui/styles";
6 | import ImdbRating from "../../components/ImdbRating";
7 | import Typography from "@material-ui/core/Typography";
8 | import Button from "@material-ui/core/Button";
9 | import PlayArrowIcon from "@material-ui/icons/PlayArrow";
10 | import DoneIcon from "@material-ui/icons/Done";
11 | import AddIcon from "@material-ui/icons/Add";
12 | import { useDispatch } from "react-redux";
13 | import { handleModal } from "../../actions/watchActions";
14 | import IconButton from "@material-ui/core/IconButton";
15 | import ThumbUpOutlinedIcon from "@material-ui/icons/ThumbUpOutlined";
16 | import ThumbDownOutlinedIcon from "@material-ui/icons/ThumbDownOutlined";
17 | import ThumbUpIcon from "@material-ui/icons/ThumbUp";
18 | import ThumbDownIcon from "@material-ui/icons/ThumbDown";
19 |
20 | const MovieDetails = ({ title, vote_average, genres, runtime, year, overview }) => {
21 | const dispatch = useDispatch();
22 | const classes = useStyles();
23 | const [listed, setListed] = useState(false);
24 | const [like, setLike] = useState(false);
25 | const [unlike, setUnlike] = useState(false);
26 |
27 | return (
28 |
29 |
30 | {title}
31 |
32 |
33 |
34 | {genres.slice(0, 3).map((elem,k) => (
35 | {elem.name}
36 | ))}
37 | {year}
38 |
39 | {`${Math.floor(runtime / 60)} h ${runtime - Math.floor(runtime / 60) * 60} min`}{" "}
40 |
41 |
42 |
43 |
44 | {overview.length > 300 ? `${overview.slice(0, 300)}...` : overview}
45 |
46 |
47 |
48 | }
53 | size="large"
54 | onClick={() => {
55 | dispatch(handleModal(true));
56 | }}
57 | >
58 | Lecture
59 |
60 | setListed(!listed)}>
61 | {listed ? : }
62 |
63 | {
66 | setUnlike(false);
67 | setLike(!like);
68 | }}
69 | >
70 | {like ? : }
71 |
72 | {
75 | setLike(false);
76 | setUnlike(!unlike);
77 | }}
78 | >
79 | {unlike ? : }
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | const useStyles = makeStyles((theme) => ({
87 | title: {
88 | letterSpacing: "2px",
89 | textShadow: "2px 2px 4px rgb(0 0 0 / 45%)"
90 | },
91 | desc: {
92 | display: "flex",
93 | alignItems: "center",
94 | textShadow: "1px 1px 2px rgb(0 0 0 / 100%)",
95 | },
96 | infos: {
97 | padding: "5px 10px",
98 | marginRight: theme.spacing(1),
99 | },
100 | playBtn: {
101 | fontWeight: 700,
102 | marginRight: theme.spacing(2),
103 | textTransform: "capitalize",
104 | width: "250px",
105 | height: "50px",
106 | },
107 | listBtn: {
108 | backgroundColor: "rgba(109, 109, 110, 0.7)",
109 | fontWeight: 700,
110 | color: "#fff",
111 | "&:hover": {
112 | backgroundColor: "rgba(109, 109, 110, 0.4)",
113 | },
114 | textTransform: "capitalize",
115 |
116 | height: "50px",
117 | },
118 | overview: {
119 | textShadow: "1px 1px 2px rgb(0 0 0 / 100%)",
120 | fontSize: "18px",
121 | },
122 | }));
123 |
124 | export default MovieDetails;
125 |
--------------------------------------------------------------------------------
/client/src/pages/Movie/Trailers.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Grid from "@material-ui/core/Grid";
3 | import Typography from "@material-ui/core/Typography";
4 | import Paper from "@material-ui/core/Paper";
5 | import { makeStyles } from "@material-ui/core/styles";
6 |
7 | import Modal from "../../components/Modal";
8 |
9 | const Trailers = ({ title, trailers, crew }) => {
10 | const classes = useStyles();
11 | const director = crew
12 | .filter((elem) => elem.job === "Director")
13 | .map((elem) => elem.name)
14 | .join(", ");
15 | const screenplay = crew
16 | .filter((elem) => elem.job === "Screenplay")
17 | .map((elem) => elem.name)
18 | .join(", ");
19 | const producer = crew
20 | .filter((elem) => elem.job === "Producer")
21 | .map((elem) => elem.name)
22 | .join(", ");
23 | const author = crew
24 | .filter((elem) => elem.job === "Author")
25 | .map((elem) => elem.name)
26 | .join(", ");
27 | return (
28 |
29 |
30 | {director && (
31 |
32 | Réalisateur: {director}
33 |
34 | )}
35 | {screenplay && (
36 |
37 | Scénariste: {screenplay}
38 |
39 | )}
40 | {producer && (
41 |
42 | Producteur: {producer}
43 |
44 | )}
45 | {author && (
46 |
47 | Auteur: {author}
48 |
49 | )}
50 |
51 | {trailers.slice(0,3).map((elem) => (
52 |
53 |
54 |
64 |
65 |
66 | ))}
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | const useStyles = makeStyles((theme) => ({
74 | iframeContainer: {
75 | position: "relative",
76 | width: "100%",
77 | paddingBottom: "56.25%",
78 | height: 0,
79 | borderRadius: "7px",
80 | overflow: "hidden",
81 | },
82 | iframe: {
83 | position: "absolute",
84 | top: 0,
85 | left: 0,
86 | width: "100%",
87 | height: "100%",
88 | },
89 | }));
90 |
91 | export default Trailers;
92 |
--------------------------------------------------------------------------------
/client/src/pages/Movie/index.js:
--------------------------------------------------------------------------------
1 | import Movie from './Movie'
2 | export default Movie
--------------------------------------------------------------------------------
/client/src/pages/MyList/MyList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const MyList = () => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default MyList
12 |
--------------------------------------------------------------------------------
/client/src/pages/MyList/index.js:
--------------------------------------------------------------------------------
1 | import MyList from './MyList'
2 | export default MyList
--------------------------------------------------------------------------------
/client/src/pages/NotFound/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NotFound = () => {
4 | return (
5 |
6 |
Not Found
7 |
8 | );
9 | };
10 |
11 | export default NotFound;
12 |
--------------------------------------------------------------------------------
/client/src/pages/NotFound/index.js:
--------------------------------------------------------------------------------
1 | import NotFound from './NotFound'
2 |
3 | export default NotFound
--------------------------------------------------------------------------------
/client/src/pages/SearchContent/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import "./style.css";
4 | import MovieCard from "../../components/MovieCard";
5 | import Loader from "../../components/Loader/Loader";
6 | import axios from "axios";
7 |
8 | const SearchContent = () => {
9 | const [isLoading, setIsLoading] = useState(true);
10 |
11 | const baseImgUrl = "https://image.tmdb.org/t/p/original/";
12 | const [movies, setMovies] = useState([]);
13 | const { search_term } = useParams();
14 |
15 | const API_KEY = "71741288544550e3b57f3a8dca4493fc";
16 | const fetchUrl = `https://api.themoviedb.org/3/search/multi?api_key=${API_KEY}&query=${search_term}&include_adult=false`;
17 |
18 | useEffect(() => {
19 | setIsLoading(true)
20 | async function fetchData() {
21 | const request = await axios.get(fetchUrl);
22 | setMovies(request.data.results);
23 |
24 | setIsLoading(false)
25 | return request;
26 | }
27 | fetchData();
28 | }, [fetchUrl]);
29 |
30 | return (
31 |
32 | {isLoading ? (
33 |
34 | ) :"" }{
35 | movies.filter(movie=>movie.poster_path != null).map((movie) => (
36 |
44 | ))
45 | }
46 |
47 | );
48 | };
49 |
50 | export default SearchContent;
51 |
--------------------------------------------------------------------------------
/client/src/pages/SearchContent/style.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300&display=swap");
2 |
3 |
4 | .search-content{
5 | padding: 0 7px;
6 | margin-top:100px;
7 | display:flex;
8 | flex-wrap: wrap;
9 | width:auto;
10 | }
--------------------------------------------------------------------------------
/client/src/pages/SignIn/SignIn.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Link as RouterLink, useHistory } from "react-router-dom";
3 | import { useFormik } from "formik";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { signIn } from "../../actions/userActions";
6 | import * as Yup from "yup";
7 |
8 | import { makeStyles } from "@material-ui/core/styles";
9 | import InputAdornment from "@material-ui/core/InputAdornment";
10 | import IconButton from "@material-ui/core/IconButton";
11 | import Container from "@material-ui/core/Container";
12 | import CircularProgress from "@material-ui/core/CircularProgress";
13 | import Icon from "@material-ui/core/Icon";
14 | import Logo from "../../assets/images/logo.png";
15 | import Button from "@material-ui/core/Button";
16 | import Paper from "@material-ui/core/Paper";
17 | import TextField from "@material-ui/core/TextField";
18 | import FormControlLabel from "@material-ui/core/FormControlLabel";
19 | import Checkbox from "@material-ui/core/Checkbox";
20 | import Link from "@material-ui/core/Link";
21 | import Grid from "@material-ui/core/Grid";
22 | import Typography from "@material-ui/core/Typography";
23 | import Visibility from "@material-ui/icons/Visibility";
24 | import VisibilityOff from "@material-ui/icons/VisibilityOff";
25 | import MainContainer from "../../containers/MainContainer";
26 |
27 | const SignIn = () => {
28 | const classes = useStyles();
29 | const dispatch = useDispatch();
30 | const [showPassword, setShowPassword] = useState(false);
31 | const isLoadingLocal = useSelector((state) => state.userReducer.isLoading);
32 | const isAuth = useSelector((state) => state.userReducer.isAuth);
33 | const error = useSelector((state) => state.userReducer.error);
34 | const history = useHistory();
35 |
36 | const formik = useFormik({
37 | initialValues: {
38 | email: "",
39 | password: "",
40 | },
41 | validationSchema: Yup.object({
42 | email: Yup.string().email("L'e-mail n'est pas valide").required("L'adresse e-mail est requise"),
43 | password: Yup.string()
44 | .min(6, "le mot de passe doit faire entre 6 et 20 caractères")
45 | .max(20, "le mot de passe doit faire entre 6 et 20 caractères")
46 | .required("Le mot de passe est requis"),
47 | }),
48 | onSubmit: (values) => {
49 | dispatch(signIn(values));
50 | },
51 | validateOnChange:false,
52 | validateOnBlur:false
53 | });
54 |
55 | useEffect(() => {
56 | if (isAuth) {
57 | history.push("/browse");
58 | }
59 | }, [isAuth, history]);
60 |
61 | return (
62 |
63 |
64 |
65 |
66 | Connexion
67 |
68 |
151 |
152 |
153 | );
154 | };
155 |
156 | const useStyles = makeStyles((theme) => ({
157 | paper: {
158 | margin: theme.spacing(8, "auto"),
159 | display: "flex",
160 | flexDirection: "column",
161 | alignItems: "center",
162 | },
163 | logo: {
164 | width: 200,
165 | marginBottom: theme.spacing(6),
166 | },
167 | form: {
168 | width: "100%", // Fix IE 11 issue.
169 | },
170 | submitBtn: {
171 | margin: theme.spacing(2, 0, 2, 0),
172 | },
173 | googleBtn: {
174 | margin: theme.spacing(0, 0, 4, 0),
175 | color: "#fff",
176 | },
177 | title: {
178 | letterSpacing: "10px",
179 | margin: theme.spacing(0, 0, 4, 0),
180 | },
181 | error: {
182 | backgroundColor: theme.palette.error.main,
183 | padding: theme.spacing(2),
184 | },
185 | }));
186 |
187 | export default SignIn;
188 |
--------------------------------------------------------------------------------
/client/src/pages/SignIn/index.js:
--------------------------------------------------------------------------------
1 | import Login from './SignIn'
2 |
3 | export default Login
--------------------------------------------------------------------------------
/client/src/pages/SignUp/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useHistory, Link as RouterLink } from "react-router-dom";
3 | import { useFormik } from "formik";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { signUp } from "../../actions/userActions";
6 | import * as Yup from "yup";
7 |
8 | import CircularProgress from "@material-ui/core/CircularProgress";
9 | import Logo from "../../assets/images/logo.png";
10 | import Button from "@material-ui/core/Button";
11 | import TextField from "@material-ui/core/TextField";
12 | import FormControlLabel from "@material-ui/core/FormControlLabel";
13 | import Checkbox from "@material-ui/core/Checkbox";
14 | import Link from "@material-ui/core/Link";
15 | import Grid from "@material-ui/core/Grid";
16 | import FormHelperText from "@material-ui/core/FormHelperText";
17 | import Typography from "@material-ui/core/Typography";
18 | import { makeStyles } from "@material-ui/core/styles";
19 | import Container from "@material-ui/core/Container";
20 |
21 | const useStyles = makeStyles((theme) => ({
22 | paper: {
23 | margin: theme.spacing(8, "auto"),
24 | display: "flex",
25 | flexDirection: "column",
26 | alignItems: "center",
27 | },
28 | title: {
29 | letterSpacing: "10px",
30 | margin: theme.spacing(0, 0, 4, 0),
31 | },
32 | logo: {
33 | width: 200,
34 | marginBottom: theme.spacing(6),
35 | },
36 | form: {
37 | width: "100%", // Fix IE 11 issue.
38 | },
39 | submitBtn: {
40 | margin: theme.spacing(2, 0, 2, 0),
41 | },
42 | }));
43 |
44 | const SignIn = () => {
45 | const classes = useStyles();
46 | const dispatch = useDispatch();
47 | const isLoading = useSelector((state) => state.userReducer.isLoading);
48 | const isAuth = useSelector((state) => state.userReducer.isAuth);
49 | const history = useHistory();
50 | const formik = useFormik({
51 | initialValues: {
52 | firstName: "",
53 | lastName: "",
54 | email: "",
55 | password: "",
56 | policy: false,
57 | },
58 | validationSchema: Yup.object({
59 | firstName: Yup.string().max(20).required("Le prénom est requis"),
60 | lastName: Yup.string().max(20).required("Le nom est requis"),
61 | email: Yup.string().email("L'e-mail n'est pas valide").required("L'e-mail est requis"),
62 | password: Yup.string()
63 | .min(6, "le mot de passe doit faire entre 6 et 20 caractères")
64 | .max(20, "le mot de passe doit faire entre 6 et 20 caractères")
65 | .required("Le mot de passe est requis"),
66 | confirmPassword: Yup.string()
67 | .min(6)
68 | .max(20)
69 | .required("La confirmation du mot de passe est requise")
70 | .oneOf([Yup.ref("password"), null], "Les mots de passes de correspondent pas"),
71 | policy: Yup.boolean().oneOf([true], "Vous devez acceptez les conditions d'utlisations"),
72 | }),
73 | onSubmit: (values) => {
74 | dispatch(
75 | signUp({
76 | firstName: values.firstName,
77 | lastName: values.lastName,
78 | email: values.email,
79 | password: values.password,
80 | })
81 | );
82 | },
83 | validateOnChange:false,
84 | validateOnBlur:false
85 | });
86 |
87 | useEffect(() => {
88 | if (isAuth) {
89 | history.push("/browse");
90 | }
91 | }, [isAuth,history]);
92 |
93 | return (
94 |
95 |
96 |
97 | Inscription
98 |
99 |
215 |
216 | );
217 | };
218 |
219 | export default SignIn;
220 |
--------------------------------------------------------------------------------
/client/src/pages/SignUp/index.js:
--------------------------------------------------------------------------------
1 | import SignUp from './Signup'
2 |
3 | export default SignUp
--------------------------------------------------------------------------------
/client/src/pages/TVShow/Cast.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Paper, Typography } from "@material-ui/core";
3 | import {makeStyles} from '@material-ui/core/styles'
4 | import Container from '@material-ui/core/Container'
5 |
6 | const Cast = ({ cast }) => {
7 | const classes = useStyles()
8 | return (
9 |
10 | {cast.map((elem) => (
11 |
12 |
13 | {elem.name}
14 | {elem.character.split('/')[0].trim()}
15 |
16 | ))}
17 |
18 | );
19 | };
20 |
21 | const useStyles = makeStyles((theme) => ({
22 | container:{
23 | display:'flex',
24 | justifyContent :'center',
25 | overflow:'hidden',
26 | padding: theme.spacing(0,1),
27 | },
28 | actor:{
29 | backgroundColor:'rgba(0,0,0,0)',
30 | padding: theme.spacing(1),
31 | cursor:'pointer',
32 | borderRadius: '4px',
33 | transition:'.2s',
34 | '&:hover': {
35 | backgroundColor:'rgba(0,0,0,1)',
36 | },
37 |
38 |
39 | },
40 | actorImage:{
41 | borderRadius: '4px',
42 | width:'100%'
43 | }
44 | }))
45 |
46 | export default Cast;
47 |
--------------------------------------------------------------------------------
/client/src/pages/TVShow/Episodes.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Box, FormControl, InputLabel, MenuItem, Select, Typography, Grid, Container } from "@material-ui/core";
3 | import List from "@material-ui/core/List";
4 | import ListItem from "@material-ui/core/ListItem";
5 | import { useDispatch } from "react-redux";
6 | import { handleModal } from "../../actions/watchActions";
7 | import Divider from "@material-ui/core/Divider";
8 | import { makeStyles } from "@material-ui/core/styles";
9 |
10 | const Episodes = ({ seasons }) => {
11 | const [selectedEpisode,setSelectedEpisode]=useState(0)
12 | const dispatch = useDispatch();
13 | const classes = useStyles();
14 | const [selectedSeason, setSelectedSeason] = useState(0);
15 | return (
16 |
17 |
18 |
19 | Episodes
20 |
21 |
22 |
45 |
46 |
47 |
48 |
49 | {seasons[selectedSeason].episodes.map((elem, k) => {
50 | return (
51 |
52 |
{
56 | dispatch(handleModal(true));
57 | setSelectedEpisode(k)
58 | }}
59 | selected={selectedEpisode === k}
60 | >
61 |
62 |
63 |
68 | {elem.episode_number}
69 |
70 |
71 |
72 |
73 |
74 |
79 |
80 |
81 |
82 | {elem.name}
83 |
84 |
85 |
86 | {elem.overview.length > 300
87 | ? `${elem.overview.slice(0, 300)}...`
88 | : elem.overview}
89 |
90 |
91 |
92 |
93 |
94 |
95 | {!(k === seasons[selectedSeason].episodes.length - 1) &&
}
96 |
97 | );
98 | })}
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | const useStyles = makeStyles((theme) => ({
106 | episodeSelectorHeader: {
107 | display: "flex",
108 | },
109 | formControl: {
110 | margin: theme.spacing(1),
111 | minWidth: 120,
112 | },
113 | episodeSelectorLabel: {
114 | flexGrow: 1,
115 | },
116 | selectorMenu: {
117 | "& .MuiPaper-root": {
118 | backgroundColor: "rgba(0,0,0,.9)",
119 | border: "1px solid rgb(20,20,20)",
120 | },
121 | },
122 | }));
123 |
124 | export default Episodes;
125 |
--------------------------------------------------------------------------------
/client/src/pages/TVShow/Modal.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/client/src/pages/TVShow/Modal.js
--------------------------------------------------------------------------------
/client/src/pages/TVShow/TVShow.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import { Helmet } from "react-helmet";
5 | import { getMedia } from "../../actions/watchActions";
6 | import Loader from "../../components/Loader";
7 | import Grid from "@material-ui/core/Grid";
8 | import { makeStyles } from "@material-ui/core/styles";
9 | import TVShowDetails from "./TVShowDetails";
10 | import Trailers from "./Trailers";
11 | import Modal from "../../components/Modal/Modal";
12 | import { handleModal } from "../../actions/watchActions";
13 | import Episodes from "./Episodes";
14 | import { Container } from "@material-ui/core";
15 | import MainContainer from "../../containers/MainContainer";
16 | import Cast from "./Cast";
17 |
18 | const TVShow = () => {
19 | const { tvshow_id } = useParams();
20 | const dispatch = useDispatch();
21 | const isLoading = useSelector((state) => state.watchReducer.isLoading);
22 | const modalIsOpen = useSelector((state) => state.watchReducer.modalIsOpen);
23 | const media = useSelector((state) => state.watchReducer.media);
24 | const mediaType = useSelector((state) => state.watchReducer.mediaType);
25 | const classes = useStyles();
26 |
27 | useEffect(() => {
28 | dispatch(getMedia(tvshow_id, "tv"));
29 | }, [dispatch, tvshow_id]);
30 |
31 | return !isLoading && media && (mediaType === 'tv') ? (
32 | <>
33 |
34 | {`X-Netflix - ${media.name}`}
35 |
36 |
37 |
38 |
39 |

40 |
41 |
42 |
43 |
44 |
45 |
46 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | dispatch(handleModal(false))}
67 | videoLink={media.videoLink}
68 | tmdbId={media.tmdb_id}
69 | />
70 |
71 | >
72 | ) : (
73 |
74 | );
75 | };
76 |
77 | const useStyles = makeStyles((theme) => ({
78 |
79 | backgroundContainer: {
80 | position: "absolute",
81 | top: 0,
82 | left: 0,
83 | zIndex: -1,
84 | },
85 | fillContainer: {
86 | padding: theme.spacing(30, 10, 0, 10),
87 | },
88 | background: {
89 | position: "relative",
90 | width: "100%",
91 | },
92 | backdropPath: {
93 | width: "100%",
94 | filter: "brightness(50%) blur(4px)",
95 | },
96 | vignette: {
97 | position: "absolute",
98 | bottom: 0,
99 | transform:'translateY(10px)',
100 | background: "linear-gradient(180deg,transparent 20%,rgb(20, 20, 20) 80%)",
101 | width: "100%",
102 | height: "500px",
103 | },
104 | }));
105 |
106 | export default TVShow;
107 |
--------------------------------------------------------------------------------
/client/src/pages/TVShow/TVShowDetails.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Grid from "@material-ui/core/Grid";
3 | import Paper from "@material-ui/core/Paper";
4 | import { makeStyles } from "@material-ui/styles";
5 | import ImdbRating from "../../components/ImdbRating";
6 | import Typography from "@material-ui/core/Typography";
7 | import Button from "@material-ui/core/Button";
8 | import PlayArrowIcon from "@material-ui/icons/PlayArrow";
9 | import DoneIcon from "@material-ui/icons/Done";
10 | import AddIcon from "@material-ui/icons/Add";
11 | import { useDispatch } from "react-redux";
12 | import { handleModal } from "../../actions/watchActions";
13 | import IconButton from "@material-ui/core/IconButton";
14 | import ThumbUpOutlinedIcon from "@material-ui/icons/ThumbUpOutlined";
15 | import ThumbDownOutlinedIcon from "@material-ui/icons/ThumbDownOutlined";
16 | import ThumbUpIcon from "@material-ui/icons/ThumbUp";
17 | import ThumbDownIcon from "@material-ui/icons/ThumbDown";
18 |
19 | const TVShowDetails = ({ name, vote_average, genres, number_of_seasons, year, overview }) => {
20 | const dispatch = useDispatch();
21 | const classes = useStyles();
22 | const [listed, setListed] = useState(false);
23 | const [like, setLike] = useState(false);
24 | const [unlike, setUnlike] = useState(false);
25 |
26 | return (
27 |
28 |
29 | {name}
30 |
31 |
32 |
33 | {genres.slice(0, 3).map((elem) => (
34 | {elem.name}
35 | ))}
36 | {year}
37 |
38 | {`${number_of_seasons} saison${(number_of_seasons > 1 )? "s" : ""}`}
39 |
40 |
41 |
42 |
43 | {overview.length > 300 ? `${overview.slice(0, 300)}...` : overview}
44 |
45 |
46 |
47 | }
52 | size="large"
53 | onClick={() => {
54 | dispatch(handleModal(true));
55 | }}
56 | >
57 | Reprendre
58 |
59 | setListed(!listed)}>
60 | {listed ? : }
61 |
62 | {
65 | setUnlike(false);
66 | setLike(!like);
67 | }}
68 | >
69 | {like ? : }
70 |
71 | {
74 | setLike(false);
75 | setUnlike(!unlike);
76 | }}
77 | >
78 | {unlike ? : }
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | const useStyles = makeStyles((theme) => ({
86 | name: {
87 | letterSpacing: "2px",
88 | textShadow: "2px 2px 4px rgb(0 0 0 / 45%)",
89 | },
90 | desc: {
91 | display: "flex",
92 | alignItems: "center",
93 | textShadow: "1px 1px 2px rgb(0 0 0 / 100%)",
94 | },
95 | infos: {
96 | padding: "5px 10px",
97 | marginRight: theme.spacing(1),
98 | },
99 | playBtn: {
100 | fontWeight: 700,
101 | marginRight: theme.spacing(2),
102 | textTransform: "capitalize",
103 | width: "250px",
104 | height: "50px",
105 | },
106 | listBtn: {
107 | backgroundColor: "rgba(109, 109, 110, 0.7)",
108 | fontWeight: 700,
109 | color: "#fff",
110 | "&:hover": {
111 | backgroundColor: "rgba(109, 109, 110, 0.4)",
112 | },
113 | textTransform: "capitalize",
114 |
115 | height: "50px",
116 | },
117 | overview: {
118 | textShadow: "1px 1px 2px rgb(0 0 0 / 100%)",
119 | fontSize: "18px",
120 | },
121 | }));
122 |
123 | export default TVShowDetails;
124 |
--------------------------------------------------------------------------------
/client/src/pages/TVShow/Trailers.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Grid from "@material-ui/core/Grid";
3 | import Typography from "@material-ui/core/Typography";
4 | import Paper from "@material-ui/core/Paper";
5 | import { makeStyles } from "@material-ui/core/styles";
6 |
7 | import Modal from "../../components/Modal";
8 |
9 | const Trailers = ({ title,crew, trailers }) => {
10 | const classes = useStyles();
11 | const director = crew
12 | .filter((elem) => elem.job === "Director")
13 | .map((elem) => elem.name)
14 | .join(", ");
15 | const screenplay = crew
16 | .filter((elem) => elem.job === "Screenplay")
17 | .map((elem) => elem.name)
18 | .join(", ");
19 | const producer = crew
20 | .filter((elem) => elem.job === "Producer")
21 | .map((elem) => elem.name)
22 | .join(", ");
23 | const author = crew
24 | .filter((elem) => elem.job === "Author")
25 | .map((elem) => elem.name)
26 | .join(", ");
27 | return (
28 |
29 |
30 | {director && (
31 |
32 | Réalisateur: {director}
33 |
34 | )}
35 | {screenplay && (
36 |
37 | Scénariste: {screenplay}
38 |
39 | )}
40 | {producer && (
41 |
42 | Producteur: {producer}
43 |
44 | )}
45 | {author && (
46 |
47 | Auteur: {author}
48 |
49 | )}
50 |
51 | {trailers &&
52 | {trailers.slice(0,3).map((elem) => (
53 |
54 |
64 |
65 | ))}
66 | }
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | const useStyles = makeStyles((theme) => ({
74 | iframeContainer: {
75 | position: "relative",
76 | width: "100%",
77 | paddingBottom: "56.25%",
78 | height: 0,
79 | borderRadius: "4px",
80 | overflow: "hidden",
81 | },
82 | iframe: {
83 | position: "absolute",
84 | top: 0,
85 | left: 0,
86 | width: "100%",
87 | height: "100%",
88 | },
89 | }));
90 |
91 | export default Trailers;
92 |
--------------------------------------------------------------------------------
/client/src/pages/TVShow/index.js:
--------------------------------------------------------------------------------
1 | import TVShow from './TVShow'
2 |
3 | export default TVShow
--------------------------------------------------------------------------------
/client/src/reducers/browseReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | BROWSE_HOME_LOAD,
3 | BROWSE_HOME_SUCCESS,
4 | BROWSE_HOME_FAIL,
5 | BROWSE_MOVIES_LOAD,
6 | BROWSE_MOVIES_SUCCESS,
7 | BROWSE_MOVIES_FAIL,
8 | BROWSE_TVSHOWS_LOAD,
9 | BROWSE_TVSHOWS_SUCCESS,
10 | BROWSE_TVSHOWS_FAIL,
11 | } from "../constants/actionTypes";
12 |
13 | const initialState = {
14 | isLoadingHome: false,
15 | isLoadingMovies: false,
16 | isLoadingTVShows: false,
17 | bannerHome: null,
18 | rowsHome: null,
19 | bannerMovies: null,
20 | rowsMovies: null,
21 | bannerTVShows: null,
22 | rowsTVShows: null,
23 | errorHome: null,
24 | errorMovies: null,
25 | errorTVShows: null,
26 | };
27 |
28 | const browseReducer = (state = initialState, { type, payload }) => {
29 | switch (type) {
30 | case BROWSE_HOME_LOAD:
31 | return { ...state, isLoadingHome: true };
32 | case BROWSE_MOVIES_LOAD:
33 | return { ...state, isLoadingMovies: true };
34 | case BROWSE_TVSHOWS_LOAD:
35 | return { ...state, isLoadingTVShows: true };
36 | case BROWSE_HOME_SUCCESS:
37 | return { ...state, bannerHome: payload.banner, rowsHome: payload.rows, isLoadingHome: false, errorHome:null };
38 | case BROWSE_MOVIES_SUCCESS:
39 | return { ...state, bannerMovies: payload.banner, rowsMovies: payload.rows, isLoadingMovies: false,errorMovies:null };
40 | case BROWSE_TVSHOWS_SUCCESS:
41 | return { ...state, bannerTVShows: payload.banner, rowsTVShows: payload.rows, isLoadingTVShows: false,errorTVShows:null };
42 | case BROWSE_HOME_FAIL:
43 | return { ...state, isLoadingHome: false, errorHome:payload };
44 | case BROWSE_MOVIES_FAIL:
45 | return { ...state, isLoadingMovies:false, errorMovies:payload };
46 | case BROWSE_TVSHOWS_FAIL:
47 | return { ...state, isLoadingTVShows:false, errorTVShows:payload };
48 | default:
49 | return state;
50 | }
51 | };
52 |
53 | export default browseReducer;
54 |
--------------------------------------------------------------------------------
/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux'
2 | import userReducer from './userReducer'
3 | import browseReducer from './browseReducer'
4 | import watchReducer from './watchReducer'
5 |
6 |
7 | const rootReducer = combineReducers({userReducer,browseReducer,watchReducer})
8 |
9 | export default rootReducer
--------------------------------------------------------------------------------
/client/src/reducers/userReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | SIGN_IN_USER_SUCCESS,
3 | SIGN_UP_USER_SUCCESS,
4 | SIGN_OUT_USER_SUCCESS,
5 | SET_LOADING,
6 | AUTH_FAIL,
7 | GET_AUTH_USER,
8 | SET_LOADING_LOCAL,
9 | SET_LOADING_GOOGLE,
10 | SET_DARK_MODE,
11 | } from "../constants/actionTypes";
12 |
13 | const initialState = {
14 | token: null,
15 | user: null,
16 | isLoading: true,
17 | isAuth: false,
18 | error: null,
19 | darkMode: true,
20 | };
21 |
22 | const userReducer = (state = initialState, { type, payload }) => {
23 | switch (type) {
24 | case SIGN_IN_USER_SUCCESS:
25 | return {
26 | ...state,
27 | isLoading: false,
28 | isLoadingLocal: false,
29 | isLoadingGoogle: false,
30 | isAuth: true,
31 | token: payload.token,
32 | user: payload.user,
33 | error: null,
34 | };
35 | case SIGN_UP_USER_SUCCESS:
36 | return {
37 | ...state,
38 | isLoading: false,
39 | isAuth: true,
40 | token: payload.token,
41 | user: payload.user,
42 | error: null,
43 | };
44 | case SET_LOADING:
45 | return { ...state, isLoading: true };
46 | case SET_LOADING_LOCAL:
47 | return { ...state, isLoading: true, isLoadingLocal: true, isLoadingGoogle: false };
48 | case SET_LOADING_GOOGLE:
49 | return { ...state, isLoading: true, isLoadingGoogle: true, isLoadingLocal: false };
50 | case AUTH_FAIL:
51 | return {
52 | ...state,
53 | token: null,
54 | user: null,
55 | isLoading: false,
56 | isLoadingLocal: false,
57 | isLoadingGoogle: false,
58 | isAuth: false,
59 | error: payload,
60 | };
61 | case GET_AUTH_USER:
62 | return {
63 | ...state,
64 | isLoading: false,
65 | isLoadingGoogle: false,
66 | isLoadingLocal: false,
67 | isAuth: true,
68 | user: payload.user,
69 | };
70 | case SIGN_OUT_USER_SUCCESS:
71 | return {
72 | ...state,
73 | token: null,
74 | user: null,
75 | isLoading: false,
76 | isLoadingLocal: false,
77 | isLoadingGoogle: false,
78 | isAuth: false,
79 | error: payload,
80 | };
81 | case SET_DARK_MODE:
82 | return { ...state, darkMode: payload };
83 | default:
84 | return state;
85 | }
86 | };
87 |
88 | export default userReducer;
89 |
--------------------------------------------------------------------------------
/client/src/reducers/watchReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MEDIA_LOAD,GET_MEDIA_SUCCESS,GET_MEDIA_FAIL,HANDLE_MODAL_IS_OPEN
3 | } from "../constants/actionTypes";
4 |
5 | const initialState = {
6 | media: null,
7 | mediaType: null,
8 | isLoading: false,
9 | modalIsOpen : false,
10 | error:null
11 | };
12 |
13 | const watchReducer = (state = initialState, { type, payload }) => {
14 | switch (type) {
15 | case GET_MEDIA_LOAD:
16 | return {
17 | ...state,
18 | isLoading: true,
19 | };
20 | case GET_MEDIA_SUCCESS:
21 | return {
22 | ...state,
23 | isLoading:false,
24 | media: payload.media,
25 | mediaType: payload.mediaType,
26 | error: null,
27 | };
28 | case GET_MEDIA_FAIL:
29 | return {
30 | ...state,
31 | isLoading:false,
32 | error: payload,
33 | };
34 | case HANDLE_MODAL_IS_OPEN:
35 | return {
36 | ...state,
37 | modalIsOpen:payload
38 | }
39 | default:
40 | return state;
41 | }
42 | };
43 |
44 | export default watchReducer;
45 |
--------------------------------------------------------------------------------
/client/src/requests.js:
--------------------------------------------------------------------------------
1 | const API_KEY = "71741288544550e3b57f3a8dca4493fc"
2 |
3 | const requests = [
4 | {name:"Trending TV Shows",url:`https://api.themoviedb.org/3/trending/tv/week?api_key=${API_KEY}`},
5 | {name:"Trending movies",url:`https://api.themoviedb.org/3/trending/movie/week?api_key=${API_KEY}`},
6 | {name:"Action movies",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=28`},
7 | ]
8 |
9 | const requestsMovies = [
10 | {name:"Trending",url:`https://api.themoviedb.org/3/trending/movie/week?api_key=${API_KEY}`},
11 | {name:"Action",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=28`},
12 | {name:"Adventure",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=12`},
13 | {name:"Comedy",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=35`},
14 | {name:"Animation",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=16`},
15 | {name:"Crime",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=80`},
16 | {name:"Drama",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=18`},
17 | {name:"Family",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=10751`},
18 | {name:"Fantasy",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=14`},
19 | {name:"History",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=36`},
20 | {name:"Horror",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=27`},
21 | {name:"Mystery",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=9648`},
22 | {name:"ScienceFiction",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=878`},
23 | {name:"Documentary",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=99`},
24 | ]
25 |
26 | const requestsTVShows = [
27 | {name:"Trending",url:`https://api.themoviedb.org/3/trending/tv/week?api_key=${API_KEY}`},
28 | {name:"Action & Adventure",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=10759`},
29 | {name:"Animation",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=16`},
30 | {name:"Comedy",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=35`},
31 | {name:"Drama",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=18`},
32 | {name:"Mystery",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=9648`},
33 | {name:"Family",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=10751`},
34 | {name:"Kids",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=10762`},
35 | {name:"Sci-Fi & Fantasy",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=10765`},
36 | {name:"War & Politics",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=10768`},
37 | {name:"Documentary",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=99`},
38 | ]
39 |
40 | export {requests,requestsMovies,requestsTVShows};
--------------------------------------------------------------------------------
/client/src/store/index.js:
--------------------------------------------------------------------------------
1 | import {createStore,compose,applyMiddleware} from 'redux'
2 | import thunk from 'redux-thunk'
3 | import rootReducer from '../reducers'
4 |
5 | const devtools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
6 |
7 | const store = createStore(rootReducer, compose(applyMiddleware(thunk), devtools))
8 |
9 | export default store
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tunflix",
3 | "version": "1.0.0",
4 | "description": "streaming website",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "concurrently \"babel-node server\" \"cd client && npm start\"",
8 | "dev": "concurrently \"nodemon --exec node_modules/.bin/babel-node server\" \"cd client && npm start\"",
9 | "server": "nodemon --exec node_modules/.bin/babel-node server"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@material-ui/icons": "^4.11.2",
15 | "@material-ui/lab": "^4.0.0-alpha.58",
16 | "@splidejs/react-splide": "^0.4.4",
17 | "axios": "^0.21.1",
18 | "bcrypt": "^5.0.1",
19 | "cheerio": "^1.0.0-rc.10",
20 | "cors": "^2.8.5",
21 | "dotenv": "^10.0.0",
22 | "express": "^4.17.1",
23 | "express-validator": "^6.12.0",
24 | "got": "^9.6.0",
25 | "jsonwebtoken": "^8.5.1",
26 | "mongoose": "^5.12.13",
27 | "morgan": "^1.10.0",
28 | "p-map": "^1.2.0",
29 | "passport": "^0.4.1",
30 | "passport-google-oauth20": "^2.0.0",
31 | "passport-jwt": "^4.0.0",
32 | "react-helmet": "^6.1.0",
33 | "srt-to-vtt": "^1.1.3",
34 | "streamz": "^1.8.12",
35 | "theelitesubs": "^1.0.0",
36 | "unzip-stream": "^0.3.1",
37 | "unzipper": "^0.9.15",
38 | "yifysubtitles": "^2.1.5",
39 | "ytssubs": "^1.0.2"
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.14.5",
43 | "@babel/core": "^7.14.6",
44 | "@babel/node": "^7.14.5",
45 | "@babel/preset-env": "^7.14.5",
46 | "concurrently": "^6.2.0",
47 | "eslint": "^7.29.0",
48 | "eslint-plugin-import": "^2.23.4",
49 | "eslint-plugin-jsx-a11y": "^6.4.1",
50 | "eslint-plugin-prettier": "^3.4.0",
51 | "eslint-plugin-react": "^7.24.0",
52 | "eslint-plugin-react-hooks": "^4.2.0",
53 | "nodemon": "^2.0.7",
54 | "prettier": "^2.3.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/screenshots/browse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/screenshots/browse.png
--------------------------------------------------------------------------------
/screenshots/connexion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/screenshots/connexion.png
--------------------------------------------------------------------------------
/screenshots/inscription.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/screenshots/inscription.png
--------------------------------------------------------------------------------
/screenshots/movie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/screenshots/movie.png
--------------------------------------------------------------------------------
/screenshots/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/screenshots/player.png
--------------------------------------------------------------------------------
/screenshots/tvshow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehdibha/Tunflix/ca9b39fc737be792fbb17611a258b939233fff80/screenshots/tvshow.png
--------------------------------------------------------------------------------
/server/config/connectDB.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 | import config from '.'
3 |
4 | const connectDB = async () => {
5 | try {
6 | await mongoose.connect(config.mongoUri, config.mongoOptions);
7 | console.log("The DB is connected");
8 | } catch (error) {
9 | console.log('DB connection error :' , error);
10 | }
11 | };
12 |
13 | export default connectDB;
14 |
--------------------------------------------------------------------------------
/server/config/env.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv'
2 | dotenv.config({ path: "./server/config/.env" }); // dotenv path
3 |
--------------------------------------------------------------------------------
/server/config/index.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | env: process.env.NODE_ENV || 'development',
3 | port: process.env.PORT || 4444,
4 | jwtSecret: process.env.JWT_SECRET,
5 | mongoUri: process.env.MONGODB_URI,
6 | mongoOptions: {
7 | useNewUrlParser: true,
8 | useUnifiedTopology: true,
9 | useFindAndModify: false,
10 | useCreateIndex: true,
11 | },
12 | jwtSecret: process.env.JWT_SECRET,
13 | googleIdClient : process.env.GOOGLE_ID_CLIENT,
14 | googleSecret : process.env.GOOGLE_SECRET,
15 | TMDBApiKey : process.env.TMDB_API_KEY
16 | }
17 |
18 | export default config
--------------------------------------------------------------------------------
/server/config/passport.js:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 | import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
3 | import { Strategy as GoogleStrategy } from "passport-google-oauth20";
4 | import config from "./index";
5 | import User from "../models/user.model";
6 |
7 | const JWToptions = {
8 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
9 | secretOrKey: config.jwtSecret,
10 | };
11 |
12 | const googleOptions = {
13 | clientID: config.googleIdClient,
14 | clientSecret: config.googleSecret,
15 | callbackURL: "http://localhost:4444/api/auth/google/callback",
16 | };
17 |
18 | passport.use(
19 | new JwtStrategy(JWToptions, async (paylaod, done) => {
20 | try {
21 | const user = await User.findById(paylaod.userId).select("-password");
22 | if (!user) {
23 | done(null, false);
24 | }
25 | done(null, user);
26 | } catch (err) {
27 | done(err, false);
28 | }
29 | })
30 | );
31 |
32 | passport.serializeUser((user, done) => {
33 | done(null, user.id);
34 | });
35 |
36 | passport.deserializeUser((id, done) => {
37 | User.findById(id, (err, user) => {
38 | done(err, user);
39 | });
40 | });
41 |
42 | passport.use(
43 | new GoogleStrategy(googleOptions, async (accessToken, refreshToken, profile, done) => {
44 | try {
45 | let user = await User.findOne({ email: profile.emails[0].value });
46 | if (user) {
47 | if (!user.googleId) {
48 | user.googleId = profile.id;
49 | user.avatar = profile.photos[0].value;
50 | await user.save();
51 | }
52 | done(null, user);
53 | } else {
54 | user = new User({
55 | email: profile.emails[0].value,
56 | firstName: profile.name.givenName,
57 | lastName: profile.name.familyName,
58 | googleId: profile.id,
59 | avatar: profile.picture,
60 | });
61 | }
62 | await user.save();
63 | done(null, user);
64 | } catch (error) {
65 | console.log(error);
66 | }
67 | })
68 | );
69 |
--------------------------------------------------------------------------------
/server/controllers/auth.controller.js:
--------------------------------------------------------------------------------
1 | import User from "../models/user.model";
2 | import bcrypt from "bcrypt";
3 | import jwt from "jsonwebtoken";
4 | import config from "../config";
5 |
6 | const signUp = async (req, res) => {
7 | try {
8 | const { firstName, lastName, email, password } = req.body;
9 | let user = await User.findOne({ email });
10 | if (user) {
11 | return res.status(400).json({ success: false, msg: "L'email est déjà utilisé" });
12 | }
13 | user = new User({ firstName, lastName, email, password });
14 | const salt = await bcrypt.genSalt(10);
15 | user.password = await bcrypt.hash(password, salt);
16 |
17 | await user.save();
18 |
19 | delete user['password'];
20 |
21 | const payload = {
22 | userId: user.id,
23 | };
24 |
25 | const token = jwt.sign(payload, config.jwtSecret, { expiresIn: "7d" });
26 |
27 | res.status(201).json({
28 | token: `Bearer ${token}`,
29 | user
30 | });
31 | } catch (err) {
32 | res.status(500).json([{ msg: err.message }]);
33 | }
34 | };
35 |
36 | const signIn = async (req, res) => {
37 | const { email, password } = req.body;
38 | try {
39 | let user = await User.findOne({ email });
40 | if (!user) {
41 | return res.status(400).json([{ msg: "Email ou mot de passe incorrect" }]);
42 | }
43 | const isMatch = await bcrypt.compare(password, user.password);
44 |
45 | if (!isMatch) {
46 | return res.status(400).json([{ msg: "Email ou mot de passe incorrect" }]);
47 | }
48 |
49 | const payload = {
50 | userId: user.id,
51 | };
52 | const token = jwt.sign(payload, config.jwtSecret, { expiresIn: "7d" });
53 | res.status(201).json({
54 | token: `Bearer ${token}`,
55 | user: {
56 | firstName: user.firstName,
57 | lastName: user.lastName,
58 | email: user.email,
59 | },
60 | });
61 | } catch (error) {
62 | res.json([{ msg: error }]);
63 | }
64 | };
65 |
66 | const googleSignIn = (req, res) => {
67 | try {
68 | const payload = {
69 | userId: req.user.id,
70 | };
71 | const token = jwt.sign(payload, config.jwtSecret, { expiresIn: "7d" });
72 | res.cookie('auth-cookie',`Bearer ${token}`)
73 | res.redirect('http://localhost:3000/browse')
74 | } catch (error) {
75 | res.send([{ msg: err.message }])
76 | }
77 | };
78 |
79 | const forgotPassword = async (req, res) => {
80 | try {
81 | res.send("forgotPWD is ok");
82 | } catch (error) {
83 | res.send(error);
84 | }
85 | };
86 |
87 | const resetPassword = async (req, res) => {
88 | try {
89 | res.send("resetPWD is ok");
90 | } catch (error) {
91 | res.send(error);
92 | }
93 | };
94 |
95 | const getAuthUser = (req, res) => {
96 | try {
97 | res.send({ user: req.user });
98 | } catch (error) {
99 | res.status(401).send([{msg:'Unauthorized'}])
100 | }
101 | };
102 |
103 | export { signUp, signIn, forgotPassword, resetPassword, getAuthUser, googleSignIn };
104 |
--------------------------------------------------------------------------------
/server/controllers/browse.controller.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { requestsHome, requestsMovies, requestsTVShows } from "../utils/requests";
3 | import {fetchTMDB} from '../utils/movie.utils'
4 | import {fetchTMDB as fetchTMDBshows} from '../utils/tvshow.utils'
5 |
6 | const browseHome = async (req, res) => {
7 | try {
8 | const fetchData = async ({ name, url }) => {
9 | const { data } = await axios.get(url);
10 | return { title: name, data: data.results };
11 | };
12 | const rows = [
13 | ...requestsHome,
14 | ...requestsMovies
15 | .filter((elem) => elem.name !== "Trending")
16 | .map((elem) => ({ ...elem, name: `${elem.name} movies` })),
17 | ...requestsTVShows
18 | .filter((elem) => elem.name !== "Trending")
19 | .map((elem) => ({ ...elem, name: `${elem.name} TV Shows` })),
20 | ];
21 | const results = await axios.all(rows.map((elem) => fetchData(elem)));
22 | let random = results.filter((elem) => elem.title === "Trending movies")[0]
23 | random = random.data[Math.floor(Math.random() * (random.data.length - 1))];
24 | const banner = await fetchTMDB(random.id)
25 | res.json({
26 | banner,
27 | rows: results,
28 | });
29 | } catch (error) {
30 | console.log(error);
31 | }
32 | };
33 |
34 | const browseMovies = async (req, res) => {
35 | try {
36 | const fetchData = async ({ name, url }) => {
37 | const { data } = await axios.get(url);
38 | return { title: name, data: data.results };
39 | };
40 | const rows = requestsMovies.map((elem) => ({ ...elem, name: `${elem.name} movies` }));
41 | const results = await axios.all(rows.map((elem) => fetchData(elem)));
42 | let random = results.filter((elem) => elem.title === "Trending movies")[0]
43 | random = random.data[Math.floor(Math.random() * (random.data.length - 1))];
44 | const banner = await fetchTMDB(random.id)
45 | res.json({
46 | banner,
47 | rows: results,
48 | });
49 | } catch (error) {
50 | console.log(error);
51 | }
52 | };
53 |
54 | const browseTVShows = async (req, res) => {
55 | try {
56 | const fetchData = async ({ name, url }) => {
57 | const { data } = await axios.get(url);
58 | return { title: name, data: data.results };
59 | };
60 | const rows = requestsTVShows.map((elem) => ({ ...elem, name: `${elem.name} TV Shows` }));
61 | const results = await axios.all(rows.map((elem) => fetchData(elem)));
62 | let random = results.filter((elem) => elem.title === "Trending TV Shows")[0]
63 | random = random.data[Math.floor(Math.random() * (random.data.length - 1))];
64 | const banner = await fetchTMDBshows(random.id)
65 | res.json({
66 | banner,
67 | rows: results,
68 | });
69 | } catch (error) {
70 | console.log(error);
71 | }
72 | };
73 |
74 | export { browseHome, browseMovies, browseTVShows };
75 |
--------------------------------------------------------------------------------
/server/controllers/movie.controller.js:
--------------------------------------------------------------------------------
1 | import {
2 | fetchTMDB,
3 | } from "../utils/movie.utils";
4 |
5 | const getMovie = async (req, res) => {
6 | try {
7 | const tmdb_id = req.params.tmdb_id;
8 | const movie = await fetchTMDB(tmdb_id);
9 | res.status(200).json({
10 | ...movie,
11 | videoLinks: [
12 | {
13 | lang: "fr",
14 | url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
15 | },
16 | {
17 | lang : 'en',
18 | url : 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4'
19 | }
20 | ],
21 | });
22 | } catch (error) {
23 | res.json([{ msg: error.message }]);
24 | }
25 | };
26 |
27 | // const fetchSubs = async (req, res, next) => {
28 | // try {
29 | // const tmdb_id = req.params.tmdb_id;
30 | // const movie = await fetchTMDB(tmdb_id);
31 | // const subs = await fetchYTS(
32 | // movie.imdb_id,
33 | // ["Arabic", "French", "English"],
34 | // __dirname + `/../assets/captions/${tmdb_id}`
35 | // );
36 | // next();
37 | // } catch (error) {
38 | // res.json([{ msg: error.message }]);
39 | // }
40 | // };
41 |
42 | const getSub = async (req, res, next) => {
43 | const languages = { en: "English", fr: "French", ar: "Arabic" };
44 | try {
45 | res.sendFile(`/assets/captions/51876${languages[req.params.language]}.vtt`, {
46 | root: __dirname + "/..",
47 | });
48 | } catch (error) {
49 | res.status(400).json([{ msg: error.message }]);
50 | }
51 | };
52 |
53 | export { getMovie, getSub };
54 |
--------------------------------------------------------------------------------
/server/controllers/tvshow.controller.js:
--------------------------------------------------------------------------------
1 | import { fetchTMDB } from "../utils/tvshow.utils";
2 |
3 |
4 | const getTVShow = async (req, res) => {
5 | try {
6 | const tmdb_id = req.params.tmdb_id;
7 | const show = await fetchTMDB(tmdb_id);
8 | res.status(200).json({ ...show });
9 | } catch (error) {
10 | res.json([{ msg: error.message }]);
11 | }
12 | };
13 |
14 | export { getTVShow }
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import "./config/env";
2 | import express from "express";
3 | import cors from "cors";
4 | import connectDB from "./config/connectDB.js";
5 | import authRoute from "./routes/auth.routes.js";
6 | import browseRoute from "./routes/browse.routes.js";
7 | import movieRoute from "./routes/movie.routes.js";
8 | import tvshowRoute from "./routes/tvshow.routes.js";
9 | import passport from "passport";
10 | import morgan from "morgan";
11 | import config from "./config";
12 | import "./config/passport";
13 |
14 | const app = express();
15 | connectDB(); // connect DB
16 |
17 | // middleWares
18 | app.use(cors());
19 | app.use(express.json());
20 | app.use(morgan("dev"));
21 | app.use(passport.initialize());
22 |
23 | // routes
24 | app.use("/api/auth", authRoute);
25 | app.use("/api/browse", browseRoute);
26 | app.use("/api/movie", movieRoute);
27 | app.use("/api/tv", tvshowRoute);
28 |
29 |
30 | // server
31 | app.listen(config.port, () => console.log(`*** Server running on port ${config.port} ***`));
32 |
33 | process.on("SIGINT", () => process.exit(1));
34 |
--------------------------------------------------------------------------------
/server/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | import passport from "passport";
2 |
3 | const auth = passport.authenticate("jwt", { session: false });
4 |
5 | const authGoogle = passport.authenticate("google", {
6 | scope: ["profile", "email"],
7 | });
8 |
9 | const authGoogleCallback = passport.authenticate("google", { failureRedirect: "/signin", session: false });
10 |
11 | export { auth, authGoogle, authGoogleCallback };
12 |
--------------------------------------------------------------------------------
/server/middlewares/validator.js:
--------------------------------------------------------------------------------
1 | import { body, validationResult } from "express-validator";
2 |
3 | const registerRules = () => [
4 | body("firstName", "Le prénom n'est pas valide").notEmpty().isLength({ max: 20 }),
5 | body("lastName", "Le nom n'est pas valide").notEmpty().isLength({ max: 20 }),
6 | body("email", "L'email n'est pas valide").isEmail(),
7 | body("password", "Le mot de passe n'est pas valide").isLength({
8 | min: 6,
9 | max: 20,
10 | }),
11 | ];
12 |
13 | const loginRules = () => [
14 | body("email", "L'email n'est pas valide").isEmail(),
15 | body("password", "Le mot de passe n'est pas valide").isLength({
16 | min: 6,
17 | max: 20,
18 | }),
19 | ];
20 |
21 | const customErrors = (Array) => Array.map((err) => ({ msg: err.msg }));
22 |
23 | const validator = (req, res, next) => {
24 | const errors = validationResult(req);
25 | if (!errors.isEmpty()) {
26 | return res.status(400).json(customErrors(errors.array()));
27 | } else {
28 | next();
29 | }
30 | };
31 |
32 | export {
33 | registerRules,
34 | loginRules,
35 | validator,
36 | };
37 |
--------------------------------------------------------------------------------
/server/models/movie.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const movieSchema = new mongoose.Schema(
4 | {
5 | tmdb_id: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | },
10 | imdb_id: {
11 | type: String,
12 | unique: true,
13 | },
14 | title: {
15 | type: String,
16 | required: true,
17 | },
18 | overview: {
19 | type: String,
20 | required: true,
21 | },
22 | genres: {
23 | type: [
24 | {
25 | genreId: String,
26 | genreName: String,
27 | },
28 | ],
29 | required: true,
30 | },
31 | release_date: {
32 | type: String,
33 | required: true,
34 | },
35 | runtime: {
36 | type: Number,
37 | required: true,
38 | },
39 | poster_path: {
40 | type: String,
41 | required: true,
42 | },
43 | backdrop_path: {
44 | type: String,
45 | required: true,
46 | },
47 | imdb_rating: {
48 | type: Number,
49 | },
50 | trailers: {
51 | type: [
52 | {
53 | name: String,
54 | key: String,
55 | },
56 | ],
57 | required: true,
58 | },
59 | cast: {
60 | type: [{ id: String, name: String, profile_path: String, character: String }],
61 | required: true,
62 | },
63 | crew: {
64 | type: [{ id: String, name: String, job: String }],
65 | required: true,
66 | },
67 | likers: {
68 | type: [String],
69 | required: true,
70 | },
71 | unlikers: {
72 | type: [String],
73 | required: true,
74 | },
75 | comments: {
76 | type: [
77 | {
78 | commenterId: String,
79 | commenterFullName: String,
80 | text: String,
81 | timestamp: Number,
82 | },
83 | ],
84 | required: true,
85 | },
86 | media_path: {
87 | type : [
88 | {
89 | source : String,
90 | url : String
91 | }
92 | ]
93 | }
94 | },
95 | {
96 | timestamps: true,
97 | }
98 | );
99 |
100 | export default mongoose.model("movie", movieSchema);
101 |
--------------------------------------------------------------------------------
/server/models/user.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = new mongoose.Schema(
4 | {
5 | firstName: {
6 | type: String,
7 | required: [true, "Le prénom est requis"],
8 | trim:true
9 | },
10 | lastName: {
11 | type: String,
12 | required: [true, "le nom est requis"],
13 | trim:true
14 | },
15 | email: {
16 | type: String,
17 | required: [true, "L'email est requis"],
18 | trim:true,
19 | unique: true,
20 | },
21 | password: {
22 | type: String,
23 | required: false,
24 | minLength: 6
25 | },
26 | avatar: {
27 | type: String,
28 | default: "https://ih0.redbubble.net/image.618427277.3222/flat,1000x1000,075,f.u2.jpg",
29 | },
30 | googleId: {
31 | type: String,
32 | },
33 | likes: {
34 | type: [String],
35 | },
36 | unlikes : {
37 | type: [String]
38 | },
39 | watchList: {
40 | type: [String],
41 | },
42 | resetPasswordToken: {
43 | type: String,
44 | },
45 | resetPasswordExpire: {
46 | type: Date,
47 | },
48 | },
49 | {
50 | timestamps: true,
51 | }
52 | );
53 |
54 | export default mongoose.model("user", userSchema);
55 |
--------------------------------------------------------------------------------
/server/routes/auth.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import {signIn,signUp, googleSignIn, forgotPassword,resetPassword, getAuthUser} from "../controllers/auth.controller";
3 | import {loginRules,registerRules,validator} from "../middlewares/validator"
4 | import {auth,authGoogle,authGoogleCallback} from '../middlewares/auth'
5 |
6 | const router = express.Router();
7 |
8 | router.route("/signup").post(registerRules(),validator,signUp);
9 |
10 | router.route("/signin").post(loginRules(),validator,signIn);
11 |
12 | router.route('/google').get(authGoogle)
13 |
14 | router.route('/google/callback').get(authGoogleCallback,googleSignIn)
15 |
16 | router.route("/current").get(auth, getAuthUser);
17 |
18 | router.route("/forgotpassword").post(forgotPassword);
19 |
20 | router.route("/resetpassword/:resetToken").post(resetPassword);
21 |
22 | export default router;
--------------------------------------------------------------------------------
/server/routes/browse.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import {browseHome,browseMovies,browseTVShows} from '../controllers/browse.controller'
3 | import {auth} from '../middlewares/auth'
4 |
5 |
6 | const router = express.Router();
7 |
8 | router.route("/home").get(auth,browseHome);
9 | router.route("/movies").get(auth,browseMovies);
10 | router.route("/tvshows").get(auth,browseTVShows);
11 |
12 |
13 | export default router;
--------------------------------------------------------------------------------
/server/routes/movie.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import {auth} from '../middlewares/auth'
3 | import {getMovie,getSub} from "../controllers/movie.controller"
4 |
5 |
6 | const router = express.Router();
7 |
8 | router.route("/:tmdb_id").get(auth,getMovie);
9 | router.route("/subs/:tmdb_id/:language").get(getSub);
10 |
11 |
12 |
13 | export default router;
--------------------------------------------------------------------------------
/server/routes/tvshow.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import {auth} from '../middlewares/auth'
3 | import { getTVShow } from "../controllers/tvshow.controller";
4 | import {getMovie, fetchSubs,getSub} from "../controllers/movie.controller"
5 |
6 |
7 | const router = express.Router();
8 |
9 | router.route("/:tmdb_id").get(auth,getTVShow);
10 |
11 |
12 |
13 | export default router;
--------------------------------------------------------------------------------
/server/utils/captions.utils.js:
--------------------------------------------------------------------------------
1 | import ytssubs from "ytssubs";
2 | import got from "got";
3 | import { createWriteStream } from "fs";
4 | import unzip from "unzip-stream";
5 | import srt2vtt from 'srt-to-vtt';
6 |
7 | const fetchYTS = async (imdb_id, langArray, path) => {
8 | ytssubs.getSubs(imdb_id, (err, results) => {
9 | if (err) {
10 | throw err;
11 | }
12 | if (!results){
13 | throw err
14 | }
15 | const filteredResults = langArray.reduce((acc, l) => {
16 | const languages = results.subs.filter((s) => s.lang === l).sort((a, b) => b.rating - a.rating);
17 | if (languages.length > 0) {
18 | acc[l] = languages[0];
19 | }
20 | return acc;
21 | }, {});
22 | const prom = Object.keys(filteredResults).map(elem => {
23 | got.stream(filteredResults[elem].url).pipe(unzip.Parse()).on('entry', (entry) => {
24 | var filePath = path;
25 | if (filePath === path) {
26 | entry.pipe(srt2vtt()).pipe(createWriteStream(path + `${elem}.vtt`));
27 | } else {
28 | entry.autodrain();
29 | }
30 | })
31 | });
32 | });
33 | };
34 |
35 | export { fetchYTS };
36 |
--------------------------------------------------------------------------------
/server/utils/movie.utils.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import config from "../config";
3 | import cheerio from "cheerio";
4 | import Movie from "../models/movie.model";
5 |
6 | const fetchTMDB = async (id) => {
7 | try {
8 | const { data } = await axios.get(
9 | `http://api.themoviedb.org/3/movie/${id}?api_key=${config.TMDBApiKey}&append_to_response=videos,credits`
10 | );
11 | const imdb_id = data.imdb_id ? data.imdb_id : null;
12 | const overview = data.overview ? data.overview : null;
13 | const title = data.title || data.original_title;
14 | const popularity = data.popularity ? data.popularity : null;
15 | const backdrop_path = data.backdrop_path ? data.backdrop_path : null;
16 | const poster_path = data.poster_path ? data.poster_path : null;
17 | const year = data.release_date ? data.release_date : null;
18 | const imdb_rating = data.vote_average ? data.vote_average : null;
19 | const runtime = data.runtime ? data.runtime : null;
20 | const genres = data.genres ? data.genres : [];
21 | const videos = data.videos.results
22 | ? data.videos.results
23 | .map((elem) => ({ name: elem.name, key: elem.key, type: elem.type }))
24 | : [];
25 | let cast = data.credits.cast
26 | .filter((elem) => elem["known_for_department"] === "Acting")
27 | .filter((elem) => elem.profile_path !== null && elem.name !== null && elem.character !== null)
28 | .sort((elemA, elemB) => elemA.order - elemB.order)
29 | .slice(0, 12)
30 | .map((elem) => ({
31 | id: elem.id,
32 | name: elem.name || elem.original_name,
33 | profile_path: elem.profile_path,
34 | character: elem.character,
35 | }));
36 |
37 | const crew = data.credits.crew
38 | .filter(
39 | (elem) =>
40 | elem.job === "Producer" ||
41 | elem.job === "Director" ||
42 | elem.job === "Author" ||
43 | elem.job === "Screenplay"
44 | )
45 | .map((elem) => ({ id: elem.id, name: elem.name, job: elem.job }));
46 | return {
47 | tmdb_id: id,
48 | imdb_id,
49 | title,
50 | popularity,
51 | overview,
52 | genres,
53 | poster_path,
54 | backdrop_path,
55 | year,
56 | imdb_rating,
57 | runtime,
58 | videos,
59 | cast,
60 | crew,
61 | };
62 | } catch (error) {
63 | console.log(error);
64 | return null;
65 | }
66 | };
67 |
68 | export {
69 | fetchTMDB,
70 | };
71 |
--------------------------------------------------------------------------------
/server/utils/requests.js:
--------------------------------------------------------------------------------
1 | import config from '../config'
2 |
3 | const API_KEY = config.TMDBApiKey
4 |
5 | const requestsHome = [
6 | {name:"Trending movies",url:`https://api.themoviedb.org/3/trending/movie/week?api_key=${API_KEY}`},
7 | {name:"Trending TV Shows",url:`https://api.themoviedb.org/3/trending/tv/week?api_key=${API_KEY}`},
8 | ]
9 |
10 | const requestsMovies = [
11 | {name:"Trending",url:`https://api.themoviedb.org/3/trending/movie/week?api_key=${API_KEY}`},
12 | {name:"Action",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=28`},
13 | {name:"Adventure",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=12`},
14 | {name:"Comedy",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=35`},
15 | {name:"Animation",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=16`},
16 | {name:"Crime",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=80`},
17 | {name:"Drama",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=18`},
18 | {name:"Family",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=10751`},
19 | {name:"Fantasy",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=14`},
20 | {name:"History",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=36`},
21 | {name:"Horror",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=27`},
22 | {name:"Mystery",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=9648`},
23 | {name:"ScienceFiction",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=878`},
24 | {name:"Documentary",url:`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&with_genres=99`},
25 | ]
26 |
27 | const requestsTVShows = [
28 | {name:"Trending",url:`https://api.themoviedb.org/3/trending/tv/week?api_key=${API_KEY}`},
29 | {name:"Action & Adventure",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=10759`},
30 | {name:"Animation",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=16`},
31 | {name:"Comedy",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=35`},
32 | {name:"Drama",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=18`},
33 | {name:"Mystery",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=9648`},
34 | {name:"Family",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=10751`},
35 | {name:"Kids",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=10762`},
36 | {name:"Sci-Fi & Fantasy",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=10765`},
37 | {name:"War & Politics",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=10768`},
38 | {name:"Documentary",url:`https://api.themoviedb.org/3/discover/tv?api_key=${API_KEY}&with_genres=99`},
39 | ]
40 |
41 | export {requestsHome,requestsMovies,requestsTVShows};
--------------------------------------------------------------------------------
/server/utils/tvshow.utils.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import config from "../config";
3 |
4 | const fetchTMDB = async (id) => {
5 | try {
6 | const [{ data }, requestTrailer] = await axios.all([
7 | axios.get(
8 | `http://api.themoviedb.org/3/tv/${id}?api_key=${config.TMDBApiKey}&append_to_response=credits&language=fr`
9 | ),
10 | axios.get(`http://api.themoviedb.org/3/tv/${id}/videos?api_key=${config.TMDBApiKey}&language=fr`),
11 | ]);
12 | const name = data.name ? data.name : null;
13 | const original_name = data.original_name ? data.original_name : null;
14 | const overview = data.overview ? data.overview : null;
15 | const runtime = data.episode_runtime ? data.episode_runtime : null;
16 | const year = data.first_air_date ? data.first_air_date.slice(0, 4) : null;
17 | const genres = data.genres ? data.genres : [];
18 | const networks = data.networks
19 | ? data.networks.map((elem) => ({ name: elem.name, logo_path: elem.logo_path }))
20 | : null;
21 | const backdrop_path = data.backdrop_path ? `https://image.tmdb.org/t/p/original/${data.backdrop_path}` : null;
22 | const poster_path = data.poster_path ? `https://image.tmdb.org/t/p/original/${data.poster_path}` : null;
23 | const vote_average = data.vote_average ? data.vote_average : null;
24 | const number_of_seasons = data.number_of_seasons ? data.number_of_seasons : null;
25 | const trailers =
26 | requestTrailer.data.results.length === 0
27 | ? null
28 | : requestTrailer.data.results
29 | .filter((elem) => !elem.name.toLowerCase().includes("vost"))
30 | .map((elem) => elem.key);
31 | let seasons = await axios.all(
32 | [...Array(number_of_seasons).keys()].map((k) =>
33 | axios.get(
34 | `https://api.themoviedb.org/3/tv/${id}/season/${k + 1}?api_key=${config.TMDBApiKey}&language=fr`
35 | )
36 | )
37 | );
38 | seasons = seasons
39 | .map((elem) => elem.data)
40 | .map((elem) => ({
41 | name: elem.name,
42 | overview: elem.overview,
43 | poster_path: elem.poster_path,
44 | season_number: elem.season_number,
45 | air_date: elem.air_date,
46 | episodes: elem.episodes.map((el) => ({
47 | name: el.name,
48 | overview: el.overview,
49 | poster_path: el.still_path,
50 | air_date: el.air_date,
51 | episode_number: el.episode_number,
52 | vote_average: el.vote_average,
53 | })),
54 | }));
55 | let cast = data.credits.cast
56 | .filter((elem) => elem.profile_path !== null && elem.name !== null && elem.character !== null)
57 | .slice(0, 8);
58 | if (cast) {
59 | cast = cast.map((elem) => ({
60 | id: elem.id,
61 | name: elem.name,
62 | profile_path: elem.profile_path,
63 | character: elem.character,
64 | }));
65 | }
66 | const crew = data.credits.crew
67 | .filter(
68 | (elem) =>
69 | elem.job === "Producer" ||
70 | elem.job === "Director" ||
71 | elem.job === "Author" ||
72 | elem.job === "Screenplay"
73 | )
74 | .map((elem) => ({ id: elem.id, name: elem.name, job: elem.job }));
75 | return {
76 | name,
77 | original_name,
78 | overview,
79 | runtime,
80 | year,
81 | genres,
82 | networks,
83 | poster_path,
84 | backdrop_path,
85 | vote_average,
86 | number_of_seasons,
87 | trailers,
88 | seasons,
89 | cast,
90 | crew,
91 | };
92 | } catch (error) {
93 | return null;
94 | }
95 | };
96 |
97 | export { fetchTMDB };
98 |
--------------------------------------------------------------------------------