├── src ├── App.css ├── Components │ ├── TopRatedMovies │ │ ├── TopRatedMovies.css │ │ └── TopRatedMovies.jsx │ ├── RecommendedMovies │ │ ├── RecommendedMovies.css │ │ └── RecommendedMovies.jsx │ ├── GenrePage │ │ ├── GenrePage.css │ │ └── GenrePage.jsx │ ├── MoviesPage │ │ ├── MoviesPage.css │ │ └── MoviesPage.jsx │ ├── Pagination │ │ ├── Pagination.css │ │ └── Pagination.jsx │ ├── TrailerComponent │ │ └── TrailerComponent.jsx │ ├── Render │ │ ├── SingleCard.jsx │ │ ├── Stars.jsx │ │ ├── MoviesWatched.jsx │ │ └── CardsRow.jsx │ ├── NotFound │ │ └── NotFound.jsx │ ├── ErrorPage │ │ └── ErrorPage.jsx │ ├── WatchList │ │ └── WatchList.jsx │ ├── SearchComponent │ │ └── SearchPage.jsx │ ├── MainComponent │ │ └── MainComponent.jsx │ ├── HeaderComponent │ │ └── HeaderComponent.jsx │ ├── ManageUsers │ │ └── ManageUsers.jsx │ ├── LoginComponent │ │ └── LoginComponent.jsx │ ├── RegisterComponent │ │ └── RegisterComponent.jsx │ ├── ProfilePage │ │ └── ProfilePage.jsx │ └── MovieDetail │ │ └── MovieDetail.jsx ├── Shared │ ├── js │ │ ├── r42.js │ │ ├── ProtectedRoute.js │ │ └── user-context.js │ └── images │ │ ├── 404.svg │ │ ├── Error2.svg │ │ ├── profile.svg │ │ ├── login.svg │ │ └── Register.svg ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── index.js ├── App.js ├── Utilities │ └── apiCalls.js ├── logo.svg └── App.scss ├── public ├── _redirects ├── movie.ico ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── .env-cmdrc.json ├── .gitignore ├── .vscode └── settings.json ├── README.md └── package.json /src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | 3 | -------------------------------------------------------------------------------- /public/movie.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zorgonide/movieflix/HEAD/public/movie.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zorgonide/movieflix/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zorgonide/movieflix/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zorgonide/movieflix/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/Components/TopRatedMovies/TopRatedMovies.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } -------------------------------------------------------------------------------- /src/Components/RecommendedMovies/RecommendedMovies.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } -------------------------------------------------------------------------------- /src/Shared/js/r42.js: -------------------------------------------------------------------------------- 1 | export const trackEvent = (dataElement) => { 2 | // window._st('resetTags'); 3 | // window._st('addTagProperties', dataElement); 4 | // window._st('loadTags'); 5 | }; 6 | -------------------------------------------------------------------------------- /src/Components/GenrePage/GenrePage.css: -------------------------------------------------------------------------------- 1 | .genreList { 2 | display: block; 3 | } 4 | .seeMore { 5 | cursor: pointer; 6 | } 7 | .btn-pill { 8 | border-radius: 15px; 9 | } 10 | .listOfGenres { 11 | display: block; 12 | } -------------------------------------------------------------------------------- /src/Components/MoviesPage/MoviesPage.css: -------------------------------------------------------------------------------- 1 | /* .carouselSlider { 2 | display: flex; 3 | } 4 | .heading { 5 | display: flex; 6 | justify-content: space-between; 7 | } 8 | .heading a { 9 | display: flex; 10 | align-items: end; 11 | } 12 | */ -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /.env-cmdrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "REACT_APP_BASE_TOKEN": "e73306b88806162f60257a321d9e2b2c", 4 | "REACT_APP_BASE_URL": "https://api.themoviedb.org" 5 | }, 6 | "production": { 7 | "REACT_APP_BASE_TOKEN": "e73306b88806162f60257a321d9e2b2c", 8 | "REACT_APP_BASE_URL": "https://api.themoviedb.org" 9 | } 10 | } -------------------------------------------------------------------------------- /src/Components/Pagination/Pagination.css: -------------------------------------------------------------------------------- 1 | .pagination { 2 | display: flex; 3 | /* align-items: center; */ 4 | } 5 | .pagination .btn { 6 | color: white; 7 | font-size: large; 8 | font-weight: 800; 9 | /* border: 1px #0d6efd solid; 10 | height: 30px; */ 11 | } 12 | .heading p{ 13 | margin: auto; 14 | } 15 | .pages { 16 | color: white 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | build.zip -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import "font-awesome/css/font-awesome.css"; 7 | 8 | ReactDOM.render(, document.getElementById("root")); 9 | 10 | // If you want to start measuring performance in your app, pass a function 11 | // to log results (for example: reportWebVitals(console.log)) 12 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 13 | reportWebVitals(); 14 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.min.css'; 2 | import './App.scss'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import MainComponent from './Components/MainComponent/MainComponent'; 5 | import { UserProvider } from './Shared/js/user-context'; 6 | 7 | function App() { 8 | return ( 9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "movie.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/Shared/js/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate, Outlet } from "react-router-dom"; 3 | import Header from "../../Components/HeaderComponent/HeaderComponent"; 4 | import { useUser } from "./user-context"; 5 | 6 | function ProtectedRoute(props) { 7 | const { 8 | state: { user }, 9 | } = useUser(); 10 | const { dispatch } = useUser(); 11 | let verified = localStorage.getItem("user") ? true : false; 12 | if (verified && !user) { 13 | dispatch({ type: "login", user: JSON.parse(localStorage.getItem("user")) }); 14 | } 15 | return verified ? ( 16 | <> 17 |
18 | 19 | 20 | ) : ( 21 | 22 | ); 23 | } 24 | 25 | export default ProtectedRoute; 26 | -------------------------------------------------------------------------------- /src/Components/TrailerComponent/TrailerComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TrailerFrame = ({ trailerData }) => { 4 | if (!trailerData) { 5 | return
No trailer...
; 6 | } 7 | const embedUrl = `https://www.youtube.com/embed/${trailerData.key}`; 8 | return ( 9 |
10 | 19 |
20 | ); 21 | }; 22 | 23 | export default TrailerFrame; 24 | -------------------------------------------------------------------------------- /src/Components/Pagination/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Pagination.css"; 3 | const Pagination = ({ paginate, totalPages, currentPage }) => { 4 | const pageNumbers = []; 5 | 6 | for (let i = 1; i <= totalPages; i++) { 7 | pageNumbers.push(i); 8 | } 9 | return ( 10 |
11 |
12 |

{`${currentPage}/${totalPages}`}

13 |
14 | 21 | 28 |
29 | ); 30 | }; 31 | 32 | export default Pagination; 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#5a8dca", 4 | "activityBar.background": "#5a8dca", 5 | "activityBar.foreground": "#15202b", 6 | "activityBar.inactiveForeground": "#15202b99", 7 | "activityBarBadge.background": "#912f5b", 8 | "activityBarBadge.foreground": "#e7e7e7", 9 | "commandCenter.border": "#e7e7e799", 10 | "sash.hoverBorder": "#5a8dca", 11 | "statusBar.background": "#3b73b6", 12 | "statusBar.foreground": "#e7e7e7", 13 | "statusBarItem.hoverBackground": "#5a8dca", 14 | "statusBarItem.remoteBackground": "#3b73b6", 15 | "statusBarItem.remoteForeground": "#e7e7e7", 16 | "titleBar.activeBackground": "#3b73b6", 17 | "titleBar.activeForeground": "#e7e7e7", 18 | "titleBar.inactiveBackground": "#3b73b699", 19 | "titleBar.inactiveForeground": "#e7e7e799" 20 | }, 21 | "peacock.color": "#3b73b6" 22 | } -------------------------------------------------------------------------------- /src/Components/Render/SingleCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HideUntilLoaded } from "react-animation"; 3 | import { Rings } from "react-loader-spinner"; 4 | 5 | function SingleCard({ movie }) { 6 | return ( 7 |
8 |
9 | } 13 | > 14 | {movie.title} 19 | 20 | {movie.imdbRating} 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default SingleCard; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MovieFlix 2 | ### [Project demo link](https://zen-ride-bc37b1.netlify.app/) 3 | ![image](https://github.com/zorgonide/movieflix/assets/48021258/4e493b60-db11-43a7-ae16-f45d3c867870) 4 | ![image](https://github.com/zorgonide/movieflix/assets/48021258/ad0ebef0-7207-469b-b29c-0d17495e45da) 5 | ![image](https://github.com/zorgonide/movieflix/assets/48021258/82c6e2cd-14f5-4ace-8517-72261bfcbe49) 6 | ![image](https://github.com/zorgonide/movieflix/assets/48021258/dec4d1c8-9311-4180-ab64-f6ae7f2dc386) 7 | ![image](https://github.com/zorgonide/movieflix/assets/48021258/bf49d489-0123-4143-9b59-6d24f5dc0ba3) 8 | 9 | This application suggests movies based on genres selected using The Movie DB API. You can log in, review movies with comments, and interact with others on the platform. Makes browsing through Netflix a tiny bit easier. 10 | 11 | 12 | # Setup 13 | 14 | Clone this repo – 15 | 16 | git clone https://github.com/zorgonide/movieflix.git 17 | 18 | Move into project directory **movieflix** 19 | 20 | Run command – 21 | 22 | npm install 23 | 24 | This will install the node_modules folder 25 | 26 | Run command – 27 | 28 | npm run start-dev 29 | 30 | To run project on localhost:3000 31 | 32 | -------------------------------------------------------------------------------- /src/Shared/js/user-context.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const UserContext = React.createContext(); 4 | 5 | function userReducer(state, action) { 6 | switch (action.type) { 7 | case "login": { 8 | localStorage.setItem("user", JSON.stringify(action.user)); 9 | return { 10 | user: action.user, 11 | loggedIn: true, 12 | genres: action.user.genres, 13 | }; 14 | } 15 | case "logout": { 16 | localStorage.removeItem("user"); 17 | return { user: {}, loggedIn: false, genres: {} }; 18 | } 19 | case "genres": { 20 | return { 21 | user: { ...state.user, Genres: action.genres }, 22 | loggedIn: true, 23 | Genres: action.genres, 24 | }; 25 | } 26 | default: { 27 | throw new Error(`Unhandled action type: ${action.type}`); 28 | } 29 | } 30 | } 31 | 32 | function UserProvider({ children }) { 33 | const [state, dispatch] = React.useReducer(userReducer, { 34 | user: JSON.parse(localStorage.getItem("user")), 35 | }); 36 | const value = { state, dispatch }; 37 | return {children}; 38 | } 39 | 40 | function useUser() { 41 | const context = React.useContext(UserContext); 42 | if (context === undefined) { 43 | throw new Error("useUser must be used within a UserProvider"); 44 | } 45 | return context; 46 | } 47 | export { UserProvider, useUser }; 48 | -------------------------------------------------------------------------------- /src/Components/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import NotFound404 from "../../Shared/images/404.svg"; 4 | 5 | export class NotFound extends Component { 6 | render() { 7 | return ( 8 |
12 |
13 |
14 |
15 | login 22 |

Oops!

23 |

404 Not Found

24 |
25 | Sorry, an error has occurred, Requested page was not found! 26 |
27 |

28 |
29 | 30 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default NotFound; 44 | -------------------------------------------------------------------------------- /src/Components/ErrorPage/ErrorPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Pic from "../../Shared/images/Error2.svg"; 3 | // import Loader from 'react-loader-spinner' 4 | import { HideUntilLoaded } from "react-animation"; 5 | 6 | //Standard error component 7 | 8 | function Error({ error }) { 9 | return ( 10 | <> 11 | {/*
12 |
13 |
14 |
15 | } 19 | > 20 | Error 21 | 22 |
23 |
Error
24 |

{error}

25 |
26 |
27 |
28 |
29 |
*/} 30 | 31 | ); 32 | } 33 | 34 | export default Error; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hevo-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@adobe/adobe-client-data-layer": "^2.0.2", 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^12.0.0", 9 | "@testing-library/user-event": "^13.2.1", 10 | "axios": "^0.24.0", 11 | "bootstrap": "^5.1.3", 12 | "dayjs": "^1.10.7", 13 | "env-cmd": "^10.1.0", 14 | "firebase": "^9.6.3", 15 | "font-awesome": "^4.7.0", 16 | "formik": "^2.2.9", 17 | "react": "^16.8.0", 18 | "react-animation": "^1.2.2", 19 | "react-comments-section": "^2.0.10", 20 | "react-dom": "^16.8.0", 21 | "react-loader-spinner": "^5.0.10", 22 | "react-router-dom": "^6.2.1", 23 | "react-scripts": "5.0.0", 24 | "reactstrap": "^9.0.1", 25 | "sass": "^1.58.3", 26 | "sweetalert2": "^11.7.3", 27 | "web-vitals": "^2.1.0", 28 | "yup": "^0.32.11" 29 | }, 30 | "scripts": { 31 | "start-dev": "env-cmd -e development react-scripts start", 32 | "build-dev": "env-cmd -e development react-scripts build", 33 | "start-prod": "env-cmd -e production react-scripts start", 34 | "build-prod": "env-cmd -e production react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject" 37 | }, 38 | "eslintConfig": { 39 | "extends": [ 40 | "react-app", 41 | "react-app/jest" 42 | ] 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Components/Render/Stars.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "font-awesome/css/font-awesome.min.css"; 3 | import { postBackend } from "../../Utilities/apiCalls"; 4 | 5 | const StarRating = ({ userRating, onChange, Title, Movie_ID }) => { 6 | const [hoverRating, setHoverRating] = useState(0); 7 | 8 | const handleRatingClick = (rating) => { 9 | postBackend({ 10 | url: "api/rating/" + Movie_ID, 11 | data: { 12 | userRating: rating, 13 | }, 14 | }) 15 | .then(() => { 16 | if (onChange) { 17 | onChange(rating); 18 | } 19 | }) 20 | .then(() => { 21 | window.adobeDataLayer = window.adobeDataLayer || []; 22 | 23 | window.adobeDataLayer.push({ 24 | event: "rating", 25 | rating: { 26 | id: Movie_ID, 27 | title: Title, 28 | rating: rating, 29 | }, 30 | }); 31 | }); 32 | }; 33 | 34 | const handleRatingHover = (rating) => { 35 | setHoverRating(rating); 36 | }; 37 | 38 | const stars = []; 39 | const maxRating = 5; 40 | 41 | for (let i = 1; i <= maxRating; i++) { 42 | const filled = i <= (userRating || hoverRating); 43 | 44 | stars.push( 45 | handleRatingHover(i)} 55 | onMouseLeave={() => handleRatingHover(0)} 56 | onClick={() => handleRatingClick(i)} 57 | /> 58 | ); 59 | } 60 | 61 | return
{stars}
; 62 | }; 63 | 64 | export default StarRating; 65 | -------------------------------------------------------------------------------- /src/Components/WatchList/WatchList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Error from '../ErrorPage/ErrorPage'; 3 | import { Rings } from 'react-loader-spinner'; 4 | import { getBackend, postBackend } from '../../Utilities/apiCalls'; 5 | import MoviesWatched from '../Render/MoviesWatched'; 6 | 7 | function WatchList() { 8 | const [error, setError] = useState(null); 9 | const [isLoaded, setIsLoaded] = useState(false); 10 | const [moviesWatched, setMoviesWatched] = useState([]); 11 | const MoviesWatchedFunction = () => { 12 | return getBackend({ 13 | url: 'api/watchlist', 14 | }) 15 | .then((res) => res.data) 16 | .then((res) => { 17 | setMoviesWatched(res.data.map((movie) => movie.movie)); 18 | }); 19 | }; 20 | useEffect(() => { 21 | Promise.all([MoviesWatchedFunction()]) 22 | .then(() => { 23 | setIsLoaded(true); 24 | }) 25 | .catch((err) => { 26 | setError(err); 27 | }); 28 | }, []); 29 | if (error) { 30 | return ; 31 | } else if (!isLoaded) { 32 | return ( 33 |
42 | 43 |
44 | ); 45 | } else 46 | return ( 47 |
48 |
49 |
50 | 54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | export default WatchList; 61 | -------------------------------------------------------------------------------- /src/Utilities/apiCalls.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | // const backend = "http://127.0.0.1:3000/"; 3 | // const backend = "https://guanxinyumovieflix.me/"; 4 | const backend = "https://movieflix-wpa8.onrender.com/"; 5 | 6 | let cache = {}; 7 | export const fget = async ({ url }) => { 8 | if (cache[url]) { 9 | return cache[url]; 10 | } 11 | const res = await axios.get(process.env.REACT_APP_BASE_URL + `${url}`, {}); 12 | cache[url] = res; 13 | return res; 14 | }; 15 | 16 | export const fpatch = async ({ url, data }) => { 17 | if (cache[url]) { 18 | return cache[url]; 19 | } 20 | const res = await axios.patch( 21 | process.env.REACT_APP_BASE_URL + `${url}`, 22 | data, 23 | {} 24 | ); 25 | cache[url] = res; 26 | 27 | return res; 28 | }; 29 | 30 | export const fpost = async ({ url, data }) => { 31 | if (cache[url]) { 32 | return cache[url]; 33 | } 34 | const res = await axios.post( 35 | process.env.REACT_APP_BASE_URL + `${url}`, 36 | data, 37 | {} 38 | ); 39 | cache[url] = res; 40 | 41 | return res; 42 | }; 43 | 44 | export const fdelete = async ({ url }) => { 45 | const res = await axios.delete(process.env.REACT_APP_BASE_URL + `${url}`, {}); 46 | return res; 47 | }; 48 | export const getBackend = async ({ url }) => { 49 | try { 50 | const res = await axios.get(backend + `${url}`, { 51 | headers: { 52 | Authorization: `Bearer ${localStorage.getItem("token")}`, 53 | }, 54 | }); 55 | return res; 56 | } catch (err) { 57 | return err; 58 | } 59 | }; 60 | 61 | export const patchBackend = async ({ url, data }) => { 62 | try { 63 | const res = await axios.put(backend + `${url}`, data, { 64 | headers: { 65 | Authorization: `Bearer ${localStorage.getItem("token")}`, 66 | }, 67 | }); 68 | return res; 69 | } catch (err) { 70 | return err; 71 | } 72 | }; 73 | 74 | export const postBackend = async ({ url, data }) => { 75 | try { 76 | let res = await axios.post(backend + `${url}`, data, { 77 | headers: { 78 | Authorization: `Bearer ${localStorage.getItem("token")}`, 79 | }, 80 | }); 81 | return res; 82 | } catch (err) { 83 | return err; 84 | } 85 | }; 86 | 87 | export const deleteBackend = async ({ url }) => { 88 | try { 89 | const res = await axios.delete(backend + `${url}`, { 90 | headers: { 91 | Authorization: `Bearer ${localStorage.getItem("token")}`, 92 | }, 93 | }); 94 | return res; 95 | } catch (err) { 96 | return err; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Components/Render/MoviesWatched.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | function MoviesWatched({ movies, title }) { 4 | let navigate = useNavigate(); 5 | let moviesArray = movies; 6 | return ( 7 |
8 |
9 |

{title}

10 |
11 |
    12 | {moviesArray.length ? ( 13 | moviesArray.map((ele) => { 14 | return ( 15 |
  • 16 |
    18 | navigate(`/movie/${ele.id}`, { 19 | state: { movie: ele }, 20 | }) 21 | } 22 | > 23 |
    24 | Movie 31 |
    32 |

    33 | {ele.title} 34 |

    35 |

    36 | IMDB Rating:{' '} 37 | {ele.imdbRating} 38 |

    39 |
    40 |
    41 |
    42 |
  • 43 | ); 44 | }) 45 | ) : ( 46 |

    No movies :(

    47 | )} 48 |
49 |
50 |
51 | ); 52 | } 53 | 54 | export default MoviesWatched; 55 | -------------------------------------------------------------------------------- /src/Components/SearchComponent/SearchPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { fget } from '../../Utilities/apiCalls'; 4 | import MoviesWatched from '../Render/MoviesWatched'; 5 | 6 | function SearchPage() { 7 | const [searchResults, setSearchResults] = useState([]); 8 | const location = useLocation(); 9 | 10 | useEffect(() => { 11 | const searchParams = new URLSearchParams(location.search); 12 | const searchTerm = searchParams.get('q'); 13 | 14 | fget({ 15 | url: `/3/search/movie?api_key=${process.env.REACT_APP_BASE_TOKEN}&query=${searchTerm}&include_adult=false&language=en-US&page=1`, 16 | }) 17 | .then((response) => response.data) 18 | .then((data) => { 19 | let refined = data.results.map((ele) => { 20 | return { 21 | id: ele.id, 22 | poster: 23 | 'https://image.tmdb.org/t/p/original' + 24 | ele.poster_path, 25 | imdbRating: Math.round(ele.vote_average * 10) / 10, 26 | title: ele.title, 27 | release_date: ele.release_date, 28 | overview: ele.overview, 29 | poster_path: ele.poster_path, 30 | backdrop_path: ele.backdrop_path, 31 | }; 32 | }); 33 | // remove movies with no poster or imdbRating 0 34 | refined = refined.filter( 35 | (ele) => 36 | ele.poster !== 37 | 'https://image.tmdb.org/t/p/originalnull' && 38 | ele.imdbRating !== 0 39 | ); 40 | 41 | setSearchResults(refined); 42 | window.adobeDataLayer.push({ 43 | event: 'search', 44 | search: searchTerm, 45 | results: refined.length, 46 | }); 47 | }) 48 | .catch((error) => console.error(error)); 49 | }, [location.search, searchResults.length]); 50 | 51 | return ( 52 |
53 |
54 |
55 | 59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | export default SearchPage; 66 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 48 | MovieFlix 49 | 50 | 51 | 52 |
53 | 63 | 64 | -------------------------------------------------------------------------------- /src/Components/TopRatedMovies/TopRatedMovies.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Rings } from "react-loader-spinner"; 3 | import { fget } from "../../Utilities/apiCalls"; 4 | import Error from "../ErrorPage/ErrorPage"; 5 | import "./TopRatedMovies.css"; 6 | import CardsRow from "../Render/CardsRow"; 7 | import Pagination from "../Pagination/Pagination"; 8 | 9 | function TopRatedMovies() { 10 | const [error, setError] = useState(null); 11 | const [isLoaded, setIsLoaded] = useState(false); 12 | const [topRated, setTopRated] = useState([]); 13 | const [currentPage, setCurrentPage] = useState(1); 14 | const [totalPages, setTotalPages] = useState(null); 15 | const paginate = (pageNumber) => { 16 | setCurrentPage(pageNumber); 17 | getMovies(pageNumber); 18 | }; 19 | const getMovies = (page = 1) => { 20 | fget({ 21 | url: `/3/movie/top_rated?api_key=${process.env.REACT_APP_BASE_TOKEN}&language=en-US&page=${page}`, 22 | }) 23 | .then((res) => res.data) 24 | .then( 25 | (result) => { 26 | setTopRated(result.results.slice(0, -2)); 27 | setTotalPages(result.total_pages); 28 | setIsLoaded(true); 29 | }, 30 | (error) => { 31 | setIsLoaded(true); 32 | setError(error); 33 | } 34 | ); 35 | }; 36 | useEffect(() => { 37 | getMovies(); 38 | }, []); 39 | if (error) { 40 | return ; 41 | } else if (!isLoaded) { 42 | return ( 43 |
52 | 53 |
54 | ); 55 | } else 56 | return ( 57 |
58 |
59 |
60 |
61 |

Top Rated Movies

62 |
63 | 68 |
69 |
70 | 71 |
72 |
73 |
74 | ); 75 | } 76 | 77 | export default TopRatedMovies; 78 | -------------------------------------------------------------------------------- /src/Components/Render/CardsRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HideUntilLoaded } from 'react-animation'; 3 | import { Rings } from 'react-loader-spinner'; 4 | import * as dayjs from 'dayjs'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | const MovieCard = ({ ele }) => { 8 | return ( 9 |
10 | ( 16 | 17 | )} 18 | > 19 | {ele.title} 26 |
27 | 28 | 37 | {Math.round(ele.vote_average * 10) / 10} 38 | 39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | const CardsRow = ({ movies }) => { 46 | let navigate = useNavigate(); 47 | return ( 48 |
49 | {movies.map((ele) => { 50 | return ( 51 |
55 | navigate(`/movie/${ele.id}`, { 56 | state: { movie: ele }, 57 | }) 58 | } 59 | > 60 | 61 |
62 |

66 | {ele.title.length < 30 67 | ? ele.title 68 | : ele.title.slice(0, 30) + '...'} 69 |

70 |

71 | {dayjs(ele.release_date).format('YYYY')} 72 |

73 |
74 |
75 | ); 76 | })} 77 |
78 | ); 79 | }; 80 | 81 | export default CardsRow; 82 | -------------------------------------------------------------------------------- /src/Components/RecommendedMovies/RecommendedMovies.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Rings } from "react-loader-spinner"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { fget } from "../../Utilities/apiCalls"; 5 | import Error from "../ErrorPage/ErrorPage"; 6 | import "./RecommendedMovies.css"; 7 | import CardsRow from "../Render/CardsRow"; 8 | import Pagination from "../Pagination/Pagination"; 9 | import { useUser } from "../../Shared/js/user-context"; 10 | 11 | function RecommendedMovies() { 12 | const [error, setError] = useState(null); 13 | const [isLoaded, setIsLoaded] = useState(false); 14 | const [recommended, setRecommended] = useState([]); 15 | const [currentPage, setCurrentPage] = useState(1); 16 | const [totalPages, setTotalPages] = useState(null); 17 | let navigate = useNavigate(); 18 | const { 19 | state: { user }, 20 | } = useUser(); 21 | const genres = Array.isArray(user.genres) 22 | ? user.genres.join(",") 23 | : user.genres; 24 | 25 | const paginate = (pageNumber) => { 26 | setCurrentPage(pageNumber); 27 | getMovies(pageNumber); 28 | }; 29 | 30 | const getMovies = (page = 1) => { 31 | fget({ 32 | url: `/3/discover/movie?api_key=${process.env.REACT_APP_BASE_TOKEN}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_watch_monetization_types=flatrate&with_genres=${genres}`, 33 | }) 34 | .then((res) => res.data) 35 | .then( 36 | (result) => { 37 | setRecommended(result.results.slice(0, -2)); 38 | setTotalPages(result.total_pages); 39 | setIsLoaded(true); 40 | }, 41 | (error) => { 42 | setIsLoaded(true); 43 | setError(error); 44 | } 45 | ); 46 | }; 47 | 48 | useEffect(() => { 49 | if (!genres) { 50 | navigate("/genres"); 51 | return; 52 | } 53 | getMovies(); 54 | }, []); 55 | if (error) { 56 | return ; 57 | } else if (!isLoaded) { 58 | return ( 59 |
68 | 69 |
70 | ); 71 | } else 72 | return ( 73 |
74 |
75 |
76 |
77 |

Recommended Movies

78 |
79 | 84 |
85 |
86 | 87 |
88 |
89 |
90 | ); 91 | } 92 | 93 | export default RecommendedMovies; 94 | -------------------------------------------------------------------------------- /src/Components/MainComponent/MainComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Route, Routes, useLocation } from 'react-router-dom'; 3 | import ProtectedRoute from '../../Shared/js/ProtectedRoute'; 4 | import GenrePage from '../GenrePage/GenrePage'; 5 | import LoginComponent from '../LoginComponent/LoginComponent'; 6 | import ManageUsers from '../ManageUsers/ManageUsers'; 7 | import MovieDetail from '../MovieDetail/MovieDetail'; 8 | import MoviesPage from '../MoviesPage/MoviesPage'; 9 | import NotFound from '../NotFound/NotFound'; 10 | import ProfilePage from '../ProfilePage/ProfilePage'; 11 | import RecommendedMovies from '../RecommendedMovies/RecommendedMovies'; 12 | import RegisterComponent from '../RegisterComponent/RegisterComponent'; 13 | import TopRatedMovies from '../TopRatedMovies/TopRatedMovies'; 14 | import WatchList from '../WatchList/WatchList'; 15 | import { trackEvent } from '../../Shared/js/r42'; 16 | import SearchPage from '../SearchComponent/SearchPage'; 17 | 18 | export const GenreContext = React.createContext(); 19 | 20 | function MainComponent(props) { 21 | const [genres, setGenres] = useState(null); 22 | const location = useLocation(); 23 | 24 | // Function to push data to Adobe Data Layer 25 | const pushToAdobeDataLayer = (pageName) => { 26 | window.adobeDataLayer = window.adobeDataLayer || []; 27 | window.adobeDataLayer.push({ 28 | event: 'page-view', 29 | page: pageName, 30 | }); 31 | }; 32 | const pushToRelay42 = (pageName) => { 33 | // let page = pageName.slice(1); 34 | // if (/^\/movie\/\d+$/.test(pageName)) { 35 | // page = 'movie'; 36 | // } 37 | // if (page === '') { 38 | // page = 'main'; 39 | // } 40 | // window._st('resetTags'); 41 | // window._st('setPageStructure', page); 42 | // trackEvent(); 43 | }; 44 | 45 | // Call pushToAdobeDataLayer whenever the route changes 46 | React.useEffect(() => { 47 | pushToAdobeDataLayer(location.pathname); 48 | pushToRelay42(location.pathname); 49 | }, [location.pathname]); 50 | return ( 51 | <> 52 | 53 | } /> 54 | } /> 55 | }> 56 | } 59 | /> 60 | 65 | 66 | 67 | } 68 | /> 69 | 74 | 75 | 76 | } 77 | /> 78 | } /> 79 | } 83 | /> 84 | } /> 85 | } /> 86 | } /> 87 | } /> 88 | 89 | } /> 90 | 91 | 92 | ); 93 | } 94 | 95 | export default MainComponent; 96 | -------------------------------------------------------------------------------- /src/Components/HeaderComponent/HeaderComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { Link, useNavigate } from 'react-router-dom'; 4 | import { useUser } from '../../Shared/js/user-context'; 5 | function Header() { 6 | const [isNavCollapsed, setIsNavCollapsed] = useState(true); 7 | const [searchTerm, setSearchTerm] = useState(''); 8 | const handleNavCollapse = () => setIsNavCollapsed(!isNavCollapsed); 9 | 10 | const { dispatch } = useUser(); 11 | const { 12 | state: { user }, 13 | } = useUser(); 14 | let navigate = useNavigate(); 15 | return ( 16 | 102 | ); 103 | } 104 | 105 | export default Header; 106 | -------------------------------------------------------------------------------- /src/Components/ManageUsers/ManageUsers.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Swal from "sweetalert2"; 3 | import { deleteBackend, getBackend } from "../../Utilities/apiCalls"; 4 | 5 | function ManageUsers() { 6 | const [error, setError] = useState(null); 7 | const [isLoaded, setIsLoaded] = useState(false); 8 | const [totalItems, setTotalItems] = useState([]); 9 | const [search, setSearch] = useState(""); 10 | useEffect(() => { 11 | fetchUsers(); 12 | }, []); 13 | const deleteUser = (id) => { 14 | Swal.fire({ 15 | confirmButtonColor: "#e31c5f", 16 | title: "Confirm Deletion", 17 | text: `Do you delete user #${id}`, 18 | icon: "question", 19 | confirmButtonText: "Yes", 20 | showCancelButton: true, 21 | }).then((res) => { 22 | if (res.isConfirmed) { 23 | deleteBackend({ 24 | url: `user/`, 25 | data: { 26 | id: id, 27 | }, 28 | }).then(() => { 29 | Swal.fire({ 30 | confirmButtonColor: "#e31c5f", 31 | title: "User deleted", 32 | icon: "success", 33 | confirmButtonText: "Dismiss", 34 | }); 35 | fetchUsers(); 36 | }); 37 | } 38 | }); 39 | }; 40 | const fetchUsers = () => { 41 | getBackend({ 42 | url: `users/`, 43 | data: {}, 44 | }) 45 | .then((res) => res.data) 46 | .then( 47 | (result) => { 48 | setTotalItems(result); 49 | setIsLoaded(true); 50 | }, 51 | (error) => { 52 | setIsLoaded(true); 53 | setError(error); 54 | } 55 | ); 56 | }; 57 | const Headers = () => { 58 | return ( 59 |
60 |
61 |

ID

62 |
63 |
64 |

First Name

65 |
66 |
67 |

Last Name

68 |
69 |
70 |

Email

71 |
72 |
73 |

User Type

74 |
75 |
76 |

Remove

77 |
78 |
79 | ); 80 | }; 81 | const RenderList = () => { 82 | return ( 83 | <> 84 | {filteredUsers.map((element, index) => { 85 | return ( 86 |
navigate(`/action/${element.id}`)} 90 | > 91 |
92 |

93 | {element.id} 94 |

95 |
96 |
97 |

{element.firstName}

98 |
99 |
100 |

{element.lastName}

101 |
102 |
103 |

{element.email}

104 |
105 |
106 |

107 | {element.role === "ADMIN" ? "Admin User" : "Normal User"} 108 |

109 |
110 |
deleteUser(element.id)} 113 | > 114 |

115 | 116 |

117 |
118 |
119 | ); 120 | })} 121 | 122 | ); 123 | }; 124 | let filteredUsers = totalItems 125 | ? totalItems.filter((user) => { 126 | let fullName = user.firstName + user.lastName; 127 | return fullName.toLowerCase().indexOf(search.toLowerCase()) !== -1; 128 | }) 129 | : []; 130 | return ( 131 |
132 |
133 |
134 |
135 |
136 |

137 | Manage Users 138 |

139 |
140 |
141 |
142 | setSearch(e.target.value)} 148 | /> 149 |
150 |
151 |
152 |
153 | {filteredUsers.length === 0 ? ( 154 |
155 |

No user found

156 |
157 | ) : ( 158 | <> 159 | 160 | 161 | 162 | )} 163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | ); 171 | } 172 | 173 | export default ManageUsers; 174 | -------------------------------------------------------------------------------- /src/Components/MoviesPage/MoviesPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Rings } from "react-loader-spinner"; 3 | import { Link, useNavigate } from "react-router-dom"; 4 | import { UncontrolledCarousel } from "reactstrap"; 5 | import { fget } from "../../Utilities/apiCalls"; 6 | import Error from "../ErrorPage/ErrorPage"; 7 | import "./MoviesPage.css"; 8 | import CardsRow from "../Render/CardsRow"; 9 | import { useUser } from "../../Shared/js/user-context"; 10 | 11 | function MoviesPage(props) { 12 | const { 13 | state: { user }, 14 | } = useUser(); 15 | const [error, setError] = useState(null); 16 | const [isLoaded, setIsLoaded] = useState(false); 17 | const [upcoming, setUpcoming] = useState([]); 18 | const [topRated, setTopRated] = useState([]); 19 | const [recommended, setRecommended] = useState([]); 20 | const genres = Array.isArray(user.genres) 21 | ? user.genres.join(",") 22 | : user.genres; 23 | 24 | let navigate = useNavigate(); 25 | 26 | const formatFunctionUpcoming = (movies) => { 27 | return movies.map((ele) => { 28 | return { 29 | key: ele.id, 30 | src: "https://image.tmdb.org/t/p/original" + ele.backdrop_path, 31 | caption: ele.title, 32 | }; 33 | }); 34 | }; 35 | useEffect(() => { 36 | if (!genres) { 37 | navigate("/genres"); 38 | return; 39 | } 40 | fget({ 41 | url: `/3/movie/upcoming?api_key=${process.env.REACT_APP_BASE_TOKEN}&language=en-US&page=1`, 42 | }) 43 | .then((res) => res.data) 44 | .then( 45 | (result) => { 46 | setUpcoming( 47 | formatFunctionUpcoming(result.results.slice(0, 10)) 48 | ); 49 | setIsLoaded(true); 50 | }, 51 | (error) => { 52 | setIsLoaded(true); 53 | setError(error); 54 | } 55 | ); 56 | fget({ 57 | url: `/3/movie/top_rated?api_key=${process.env.REACT_APP_BASE_TOKEN}&language=en-US&page=1`, 58 | }) 59 | .then((res) => res.data) 60 | .then( 61 | (result) => { 62 | setTopRated(result.results.slice(0, 6)); 63 | setIsLoaded(true); 64 | }, 65 | (error) => { 66 | setIsLoaded(true); 67 | setError(error); 68 | } 69 | ); 70 | fget({ 71 | url: `/3/discover/movie?api_key=${process.env.REACT_APP_BASE_TOKEN}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_watch_monetization_types=flatrate&with_genres=${genres}`, 72 | }) 73 | .then((res) => res.data) 74 | .then( 75 | (result) => { 76 | setRecommended(result.results.slice(0, 6)); 77 | setIsLoaded(true); 78 | }, 79 | (error) => { 80 | setIsLoaded(true); 81 | setError(error); 82 | } 83 | ); 84 | }, []); 85 | if (error) { 86 | return ; 87 | } else if (!isLoaded) { 88 | return ( 89 |
98 | 99 |
100 | ); 101 | } else 102 | return ( 103 |
104 |

105 |
106 |
107 |
108 |

Recommended Movies

109 |
110 | 111 | More 112 | 113 |
114 |
115 | 116 |
117 |
118 |

119 |
120 |
121 |
122 |

Top Rated Movies

123 |
124 | 125 | More 126 | 127 |
128 |
129 | 130 |
131 |
132 |

133 |
134 |
135 |

Upcoming Movies

136 |
137 |
138 | 144 |
145 |
146 |

147 |
148 | ); 149 | } 150 | 151 | export default MoviesPage; 152 | -------------------------------------------------------------------------------- /src/Shared/images/404.svg: -------------------------------------------------------------------------------- 1 | server down -------------------------------------------------------------------------------- /src/Shared/images/Error2.svg: -------------------------------------------------------------------------------- 1 | server down -------------------------------------------------------------------------------- /src/Shared/images/profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Components/GenrePage/GenrePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Rings } from "react-loader-spinner"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { useUser } from "../../Shared/js/user-context"; 5 | import { fget, patchBackend } from "../../Utilities/apiCalls"; 6 | import Error from "../ErrorPage/ErrorPage"; 7 | import Genre from "../../Shared/images/Register.svg"; 8 | import "./GenrePage.css"; 9 | import Swal from "sweetalert2"; 10 | 11 | function GenrePage(props) { 12 | const [error, setError] = useState(null); 13 | const [isLoaded, setIsLoaded] = useState(false); 14 | const [genres, setGenres] = useState([]); 15 | const [seeMore, setSeeMore] = useState(false); 16 | let [highlightedButtons, setHighlightedButtons] = useState([]); 17 | const { dispatch } = useUser(); 18 | let navigate = useNavigate(); 19 | useEffect(() => { 20 | fget({ 21 | url: `/3/genre/movie/list?api_key=${process.env.REACT_APP_BASE_TOKEN}&language=en-US`, 22 | }) 23 | .then((res) => res.data) 24 | .then( 25 | (result) => { 26 | result.genres.forEach((ele) => { 27 | ele.isSelected = false; 28 | }); 29 | setGenres(result.genres); 30 | setIsLoaded(true); 31 | }, 32 | (error) => { 33 | setIsLoaded(true); 34 | setError(error); 35 | } 36 | ); 37 | }, []); 38 | const highlightButton = (ele, index) => { 39 | let newSet = [...genres]; 40 | newSet[index].isSelected = !newSet[index].isSelected; 41 | setGenres(() => [...newSet]); 42 | if (highlightedButtons.indexOf(ele.id) === -1) 43 | setHighlightedButtons(() => [...highlightedButtons, ele.id]); 44 | // highlightedButtons.push(ele.id); 45 | else { 46 | // highlightedButtons.splice(highlightedButtons.indexOf(ele.id), 1); 47 | setHighlightedButtons(() => [ 48 | ...highlightedButtons.splice( 49 | highlightedButtons.indexOf(ele.id), 50 | 1 51 | ), 52 | ]); 53 | } 54 | }; 55 | const GenreList = () => { 56 | let buttonClass = "btn rounded-pill btn-sm btn-outline-danger m-1"; 57 | let buttonSelectedClass = "btn rounded-pill btn-sm btn-danger m-1"; 58 | return ( 59 | <> 60 | {genres.map((ele, index) => { 61 | if (index < 9) 62 | return ( 63 | 75 | ); 76 | else 77 | return ( 78 | 92 | ); 93 | })} 94 | 95 | ); 96 | }; 97 | const goToMoviesPage = () => { 98 | if (highlightedButtons.length) { 99 | patchBackend({ 100 | url: "user/", 101 | data: { 102 | genres: highlightedButtons.join(","), 103 | }, 104 | }) 105 | .then((res) => res.data) 106 | .then((res) => { 107 | dispatch({ type: "genres", Genres: res.user.genres }); 108 | dispatch({ type: "login", user: res.user }); 109 | navigate("/"); 110 | }); 111 | } else { 112 | Swal.fire({ 113 | title: "Error", 114 | text: `Select a genre to continue`, 115 | icon: "error", 116 | confirmButtonText: "Dismiss", 117 | }); 118 | } 119 | }; 120 | if (error) { 121 | return ; 122 | } else if (!isLoaded) { 123 | return ( 124 |
133 | 134 |
135 | ); 136 | } else 137 | return ( 138 |
139 |
140 |
141 |
142 |
143 |
144 | login 145 |
146 |

147 | Select the genres you're interested in... 148 |

149 |
150 | 151 |

setSeeMore(!seeMore)} 154 | > 155 | {seeMore ? "See Less" : "See More"} 156 |

157 |
158 |
159 |
160 | 170 |
171 |
172 |
173 |
174 |
175 | ); 176 | } 177 | export default GenrePage; 178 | -------------------------------------------------------------------------------- /src/Components/LoginComponent/LoginComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate, Link } from "react-router-dom"; 3 | import { postBackend } from "../../Utilities/apiCalls"; 4 | import * as Yup from "yup"; 5 | import { Formik, Field, Form, ErrorMessage } from "formik"; 6 | import { useUser } from "../../Shared/js/user-context"; 7 | import Swal from "sweetalert2"; 8 | import Login from "../../Shared/images/login.svg"; 9 | 10 | const LoginSchema = Yup.object().shape({ 11 | email: Yup.string().email("Invalid email").required("Required"), 12 | password: Yup.string().required("Required"), 13 | }); 14 | function LoginComponent() { 15 | const { dispatch } = useUser(); 16 | const navigate = useNavigate(); 17 | const [loading, setLoading] = useState(false); 18 | 19 | return ( 20 |
21 |
22 |
23 |
24 |
25 |
26 |

Log In

27 |
28 |
29 | login 30 |
31 | { 38 | setLoading(true); 39 | postBackend({ 40 | url: `signin`, 41 | data: { 42 | email: values.email, 43 | password: values.password, 44 | }, 45 | }) 46 | .then((res) => res.data) 47 | .then((res) => { 48 | setLoading(false); 49 | dispatch({ 50 | type: "login", 51 | user: res.user, 52 | }); 53 | localStorage.setItem( 54 | "token", 55 | res.token 56 | ); 57 | window.adobeDataLayer = 58 | window.adobeDataLayer || []; 59 | 60 | window.adobeDataLayer.push({ 61 | event: "user-login", 62 | user: res.user.id, 63 | }); 64 | 65 | if (res.user.role === "admin") 66 | navigate("/manage"); 67 | else 68 | res.user.genres.length 69 | ? navigate("/") 70 | : navigate("/genres"); 71 | }) 72 | .catch((err) => { 73 | console.log(err); 74 | Swal.fire({ 75 | title: "Error", 76 | text: `Incorrect username or password`, 77 | icon: "error", 78 | confirmButtonText: "Dismiss", 79 | }); 80 | }); 81 | }} 82 | > 83 | {({ errors, touched }) => ( 84 |
85 |
86 | 98 | 99 |
100 | 101 |
102 |
103 |
104 | 117 | 120 |
121 | 122 |
123 |
124 |
125 | 134 |
135 |
136 | )} 137 |
138 |
139 |
140 | Not registered? Sign Up 141 |
142 |
143 |
144 |
145 |
146 | ); 147 | } 148 | 149 | export default LoginComponent; 150 | -------------------------------------------------------------------------------- /src/Components/RegisterComponent/RegisterComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate, Link } from "react-router-dom"; 3 | import { postBackend } from "../../Utilities/apiCalls"; 4 | import * as Yup from "yup"; 5 | import { Formik, Field, Form, ErrorMessage } from "formik"; 6 | import Swal from "sweetalert2"; 7 | 8 | const SignUpSchema = Yup.object().shape({ 9 | email: Yup.string().email("Invalid email").required("Required"), 10 | password: Yup.string() 11 | .label("password") 12 | .required("Required") 13 | .min(8, "Seems a bit short..."), 14 | 15 | // .matches( 16 | // /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/, 17 | // "At least 1 letter, 1 number and 1 special character" 18 | // ), 19 | confirmpassword: Yup.string() 20 | .required("Required") 21 | .label("confirmpassword") 22 | .test("passwords-match", "Passwords don't match", function (value) { 23 | return this.parent.password === value; 24 | }), 25 | firstname: Yup.string() 26 | .matches(/^[A-Za-z]+$/, "Only letters allowed") 27 | .required("Required"), //put in number validation 28 | lastname: Yup.string() 29 | .matches(/^[A-Za-z]+$/, "Only letters allowed") 30 | .required("Required"), 31 | }); 32 | 33 | function RegisterComponent() { 34 | const navigate = useNavigate(); 35 | const [loading, setLoading] = useState(false); 36 | 37 | return ( 38 |
39 |
40 |
41 |
42 |
43 |
44 |

Register

45 |
46 |
47 | { 57 | setLoading(true); 58 | postBackend({ 59 | url: "user/", 60 | data: { 61 | firstName: values.firstname, 62 | lastName: values.lastname, 63 | email: values.email, 64 | password: values.password, 65 | }, 66 | }) 67 | .then((res) => res.data) 68 | .then((res) => { 69 | if (res.token) { 70 | setLoading(false); 71 | Swal.fire({ 72 | confirmButtonColor: "#e31c5f", 73 | title: "Success", 74 | text: `New user created`, 75 | icon: "success", 76 | confirmButtonText: "Log In", 77 | }).then((res) => { 78 | if (res.isConfirmed) navigate("/login"); 79 | }); 80 | } else throw new Error(); 81 | }) 82 | .catch(() => { 83 | Swal.fire({ 84 | title: "Error", 85 | text: `User already exists`, 86 | icon: "error", 87 | confirmButtonText: "Dismiss", 88 | }); 89 | }); 90 | }} 91 | > 92 | {({ errors, touched }) => ( 93 |
94 |
95 | 105 | 106 |
107 | 108 |
109 |
110 |
111 | 121 | 122 |
123 | 124 |
125 |
126 |
127 | 137 | 138 |
139 | 140 |
141 |
142 |
143 | 155 | 156 |
157 | 158 |
159 |
160 |
161 | 173 | 176 |
177 | 178 |
179 |
180 |
181 | 185 |
186 |
187 | )} 188 |
189 |
190 |
191 |
192 | Already registered? Log In 193 |
194 |
195 |
196 |
197 |
198 | ); 199 | } 200 | 201 | export default RegisterComponent; 202 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | $theme-red: #e31c5f; 2 | $title-text: #9e4784; 3 | $title: #d27685; 4 | .movie-title { 5 | color: $title-text; 6 | font-weight: 500; 7 | font-size: large; 8 | } 9 | .sub-title { 10 | color: whitesmoke; 11 | } 12 | .error { 13 | color: $theme-red; 14 | } 15 | .centered { 16 | height: 100%; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | min-height: 100vh; 21 | } 22 | .centered1 { 23 | height: 100%; 24 | display: flex; 25 | justify-content: center; 26 | min-height: 100vh; 27 | } 28 | .row { 29 | width: 100%; 30 | } 31 | .card { 32 | box-shadow: 0 5px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 33 | } 34 | .clickable { 35 | cursor: pointer; 36 | transition: transform 0.3s ease; 37 | .card:hover { 38 | transform: scale(1.05); 39 | } 40 | } 41 | .display-6 { 42 | color: $theme-red; 43 | } 44 | .App { 45 | background-color: #37306b; 46 | min-height: 100vh; 47 | } 48 | .card img { 49 | width: 100%; 50 | height: 18rem; 51 | object-fit: cover; 52 | } 53 | h1 { 54 | position: relative; 55 | padding: 0; 56 | margin: 0; 57 | font-family: 'Raleway', sans-serif; 58 | font-weight: 300; 59 | font-size: 40px; 60 | color: white; 61 | -webkit-transition: all 0.4s ease 0s; 62 | -o-transition: all 0.4s ease 0s; 63 | transition: all 0.4s ease 0s; 64 | } 65 | .error-details { 66 | color: white; 67 | } 68 | h1 span { 69 | display: block; 70 | font-size: 0.5em; 71 | line-height: 1.3; 72 | } 73 | h1 em { 74 | font-style: normal; 75 | font-weight: 600; 76 | } 77 | .ten h1 { 78 | font-size: 24px; 79 | color: $title; 80 | font-weight: 600; 81 | } 82 | .ten h1:before { 83 | background-color: #0d6efd; 84 | border-radius: 0.25rem; 85 | content: ''; 86 | display: block; 87 | height: 0.25rem; 88 | width: 42px; 89 | margin-bottom: 1.25rem; 90 | } 91 | .twelve h1 { 92 | font-size: 26px; 93 | font-weight: 700; 94 | letter-spacing: 1px; 95 | text-transform: uppercase; 96 | width: 160px; 97 | text-align: center; 98 | margin: auto; 99 | white-space: nowrap; 100 | padding-bottom: 13px; 101 | color: black; 102 | } 103 | .twelve h1:before { 104 | background-color: $theme-red; 105 | content: ''; 106 | display: block; 107 | height: 3px; 108 | width: 75px; 109 | margin-bottom: 5px; 110 | } 111 | .twelve h1:after { 112 | background-color: $theme-red; 113 | content: ''; 114 | display: block; 115 | position: absolute; 116 | right: 0; 117 | bottom: 0; 118 | height: 3px; 119 | width: 75px; 120 | margin-bottom: 0.25em; 121 | } 122 | .logo h1 { 123 | font-size: 26px; 124 | font-weight: 700; 125 | letter-spacing: 1px; 126 | text-transform: uppercase; 127 | width: 160px; 128 | text-align: center; 129 | margin: auto; 130 | white-space: nowrap; 131 | padding-bottom: 13px; 132 | color: white; 133 | cursor: pointer; 134 | } 135 | .logo h1:before { 136 | background-color: $theme-red; 137 | content: ''; 138 | display: block; 139 | height: 3px; 140 | width: 75px; 141 | margin-bottom: 5px; 142 | } 143 | .logo h1:after { 144 | background-color: $theme-red; 145 | content: ''; 146 | display: block; 147 | position: absolute; 148 | right: 0; 149 | bottom: 0; 150 | height: 3px; 151 | width: 75px; 152 | margin-bottom: 0.25em; 153 | } 154 | .button { 155 | background-color: $theme-red; /* Green */ 156 | border: none; 157 | color: white; 158 | padding: 16px 32px; 159 | text-align: center; 160 | text-decoration: none; 161 | display: inline-block; 162 | font-size: 16px; 163 | margin: 4px 2px; 164 | transition-duration: 0.4s; 165 | cursor: pointer; 166 | } 167 | .nav-link { 168 | cursor: pointer; 169 | } 170 | .button1 { 171 | background-color: white; 172 | color: $theme-red; 173 | border: 2px solid $theme-red; 174 | font-weight: 600; 175 | letter-spacing: 0.1em; 176 | text-transform: uppercase; 177 | } 178 | .buttonSelected { 179 | background-color: $theme-red; 180 | color: white; 181 | border: 2px solid $theme-red; 182 | font-weight: 600; 183 | letter-spacing: 0.1em; 184 | text-transform: uppercase; 185 | cursor: default; 186 | } 187 | 188 | .button1:hover { 189 | background-color: $theme-red; 190 | color: white; 191 | } 192 | .link { 193 | a:visited { 194 | color: $theme-red !important; 195 | } 196 | a:active { 197 | color: #1b6970 !important; 198 | } 199 | a { 200 | color: $theme-red !important; 201 | text-decoration: none; 202 | } 203 | } 204 | .img { 205 | overflow: auto; 206 | width: fit-content; 207 | } 208 | .view-more { 209 | color: white; 210 | text-decoration: none; 211 | font-size: large; 212 | border-style: solid; 213 | padding: 2px 8px; 214 | border-width: 1px; 215 | border-radius: 5px; 216 | :hover, 217 | :active { 218 | color: $theme-red; 219 | } 220 | } 221 | .logout { 222 | display: flex; 223 | justify-content: flex-end; 224 | text-align: right; 225 | } 226 | .nav-link { 227 | color: white !important; 228 | font-size: larger; 229 | } 230 | .navbar-dark { 231 | background-color: #66347f; 232 | } 233 | .description { 234 | color: white; 235 | font-size: larger; 236 | } 237 | .comments { 238 | .halfDiv, 239 | .no-comDiv { 240 | color: white; 241 | } 242 | .comment-title { 243 | color: $title-text; 244 | } 245 | } 246 | 247 | .movie-list { 248 | list-style: none; 249 | margin: 0; 250 | padding: 0; 251 | } 252 | 253 | .movie { 254 | display: flex; 255 | align-items: center; 256 | margin-bottom: 20px; 257 | :hover { 258 | cursor: pointer; 259 | scale: 1.011; 260 | } 261 | } 262 | 263 | .movie img { 264 | width: 100px; 265 | height: 70px; 266 | margin-right: 20px; 267 | object-fit: scale-down; 268 | } 269 | 270 | .movie-details { 271 | flex: 1; 272 | } 273 | 274 | .movie-title { 275 | font-size: 24px; 276 | margin: 0; 277 | } 278 | 279 | .movie-rating { 280 | font-size: 18px; 281 | margin: 0; 282 | } 283 | .user-rating { 284 | color: white; 285 | } 286 | .profilePic { 287 | border-radius: 50%; 288 | } 289 | .list-card { 290 | text-align: left; 291 | border: 0.5px solid $theme-red; 292 | text-decoration: none; 293 | margin-bottom: 5px; 294 | cursor: pointer; 295 | font-weight: 500; 296 | // font-size: 1.5vw; 297 | :hover { 298 | .name { 299 | color: $theme-red; 300 | } 301 | } 302 | } 303 | .list-card1 { 304 | text-align: left; 305 | border: 0.5px solid $theme-red; 306 | text-decoration: none; 307 | margin-bottom: 5px; 308 | // cursor: pointer; 309 | font-weight: 500; 310 | // font-size: 1.5vw; 311 | } 312 | .deleteButton { 313 | color: red; 314 | padding: 5px; 315 | cursor: pointer; 316 | } 317 | .container-home { 318 | display: flex; 319 | width: 100%; 320 | padding: 0 1rem; 321 | margin-right: auto; 322 | margin-left: auto; 323 | justify-content: center; 324 | flex-direction: column; 325 | align-content: center; 326 | flex-wrap: wrap; 327 | } 328 | .movie-card { 329 | position: relative; 330 | } 331 | 332 | .overlay-card { 333 | position: absolute; 334 | top: 0; 335 | right: 0; 336 | display: none; 337 | justify-content: center; 338 | align-items: center; 339 | width: 100%; 340 | height: 100%; 341 | background-color: rgba(0, 0, 0, 0.5); 342 | } 343 | 344 | .movie-card:hover .overlay-card { 345 | display: flex; 346 | } 347 | 348 | .overlay-card .badge { 349 | position: relative; 350 | z-index: 1; 351 | } 352 | 353 | .card-img-top { 354 | transition: transform 0.3s ease-in-out; 355 | } 356 | 357 | .movie-card:hover .card-img-top { 358 | transform: scale(1.05); 359 | } 360 | .text-overlay { 361 | font-weight: bold; 362 | color: white; 363 | font-size: 1.5rem; 364 | } 365 | .trailer-container { 366 | display: flex; 367 | justify-content: center; 368 | } 369 | .iframe-container { 370 | position: relative; 371 | width: 100%; 372 | padding-bottom: 56.25%; 373 | height: 0; 374 | } 375 | .iframe-container iframe { 376 | position: absolute; 377 | top: 0; 378 | left: 0; 379 | width: 100%; 380 | height: 100%; 381 | } 382 | .search-bar { 383 | box-shadow: inset #abacaf 0 0 0 2px; 384 | border: 0; 385 | height: 100%; 386 | // background: rgba(0, 0, 0, 0); 387 | appearance: none; 388 | width: 100%; 389 | position: relative; 390 | border-radius: 3px; 391 | padding: 9px 12px; 392 | line-height: 1.4; 393 | color: rgb(0, 0, 0); 394 | font-size: 16px; 395 | font-weight: 400; 396 | transition: all 0.2s ease; 397 | :hover { 398 | box-shadow: 0 0 0 0 #fff inset, #1de9b6 0 0 0 2px; 399 | } 400 | :focus { 401 | background: #fff; 402 | outline: 0; 403 | box-shadow: 0 0 0 0 #fff inset, #1de9b6 0 0 0 3px; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/Shared/images/login.svg: -------------------------------------------------------------------------------- 1 | home_cinema -------------------------------------------------------------------------------- /src/Components/ProfilePage/ProfilePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import Error from "../ErrorPage/ErrorPage"; 4 | import { Rings } from "react-loader-spinner"; 5 | import { ErrorMessage, Field, Form, Formik } from "formik"; 6 | import { getBackend, patchBackend } from "../../Utilities/apiCalls"; 7 | import Swal from "sweetalert2"; 8 | function ProfilePage() { 9 | const [error, setError] = useState(null); 10 | const [isLoaded, setIsLoaded] = useState(false); 11 | const [profile, setProfile] = useState({}); 12 | 13 | const fetchUser = async () => { 14 | return getBackend({ 15 | url: "user/", 16 | }) 17 | .then((res) => res.data) 18 | .then((res) => { 19 | setProfile(res); 20 | }) 21 | .catch(() => { 22 | setError(true); 23 | Swal.fire({ 24 | title: "Error", 25 | text: `Some error occurred in fetching user`, 26 | icon: "error", 27 | confirmButtonText: "Dismiss", 28 | }); 29 | }); 30 | }; 31 | 32 | useEffect(() => { 33 | fetchUser().then(() => { 34 | setIsLoaded(true); 35 | }); 36 | }, []); 37 | if (error) { 38 | return ; 39 | } else if (!isLoaded) { 40 | return ( 41 |
50 | 51 |
52 | ); 53 | } else 54 | return ( 55 |
56 |
57 |
58 |
59 |
60 |

61 | Profile 62 |

63 |
64 |
65 | pic 75 |
76 | { 85 | let data = { 86 | firstName: values.firstname, 87 | lastName: values.lastname, 88 | email: values.email, 89 | password: values.password, 90 | }; 91 | if (values.password) { 92 | data["Password"] = values.password; 93 | } 94 | patchBackend({ 95 | url: "user/", 96 | data: data, 97 | }).then(() => { 98 | fetchUser(); 99 | Swal.fire({ 100 | confirmButtonColor: "#e31c5f", 101 | title: "Success", 102 | text: `Saved Changes`, 103 | icon: "success", 104 | confirmButtonText: "Dismiss", 105 | }); 106 | }); 107 | }} 108 | > 109 | {({ errors, touched }) => ( 110 |
111 |
112 | 119 | 122 |
123 | 124 |
125 |
126 |
127 | 133 | 136 |
137 | 138 |
139 |
140 |
141 | 147 | 150 |
151 | 152 |
153 |
154 |
155 | 161 | 164 |
165 | 166 |
167 |
168 |
169 | 175 | 178 |
179 | 180 |
181 |
182 |
183 | 190 |
191 |
192 | )} 193 |
194 |
195 |
196 | Change recommendations?{" "} 197 | Edit Genres 198 |
199 |
200 |
201 |
202 |
203 | ); 204 | } 205 | 206 | export default ProfilePage; 207 | -------------------------------------------------------------------------------- /src/Components/MovieDetail/MovieDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Rings } from 'react-loader-spinner'; 3 | import { useParams, useLocation } from 'react-router-dom'; 4 | import { 5 | deleteBackend, 6 | fget, 7 | getBackend, 8 | patchBackend, 9 | postBackend, 10 | } from '../../Utilities/apiCalls'; 11 | import SingleCard from '../Render/SingleCard'; 12 | import Error from '../ErrorPage/ErrorPage'; 13 | import { CommentSection } from 'react-comments-section'; 14 | import 'react-comments-section/dist/index.css'; 15 | import { useUser } from '../../Shared/js/user-context'; 16 | import StarRating from '../Render/Stars'; 17 | import TrailerFrame from '../TrailerComponent/TrailerComponent'; 18 | import { trackEvent } from '../../Shared/js/r42'; 19 | const watchNowStyle = { 20 | display: 'inline-block', 21 | padding: '10px 20px', 22 | backgroundColor: 'black', 23 | color: 'white', 24 | textDecoration: 'none', 25 | borderRadius: '5px', 26 | fontSize: '16px', 27 | fontWeight: 'bold', 28 | margin: '10px 0', 29 | }; 30 | function MovieDetail() { 31 | let { movieId } = useParams(); 32 | const location = useLocation(); 33 | const [error, setError] = useState(null); 34 | const [isLoaded, setIsLoaded] = useState(false); 35 | const [isCommentLoaded, setIsCommentLoaded] = useState(false); 36 | const [isMovieWatched, setIsMovieWatched] = useState(false); 37 | const [movie, setMovie] = useState(location.state.movie); 38 | const [comments, setComments] = useState(null); 39 | const [backendMovie, setBackendMovie] = useState(null); 40 | const [userRating, setUserRating] = useState(0); 41 | const [trailer, setTrailer] = useState(null); 42 | const [watchOptions, setWatchOptions] = useState(null); 43 | const handleRatingChange = (rating) => { 44 | setUserRating(rating); 45 | }; 46 | const { 47 | state: { user }, 48 | } = useUser(); 49 | 50 | const addToWatchlist = () => { 51 | window.adobeDataLayer.push({ 52 | event: 'watchlist', 53 | watchlist: { 54 | id: movieId, 55 | title: backendMovie.title, 56 | }, 57 | }); 58 | setR42('addToWatchList'); 59 | postBackend({ 60 | url: `api/watchlist/${movieId}`, 61 | data: {}, 62 | }).then(() => isMovieWatchedFunction()); 63 | }; 64 | 65 | const getTrailer = () => { 66 | return fget({ 67 | url: `/3/movie/${movieId}/videos?api_key=${process.env.REACT_APP_BASE_TOKEN}`, 68 | }) 69 | .then((res) => res.data) 70 | .then( 71 | (result) => { 72 | if (result.results.length > 0) 73 | setTrailer( 74 | result.results.find( 75 | (ele) => ele.type === 'Trailer' 76 | ) || result.results[0] 77 | ); 78 | else setTrailer(null); 79 | }, 80 | (error) => { 81 | setError(error); 82 | } 83 | ); 84 | }; 85 | 86 | const getMovie = () => { 87 | return postBackend({ 88 | url: 'api/movie/' + movieId, 89 | data: { 90 | id: +movieId, 91 | title: movie.title || '', 92 | release_date: movie.release_date || '', 93 | overview: movie.overview || '', 94 | imdbRating: +movie.vote_average || 0, 95 | poster_path: movie.poster_path || '', 96 | backdrop_path: movie.backdrop_path || '', 97 | }, 98 | }) 99 | .then((res) => res.data) 100 | .then( 101 | (result) => { 102 | setBackendMovie(result); 103 | window.adobeDataLayer.push({ 104 | event: 'movie-detail', 105 | movie: { 106 | id: movieId, 107 | title: result.title, 108 | release_date: result.year, 109 | }, 110 | }); 111 | }, 112 | (error) => { 113 | setError(error); 114 | } 115 | ); 116 | }; 117 | const getComments = () => { 118 | return getBackend({ 119 | url: `api/comment/${movieId}`, 120 | }) 121 | .then((res) => res.data) 122 | .then( 123 | (result) => { 124 | setIsCommentLoaded(true); 125 | let newC = result.data.map((ele) => { 126 | return { 127 | userId: ele.userId, 128 | comId: ele.id, 129 | fullName: 130 | ele.user.firstName + ' ' + ele.user.lastName, 131 | text: ele.comment, 132 | avatarUrl: `https://ui-avatars.com/api/name=${ 133 | ele.user.firstName + '+' + ele.user.lastName 134 | }&background=random`, 135 | replies: [], 136 | }; 137 | }); 138 | setComments(newC); 139 | }, 140 | (error) => { 141 | setIsCommentLoaded(true); 142 | setError(error); 143 | } 144 | ); 145 | }; 146 | const getMovieRating = () => { 147 | return getBackend({ 148 | url: `api/rating/${movieId}`, 149 | }) 150 | .then((res) => res.data) 151 | .then((res) => { 152 | setUserRating(+res.data); 153 | }); 154 | }; 155 | const isMovieWatchedFunction = () => { 156 | return postBackend({ 157 | url: `api/watchlist/check/${movieId}`, 158 | }) 159 | .then((res) => res.data) 160 | .then((res) => { 161 | setIsMovieWatched(res.data); 162 | }); 163 | }; 164 | const removeFromWatchList = () => { 165 | deleteBackend({ 166 | url: `api/watchlist/${movieId}`, 167 | data: {}, 168 | }).then(() => isMovieWatchedFunction()); 169 | }; 170 | const getWatchOptions = () => { 171 | let location = navigator.language.split('-')[1]; 172 | return fget({ 173 | url: `/3/movie/${movieId}/watch/providers?api_key=${process.env.REACT_APP_BASE_TOKEN}`, 174 | }) 175 | .then((res) => res.data) 176 | .then( 177 | (result) => { 178 | setWatchOptions( 179 | result.results[location] || 180 | result.results.GB || 181 | result.results.US || 182 | result.results.IN 183 | ); 184 | }, 185 | (error) => { 186 | setError(error); 187 | } 188 | ); 189 | }; 190 | const setR42 = (action) => { 191 | let data = { 192 | movieIMDBRating: movie.vote_average, 193 | movieReleaseDate: movie.release_date, 194 | movieID: movieId, 195 | movieTitle: movie.title, 196 | dateTime: new Date().toISOString(), 197 | }; 198 | if (action) { 199 | data['action'] = action; 200 | } 201 | 202 | trackEvent(data); 203 | }; 204 | 205 | useEffect(() => { 206 | window.scrollTo(0, 0); 207 | Promise.all([ 208 | getMovie(), 209 | getComments(), 210 | isMovieWatchedFunction(), 211 | getTrailer(), 212 | getWatchOptions(), 213 | ]) 214 | .then(() => { 215 | getMovieRating(); 216 | }) 217 | .then(() => { 218 | setIsLoaded(true); 219 | setR42(); 220 | }); 221 | }, [movie]); 222 | if (error) { 223 | return ; 224 | } else if (!isLoaded) { 225 | return ( 226 |
235 | 236 |
237 | ); 238 | } else 239 | return ( 240 |
241 |
242 |
243 | 244 | 245 |
246 | {!isMovieWatched ? ( 247 | 254 | ) : ( 255 | 262 | )} 263 |
264 |
265 |
266 |
267 |
268 |

{backendMovie.title}

269 |
270 |
271 |
272 | 273 |
274 |
275 |
276 |
277 |
278 |

User Rating

279 |
280 | 287 |
288 |
289 |
290 |

IMDB Rating

291 |

292 | {' '} 293 | {Math.round( 294 | movie.vote_average * 10 295 | ) / 10} 296 |

297 |
298 |
299 |
300 |
301 |
302 | {backendMovie.description} 303 |
304 | 317 |
318 | { 329 | postBackend({ 330 | url: 'api/comment/' + movieId, 331 | data: { 332 | comment: data.text, 333 | }, 334 | }) 335 | .then(() => getComments()) 336 | .then(() => { 337 | window.adobeDataLayer.push({ 338 | event: 'comment', 339 | comment: { 340 | id: movieId, 341 | title: backendMovie.title, 342 | }, 343 | }); 344 | }); 345 | }} 346 | onDeleteAction={(data, rest) => { 347 | deleteBackend({ 348 | url: 349 | 'api/comment/' + data.comIdToDelete, 350 | }).then(() => getComments()); 351 | }} 352 | onEditAction={(data) => { 353 | patchBackend({ 354 | url: 'api/comment/' + movieId, 355 | data: { 356 | id: data.comId, 357 | comment: data.text, 358 | }, 359 | }).then(() => getComments()); 360 | }} 361 | /> 362 |
363 |
364 |
365 |
366 | ); 367 | } 368 | export default MovieDetail; 369 | -------------------------------------------------------------------------------- /src/Shared/images/Register.svg: -------------------------------------------------------------------------------- 1 | horror movie --------------------------------------------------------------------------------