├── .gitignore
├── README.md
├── cypress.json
├── cypress
├── integration
│ ├── browse-home.spec.js
│ ├── browse-navbar.spec.js
│ ├── landing-section.spec.js
│ ├── login-form.spec.js
│ └── sign-in-flow.spec.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── flixdemo.gif
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── 404.html
├── index.html
└── manifest.json
└── src
├── App.js
├── assets
└── images
│ ├── howl.png
│ ├── index.js
│ ├── landingPage.jpg
│ ├── netflix.png
│ ├── normal.jpg
│ ├── profile.jpg
│ ├── smile.png
│ └── weird.png
├── baseAxios.js
├── components
├── Modals
│ ├── ProfileModal
│ │ ├── ProfileModal.css
│ │ └── ProfileModal.js
│ └── VideoModal
│ │ ├── VideoModal.css
│ │ └── VideoModal.js
├── Navigation
│ └── Dropdown
│ │ ├── Dropdown.css
│ │ └── Dropdown.js
├── StaticPages
│ ├── ErrorPage
│ │ ├── ErrorPage.css
│ │ └── ErrorPage.js
│ ├── LoadingScreen
│ │ ├── LoadingScreen.css
│ │ └── LoadingScreen.js
│ └── NotFoundPage
│ │ ├── NotFoundPage.css
│ │ └── NotFoundPage.js
├── UI
│ ├── Button
│ │ ├── Button.css
│ │ └── Button.js
│ ├── CircularSoundButton
│ │ ├── CircularSoundButton.css
│ │ └── CircularSoundButton.js
│ ├── DarkComponent
│ │ └── DarkComponent.js
│ ├── FAQComponent
│ │ ├── FAQComponent.css
│ │ └── FAQComponent.js
│ └── ProfileCard
│ │ ├── ProfileCard.css
│ │ └── ProfileCard.js
└── Video
│ ├── TopTrailerComponent
│ ├── TopTrailerComponent.css
│ └── TopTrailerComponent.js
│ ├── VideoCard
│ ├── VideoCard.css
│ └── VideoCard.js
│ └── VideoCarousel
│ ├── VideoCarousel.css
│ └── VideoCarousel.js
├── containers
├── Browse
│ ├── Browse.js
│ ├── BrowseContent
│ │ ├── BrowseContent.css
│ │ └── BrowseContent.js
│ ├── SearchContent
│ │ ├── SearchContent.css
│ │ └── SearchContent.js
│ └── routes
│ │ ├── Home.js
│ │ ├── LatestVideo.js
│ │ ├── List.js
│ │ ├── Movies.js
│ │ ├── Tv.js
│ │ └── index.js
├── LandingSection
│ ├── LandingSection.css
│ ├── LandingSection.js
│ └── LandingSectionTexts.js
├── Login
│ ├── Login.css
│ └── Login.js
├── NavBar
│ ├── NavBar.css
│ └── NavBar.js
└── Search
│ ├── Search.css
│ └── Search.js
├── context
└── Authentication.js
├── hoc
├── Layout.js
└── ScrollToTop
│ └── ScrollToTop.js
├── hooks
├── useDropdown.js
├── useHoverStyleButton.js
├── useNavbar.js
└── useVideoInfoHandlers.js
├── index.js
├── store
└── reducers
│ ├── slices
│ ├── latestVideoSlice.js
│ ├── moviesByGenreSlice.js
│ ├── netflixOriginalsSlice.js
│ ├── topratedSlice.js
│ ├── trendingSlice.js
│ └── tvByGenreSlice.js
│ └── store.js
├── styles.css
└── utils
├── animations.js
├── sorting.js
├── time.js
├── transformations.js
└── validation.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | node_modules/
4 | build
5 | .DS_Store
6 | *.tgz
7 | my-app*
8 | template/src/__tests__/__snapshots__/
9 | lerna-debug.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | /.changelog
14 | .npm/
15 | yarn.lock
16 | .env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Netflix-Clone
2 |
3 | 
4 |
5 | A Netflix clone I created for the sake of practicing React and Redux. It features design
6 | patterns recommended by the documentation. Some of the tools used include:
7 |
8 | * Hooks (and custom hooks)
9 | * React Router
10 | * Redux Toolkit
11 | * Context API
12 | * Responsive web design
13 | * Cypress end-to-end testing
14 |
15 |
16 |
17 | It is a work in progress, and my first real project with React. Any tips on how to better write the
18 | code, manage the folder structure, etc would be really appreciated.
19 |
20 | The future of this project:
21 |
22 | * Integrate it with a Django backend
23 | * Create an authentication flow
24 | * Add REST API endpoints for every user-related event, such as adding Netflix profiles
25 |
26 | ## Architecture Diagram
27 |
28 | 
29 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000"
3 | }
--------------------------------------------------------------------------------
/cypress/integration/browse-home.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe(' -> ', () => {
4 | beforeEach(() => {
5 | localStorage.setItem('profileSelected', true)
6 | cy.visit('/browse')
7 | cy.window()
8 | .its('store')
9 | .invoke('getState')
10 | .as('reduxState')
11 |
12 | })
13 |
14 | it('fetches trending, top-rated, and netflix originals successfully', () => {
15 | cy.get('@reduxState')
16 | .its('trending')
17 | .its('ids')
18 | .should('have.length', 20)
19 |
20 | cy.get('@reduxState')
21 | .its('toprated')
22 | .its('ids')
23 | .should('have.length', 20)
24 |
25 | cy.get('@reduxState')
26 | .its('netflixOriginals')
27 | .its('ids')
28 | .should('have.length', 20)
29 | })
30 |
31 | it('Ensure that the first video of trending section is placed on the topTrailer', () => {
32 | cy.get('@reduxState')
33 | .its('trending')
34 | .its('ids')
35 | .then($idArray => {
36 | const firstElem = $idArray[0]
37 | cy.get('@reduxState')
38 | .its('trending')
39 | .its('entities')
40 | .then(($entities => {
41 | const item = Cypress._.find($entities, (element => element.id === firstElem))
42 | cy.get('.VideoComponent')
43 | .invoke('attr', 'style')
44 | .then(style => {
45 | expect(style).to.include(item.poster_path)
46 | })
47 | }))
48 | })
49 | })
50 | })
--------------------------------------------------------------------------------
/cypress/integration/browse-navbar.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe(' and ', () => {
4 | beforeEach(() => {
5 | localStorage.setItem('profileSelected', true)
6 | cy.visit('/browse')
7 | })
8 |
9 | context('', () => {
10 | beforeEach(() => {
11 | cy.get('.OptionsContainer .Dropdown > svg')
12 | .as('dropbox')
13 | })
14 |
15 | it('finds floating box on hover and fails to find it on hover off', () => {
16 | cy.get('@dropbox')
17 | .trigger('mouseover')
18 | .then(() => {
19 | cy.get('.FloatingBox')
20 | .should('exist')
21 | })
22 | .trigger('mouseleave', 'bottom')
23 | .then(() => {
24 | cy.get('FloatingBox')
25 | .should('not.exist')
26 | })
27 | })
28 |
29 | it('logs out and removes the local storage token on sign out press', () => {
30 | cy.get('@dropbox')
31 | .trigger('mouseover')
32 | .get('.FloatingBox')
33 | .find('span')
34 | .contains('Sign out of Netflix')
35 | .click()
36 | .then(() => {
37 | expect(localStorage.getItem('profileSelected')).to.not.exist
38 | cy.location().should(loc => {
39 | expect(loc.pathname).to.eq('/')
40 | })
41 | })
42 | })
43 | })
44 |
45 | context('', () => {
46 | beforeEach(() => {
47 | cy.get('.SearchBox')
48 | .click()
49 | })
50 | it('opens search box on click and closes on background click', () => {
51 | cy.get('.Holder')
52 | .should('exist')
53 |
54 | cy.get('.NavBar')
55 | .click('center')
56 | .find('.Holder')
57 | .should('not.exist')
58 | })
59 |
60 | it('cross becomes visible after typing and invisible when input length is 0', () => {
61 | cy.get('.Holder')
62 | .children('input')
63 | .type('Some movie title')
64 |
65 | cy.get('.Holder')
66 | .children('svg')
67 | .last()
68 | .click()
69 | .should('not.exist')
70 | })
71 | })
72 | })
--------------------------------------------------------------------------------
/cypress/integration/landing-section.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('', () => {
4 | beforeEach(() => {
5 | cy.visit('/')
6 | })
7 |
8 | context('', () => {
9 | it('leaves number of components intact even after pressing all boxes', () => {
10 | cy.get('.faqComponent')
11 | .click({ multiple: true })
12 | .should('have.length', 5)
13 | })
14 |
15 | it('has a set number of FAQ boxes if not clicked', () => {
16 | cy.get('.faqComponent')
17 | .should('have.length', 5)
18 | })
19 |
20 | it('opens an inner box on click', () => {
21 | cy.get('.tv-inner > .faqComponent')
22 | .first()
23 | .click()
24 |
25 | cy.get('.faqComponent')
26 | .should('have.length', 6)
27 | })
28 | })
29 |
30 | context('Sign In Button', () => {
31 | it("navigates to the login page on sign in button click", () => {
32 | cy.get('.Button')
33 | .contains('Sign In')
34 | .click()
35 |
36 | cy.location().should(loc => {
37 | expect(loc.pathname).to.eq('/login')
38 | })
39 | })
40 | })
41 |
42 | })
--------------------------------------------------------------------------------
/cypress/integration/login-form.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('', () => {
4 | beforeEach(() => {
5 | cy.visit('/login')
6 |
7 | cy.get('input[name="email"]')
8 | .as('inputEmail')
9 |
10 | cy.get('input[name="password"]')
11 | .as('passwordInput')
12 | })
13 |
14 | it('fails to submit form without any input and shows error spans for both inputs', () => {
15 | cy.get('.Button')
16 | .contains('Sign In')
17 | .click()
18 |
19 | cy.location().should(loc => {
20 | expect(loc.pathname).to.eq('/login')
21 | })
22 |
23 | cy.get('form')
24 | .children('span')
25 | .should('have.length', 2)
26 | })
27 |
28 | it('shows a span text on email input focus and focus lost', () => {
29 | const spanText = 'Please enter a valid email or phone number.'
30 | cy.get('@inputEmail')
31 | .focus()
32 | .blur()
33 |
34 | cy.get('form')
35 | .children('span')
36 | .should('have.length', 1)
37 | .contains(spanText)
38 | })
39 |
40 | it('changes span text on password validation passed', () => {
41 | const spanText = 'Your password must contain between 4 and 60 characters.'
42 | cy.get('@passwordInput')
43 | .focus()
44 | .blur()
45 |
46 | cy.get('form')
47 | .children('span')
48 | .as('text')
49 |
50 | cy.get('@text')
51 | .should('have.length', 1)
52 | .contains(spanText)
53 |
54 | cy.get('@passwordInput')
55 | .type('123')
56 |
57 | cy.get('@text')
58 | .contains(spanText)
59 |
60 | cy.get('@passwordInput')
61 | .type('4')
62 |
63 | cy.get('@text')
64 | .should('not.exist')
65 | })
66 |
67 | it('logs in on valid input', () => {
68 | cy.get('@inputEmail')
69 | .type('example@example.com')
70 |
71 | cy.get('@passwordInput')
72 | .type('1234')
73 |
74 | cy.get('.Button')
75 | .contains('Sign In')
76 | .click()
77 |
78 | cy.location().should(loc => {
79 | expect(loc.pathname).to.eq('/browse')
80 | })
81 | })
82 | })
--------------------------------------------------------------------------------
/cypress/integration/sign-in-flow.spec.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('', () => {
4 | beforeEach(() => {
5 | cy.validLogin()
6 | })
7 |
8 | it('opens browse section if local storage profile selection is set', () => {
9 | localStorage.setItem('profileSelected', true)
10 |
11 | cy.get('.Button')
12 | .contains('Sign In')
13 | .click()
14 |
15 | cy.get('.BrowseContent')
16 | .should('exist')
17 | })
18 |
19 | it('opens modal if local storage is not set, and continues to browse section', () => {
20 | cy.clearLocalStorage()
21 |
22 | cy.get('.Button')
23 | .contains('Sign In')
24 | .click()
25 |
26 | cy.get('.BrowseContent')
27 | .should('not.exist')
28 |
29 | cy.get('.ProfileDiv')
30 | .should('exist')
31 | .find('.ProfileCard')
32 | .as('profileCard')
33 | .should('have.length', 4)
34 |
35 | cy.get('@profileCard')
36 | .first()
37 | .click()
38 | .should(($profileCard) => {
39 | expect(localStorage.getItem('profileSelected')).to.exist
40 | expect($profileCard).to.not.exist
41 | })
42 |
43 | cy.get('.BrowseContent')
44 | .should('exist')
45 | })
46 | })
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | Cypress.Commands.add('validLogin', () => {
2 | cy.visit('/login')
3 |
4 | cy.get('input[name="email"]')
5 | .type('example@example.com')
6 |
7 | cy.get('input[name="password"]')
8 | .type('1234')
9 | })
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/flixdemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/flixdemo.gif
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": [
6 | "src"
7 | ]
8 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netflix-clone",
3 | "version": "1.0.0",
4 | "description": "",
5 | "homepage": "https://azazel5.github.io/NetflixClone/",
6 | "keywords": [],
7 | "main": "src/index.js",
8 | "dependencies": {
9 | "@fortawesome/fontawesome-svg-core": "1.2.29",
10 | "@fortawesome/free-solid-svg-icons": "5.13.1",
11 | "@fortawesome/react-fontawesome": "0.1.11",
12 | "@material-ui/core": "4.11.0",
13 | "@reduxjs/toolkit": "^1.4.0",
14 | "axios": "^0.19.2",
15 | "react": "16.12.0",
16 | "react-device-detect": "^1.13.1",
17 | "react-dom": "16.12.0",
18 | "react-modal": "^3.11.2",
19 | "react-redux": "^7.2.0",
20 | "react-router": "5.2.0",
21 | "react-router-dom": "5.2.0",
22 | "react-scripts": "3.0.1",
23 | "react-transition-group": "^4.4.1",
24 | "redux": "^4.0.5",
25 | "redux-thunk": "^2.3.0",
26 | "reselect": "^4.0.0"
27 | },
28 | "devDependencies": {
29 | "cypress": "^4.12.1",
30 | "gh-pages": "^3.1.0",
31 | "typescript": "3.8.3"
32 | },
33 | "scripts": {
34 | "start": "react-scripts start",
35 | "cypress": "cypress open",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test --env=jsdom",
38 | "eject": "react-scripts eject",
39 | "predeploy": "npm run build",
40 | "deploy": "gh-pages -d build"
41 | },
42 | "browserslist": [
43 | ">0.2%",
44 | "not dead",
45 | "not ie <= 11",
46 | "not op_mini all"
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | Netflix Clone
24 |
53 |
54 |
55 |
56 |
57 |
60 |
61 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import "./styles.css";
3 |
4 | import LandingSection from "containers/LandingSection/LandingSection";
5 | import Login from "containers/Login/Login";
6 | import Browse from 'containers/Browse/Browse'
7 | import { Switch, Route, Redirect } from "react-router-dom";
8 | import { AuthenticationContext } from 'context/Authentication'
9 | import NotFoundPage from 'components/StaticPages/NotFoundPage/NotFoundPage'
10 |
11 | export default function App() {
12 | const authContext = useContext(AuthenticationContext)
13 |
14 | const checkAuthAndSetBrowseComponent = (propsObject) => {
15 | return (authContext.authenticated || localStorage.getItem('profileSelected')) ?
16 | :
17 |
18 | }
19 |
20 | return (
21 |
22 |
23 | checkAuthAndSetBrowseComponent({ route: '/browse' })}>
24 |
25 | checkAuthAndSetBrowseComponent({ route: '/browse/tv' })}>
26 |
27 | checkAuthAndSetBrowseComponent({ route: '/browse/movies' })}>
28 |
29 | checkAuthAndSetBrowseComponent({ route: '/browse/latest' })}>
30 |
31 | checkAuthAndSetBrowseComponent({ route: '/browse/list' })}>
32 |
33 | checkAuthAndSetBrowseComponent({ route: '/search' })}>
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/assets/images/howl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/src/assets/images/howl.png
--------------------------------------------------------------------------------
/src/assets/images/index.js:
--------------------------------------------------------------------------------
1 | import Weird from './weird.png'
2 | import Profile from './profile.jpg'
3 | import Smile from './smile.png'
4 | import Normal from './normal.jpg'
5 | import NetflixLogo from './netflix.png'
6 | import LoginBackground from './landingPage.jpg'
7 |
8 | export {
9 | Weird,
10 | Profile,
11 | Smile,
12 | Normal,
13 | NetflixLogo,
14 | LoginBackground
15 | }
--------------------------------------------------------------------------------
/src/assets/images/landingPage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/src/assets/images/landingPage.jpg
--------------------------------------------------------------------------------
/src/assets/images/netflix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/src/assets/images/netflix.png
--------------------------------------------------------------------------------
/src/assets/images/normal.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/src/assets/images/normal.jpg
--------------------------------------------------------------------------------
/src/assets/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/src/assets/images/profile.jpg
--------------------------------------------------------------------------------
/src/assets/images/smile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/src/assets/images/smile.png
--------------------------------------------------------------------------------
/src/assets/images/weird.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azazel5/NetflixClone/327558846a62d4b0b5c05fdb3f5c6a08cb0aea0a/src/assets/images/weird.png
--------------------------------------------------------------------------------
/src/baseAxios.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const axe = axios.create({
4 | baseURL: 'https://api.themoviedb.org/3/'
5 | })
6 |
7 | export default axe
--------------------------------------------------------------------------------
/src/components/Modals/ProfileModal/ProfileModal.css:
--------------------------------------------------------------------------------
1 | .ProfileModal {
2 | top: 0;
3 | left: 0;
4 | height: 100%;
5 | width: 100%;
6 | background: #141414;
7 | border: none;
8 | border-radius: none;
9 | padding: 0;
10 | outline: none;
11 | }
12 |
13 | .ProfileModal > img {
14 | height: 45px;
15 | width: 140px;
16 | margin-left: 20px;
17 | padding: 8px
18 | }
19 |
20 | .ProfileDiv {
21 | width: 60%;
22 | margin: 0 auto;
23 | text-align: center;
24 | position: relative;
25 | top: 30%;
26 | transform: translateY(-50%);
27 | }
28 |
29 | .ProfileDiv > h1 {
30 | color: rgb(255, 255, 255);
31 | font-weight: lighter;
32 | font-size: 6.5vw;
33 | }
34 |
35 | .horizontalComp {
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | margin-bottom: 30px;
40 | flex-wrap: wrap;
41 | }
42 |
43 | .ProfileButton {
44 | border: 1px solid transparent;
45 | border-color: grey;
46 | background-color: transparent;
47 | color: grey;
48 | text-transform: uppercase;
49 | padding: .5em 1.5em;
50 | letter-spacing: 2px;
51 | font-size: .9em;
52 | outline: 0;
53 | }
54 |
55 | .ProfileButton:hover {
56 | border-color: white;
57 | color: white;
58 | }
59 |
60 | @media(min-width: 600px) {
61 | .ProfileDiv > h1 {
62 | font-size: 3.5vw;
63 | }
64 | }
--------------------------------------------------------------------------------
/src/components/Modals/ProfileModal/ProfileModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Modal from 'react-modal'
3 | import { NetflixLogo } from 'assets/images/'
4 | import ProfileCard from 'components/UI/ProfileCard/ProfileCard'
5 | import './ProfileModal.css'
6 |
7 | import {
8 | Weird,
9 | Profile,
10 | Smile,
11 | Normal
12 | } from 'assets/images/'
13 |
14 |
15 | if (process.env.NODE_ENV !== 'test') {
16 | Modal.setAppElement('#root');
17 | }
18 |
19 | const profileModal = props => {
20 | const { modalOpen, profileClickHandler } = props
21 |
22 | return (
23 |
29 |
30 |
31 |
Who's watching?
32 |
33 |
39 |
40 |
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default profileModal
--------------------------------------------------------------------------------
/src/components/Modals/VideoModal/VideoModal.css:
--------------------------------------------------------------------------------
1 | .ModalStyles {
2 | position: absolute;
3 | left: 50%;
4 | top: 50%;
5 | transform: translate(-50%, -50%);
6 | height: 45%;
7 | width: 100%;
8 | background: rgba(0,0,0,0.966);
9 | border: none;
10 | outline: none;
11 | font-size: 0.8rem;
12 | padding: 0;
13 | }
14 |
15 | .VideoDetailSection {
16 | height: 100%;
17 | }
18 |
19 | .shadowedSection > * {
20 | margin-bottom: 1rem;
21 | }
22 |
23 | .shadowedSection {
24 | display: flex;
25 | flex-direction: column;
26 | height: 100%;
27 | background: linear-gradient(90deg, rgba(0,0,0,0.966) 55%, transparent);
28 | color: white;
29 | padding: 18px 0 12px 4%;
30 | }
31 |
32 | .shadowedSection div {
33 | width: 65%;
34 | color: #999;
35 | }
36 |
37 | .horizontalStyles {
38 | display: flex;
39 | }
40 |
41 | .horizontalStyles > button {
42 | margin-right: 10px;
43 | }
44 |
45 | .Overview {
46 | overflow: scroll;
47 | }
48 |
49 | @media(min-width: 600px) {
50 | .ModalStyles {
51 | height: 60%;
52 | font-size: 1rem;
53 | }
54 |
55 | .shadowedSection {
56 | background: linear-gradient(90deg, rgba(0,0,0,0.966) 35%, transparent);
57 | }
58 |
59 | .shadowedSection div {
60 | width: 40%;
61 | }
62 |
63 | .shadowedSection > * {
64 | margin-bottom: 2rem;
65 | }
66 |
67 | }
68 |
69 | @media(min-width: 1650px) {
70 | .ModalStyles {
71 | font-size: 1.6rem;
72 | }
73 | }
--------------------------------------------------------------------------------
/src/components/Modals/VideoModal/VideoModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './VideoModal.css'
3 |
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faTimes } from '@fortawesome/free-solid-svg-icons'
6 | import Modal from 'react-modal'
7 | import { getSeasonsOrMovieLength } from 'utils/time'
8 | import { faPlay, faPlus } from '@fortawesome/free-solid-svg-icons'
9 | import Button from 'components/UI/Button/Button'
10 | import useHoverStyleButton from 'hooks/useHoverStyleButton'
11 |
12 |
13 | if (process.env.NODE_ENV !== 'test') {
14 | Modal.setAppElement('#root');
15 | }
16 |
17 | // Don't move this to css; it has to be here for the shouldCloseOnOverlay prop to work
18 | const overlayStyle = {
19 | overlay: {
20 | backgroundColor: 'rgba(17,17,17,0.7)'
21 | }
22 | }
23 |
24 | const VideoModal = props => {
25 | const { videoDetailModal, closeModalHandler, videoInfo } = props
26 | const [buttonHovered, onButtonHoverHandler] = useHoverStyleButton({
27 | 'playButton': true,
28 | 'plusButton': true
29 | })
30 |
31 | const {
32 | vote_average, seasons, runtime,
33 | backdrop_path, poster_path, title, name,
34 | release_date, first_air_date,
35 | overview
36 | } = videoInfo
37 |
38 | const voteAverage = vote_average * 10
39 | const voteStyle = { color: voteAverage > 49 ? '#46d369' : 'red' }
40 | const videoTime = getSeasonsOrMovieLength(seasons, runtime)
41 |
42 | const styles = {
43 | backgroundImage: `url(https://image.tmdb.org/t/p/original/${backdrop_path || poster_path}`,
44 | backgroundSize: 'cover'
45 | }
46 |
47 | return (
48 |
56 |
57 |
61 |
62 |
{title || name}
63 |
64 | {`Rating: ${voteAverage}%`}
65 | {(release_date || first_air_date).substring(0, 4)}
66 | {videoTime}
67 |
68 |
{overview}
69 |
70 |
82 |
83 |
95 |
96 |
97 |
98 |
99 | )
100 | }
101 |
102 | export default React.memo(VideoModal)
--------------------------------------------------------------------------------
/src/components/Navigation/Dropdown/Dropdown.css:
--------------------------------------------------------------------------------
1 | .Dropdown {
2 | position: relative;
3 | display: inline-block;
4 | z-index: 100;
5 | cursor: pointer;
6 | }
7 |
8 | .FloatingBox {
9 | position: absolute;
10 | margin-left: 0;
11 | padding: 0;
12 | border: solid 1px rgba(255,255,255,.15);
13 | display: flex;
14 | flex-direction: column;
15 | background: #141414;
16 | }
17 |
18 | .FloatingBox > * {
19 | padding: 1vh;
20 | }
21 |
22 | .FloatingBox span:hover {
23 | text-decoration: underline;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Navigation/Dropdown/Dropdown.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Dropdown.css'
3 |
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
6 |
7 | const Dropdown = props => {
8 | const {
9 | iconHoveredInHandler, iconHoveredOutHandler, dropdown,
10 | floatingBoxHoveredInHandler, floatingBoxHoveredOutHandler, content,
11 | boxSizing, onItemClickCloseBoxHandler
12 | } = props
13 |
14 | const { iconHovered, floatingBoxHovered } = dropdown
15 | return (
16 |
17 |
24 |
25 | {(iconHovered || floatingBoxHovered) && (
26 |
31 | {content}
32 |
)}
33 |
34 | )
35 | }
36 |
37 | export default Dropdown
--------------------------------------------------------------------------------
/src/components/StaticPages/ErrorPage/ErrorPage.css:
--------------------------------------------------------------------------------
1 | .ErrorPage {
2 | height: 100vh;
3 | background: #141414;
4 | color: white;
5 | padding: 95px;
6 | }
7 |
8 | .ErrorPage-Items {
9 | text-align: center;
10 | }
--------------------------------------------------------------------------------
/src/components/StaticPages/ErrorPage/ErrorPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './ErrorPage.css'
3 |
4 | /**
5 | * I have to check for error message or simply an error because of I've handled errors in the
6 | * reducers. The RTK documentation recommends checking for a promise rejection with value or
7 | * with an error; thus, when it rejects with value, there will be a status_message, and if that
8 | * doesn't happen, there will simply be an error object
9 | */
10 | const ErrorPage = props => {
11 | const { errors } = props
12 |
13 | let errorObjs
14 | if (Array.isArray(errors)) {
15 | errorObjs = errors[0] || errors[1] || errors[2]
16 | }
17 |
18 | const errorType = errorObjs || errors
19 | const errorMessage = errorType.message ? errorType.message : errorType
20 | return (
21 |
22 |
23 |
No matching tiles found.
24 | {errorMessage}
25 |
26 |
27 | )
28 | }
29 |
30 | export default ErrorPage
--------------------------------------------------------------------------------
/src/components/StaticPages/LoadingScreen/LoadingScreen.css:
--------------------------------------------------------------------------------
1 | .LoadingScreen {
2 | background: #141414;
3 | min-height: 100vh;
4 | }
--------------------------------------------------------------------------------
/src/components/StaticPages/LoadingScreen/LoadingScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './LoadingScreen.css'
3 |
4 | const loadingScreen = props => {
5 | return (
6 |
7 |
8 | )
9 | }
10 |
11 | export default loadingScreen
--------------------------------------------------------------------------------
/src/components/StaticPages/NotFoundPage/NotFoundPage.css:
--------------------------------------------------------------------------------
1 | .Parent {
2 | position: relative;
3 | height: 100vh;
4 | }
5 |
6 | .Parent .notfound {
7 | position: absolute;
8 | left: 50%;
9 | top: 50%;
10 | -webkit-transform: translate(-50%, -50%);
11 | -ms-transform: translate(-50%, -50%);
12 | transform: translate(-50%, -50%);
13 | }
14 |
15 | .notfound {
16 | max-width: 460px;
17 | width: 100%;
18 | text-align: center;
19 | line-height: 1.4;
20 | }
21 |
22 | .notfound .notfound-404 {
23 | position: relative;
24 | width: 180px;
25 | height: 180px;
26 | margin: 0px auto 50px;
27 | }
28 |
29 | .notfound .notfound-404>div:first-child {
30 | position: absolute;
31 | left: 0;
32 | right: 0;
33 | top: 0;
34 | bottom: 0;
35 | background: #ffa200;
36 | -webkit-transform: rotate(45deg);
37 | -ms-transform: rotate(45deg);
38 | transform: rotate(45deg);
39 | border: 5px dashed #000;
40 | border-radius: 5px;
41 | }
42 |
43 | .notfound .notfound-404>div:first-child:before {
44 | content: '';
45 | position: absolute;
46 | left: -5px;
47 | right: -5px;
48 | bottom: -5px;
49 | top: -5px;
50 | -webkit-box-shadow: 0px 0px 0px 5px rgba(0, 0, 0, 0.1) inset;
51 | box-shadow: 0px 0px 0px 5px rgba(0, 0, 0, 0.1) inset;
52 | border-radius: 5px;
53 | }
54 |
55 | .notfound .notfound-404 h1 {
56 | font-family: 'Cabin', sans-serif;
57 | color: #000;
58 | font-weight: 700;
59 | margin: 0;
60 | font-size: 90px;
61 | position: absolute;
62 | top: 50%;
63 | -webkit-transform: translate(-50%, -50%);
64 | -ms-transform: translate(-50%, -50%);
65 | transform: translate(-50%, -50%);
66 | left: 50%;
67 | text-align: center;
68 | height: 40px;
69 | line-height: 40px;
70 | }
71 |
72 | .notfound h2 {
73 | font-family: 'Cabin', sans-serif;
74 | font-size: 33px;
75 | font-weight: 700;
76 | text-transform: uppercase;
77 | letter-spacing: 7px;
78 | }
79 |
80 | .notfound p {
81 | font-family: 'Cabin', sans-serif;
82 | font-size: 16px;
83 | color: #000;
84 | font-weight: 400;
85 | }
86 |
87 | .notfound a {
88 | font-family: 'Cabin', sans-serif;
89 | display: inline-block;
90 | padding: 10px 25px;
91 | background-color: #8f8f8f;
92 | border: none;
93 | border-radius: 40px;
94 | color: #fff;
95 | font-size: 14px;
96 | font-weight: 700;
97 | text-transform: uppercase;
98 | text-decoration: none;
99 | -webkit-transition: 0.2s all;
100 | transition: 0.2s all;
101 | }
102 |
103 | .notfound a:hover {
104 | background-color: #2c2c2c;
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/StaticPages/NotFoundPage/NotFoundPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './NotFoundPage.css'
3 |
4 | const notFoundPage = () => {
5 | return (
6 |
7 |
8 |
12 |
Page not found
13 |
The page you are looking for might have been removed, had its name changed, or doesn't exist.
14 |
Back to login
15 |
16 |
17 | )
18 | }
19 |
20 | export default notFoundPage
--------------------------------------------------------------------------------
/src/components/UI/Button/Button.css:
--------------------------------------------------------------------------------
1 | .Button {
2 | display: inline-block;
3 | border-radius: 3px;
4 | border: 0;
5 | outline: 0;
6 | font-size: 1rem;
7 | }
8 |
9 | @media(max-height: 600px) {
10 | .Button {
11 | font-size: 0.9rem;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/components/UI/Button/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import './Button.css'
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 |
5 | /**
6 | * A custom button component. It has a set color scheme and takes in height, eeight, and image
7 | * as props. Netflix seems to use a standard button component with different widths.
8 | */
9 | const button = props => {
10 | let iconHolder = null;
11 | const {
12 | playButton, buttonSize, icon,
13 | height, width, backgroundColor, textColor,
14 | image, onButtonHover, hoverStatus
15 | } = props
16 |
17 | if (image) {
18 | iconHolder = (
19 |
24 | );
25 | }
26 |
27 | let orderButton = (
28 | <>
29 | {props.children}
30 | {iconHolder}
31 | >
32 | )
33 |
34 | if (playButton) {
35 | orderButton = (
36 | <>
37 | {iconHolder}
38 | {props.children}
39 | >
40 | )
41 | }
42 |
43 | const conditionalStyles = {
44 | height: height,
45 | width: width,
46 | backgroundColor: backgroundColor,
47 | color: textColor,
48 | opacity: !hoverStatus && '80%'
49 | };
50 |
51 | return (
52 |
58 | );
59 | };
60 |
61 | export default button;
62 |
63 |
--------------------------------------------------------------------------------
/src/components/UI/CircularSoundButton/CircularSoundButton.css:
--------------------------------------------------------------------------------
1 | .RoundButton {
2 | display: inline-block;
3 | border: 1px solid white;
4 | border-radius: 50%;
5 | background: transparent;
6 | color: white;
7 | text-align: center;
8 | font-size: 16px;
9 | outline: none;
10 | }
11 |
12 | @media(min-width: 1650px) {
13 | .RoundButton {
14 | font-size: 32px;
15 | }
16 | }
--------------------------------------------------------------------------------
/src/components/UI/CircularSoundButton/CircularSoundButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './CircularSoundButton.css'
3 |
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faVolumeUp, faVolumeMute } from "@fortawesome/free-solid-svg-icons";
6 |
7 | const circularSoundButton = props => {
8 | const { topTrailerSoundOn, topTrailerSoundButtonClickHandler } = props
9 | return (
10 |
13 | )
14 | }
15 |
16 | export default circularSoundButton
--------------------------------------------------------------------------------
/src/components/UI/DarkComponent/DarkComponent.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const darkComponentTextAlignStyles = {
4 | display: 'flex',
5 | flexDirection: 'column',
6 | textAlign: 'center'
7 | }
8 |
9 | const darkComponent = props => {
10 | const { topText, fontSize, bottomText, image } = props
11 | return (
12 | <>
13 |
14 | {topText && (
15 |
{topText}
16 | )}
17 | {bottomText && (
18 | {bottomText}
19 | )}
20 |
21 |
22 | {image &&
}
23 | >
24 | );
25 | };
26 |
27 | export default darkComponent;
28 |
--------------------------------------------------------------------------------
/src/components/UI/FAQComponent/FAQComponent.css:
--------------------------------------------------------------------------------
1 | .faqComponent {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | padding: 0.8em 2.2em 0.8em 1.2em;
6 | width: 100%;
7 | margin-top: 10px;
8 | background: #303030;
9 | font-size: 20px;
10 | cursor: pointer;
11 | }
12 |
13 | .faq-animation-enter {
14 | opacity: 0;
15 | }
16 |
17 | .faq-animation-enter-active {
18 | opacity: 1;
19 | -webkit-animation: swing-in-top-bck 0.6s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
20 | animation: swing-in-top-bck 0.6s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
21 | }
22 |
23 | .faq-animation-exit {
24 | opacity: 1;
25 | }
26 |
27 | .faq-animation-exit-active {
28 | opacity: 0;
29 | -webkit-animation: swing-out-top-bck 0.35s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
30 | animation: swing-out-top-bck 0.35s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
31 | }
32 |
33 | @-webkit-keyframes swing-in-top-bck {
34 | 0% {
35 | -webkit-transform: rotateX(70deg);
36 | transform: rotateX(70deg);
37 | -webkit-transform-origin: top;
38 | transform-origin: top;
39 | opacity: 0;
40 | }
41 | 100% {
42 | -webkit-transform: rotateX(0deg);
43 | transform: rotateX(0deg);
44 | -webkit-transform-origin: top;
45 | transform-origin: top;
46 | opacity: 1;
47 | }
48 | }
49 | @keyframes swing-in-top-bck {
50 | 0% {
51 | -webkit-transform: rotateX(70deg);
52 | transform: rotateX(70deg);
53 | -webkit-transform-origin: top;
54 | transform-origin: top;
55 | opacity: 0;
56 | }
57 | 100% {
58 | -webkit-transform: rotateX(0deg);
59 | transform: rotateX(0deg);
60 | -webkit-transform-origin: top;
61 | transform-origin: top;
62 | opacity: 1;
63 | }
64 | }
65 |
66 | @-webkit-keyframes swing-out-top-bck {
67 | 0% {
68 | -webkit-transform: rotateX(0deg);
69 | transform: rotateX(0deg);
70 | -webkit-transform-origin: top;
71 | transform-origin: top;
72 | opacity: 1;
73 | }
74 | 100% {
75 | -webkit-transform: rotateX(-100deg);
76 | transform: rotateX(-100deg);
77 | -webkit-transform-origin: top;
78 | transform-origin: top;
79 | opacity: 0;
80 | }
81 | }
82 |
83 | @keyframes swing-out-top-bck {
84 | 0% {
85 | -webkit-transform: rotateX(0deg);
86 | transform: rotateX(0deg);
87 | -webkit-transform-origin: top;
88 | transform-origin: top;
89 | opacity: 1;
90 | }
91 | 100% {
92 | -webkit-transform: rotateX(-100deg);
93 | transform: rotateX(-100deg);
94 | -webkit-transform-origin: top;
95 | transform-origin: top;
96 | opacity: 0;
97 | }
98 | }
99 |
100 | @media (min-width: 600px) {
101 | .faqComponent {
102 | width: 69%;
103 | }
104 | }
105 |
106 | @media(min-width: 1650px) {
107 | .faqComponent {
108 | font-size: 1.75rem;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/UI/FAQComponent/FAQComponent.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./FAQComponent.css";
3 |
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { faPlus, faMinus } from "@fortawesome/free-solid-svg-icons";
6 | import { CSSTransition } from 'react-transition-group'
7 |
8 | const faqComponent = props => {
9 | const { faqOpenHandler, text, boxOpen, boxText } = props
10 | return (
11 | <>
12 |
19 |
20 |
21 |
22 | {boxText}
23 |
24 |
25 | >
26 | );
27 | };
28 |
29 | export default faqComponent;
30 |
--------------------------------------------------------------------------------
/src/components/UI/ProfileCard/ProfileCard.css:
--------------------------------------------------------------------------------
1 | .ProfileCard {
2 | width: 25%;
3 | margin: 15px;
4 | cursor: pointer;
5 | }
6 |
7 | .ProfileCardDropdown {
8 | width: 90%;
9 | display: flex;
10 | align-items: center;
11 | }
12 |
13 | .ProfileCardDropdown > * {
14 | margin-right: 10px;
15 | }
16 |
17 | .ProfileCardDropdown:hover span {
18 | text-decoration: underline;
19 | }
20 |
21 | .ProfileCardDropdown img {
22 | width: 20%;
23 | height: 100%;
24 | }
25 |
26 | .ProfileCard span {
27 | font-size: 3vw;
28 | color: grey;
29 | }
30 |
31 | .ProfileCard img {
32 | width: 100%;
33 | height: 100%;
34 | margin-bottom: 10px;
35 | border: 3px solid transparent;
36 | }
37 |
38 | .ProfileCard:hover img {
39 | border-color: white;
40 | }
41 |
42 | .ProfileCard:hover span {
43 | color: white;
44 | }
45 |
46 | @media(min-width: 600px) {
47 | .ProfileCard {
48 | width: 18%;
49 | height: 20%;
50 | }
51 |
52 | .ProfileCard span {
53 | font-size: 1.3vw;
54 | }
55 | }
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/components/UI/ProfileCard/ProfileCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './ProfileCard.css'
3 |
4 | const ProfileCard = props => {
5 | return (
6 |
7 |

8 |
{props.username}
9 |
10 | )
11 | }
12 |
13 | export default ProfileCard
--------------------------------------------------------------------------------
/src/components/Video/TopTrailerComponent/TopTrailerComponent.css:
--------------------------------------------------------------------------------
1 | .VideoComponent {
2 | height: 580px;
3 | width: 100%;
4 | background-position: center;
5 | background-repeat: no-repeat;
6 | background-size: cover;
7 | position: relative;
8 | }
9 |
10 | @media(min-width: 1650px) {
11 | .VideoComponent {
12 | height: 800px;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/components/Video/TopTrailerComponent/TopTrailerComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './TopTrailerComponent.css'
3 |
4 | const TopTrailerComponent = (props) => {
5 | const backgroundPicture = {
6 | backgroundImage: `url(${props.image})`
7 | }
8 | return (
9 |
10 | {props.children}
11 |
12 | )
13 | }
14 |
15 | export default TopTrailerComponent
--------------------------------------------------------------------------------
/src/components/Video/VideoCard/VideoCard.css:
--------------------------------------------------------------------------------
1 | .VideoCard {
2 | display: flex;
3 | align-items: center;
4 | cursor: pointer;
5 | height: 100%;
6 | width: 100%;
7 | }
8 |
9 | .VideoCard:hover .VideoInfo {
10 | display: block;
11 | }
12 |
13 | .NetflixOriginalCard:hover .VideoInfo {
14 | display: block;
15 | }
16 |
17 | .VideoInfo {
18 | display: none;
19 | padding: 10px;
20 | margin-top: 15px;
21 | font-weight: 400;
22 | }
23 |
24 | .VideoInfo h6 {
25 | margin: 0;
26 | }
27 |
28 | .horizontalStyle {
29 | display: flex;
30 | flex-wrap: wrap;
31 | }
32 |
33 | .horizontalStyle span {
34 | font-size: 0.5em;
35 | }
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/Video/VideoCard/VideoCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './VideoCard.css'
3 | import { getSeasonsOrMovieLength } from 'utils/time'
4 |
5 | const videoCard = (props) => {
6 | const {
7 | name, poster_path, genres, runtime, seasons,
8 | vote_average
9 | } = props
10 |
11 | const image = `url(https://image.tmdb.org/t/p/w500/${poster_path})`
12 |
13 | const styles = {
14 | backgroundImage: image,
15 | backgroundSize: 'cover',
16 | }
17 |
18 | let timeSpan = getSeasonsOrMovieLength(seasons, runtime)
19 | const genreList = genres && genres.map((genre, index) => (
20 |
21 | {genre.name} {index !== genres.length - 1 ? '●' : null}
22 |
23 | ))
24 |
25 | return (
26 |
27 | {genreList ?
28 |
{name}
29 |
30 | {vote_average}
31 | {timeSpan}
32 |
33 |
34 | {genreList}
35 |
36 |
: null}
37 |
38 | )
39 | }
40 |
41 | export default React.memo(videoCard)
--------------------------------------------------------------------------------
/src/components/Video/VideoCarousel/VideoCarousel.css:
--------------------------------------------------------------------------------
1 | .CarouselParent {
2 | position: relative;
3 | margin-bottom: 30px;
4 | }
5 |
6 | .VideoCarousel {
7 | color: #e5e5e5;
8 | font-size: 2.4vw;
9 | overflow-x: auto;
10 | margin: 0 4% 0 4%;
11 | }
12 |
13 | .VideoCarousel::-webkit-scrollbar {
14 | display: none;
15 | }
16 |
17 | .items {
18 | display: inline-flex;
19 | height: 80px;
20 | }
21 |
22 | .netflix-items {
23 | display: inline-flex;
24 | height: 300px;
25 | overflow: x;
26 | }
27 |
28 | .item {
29 | position: relative;
30 | display: block;
31 | transition: transform 500ms;
32 | width: 100px;
33 | height: 100%;
34 | margin-right: 7px;
35 | }
36 |
37 | .items .item:focus,
38 | .items .item:hover {
39 | transform: scale(1.1);
40 | }
41 |
42 | .netflix-item {
43 | position: relative;
44 | display: block;
45 | transition: transform 500ms;
46 | width: 130px;
47 | height: 100%;
48 | margin-right: 7px;
49 | }
50 |
51 | .netflix-items .netflix-item:focus,
52 | .netflix-items .netflix-item:hover {
53 | transform: scale(1.1);
54 | }
55 |
56 | .CarouselParent:hover > .NavItem {
57 | display: none;
58 | }
59 |
60 | .NavItem {
61 | display: none;
62 | }
63 |
64 | @media(min-width: 600px) {
65 | .VideoCarousel {
66 | font-size: 1.4vw;
67 | }
68 | .items {
69 | height: 140px;
70 | }
71 | .item {
72 | width: 230px;
73 | height: 100%;
74 | margin-right: 7px;
75 | }
76 | .netflix-items {
77 | height: 420px;
78 | }
79 | .netflix-item {
80 | width: 230px;
81 | height: 100%;
82 | margin-right: 7px;
83 | }
84 | /* Different animation styles for the first and last items */
85 | .items .item:first-child:focus,
86 | .items .item:first-child:hover,
87 | .items .item:last-child:focus,
88 | .items .item:last-child:hover {
89 | transform: scale(1.1);
90 | }
91 | .item:first-child:focus~.item,
92 | .item:first-child:hover~.item {
93 | transform: translateX(10%);
94 | }
95 | .item:focus~.item,
96 | .item:hover~.item {
97 | transform: translateX(20%);
98 | }
99 | .items .item:not(:first-child):not(:last-child):focus,
100 | .items .item:not(:first-child):not(:last-child):hover {
101 | transform: scale(1.3);
102 | }
103 | .netflix-item:focus~.netflix-item,
104 | .netflix-item:hover~.netflix-item {
105 | transform: translateX(10%);
106 | }
107 |
108 | .CarouselParent:hover > .NavItem {
109 | display: block;
110 | position: absolute;
111 | top: 0;
112 | bottom: 0;
113 | width: 4%;
114 | color: white;
115 | background-color: transparent;
116 | outline: thin;
117 | border: none;
118 | }
119 |
120 | .CarouselParent:hover > .NavItem:hover {
121 | background-color: black;
122 | }
123 |
124 | .NavItem {
125 | display: none;
126 | }
127 |
128 | .Left {
129 | left: 0;
130 | }
131 |
132 | .Right {
133 | right: 0;
134 | }
135 | }
136 |
137 | @media(min-width: 1650px) {
138 | .items {
139 | height: 240px;
140 | }
141 | .item {
142 | width: 330px;
143 | height: 100%;
144 | margin-right: 7px;
145 | }
146 | .netflix-items {
147 | height: 520px;
148 | }
149 | .netflix-item {
150 | width: 330px;
151 | height: 100%;
152 | margin-right: 7px;
153 | }
154 | }
--------------------------------------------------------------------------------
/src/components/Video/VideoCarousel/VideoCarousel.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react'
2 | import './VideoCarousel.css'
3 |
4 | import VideoCard from '../VideoCard/VideoCard'
5 | import { buildVideoMetadata } from 'utils/transformations'
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7 | import { faChevronRight, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
8 | import { scrollTo } from 'utils/animations'
9 |
10 |
11 | const VideoCarousel = props => {
12 | const {
13 | carouselVideo, carouselName,
14 | carouselHoverHandler, videoInfo,
15 | carouselClickHandler
16 | } = props
17 |
18 | const carouselRef = useRef()
19 | const [disableScrolling, setDisableScrolling] = useState(false)
20 |
21 | const isNetflixOriginalCard = carouselName === "Netflix Originals" ? true : false
22 | const itemClass = []
23 | const itemsClass = []
24 | // Setting different transition styles for the netflix original card
25 | if (!isNetflixOriginalCard) {
26 | itemClass.push("item")
27 | itemsClass.push("items")
28 | } else {
29 | itemClass.push("netflix-item")
30 | itemsClass.push("netflix-items")
31 | }
32 |
33 | const scrollOnAbsoluteButtonClick = scrollOffset => {
34 | setDisableScrolling(true)
35 | scrollTo(carouselRef.current, carouselRef.current.scrollLeft + scrollOffset, 1250, () => {
36 | setDisableScrolling(false)
37 | })
38 | }
39 |
40 | /**
41 | * The mediaType property only exists in the trending API call. For the sake of using the same
42 | * function 'videoDetailRequest' for multiple individual API calls, I use this mediaType
43 | * variable to judge whether a video is a TV show or a movie. This fails for API calls, such as
44 | * top rated or netflix originals. Thus, I have created a 'hacky' way of determining that by
45 | * checking if two properties exist: the first_air_date (only for tv shows) or the release_date
46 | * (only for movies).
47 | */
48 | const videoCards = carouselVideo.map(item => {
49 | const { mediaType, extraInfo } = buildVideoMetadata(item, videoInfo)
50 | return (
51 | item.poster_path && carouselClickHandler(item.id, mediaType)}
53 | onMouseEnter={() => carouselHoverHandler(item.id, mediaType)}>
54 |
61 |
62 | )
63 | })
64 |
65 | return (
66 |
67 |
68 |
{carouselName}
69 |
70 | {videoCards}
71 |
72 |
73 |
74 |
80 |
86 |
87 | )
88 | }
89 |
90 | export default React.memo(VideoCarousel)
--------------------------------------------------------------------------------
/src/containers/Browse/Browse.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import ProfileModal from 'components/Modals/ProfileModal/ProfileModal'
4 | import { useHistory } from 'react-router-dom'
5 | import Layout from 'hoc/Layout'
6 | import SearchContent from './SearchContent/SearchContent'
7 | import { Home, Movies, Tv, LatestVideo, List } from './routes'
8 |
9 | /**
10 | * Remember: the component where you want to use the context is the one which you wrap
11 | * with the provider.
12 | */
13 | const Browse = props => {
14 | // Render different videoSections based on this route prop
15 | const { route } = props
16 | const initialState = localStorage.getItem('profileSelected') ? false : true
17 | const [modal, setModal] = useState(initialState)
18 | const history = useHistory()
19 |
20 | const profileClickHandler = () => {
21 | setModal(false)
22 | localStorage.setItem('profileSelected', true)
23 | }
24 |
25 | let browseContent
26 | if (route === '/browse') {
27 | browseContent =
28 | } else if (route === '/browse/movies') {
29 | browseContent =
30 | } else if (route === '/browse/tv') {
31 | browseContent =
32 | } else if (route === '/browse/latest') {
33 | browseContent =
34 | } else if(route === '/browse/list') {
35 | browseContent =
36 | }
37 | else if (route === '/search') {
38 | browseContent =
39 | }
40 |
41 | return (
42 | <>
43 |
44 | {!modal &&
45 | {browseContent}
46 | }
47 | >
48 | )
49 | }
50 |
51 | /**
52 | * Remember the thing which gave you trouble here. Never mutate state directly. Since I didn't create
53 | * a copy of the trending state array, I kept splicing each item all over the place, which
54 | * caused unnecessary problems.
55 | */
56 |
57 | export default Browse
--------------------------------------------------------------------------------
/src/containers/Browse/BrowseContent/BrowseContent.css:
--------------------------------------------------------------------------------
1 | .TextsAndButtons {
2 | display: flex;
3 | align-items: flex-end;
4 | justify-content: space-between;
5 | color: white;
6 | margin: 0 4% 0 4%;
7 | height: 100%;
8 | }
9 |
10 | .verticalItem {
11 | display: flex;
12 | flex-direction: column;
13 | justify-content: flex-end;
14 | margin-bottom: 80px;
15 | height: 200px;
16 | max-width: 50%;
17 | }
18 |
19 | .verticalItem h3 {
20 | margin: 0;
21 | font-size: 3.6vw;
22 | }
23 |
24 | .verticalItem span {
25 | font-size: 3.4vw;
26 | }
27 |
28 | .verticalItem > * {
29 | padding: 10px;
30 | }
31 |
32 | .horizontalButtonsHolder {
33 | display: flex;
34 | align-items: center;
35 | width: 100%;
36 | height: 15%;
37 | }
38 |
39 | .horizontalButtonsHolder > * {
40 | margin-right: 4px;
41 | font-weight: bold;
42 | }
43 |
44 | .horizontalButtonsHolder button {
45 | height: 37px;
46 | width: 118px;
47 | font-size: 0.8rem;
48 | }
49 |
50 | .Carousels {
51 | padding-bottom: 10px;
52 | }
53 |
54 | @media(min-width: 600px) {
55 | .horizontalButtonsHolder > * {
56 | margin-right: 10px;
57 | font-weight: bold;
58 | }
59 |
60 | .horizontalButtonsHolder button {
61 | height:38px;
62 | width: 138px;
63 | font-size: 1rem;
64 | }
65 |
66 | .verticalItem span {
67 | font-size: 1.4vw;
68 | }
69 |
70 | .verticalItem h3 {
71 | font-size: 1.6vw;
72 | }
73 | }
74 |
75 | @media(min-width: 1650px) {
76 |
77 | }
--------------------------------------------------------------------------------
/src/containers/Browse/BrowseContent/BrowseContent.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import './BrowseContent.css'
3 |
4 | import TopTrailerComponent from 'components/Video/TopTrailerComponent/TopTrailerComponent'
5 | import Button from 'components/UI/Button/Button'
6 | import VideoCarousel from 'components/Video/VideoCarousel/VideoCarousel'
7 | import { faPlay, faInfoCircle } from '@fortawesome/free-solid-svg-icons'
8 | import { buildVideoModal } from 'utils/transformations'
9 | import useVideoInfoHandlers from 'hooks/useVideoInfoHandlers'
10 | import ErrorPage from 'components/StaticPages/ErrorPage/ErrorPage'
11 | import useHoverStyleButton from 'hooks/useHoverStyleButton'
12 | import CircularSoundButton from 'components/UI/CircularSoundButton/CircularSoundButton'
13 |
14 | const BrowseContent = props => {
15 | const [
16 | videoInfo, videoInfoError, detailModal, cardClickHandler,
17 | cardHoverHandler, closeModalHandler
18 | ] = useVideoInfoHandlers()
19 |
20 | const [buttonHovered, onButtonHoverHandler] = useHoverStyleButton({
21 | 'playButton': true,
22 | 'infoButton': true
23 | })
24 |
25 | const [topTrailerSoundOn, setTopTrailerSoundOn] = useState(true)
26 |
27 | const { videoSections } = props
28 | const [firstVideo, ...remainingVideos] = videoSections[0].videos
29 | const imageUrl = firstVideo ? `https://image.tmdb.org/t/p/original/${firstVideo.poster_path}` : null
30 |
31 | const detailModalComponent = buildVideoModal(detailModal, videoInfo, { closeModalHandler })
32 |
33 | const carousels = videoSections.map((videoSection, index) => (
34 |
42 | ))
43 |
44 | const topTrailerSoundButtonClickHandler = () => setTopTrailerSoundOn(prevState => !prevState)
45 |
46 | return (!videoInfoError ? (
47 |
48 |
49 |
50 |
51 |
{firstVideo ? (firstVideo.name || firstVideo.title) : null}
52 |
{firstVideo ? firstVideo.overview : null}
53 |
54 |
62 |
63 |
70 |
71 |
72 |
73 |
76 |
77 |
78 |
79 |
80 | {carousels}
81 |
82 | {detailModalComponent}
83 |
84 |
) :
85 |
86 | )
87 | }
88 |
89 | export default BrowseContent
--------------------------------------------------------------------------------
/src/containers/Browse/SearchContent/SearchContent.css:
--------------------------------------------------------------------------------
1 | .SearchContent {
2 | padding: 95px 10px;
3 | color: white;
4 | }
5 |
6 | .SearchGrid {
7 | display: grid;
8 | grid-template-columns: repeat(3, 31.5%);
9 | grid-auto-rows: 80px;
10 | grid-gap: 10px;
11 | height: 100%;
12 | }
13 |
14 | .SearchGrid .GridItem:focus,
15 | .SearchGrid .GridItem:hover {
16 | transform: scale(1.1);
17 | }
18 |
19 | .GridItem {
20 | transition: transform 500ms;
21 | }
22 |
23 | .GridItem > * {
24 | margin: 0;
25 | }
26 |
27 | @media(min-width: 600px) {
28 | .SearchContent {
29 | margin: 0 10px;
30 | }
31 |
32 | .SearchGrid {
33 | grid-template-columns: repeat(6, 16%);
34 | grid-auto-rows: 140px;
35 | }
36 | }
37 |
38 | @media(min-width: 1650px) {
39 | .SearchContent {
40 | font-size: 1.6vw;
41 | }
42 | }
--------------------------------------------------------------------------------
/src/containers/Browse/SearchContent/SearchContent.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react'
2 | import './SearchContent.css'
3 |
4 | import axios from 'baseAxios'
5 | import VideoCard from 'components/Video/VideoCard/VideoCard'
6 | import { debounce } from 'lodash'
7 | import { buildVideoMetadata, buildVideoModal } from 'utils/transformations'
8 | import { sortVideosByPopularity } from 'utils/sorting'
9 | import useVideoInfoHandlers from 'hooks/useVideoInfoHandlers'
10 | import ErrorPage from 'components/StaticPages/ErrorPage/ErrorPage'
11 |
12 | const SearchContent = props => {
13 | const [searchedVideoList, setSearchedVideoList] = useState([])
14 | const [searchedError, setSearchedError] = useState(null)
15 | const [loading, setLoading] = useState(true)
16 | const { searchParam } = props
17 | const [
18 | videoInfo, videoInfoError, detailModal, cardClickHandler,
19 | cardHoverHandler, closeModalHandler
20 | ] = useVideoInfoHandlers()
21 |
22 | const getSearchMovies = async (searchItem) => {
23 | const movieUrl = `search/movie?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&query=${searchItem}&page=1&include_adult=false`
24 | const tvUrl = `search/tv?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&page=1&query=${searchItem}&include_adult=false`
25 |
26 | try {
27 | const responses = await Promise.all(
28 | [
29 | axios.get(movieUrl).then(response => response.data.results),
30 | axios.get(tvUrl).then(response => response.data.results)
31 | ]
32 | )
33 |
34 | setSearchedVideoList([...responses[0], ...responses[1]])
35 | setLoading(false)
36 | } catch (error) {
37 | setSearchedError(error)
38 | setLoading(false)
39 | }
40 | }
41 |
42 | const delayedAPICall = useCallback(debounce(getSearchMovies, 1000), [])
43 |
44 | useEffect(() => {
45 | delayedAPICall(searchParam)
46 | return () => {
47 | delayedAPICall.cancel()
48 | }
49 | }, [delayedAPICall, searchParam])
50 |
51 | const detailModalComponent = buildVideoModal(detailModal, videoInfo, { closeModalHandler })
52 |
53 | // we check if the video has a poster or a mediaType because these properties are missing in
54 | // some tiles, and, generally, a missing mediaType means there is no video overview or
55 | // information. It's an easy fix to skip these little known movies, as the API itself
56 | // doesn't provide information.
57 | let movieCards
58 | if (!loading) {
59 | searchedVideoList.sort(sortVideosByPopularity)
60 | movieCards = searchedVideoList.map(video => {
61 | const { mediaType, extraInfo } = buildVideoMetadata(video, videoInfo)
62 | return video.poster_path && mediaType && (
63 | cardClickHandler(video.id, mediaType)}
67 | onMouseEnter={
68 | () => cardHoverHandler(video.id, mediaType)} >
69 |
73 |
74 | )
75 | })
76 | }
77 |
78 | return (
79 | (!videoInfoError && !searchedError) ? (
80 |
81 |
82 | {movieCards}
83 |
84 | {detailModalComponent}
85 |
) :
86 |
87 | )
88 | }
89 |
90 | export default SearchContent
--------------------------------------------------------------------------------
/src/containers/Browse/routes/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | import { useSelector, useDispatch } from 'react-redux'
4 | import BrowseContent from '../BrowseContent/BrowseContent'
5 | import ErrorPage from 'components/StaticPages/ErrorPage/ErrorPage'
6 | import { fetchTrending, selectAllTrendingVideos, selectTrendingError } from 'store/reducers/slices/trendingSlice'
7 | import { fetchTopRated, selectAllTopRatedVideos, selectTopRatedError } from 'store/reducers/slices/topratedSlice'
8 | import { fetchNetflixOriginals, selectAllNetflixOriginals, selectNetflixError } from 'store/reducers/slices/netflixOriginalsSlice'
9 |
10 | const Home = () => {
11 | const trendingVideos = useSelector(selectAllTrendingVideos)
12 | const topRatedVideos = useSelector(selectAllTopRatedVideos)
13 | const netflixOriginals = useSelector(selectAllNetflixOriginals)
14 |
15 | const trendingError = useSelector(selectTrendingError)
16 | const topRatedError = useSelector(selectTopRatedError)
17 | const netflixError = useSelector(selectNetflixError)
18 | const dispatch = useDispatch()
19 |
20 | useEffect(() => {
21 | dispatch(fetchTrending())
22 | dispatch(fetchTopRated())
23 | dispatch(fetchNetflixOriginals())
24 | }, [dispatch])
25 |
26 |
27 | let videoSections = []
28 | let component
29 | if (!trendingError && !topRatedError && !netflixError) {
30 | videoSections.push({ title: "Trending", videos: trendingVideos })
31 | videoSections.push({ title: "Top Rated", videos: topRatedVideos })
32 | videoSections.push({ title: "Netflix Originals", videos: netflixOriginals })
33 | component =
34 | } else {
35 | component = (
36 |
37 | )
38 | }
39 |
40 | return component
41 | }
42 |
43 | export default Home
--------------------------------------------------------------------------------
/src/containers/Browse/routes/LatestVideo.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | import { useSelector, useDispatch } from 'react-redux'
4 | import { fetchLatestVideos, selectLatestVideos } from 'store/reducers/slices/latestVideoSlice'
5 | import BrowseContent from '../BrowseContent/BrowseContent'
6 | import LoadingScreen from 'components/StaticPages/LoadingScreen/LoadingScreen'
7 | import ErrorPage from 'components/StaticPages/ErrorPage/ErrorPage'
8 |
9 | const LatestVideo = () => {
10 | const { latestVideos, status, error } = useSelector(selectLatestVideos)
11 | const dispatch = useDispatch()
12 |
13 | useEffect(() => {
14 | if (status === 'idle') {
15 | dispatch(fetchLatestVideos())
16 | }
17 | }, [dispatch, status])
18 |
19 | let browseContent
20 | if (status === 'success') {
21 | browseContent =
22 | } else if (status === 'idle' || status === 'loading') {
23 | browseContent =
24 | } else if (status === 'error') {
25 | browseContent =
26 | }
27 |
28 | return browseContent
29 | }
30 |
31 | export default LatestVideo
--------------------------------------------------------------------------------
/src/containers/Browse/routes/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const List = () => {
4 | return (
5 | Coming soon
6 | )
7 | }
8 |
9 | export default List
--------------------------------------------------------------------------------
/src/containers/Browse/routes/Movies.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useSelector, useDispatch } from 'react-redux'
3 |
4 | import { fetchMoviesByGenre, selectMoviesByGenre } from 'store/reducers/slices/moviesByGenreSlice'
5 | import BrowseContent from '../BrowseContent/BrowseContent'
6 | import LoadingScreen from 'components/StaticPages/LoadingScreen/LoadingScreen'
7 | import ErrorPage from 'components/StaticPages/ErrorPage/ErrorPage'
8 |
9 | const Movies = () => {
10 | const {genres, status, error} = useSelector(selectMoviesByGenre)
11 | const dispatch = useDispatch()
12 |
13 | useEffect(() => {
14 | if (status === 'idle') {
15 | dispatch(fetchMoviesByGenre())
16 | }
17 | }, [dispatch, status])
18 |
19 | let browseContent
20 | if (status === 'success') {
21 | browseContent =
22 | } else if (status === 'idle' || status === 'loading') {
23 | browseContent =
24 | } else if(status === 'error') {
25 | browseContent =
26 | }
27 |
28 | return browseContent
29 | }
30 |
31 | export default Movies
--------------------------------------------------------------------------------
/src/containers/Browse/routes/Tv.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | import { useSelector, useDispatch } from 'react-redux'
4 | import { fetchTvShowsByGenres, selectTvByGenre } from 'store/reducers/slices/tvByGenreSlice'
5 | import BrowseContent from '../BrowseContent/BrowseContent'
6 | import LoadingScreen from 'components/StaticPages/LoadingScreen/LoadingScreen'
7 | import ErrorPage from 'components/StaticPages/ErrorPage/ErrorPage'
8 |
9 | const Tv = () => {
10 | const {genres, status, error} = useSelector(selectTvByGenre)
11 | const dispatch = useDispatch()
12 |
13 | useEffect(() => {
14 | if (status === 'idle') {
15 | dispatch(fetchTvShowsByGenres())
16 | }
17 | }, [dispatch, status])
18 |
19 | let browseContent
20 | if (status === 'success') {
21 | browseContent =
22 | } else if (status === 'idle' || status === 'loading') {
23 | browseContent =
24 | } else if(status === 'error') {
25 | browseContent =
26 | }
27 |
28 | return browseContent
29 | }
30 |
31 | export default Tv
--------------------------------------------------------------------------------
/src/containers/Browse/routes/index.js:
--------------------------------------------------------------------------------
1 | import Home from './Home'
2 | import Movies from './Movies'
3 | import Tv from './Tv'
4 | import LatestVideo from './LatestVideo'
5 | import List from './List'
6 |
7 | export {
8 | Home,
9 | Movies,
10 | Tv,
11 | LatestVideo,
12 | List
13 | }
14 |
--------------------------------------------------------------------------------
/src/containers/LandingSection/LandingSection.css:
--------------------------------------------------------------------------------
1 | .landingSection {
2 | height: 470px;
3 | width: 100%;
4 | border-bottom: 8px solid #222;
5 | }
6 |
7 | .landingTexts {
8 | display: flex;
9 | width: 80%;
10 | margin: auto;
11 | flex-direction: column;
12 | align-items: center;
13 | position: relative;
14 | top: calc(30% - 90px);
15 | text-align: center;
16 | color: #fff;
17 | }
18 |
19 | .landingTexts > * {
20 | margin-top: 15px;
21 | }
22 |
23 | .landingTexts h3 {
24 | margin-top: 0px;
25 | font-weight: 400;
26 | }
27 |
28 | .ButtonSticker {
29 | display: flex;
30 | flex-direction: column;
31 | justify-content: center;
32 | align-items: center;
33 | width: 100%;
34 | }
35 |
36 | .TextField {
37 | background-color: #fff;
38 | width: 80%;
39 | max-width: 500px;
40 | }
41 |
42 | .TextField:active {
43 | color: white;
44 | border: none;
45 | }
46 |
47 | .tv-section {
48 | height: 410px;
49 | background-color: black;
50 | border-bottom: 8px solid #333;
51 | color: #fff;
52 | }
53 |
54 | .tv-inner, .responsive-tv-inner {
55 | display: flex;
56 | flex-direction: column;
57 | justify-content: center;
58 | align-items: center;
59 | width: 80%;
60 | margin: auto;
61 | }
62 |
63 | .tv-section img {
64 | width: 70%;
65 | }
66 |
67 | .faq-section {
68 | background-color: black;
69 | border-bottom: 8px solid #333;
70 | color: #fff;
71 | }
72 |
73 | .GetStartedComponent {
74 | display: flex;
75 | flex-direction: column;
76 | justify-content: center;
77 | align-items: center;
78 | margin-top: 30px;
79 | width: 100%;
80 | }
81 |
82 | .GetStartedComponent > * {
83 | margin-bottom: 15px;
84 | }
85 |
86 | .ButtonSticker button {
87 | margin-top: 15px;
88 | }
89 |
90 | @media (min-width: 600px) {
91 | .landingSection {
92 | height: 620px;
93 | }
94 |
95 | .landingTexts {
96 | width: 70%;
97 | top: calc(40% - 45px);
98 | }
99 |
100 | .GetStartedComponent {
101 | width: 100%;
102 | }
103 |
104 | .tv-section {
105 | height: 450px;
106 | }
107 |
108 | .tv-section img {
109 | width: 45%;
110 | }
111 | }
112 |
113 | @media(min-width: 950px) {
114 | .ButtonSticker {
115 | display: flex;
116 | flex-direction: row;
117 | justify-content: center;
118 | align-items: center;
119 | }
120 |
121 | .ButtonSticker button {
122 | margin-top: 0;
123 | }
124 | }
125 |
126 | @media(min-width: 1199px) {
127 | .responsive-tv-inner {
128 | flex-direction: row;
129 | justify-content: space-between;
130 | }
131 | }
132 |
133 | @media(min-width: 1650px) {
134 | .landingSection,
135 | .tv-section,
136 | .faq-section {
137 | font-size: 1.75rem;
138 | }
139 |
140 | .tv-section img {
141 | width: 35%;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/containers/LandingSection/LandingSection.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./LandingSection.css";
3 |
4 | import NavBar from "../NavBar/NavBar";
5 | import LandingPage from "assets/images/landingPage.jpg";
6 | import { TextField } from "@material-ui/core";
7 | import Button from "components/UI/Button/Button";
8 | import DarkComponent from "components/UI/DarkComponent/DarkComponent";
9 | import FAQComponent from "components/UI/FAQComponent/FAQComponent";
10 | import { Link } from "react-router-dom";
11 | import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
12 | import { texualMaterial } from './LandingSectionTexts'
13 |
14 | /**
15 | * The 'homepage' of this project. Uses an object state to
16 | * dynamically determine which frequently asked box is open.
17 | * Renders components like navbars, darkComponents, etc and
18 | * passes the relevent props whenever needed.
19 | */
20 | const LandingSection = props => {
21 | const [faqBoxOpen, setFaqBoxOpen] = useState({});
22 |
23 | const faqOpenHandler = boxNumber => {
24 | setFaqBoxOpen(prevBoxState => ({
25 | [boxNumber]: !prevBoxState[boxNumber]
26 | }));
27 | };
28 |
29 | const darkComponents = texualMaterial.darkComponent.map(darkcomp => (
30 |
39 | ))
40 |
41 | const faqComponents = texualMaterial.faqComponent.map(faqcomp => (
42 | faqOpenHandler(`box${faqcomp.id}`)}
47 | boxText={faqcomp.boxText}
48 | />
49 | ))
50 |
51 | return (
52 | <>
53 |
57 |
58 |
59 |
Unlimited movies, TV shows, and more.
60 |
Watch anywhere. Cancel anytime.
61 |
62 | Ready to watch? Enter your email to create or restart your
63 | membership.
64 |
65 |
66 |
67 |
73 |
74 |
75 |
86 |
87 |
88 |
89 |
90 |
91 | {darkComponents}
92 |
93 |
94 |
95 |
99 |
100 | {faqComponents}
101 |
102 |
103 |
104 | Ready to watch? Enter your email to create or restart your
105 | membership.
106 |
107 |
108 |
109 |
115 |
116 |
117 |
128 |
129 |
130 |
131 |
132 |
133 | >
134 | );
135 | };
136 |
137 | export default LandingSection;
138 |
--------------------------------------------------------------------------------
/src/containers/LandingSection/LandingSectionTexts.js:
--------------------------------------------------------------------------------
1 | // A file for all texual material used in 'LandingSection.js'
2 | export const texualMaterial = {
3 | darkComponent: [
4 | {
5 | id: 1,
6 | topText: "Enjoy on your TV.",
7 | bottomText: "Watch on Smart TVs, Playstation, Xbox, Chromecast, Apple TV, Blu-ray players, and more.",
8 | image: "https://assets.nflxext.com/ffe/siteui/acquisition/ourStory/fuji/desktop/tv.png"
9 | },
10 | {
11 | id: 2,
12 | topText: "Download your shows to watch offline.",
13 | bottomText: "Save your favorites easily and always have something to watch.",
14 | image: "https://assets.nflxext.com/ffe/siteui/acquisition/ourStory/fuji/desktop/mobile-0819.jpg"
15 | },
16 | {
17 | id: 3,
18 | topText: "Watch everywhere.",
19 | bottomText: "Stream unlimited movies and TV shows on your phone, tablet, laptop, and TV without paying more.",
20 | image: "https://assets.nflxext.com/ffe/siteui/acquisition/ourStory/fuji/desktop/device-pile.png"
21 | },
22 | ],
23 |
24 | faqComponent: [
25 | {
26 | id: 1,
27 | text: "What is Netflix?",
28 | boxText: "Netflix is a streaming service that offers a wide variety of award-winning TV shows, movies, anime, documentaries, and more on thousands of internet-connected devices. You can watch as much as you want, whenever you want without a single commercial – all for one low monthly price. There's always something new to discover and new TV shows and movies are added every week!"
29 | },
30 |
31 | {
32 | id: 2,
33 | text: "How much does Netflix cost?",
34 | boxText: "Watch Netflix on your smartphone, tablet, Smart TV, laptop, or streaming device, all for one fixed monthly fee. Plans range from US$8.99 to US$15.99 a month. No extra costs, no contracts.",
35 | },
36 |
37 | {
38 | id: 3,
39 | text: "Where can I watch?",
40 | boxText: "Watch anywhere, anytime, on an unlimited number of devices. Sign in with your Netflix account to watch instantly on the web at netflix.com from your personal computer or on any internet-connected device that offers the Netflix app, including smart TVs, smartphones, tablets, streaming media players and game consoles. You can also download your favorite shows with the iOS, Android, or Windows 10 app. Use downloads to watch while you're on the go and without an internet connection. Take Netflix with you anywhere."
41 | },
42 |
43 | {
44 | id: 4,
45 | text: "How can I cancel?",
46 | boxText: "Netflix is flexible. There are no pesky contracts and no commitments. You can easily cancel your account online in two clicks. There are no cancellation fees – start or stop your account anytime."
47 | },
48 |
49 | {
50 | id: 5,
51 | text: "What can I watch on Netflix?",
52 | boxText: "Netflix has an extensive library of feature films, documentaries, TV shows, anime, award-winning Netflix originals, and more. Watch as much as you want, anytime you want."
53 | }
54 | ]
55 | }
--------------------------------------------------------------------------------
/src/containers/Login/Login.css:
--------------------------------------------------------------------------------
1 | .Login {
2 | width: 100%;
3 | height: 100vh;
4 | }
5 |
6 | .Login img {
7 | height: 75px;
8 | width: 178px;
9 | margin-left: 20px;
10 | }
11 |
12 | .LoginCard {
13 | background-color: rgba(0, 0, 0, 0.75);
14 | border-radius: 4px;
15 | box-sizing: border-box;
16 | display: flex;
17 | flex-direction: column;
18 | width: 100%;
19 | height: calc(100% - 75px);
20 | color: #fff;
21 | padding: 40px 38px 40px;
22 | }
23 |
24 | .LoginCard form > * {
25 | margin-top: 25px;
26 | }
27 |
28 | .LoginCard form span {
29 | color: red;
30 | font-size: 13px;
31 | }
32 |
33 | .LoginCard button {
34 | margin-top: 25px;
35 | }
36 |
37 | .textField {
38 | box-sizing: border-box;
39 | border-radius: 2px;
40 | width: 100%;
41 | }
42 |
43 | .textField input {
44 | color: white;
45 | }
46 |
47 | .HorizontalDiv {
48 | display: flex;
49 | align-items: center;
50 | justify-content: space-between;
51 | }
52 |
53 | @media (min-width: 600px) {
54 | .LoginCard {
55 | width: 450px;
56 | height: 680px;
57 | padding: 60px 68px 40px;
58 | margin: auto;
59 | }
60 | }
61 |
62 | @media(min-width: 1650px) {
63 | .LoginCard {
64 | width: 550px;
65 | height: 880px;
66 | padding: 60px 68px 40px;
67 | margin: auto;
68 | }
69 |
70 | .LoginCard form span {
71 | font-size: 20px;
72 | }
73 | }
--------------------------------------------------------------------------------
/src/containers/Login/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from "react";
2 | import "./Login.css";
3 |
4 | import { NetflixLogo, LoginBackground } from "assets/images/";
5 | import { TextField } from "@material-ui/core";
6 | import Button from "components/UI/Button/Button";
7 | import FormControlLabel from "@material-ui/core/FormControlLabel";
8 | import Checkbox from "@material-ui/core/Checkbox";
9 | import { useHistory } from "react-router-dom";
10 | import { AuthenticationContext } from 'context/Authentication'
11 | import { validEmailAndPhoneNumber } from 'utils/validation'
12 |
13 |
14 | /**
15 | * The login component, which validates the email and password
16 | * fields and uses a controlled form. Uses material UI for the
17 | * textfields.
18 | */
19 | const Login = props => {
20 | const [form, setForm] = useState({
21 | email: {
22 | value: '',
23 | touched: false,
24 | valid: false
25 | },
26 |
27 | password: {
28 | value: '',
29 | touched: false,
30 | valid: false
31 | },
32 |
33 | onSubmitInvalid: false
34 | })
35 |
36 | const history = useHistory()
37 | const authContext = useContext(AuthenticationContext)
38 |
39 | const inputChangeHandler = event => {
40 | const { name, value } = event.target;
41 | if (name === "email") {
42 | setForm(prevForm => ({
43 | ...prevForm,
44 | email: {
45 | ...prevForm.email,
46 | value: value, touched: true, valid: value.length > 0 && validEmailAndPhoneNumber(value)
47 | }
48 | }))
49 |
50 | } else if (name === "password") {
51 | setForm(prevForm => ({
52 | ...prevForm,
53 | password: {
54 | ...prevForm.password, value: value, touched: true,
55 | valid: value.length >= 4 && value.length <= 60
56 | }
57 | }))
58 | }
59 | };
60 |
61 | // For setting error spans once any of the fields are touched.
62 | const fieldBlurHandler = event => {
63 | if (event.target.name === 'email') {
64 | if (form.email.value === '') {
65 | setForm(prevForm => ({
66 | ...prevForm,
67 | email: { ...prevForm.email, touched: true }
68 | }))
69 | }
70 | }
71 |
72 | if (event.target.name === 'password') {
73 | if (form.password.value === '') {
74 | setForm(prevForm => ({
75 | ...prevForm,
76 | password: { ...prevForm.password, touched: true }
77 | }))
78 | }
79 | }
80 | };
81 |
82 | let [emailSpan, passwordSpan] = [null, null];
83 |
84 | if ((!form.email.valid && form.email.touched) || (form.onSubmitInvalid && !form.email.valid)) {
85 | emailSpan = Please enter a valid email or phone number.
86 | }
87 |
88 | if ((!form.password.valid && form.password.touched) || (form.onSubmitInvalid && !form.password.valid)) {
89 | passwordSpan = Your password must contain between 4 and 60 characters.
90 | }
91 |
92 | const formSubmitHandler = (event) => {
93 | event.preventDefault()
94 | if (!form.email.valid || !form.password.valid) {
95 | setForm(prevForm => ({ ...prevForm, onSubmitInvalid: true }))
96 | } else {
97 | authContext.login()
98 | history.push("/browse");
99 | }
100 | }
101 |
102 | return (
103 |
107 |

108 |
109 |
Sign In
110 |
157 |
158 |
159 |
163 | }
164 | label="Remember Me"
165 | />
166 | Need help?
167 |
168 |
169 |
170 | );
171 | };
172 |
173 | export default Login;
174 |
--------------------------------------------------------------------------------
/src/containers/NavBar/NavBar.css:
--------------------------------------------------------------------------------
1 | .NavBar {
2 | width: 100%;
3 | height: 45px;
4 | display: flex;
5 | align-items: center;
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | padding-top: 5px;
10 | font-size: 3vw;
11 | z-index: 100;
12 | box-sizing: border-box;
13 | }
14 |
15 | .Sticky {
16 | position: fixed;
17 | top: 0;
18 | }
19 |
20 | .NavBar img {
21 | height: 100%;
22 | width: 108px;
23 | }
24 |
25 | .inactive {
26 | color: #e5e5e5;
27 | margin: 0;
28 | padding-right: 15px;
29 | cursor: pointer;
30 | font-weight: lighter;
31 | transition: color .4s;
32 | text-decoration: none;
33 | }
34 |
35 | .NavBar a:hover {
36 | color: grey;
37 | }
38 |
39 | .active {
40 | font-weight: normal;
41 | color: white;
42 | }
43 |
44 | .LinkContainer {
45 | margin-right: auto;
46 | }
47 |
48 | .Horizontal {
49 | display: none;
50 | }
51 |
52 | .Vertical {
53 | display: block;
54 | }
55 |
56 | .ExtraOptions {
57 | display: none;
58 | }
59 |
60 | .DropdownNav {
61 | display: flex;
62 | flex-direction: column;
63 | align-items: center;
64 | justify-content: center;
65 | }
66 |
67 | .OptionsContainer {
68 | display: inline-flex;
69 | align-items: center;
70 | color: white;
71 | }
72 |
73 | .OptionsContainer > * {
74 | color: white;
75 | cursor: pointer;
76 | margin-right: 15px;
77 | }
78 |
79 | .NavBar > * {
80 | padding: 5px;
81 | }
82 |
83 | @media(min-width: 600px) {
84 | .NavBar {
85 | font-size: 1vw;
86 | }
87 |
88 | .NavBar img {
89 | width: 168px;
90 | }
91 |
92 | .NavBar > * {
93 | margin: 10px;
94 | }
95 |
96 | .OptionsContainer {
97 | display: flex;
98 | align-items: center;
99 | color: white;
100 | margin: 0;
101 | height: 80%;
102 | }
103 |
104 | .LinkContainer {
105 | margin-right: auto;
106 | }
107 |
108 | .OptionsContainer > * {
109 | margin-right: 25px;
110 | }
111 |
112 | .ExtraOptions {
113 | display: block;
114 | }
115 | }
116 |
117 | @media(min-width: 860px) {
118 | .Horizontal {
119 | display: block;
120 | }
121 |
122 | .Vertical {
123 | display: none;
124 | }
125 | }
126 |
127 | @media(min-width: 1650px) {
128 | .NavBar {
129 | height: 90px;
130 | font-size: 1.2vw;
131 | }
132 | }
--------------------------------------------------------------------------------
/src/containers/NavBar/NavBar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from "react";
2 | import "./NavBar.css";
3 |
4 | import { NetflixLogo } from "assets/images/";
5 | import Button from "components/UI/Button/Button";
6 | import { NavLink, Link } from "react-router-dom";
7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8 | import { faGift, faBell } from "@fortawesome/free-solid-svg-icons";
9 | import Search from '../Search/Search'
10 |
11 |
12 | const NavBar = props => {
13 | const { navigation, profileDropdown, navDropdown, loginButton } = props
14 | const [isNavbarAtTop, setIsNavbarAtTop] = useState(true)
15 |
16 | const scrollNavbarStateHandler = useCallback(() => {
17 | const navbarAtTop = window.scrollY < 45
18 | if (navbarAtTop !== isNavbarAtTop) {
19 | setIsNavbarAtTop(navbarAtTop)
20 | }
21 | }, [isNavbarAtTop])
22 |
23 | useEffect(() => {
24 | document.addEventListener('scroll', scrollNavbarStateHandler)
25 | return () => {
26 | document.removeEventListener('scroll', scrollNavbarStateHandler)
27 | }
28 | }, [scrollNavbarStateHandler])
29 |
30 | let navTiles = null
31 | let flexStyle = { justifyContent: 'space-between', backgroundColor: !isNavbarAtTop && 'black' }
32 |
33 | if (navigation) {
34 | navTiles = (
35 | <>
36 |
37 |
38 | Home
39 | TV Shows
40 | Movies
41 | Latest
42 | My List
43 |
44 |
45 | {navDropdown}
46 |
47 |
48 |
49 |
50 |
51 | KIDS
52 |
53 |
54 | {profileDropdown}
55 |
56 | >
57 | )
58 | }
59 |
60 | return (
61 |
62 |

63 | {navTiles}
64 | {loginButton &&
65 |
73 | }
74 |
75 | );
76 | };
77 |
78 | export default NavBar;
79 |
--------------------------------------------------------------------------------
/src/containers/Search/Search.css:
--------------------------------------------------------------------------------
1 | .SearchBox {
2 | display: inline-block;
3 | }
4 |
5 | .Holder {
6 | display: flex;
7 | align-items: center;
8 | border: solid 1px rgba(255,255,255,.85);
9 | height: 100%;
10 | width: 100%;
11 | background-color: black;
12 | height: 30px;
13 | width: 160px
14 | }
15 |
16 | .Holder svg {
17 | padding: 15px 5px;
18 | }
19 |
20 | .Holder input {
21 | border: none;
22 | outline: none;
23 | color: white;
24 | width: 100%;
25 | background-color: transparent;
26 | }
27 |
28 | .Holder input:hover {
29 | color: white;
30 | }
31 |
32 | .search-animation-enter {
33 | opacity: 0;
34 | }
35 |
36 | .search-animation-enter-active {
37 | opacity: 1;
38 | -webkit-animation: scale-up-hor-right 0.4s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;
39 | animation: scale-up-hor-right 0.4s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;
40 | }
41 |
42 | .search-animation-exit {
43 | opacity: 1;
44 | }
45 |
46 | .search-animation-exit-active {
47 | -webkit-animation: scale-down-hor-right 0.4s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
48 | animation: scale-down-hor-right 0.4s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
49 | }
50 |
51 |
52 | @-webkit-keyframes scale-up-hor-right {
53 | 0% {
54 | -webkit-transform: scaleX(0.4);
55 | transform: scaleX(0.4);
56 | -webkit-transform-origin: 100% 100%;
57 | transform-origin: 100% 100%;
58 | }
59 | 100% {
60 | -webkit-transform: scaleX(1);
61 | transform: scaleX(1);
62 | -webkit-transform-origin: 100% 100%;
63 | transform-origin: 100% 100%;
64 | }
65 | }
66 | @keyframes scale-up-hor-right {
67 | 0% {
68 | -webkit-transform: scaleX(0.4);
69 | transform: scaleX(0.4);
70 | -webkit-transform-origin: 100% 100%;
71 | transform-origin: 100% 100%;
72 | }
73 | 100% {
74 | -webkit-transform: scaleX(1);
75 | transform: scaleX(1);
76 | -webkit-transform-origin: 100% 100%;
77 | transform-origin: 100% 100%;
78 | }
79 | }
80 |
81 | @-webkit-keyframes scale-down-hor-right {
82 | 0% {
83 | -webkit-transform: scaleX(1);
84 | transform: scaleX(1);
85 | -webkit-transform-origin: 100% 100%;
86 | transform-origin: 100% 100%;
87 | }
88 | 100% {
89 | -webkit-transform: scaleX(0.3);
90 | transform: scaleX(0.3);
91 | -webkit-transform-origin: 100% 100%;
92 | transform-origin: 100% 100%;
93 | }
94 | }
95 | @keyframes scale-down-hor-right {
96 | 0% {
97 | -webkit-transform: scaleX(1);
98 | transform: scaleX(1);
99 | -webkit-transform-origin: 100% 100%;
100 | transform-origin: 100% 100%;
101 | }
102 | 100% {
103 | -webkit-transform: scaleX(0.3);
104 | transform: scaleX(0.3);
105 | -webkit-transform-origin: 100% 100%;
106 | transform-origin: 100% 100%;
107 | }
108 | }
109 |
110 | @media(min-width: 600px) {
111 | .Holder {
112 | height: 30px;
113 | width: 250px
114 | }
115 | }
116 |
117 | @media(min-width: 1650px) {
118 | .Holder {
119 | height: 50px;
120 | width: 400px;
121 | }
122 |
123 | .Holder svg {
124 | padding: 35px 15px;
125 | }
126 |
127 | .Holder input {
128 | font-size: 1.1vw;
129 | }
130 | }
--------------------------------------------------------------------------------
/src/containers/Search/Search.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import './Search.css'
3 | import { useHistory } from 'react-router-dom'
4 |
5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6 | import { faSearch, faTimes } from "@fortawesome/free-solid-svg-icons";
7 | import { CSSTransition } from 'react-transition-group'
8 |
9 | const Search = () => {
10 | const [searchBox, setSearchBox] = useState(false)
11 | const [searchIcon, setSearchIcon] = useState(true)
12 |
13 | const [searchText, setSearchText] = useState('')
14 | const [searchChanged, setSearchChanged] = useState(false)
15 | const searchBoxRef = useRef()
16 | const history = useHistory()
17 |
18 | useEffect(() => {
19 | document.addEventListener('mousedown', outsideSearchClickListener, false)
20 | return () => {
21 | document.removeEventListener('mousedown', outsideSearchClickListener, false)
22 | }
23 | }, [])
24 |
25 | useEffect(() => {
26 | if (searchText.length > 0) {
27 | history.push({
28 | pathname: '/search',
29 | search: `?q=${searchText}`
30 | })
31 |
32 | } else if (searchChanged && searchText.length === 0) {
33 | history.replace({ pathname: '/browse' })
34 | }
35 | }, [history, searchText, searchChanged])
36 |
37 | const searchClickHandler = () => {
38 | setSearchBox(true)
39 | }
40 |
41 | const outsideSearchClickListener = event => {
42 | if (searchBoxRef.current && !searchBoxRef.current.contains(event.target)) {
43 | setSearchBox(false)
44 | }
45 | }
46 |
47 | const searchTextChangeHandler = event => {
48 | const textValue = event.target.value
49 | setSearchText(textValue)
50 | setSearchChanged(true)
51 | }
52 |
53 | const clickCrossHandler = () => {
54 | setSearchText('')
55 | }
56 |
57 | const searchBar = (
58 | setSearchIcon(false)}
60 | onExiting={() => setSearchBox(false)}
61 | onExited={() => setSearchIcon(true)}>
62 |
63 |
64 |
66 | {searchText.length > 0 ?
67 | : null
68 | }
69 |
70 |
71 | )
72 |
73 | return (
74 |
75 | {searchIcon && }
76 | {searchBar}
77 |
78 | )
79 | }
80 |
81 | export default Search
--------------------------------------------------------------------------------
/src/context/Authentication.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | export const AuthenticationContext = React.createContext({
4 | authenticated: false,
5 | login: () => { },
6 | logout: () => {}
7 | })
8 |
9 | const AuthenticationContextProvider = (props) => {
10 | const [authenticated, setAuthenticated] = useState(false)
11 |
12 | const loginHandler = () => {
13 | setAuthenticated(true)
14 | }
15 |
16 | const logoutHandler = () => {
17 | setAuthenticated(false)
18 | }
19 |
20 | return (
21 |
25 | {props.children}
26 |
27 | )
28 | }
29 |
30 | export default AuthenticationContextProvider
--------------------------------------------------------------------------------
/src/hoc/Layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import useNavbar from 'hooks/useNavbar'
3 |
4 |
5 | const Layout = props => {
6 | const navBar = useNavbar()
7 | const style = {
8 | background: '#141414',
9 | minHeight: '100vh'
10 | }
11 | return (
12 |
13 | {navBar}
14 | {props.children}
15 |
16 | )
17 | }
18 |
19 | export default Layout
--------------------------------------------------------------------------------
/src/hoc/ScrollToTop/ScrollToTop.js:
--------------------------------------------------------------------------------
1 | import { Component } from "react";
2 | import { withRouter } from "react-router-dom";
3 |
4 | /**
5 | * A simple hoc as shown in the react router documentation
6 | * sample which does nothing but restore window scroll
7 | * position when going to and fro from links.
8 | */
9 | class ScrollToTop extends Component {
10 | componentDidUpdate(prevProps) {
11 | if (this.props.location !== prevProps.location) {
12 | window.scrollTo(0, 0);
13 | }
14 | }
15 |
16 | render() {
17 | return this.props.children;
18 | }
19 | }
20 |
21 | export default withRouter(ScrollToTop);
22 |
--------------------------------------------------------------------------------
/src/hooks/useDropdown.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import Dropdown from 'components/Navigation/Dropdown/Dropdown'
4 |
5 | const UseDropDown = (content, boxSizing) => {
6 | const [dropdown, setDropdown] = useState({
7 | iconHovered: false,
8 | floatingBoxHovered: false
9 | })
10 |
11 | const handlers = {
12 | iconHoveredInHandler: () => {
13 | setDropdown(prevDropdown => ({
14 | ...prevDropdown,
15 | iconHovered: true,
16 | }))
17 | },
18 |
19 | iconHoveredOutHandler: () => {
20 | setTimeout(() => {
21 | setDropdown(prevDropdown => ({
22 | ...prevDropdown,
23 | iconHovered: false,
24 | }))
25 | }, 600)
26 | },
27 |
28 | floatingBoxHoveredInHandler: () => {
29 | setDropdown(prevDropdown => ({
30 | ...prevDropdown,
31 | floatingBoxHovered: true,
32 | }))
33 | },
34 |
35 | floatingBoxHoveredOutHandler: () => {
36 | setDropdown(prevDropdown => ({
37 | ...prevDropdown,
38 | floatingBoxHovered: false,
39 | }))
40 | },
41 |
42 | onItemClickCloseBoxHandler: () => {
43 | setDropdown(false)
44 | }
45 | }
46 |
47 | return (
48 |
52 | )
53 | }
54 |
55 | export default UseDropDown
--------------------------------------------------------------------------------
/src/hooks/useHoverStyleButton.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | const UseHoverStyleButton = (buttonsObj) => {
4 | const [buttonHovered, setButtonHovered] = useState(buttonsObj)
5 |
6 | const onButtonHoverHandler = (id) => {
7 | setButtonHovered(prevHover => ({
8 | ...prevHover,
9 | [id]: !prevHover[id]
10 | }))
11 | }
12 |
13 | return [buttonHovered, onButtonHoverHandler]
14 | }
15 |
16 | export default UseHoverStyleButton
--------------------------------------------------------------------------------
/src/hooks/useNavbar.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import NavBar from 'containers/NavBar/NavBar'
4 | import { useHistory } from "react-router-dom";
5 | import { AuthenticationContext } from 'context/Authentication'
6 | import useDropdown from './useDropdown'
7 |
8 | import ProfileCard from 'components/UI/ProfileCard/ProfileCard'
9 | import { NavLink } from 'react-router-dom'
10 |
11 | import {
12 | Weird,
13 | Profile,
14 | Smile,
15 | Normal
16 | } from '../assets/images/index'
17 |
18 | const UseNavbar = () => {
19 | const logoutHandler = () => {
20 | localStorage.removeItem('profileSelected')
21 | authContext.logout()
22 | history.push('/')
23 | }
24 |
25 | const profileDropdownContent = (
26 | <>
27 |
32 |
37 |
42 |
47 |
48 | Manage Profiles
49 | Account
50 | Help Center
51 | Sign out of Netflix
52 | >
53 | )
54 |
55 | const navLinks = (
56 | <>
57 | Home
58 | TV Shows
59 | Movies
60 | Latest
61 | My List
62 | >
63 | )
64 |
65 | const profileDropdown = useDropdown(profileDropdownContent,
66 | { top: '42px', right: '0px', width: '20vh', height: '42vh' })
67 |
68 | const navDropdown = useDropdown(navLinks,
69 | { top: '15px', width: '100px' })
70 |
71 | const authContext = useContext(AuthenticationContext)
72 | const history = useHistory()
73 |
74 | return (
75 |
77 | )
78 | }
79 |
80 | export default UseNavbar
--------------------------------------------------------------------------------
/src/hooks/useVideoInfoHandlers.js:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react'
2 |
3 | import { mediaTypeToVideoDetailTransformation } from 'utils/transformations'
4 | import { isMobile } from 'react-device-detect'
5 |
6 | // A custom hook which sets all VideoCard/Carousel click/hover behavior
7 | const UseVideoInfoHandlers = () => {
8 | const [videoInfo, setVideoInfo] = useState()
9 | const [videoInfoError, setVideoInfoError] = useState(null)
10 | const [detailModal, setDetailModal] = useState(false)
11 |
12 | const cardClickHandler = useCallback((videoId, mediaType) => {
13 | if (!isMobile) {
14 | setDetailModal(true)
15 | } else {
16 | mediaTypeToVideoDetailTransformation(videoId, mediaType)
17 | .then(data => {
18 | setVideoInfo(data)
19 | setDetailModal(true)
20 | })
21 | .catch(error => {
22 | setVideoInfoError(error)
23 | })
24 | }
25 | }, [])
26 |
27 | const cardHoverHandler = useCallback((videoId, mediaType) => {
28 | mediaTypeToVideoDetailTransformation(videoId, mediaType)
29 | .then(data => {
30 | setVideoInfo(data)
31 | })
32 | .catch(error => {
33 | setVideoInfoError(error)
34 | })
35 | }, [])
36 |
37 | const closeModalHandler = useCallback(() => {
38 | setDetailModal(false)
39 | }, [])
40 |
41 | return [videoInfo, videoInfoError, detailModal, cardClickHandler, cardHoverHandler, closeModalHandler]
42 | }
43 |
44 | export default UseVideoInfoHandlers
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import App from "App";
5 | import { BrowserRouter } from "react-router-dom";
6 | import ScrollToTop from "hoc/ScrollToTop/ScrollToTop";
7 | import AuthenticationContextProvider from 'context/Authentication'
8 | import { Provider } from 'react-redux'
9 | import store from 'store/reducers/store'
10 |
11 |
12 | const app = (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
26 | if(window.Cypress) {
27 | window.store = store
28 | }
29 |
30 | const rootElement = document.getElementById("root");
31 | ReactDOM.render(app, rootElement);
32 |
--------------------------------------------------------------------------------
/src/store/reducers/slices/latestVideoSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
2 | import axios from 'baseAxios'
3 |
4 | export const fetchLatestVideos = createAsyncThunk('latestVideoSlice/fetchLatestVideos',
5 | async (_, { rejectWithValue }) => {
6 | try {
7 | const response = await Promise.all([
8 | axios.get(`discover/movie?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&sort_by=primary_release_date.desc&include_adult=false&include_video=false&page=1&vote_average.gte=6`)
9 | .then(response => ({ title: "Latest Movies", videos: response.data.results })),
10 | axios.get(`discover/tv?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&sort_by=first_air_date.desc&page=1&timezone=America%2FNew_York&vote_average.gte=6&include_null_first_air_dates=false`)
11 | .then(response => ({ title: "Latest TV Shows", videos: response.data.results }))
12 | ])
13 |
14 | return response
15 | } catch (error) {
16 | if (!error.response) {
17 | throw error
18 | }
19 |
20 | return rejectWithValue(error.response.data)
21 | }
22 | })
23 |
24 | const initialState = {
25 | latestVideos: [],
26 | status: 'idle',
27 | error: null
28 | }
29 |
30 | const latestVideoSlice = createSlice({
31 | name: 'latestVideos',
32 | initialState: initialState,
33 | extraReducers: {
34 | [fetchLatestVideos.pending]: (state, _) => {
35 | state.status = 'loading'
36 | },
37 |
38 | [fetchLatestVideos.fulfilled]: (state, action) => {
39 | action.payload.forEach(latestVideo => {
40 | state.latestVideos.push({ ...latestVideo })
41 | })
42 |
43 | state.status = 'success'
44 | },
45 |
46 | [fetchLatestVideos.rejected]: (state, action) => {
47 | state.status = 'error'
48 | if (action.payload) {
49 | state.error = action.payload.status_message
50 | } else {
51 | state.error = action.error
52 | }
53 | }
54 | }
55 | })
56 |
57 | export const selectLatestVideos = state => state.latestVideos
58 |
59 | export default latestVideoSlice.reducer
--------------------------------------------------------------------------------
/src/store/reducers/slices/moviesByGenreSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
2 | import axios from 'baseAxios'
3 | import { genreTopVideoTransformation } from 'utils/transformations'
4 |
5 | const fetchMovieGenres = async () => {
6 | try {
7 | const response = await axios.get(
8 | `genre/movie/list?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US`
9 | )
10 |
11 | return response.data.genres
12 | } catch (error) {
13 | throw new Error(error)
14 | }
15 | }
16 |
17 | export const fetchMoviesByGenre = createAsyncThunk('moviesByGenreSlice/fetchMoviesByGenre',
18 | async (_, { rejectWithValue }) => {
19 | try {
20 | const genres = await fetchMovieGenres()
21 | return await genreTopVideoTransformation(genres, 'movie')
22 | } catch (error) {
23 | if (!error.response) {
24 | throw error
25 | }
26 | return rejectWithValue(error.response.data)
27 | }
28 | }
29 | )
30 |
31 | const initalState = {
32 | genres: [],
33 | status: 'idle',
34 | error: null
35 | }
36 |
37 | const moviesByGenreSlice = createSlice({
38 | name: 'moviesByGenre',
39 | initialState: initalState,
40 | extraReducers: {
41 | [fetchMoviesByGenre.pending]: (state, _) => {
42 | state.status = 'loading'
43 | },
44 |
45 | [fetchMoviesByGenre.fulfilled]: (state, action) => {
46 | action.payload.forEach(genre => {
47 | state.genres.push({ ...genre })
48 | })
49 |
50 | state.status = 'success'
51 | },
52 |
53 | [fetchMoviesByGenre.rejected]: (state, action) => {
54 | state.status = 'error'
55 | if (action.payload) {
56 | state.error = action.payload.status_message
57 | } else {
58 | state.error = action.error
59 | }
60 | }
61 | }
62 | })
63 |
64 | export const selectMoviesByGenre = state => state.moviesByGenre
65 |
66 | export default moviesByGenreSlice.reducer
--------------------------------------------------------------------------------
/src/store/reducers/slices/netflixOriginalsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'
2 | import axios from 'baseAxios'
3 |
4 | export const netflixAdapter = createEntityAdapter()
5 |
6 | export const fetchNetflixOriginals = createAsyncThunk('netflixOriginalsSlice/fetchNetflixOriginals',
7 | async (_, { rejectWithValue }) => {
8 | try {
9 | const response = await axios.get(
10 | `discover/tv?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&sort_by=popularity.desc&page=1&timezone=America%2FNew_York&with_networks=213&include_null_first_air_dates=false`
11 | )
12 | return response.data.results
13 | } catch (error) {
14 | if (!error.response) {
15 | throw error
16 | }
17 | return rejectWithValue(error.response.data)
18 | }
19 | })
20 |
21 | const netflixOriginalsSlice = createSlice({
22 | name: 'netflixOriginals',
23 | initialState: netflixAdapter.getInitialState({ error: null }),
24 | extraReducers: {
25 | [fetchNetflixOriginals.fulfilled]: (state, action) => {
26 | netflixAdapter.upsertMany(state, action.payload)
27 | },
28 |
29 | [fetchNetflixOriginals.rejected]: (state, action) => {
30 | if (action.payload) {
31 | state.error = action.payload.status_message
32 | } else {
33 | state.error = action.error
34 | }
35 | }
36 | }
37 | })
38 |
39 | export const {
40 | selectAll: selectAllNetflixOriginals
41 | } = netflixAdapter.getSelectors(state => state.netflixOriginals)
42 | export const selectNetflixError = state => state.netflixOriginals.error
43 |
44 | export default netflixOriginalsSlice.reducer
--------------------------------------------------------------------------------
/src/store/reducers/slices/topratedSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'
2 | import axios from 'baseAxios'
3 |
4 | export const topratedAdapter = createEntityAdapter()
5 |
6 | export const fetchTopRated = createAsyncThunk('topratedSlice/fetchTopRated',
7 | async (_, { rejectWithValue }) => {
8 | try {
9 | const response = await axios.get(
10 | `movie/top_rated?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&page=1`
11 | )
12 | return response.data.results
13 | } catch (error) {
14 | if (!error.response) {
15 | throw error
16 | }
17 |
18 | return rejectWithValue(error.response.data)
19 | }
20 | })
21 |
22 | const topratedSlice = createSlice({
23 | name: 'toprated',
24 | initialState: topratedAdapter.getInitialState({ error: null }),
25 | extraReducers: {
26 | [fetchTopRated.fulfilled]: (state, action) => {
27 | topratedAdapter.upsertMany(state, action.payload)
28 | },
29 |
30 | [fetchTopRated.rejected]: (state, action) => {
31 | if (action.payload) {
32 | state.error = action.payload.status_message
33 | } else {
34 | state.error = action.error
35 | } }
36 | }
37 | })
38 |
39 | export const {
40 | selectAll: selectAllTopRatedVideos,
41 | } = topratedAdapter.getSelectors(state => state.toprated)
42 |
43 | export const selectTopRatedError = state => state.toprated.error
44 |
45 | export default topratedSlice.reducer
46 |
--------------------------------------------------------------------------------
/src/store/reducers/slices/trendingSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'
2 | import axios from 'baseAxios'
3 |
4 | export const trendingAdapter = createEntityAdapter()
5 |
6 | export const fetchTrending = createAsyncThunk('trendingSlice/fetchTrending',
7 | async (_, { rejectWithValue }) => {
8 | try {
9 | const response = await axios.get(
10 | `trending/all/day?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}`
11 | )
12 | return response.data.results
13 | } catch (error) {
14 | if (!error.response) {
15 | throw error
16 | }
17 |
18 | return rejectWithValue(error.response.data)
19 | }
20 | })
21 |
22 | const trendingSlice = createSlice({
23 | name: 'trending',
24 | initialState: trendingAdapter.getInitialState({ error: null }),
25 | reducers: {},
26 | extraReducers: {
27 | [fetchTrending.fulfilled]: (state, action) => {
28 | trendingAdapter.upsertMany(state, action.payload)
29 | },
30 |
31 | [fetchTrending.rejected]: (state, action) => {
32 | if (action.payload) {
33 | state.error = action.payload.status_message
34 | } else {
35 | state.error = action.error
36 | }
37 | }
38 | }
39 | })
40 |
41 | export const {
42 | selectAll: selectAllTrendingVideos,
43 | } = trendingAdapter.getSelectors(state => state.trending)
44 |
45 | export const selectTrendingError = state => state.trending.error
46 |
47 | export default trendingSlice.reducer
48 |
--------------------------------------------------------------------------------
/src/store/reducers/slices/tvByGenreSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
2 | import axios from 'baseAxios'
3 | import { genreTopVideoTransformation } from 'utils/transformations'
4 |
5 |
6 | const fetchTvGenres = async () => {
7 | try {
8 | const response = await axios.get(
9 | `genre/tv/list?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US`
10 | )
11 |
12 | return response.data.genres
13 | } catch (error) {
14 | throw new Error(error)
15 | }
16 | }
17 |
18 | export const fetchTvShowsByGenres = createAsyncThunk('tvByGenreSlice/fetchTvShowsByGenres',
19 | async (_, { rejectWithValue }) => {
20 | try {
21 | const genres = await fetchTvGenres()
22 | return await genreTopVideoTransformation(genres, 'tv')
23 | } catch (error) {
24 | if (!error.response) {
25 | throw error
26 | }
27 |
28 | return rejectWithValue(error.response.data)
29 | }
30 | }
31 | )
32 |
33 | const initalState = {
34 | genres: [],
35 | status: 'idle',
36 | error: null
37 | }
38 |
39 | const tvByGenreSlice = createSlice({
40 | name: 'tvByGenre',
41 | initialState: initalState,
42 | extraReducers: {
43 | [fetchTvShowsByGenres.pending]: (state, _) => {
44 | state.status = 'loading'
45 | },
46 |
47 | [fetchTvShowsByGenres.fulfilled]: (state, action) => {
48 | action.payload.forEach(genre => {
49 | state.genres.push({ ...genre })
50 | })
51 |
52 | state.status = 'success'
53 | },
54 |
55 | [fetchTvShowsByGenres.rejected]: (state, action) => {
56 | state.status = 'error'
57 | if (action.payload) {
58 | state.error = action.payload.status_message
59 | } else {
60 | state.error = action.error
61 | }
62 | }
63 | }
64 | })
65 |
66 | export const selectTvByGenre = state => state.tvByGenre
67 |
68 | export default tvByGenreSlice.reducer
--------------------------------------------------------------------------------
/src/store/reducers/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
2 | import trendingReducer from './slices/trendingSlice'
3 | import topRatedReducer from './slices/topratedSlice'
4 | import netflixOriginalsReducer from './slices/netflixOriginalsSlice'
5 | import moviesByGenresReducer from './slices/moviesByGenreSlice'
6 | import tvByGenresReducer from './slices/tvByGenreSlice'
7 | import latestVideoReducer from './slices/latestVideoSlice'
8 |
9 | const store = configureStore({
10 | reducer: {
11 | trending: trendingReducer,
12 | toprated: topRatedReducer,
13 | netflixOriginals: netflixOriginalsReducer,
14 | moviesByGenre: moviesByGenresReducer,
15 | tvByGenre: tvByGenresReducer,
16 | latestVideos: latestVideoReducer
17 | },
18 | // Clear this in production, as it is done by default
19 | middleware: [...getDefaultMiddleware({ immutableCheck: false })]
20 | })
21 |
22 | export default store
23 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
4 | -webkit-tap-highlight-color: transparent;
5 | }
6 |
7 | body button {
8 | cursor: pointer;
9 | }
10 |
11 | ::-webkit-scrollbar {
12 | display: none;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/animations.js:
--------------------------------------------------------------------------------
1 | export const scrollTo = (element, to, duration, scrollToDone = null) => {
2 | Math.easeInOutQuad = (t, b, c, d) => {
3 | t /= d / 2;
4 | if (t < 1) return c / 2 * t * t + b;
5 | t--;
6 | return -c / 2 * (t * (t - 2) - 1) + b;
7 | };
8 |
9 | let start = element.scrollLeft,
10 | change = to - start,
11 | currentTime = 0,
12 | increment = 20;
13 |
14 | const animateScroll = () => {
15 | currentTime += increment;
16 | const val = Math.easeInOutQuad(currentTime, start, change, duration);
17 | element.scrollLeft = val;
18 | if (currentTime < duration) {
19 | setTimeout(animateScroll, increment);
20 | } else {
21 | if (scrollToDone) scrollToDone();
22 | }
23 | };
24 | animateScroll();
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/sorting.js:
--------------------------------------------------------------------------------
1 | export const sortVideosByPopularity = (a, b) => {
2 | if (a.popularity > b.popularity) {
3 | return -1;
4 | }
5 | if (a.popularity < b.popularity) {
6 | return 1;
7 | }
8 | return 0;
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/utils/time.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const convertTimeToHourMinuteFormat = timeInHours => {
4 | var hours = Math.trunc(timeInHours / 60);
5 | var minutes = timeInHours % 60;
6 | return `${hours}h ${minutes}m`
7 | }
8 |
9 | export const getSeasonsOrMovieLength = (seasons, runtime) => {
10 | let timeSpan
11 | if (runtime) {
12 | timeSpan = {convertTimeToHourMinuteFormat(runtime)}
13 | } else if (seasons) {
14 | timeSpan = (
15 |
16 | {seasons.length > 1 ? `${seasons.length} Seasons` : `${seasons.length} Season`}
17 |
18 | )
19 | }
20 |
21 | return timeSpan
22 | }
--------------------------------------------------------------------------------
/src/utils/transformations.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import axios from 'baseAxios'
4 | import { isMobile } from 'react-device-detect'
5 | import VideoModal from 'components/Modals/VideoModal/VideoModal'
6 |
7 | /**
8 | *
9 | * @param {*} genres: the genres
10 | * @param {*} apiCallType: whether the subsequent API calls will be made for tv or movies
11 | *
12 | * Takes a genre object and does creates a big chain of API calls to get each genre's top trending videos
13 | *
14 | * Fetches all genres and creates a trending movies API call for each. I push the response with
15 | * a title and content to make it easier to label the video carousels later. This Promise.all
16 | * function returns a large array, so I have to parse through the action.payload later
17 | * in the slice reducer.
18 | */
19 |
20 | export const genreTopVideoTransformation = async (genres, apiCallType) => {
21 | let url
22 | if (apiCallType === 'tv') {
23 | url = `discover/tv?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&sort_by=popularity.desc&page=1&include_null_first_air_dates=false&with_genres=`
24 | } else if (apiCallType === 'movie') {
25 | url = `discover/movie?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_genres=`
26 | }
27 |
28 | const genreRequestArray = []
29 | genres.forEach(genre => {
30 | let newUrlParser = url
31 | newUrlParser += genre.id.toString()
32 | genreRequestArray.push(axios.get(newUrlParser).then(response =>
33 | ({ title: genre.name, videos: response.data.results })))
34 | })
35 |
36 | try {
37 | return await Promise.all(genreRequestArray)
38 | } catch (error) {
39 | throw new Error(error)
40 | }
41 | }
42 |
43 | export const mediaTypeToVideoDetailTransformation = async (videoId, mediaType) => {
44 | let requestURL;
45 | if (mediaType === 'movie') {
46 | requestURL = `movie/${videoId}?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US`
47 | } else if (mediaType === 'tv') {
48 | requestURL = `tv/${videoId}?api_key=${process.env.REACT_APP_MOVIEDB_API_KEY}&language=en-US`
49 | }
50 |
51 | try {
52 | const response = await axios.get(requestURL)
53 | return response.data
54 | } catch (error) {
55 | throw new Error(error)
56 | }
57 | }
58 |
59 | export const buildVideoMetadata = (videoItem, selectedVideoInfo) => {
60 | let mediaType
61 | if (videoItem.media_type) {
62 | mediaType = videoItem.media_type
63 | } else {
64 | if (videoItem.first_air_date) {
65 | mediaType = 'tv'
66 | } else if (videoItem.release_date) {
67 | mediaType = 'movie'
68 | }
69 | }
70 |
71 | let extraInfo = {}
72 | if (!isMobile) {
73 | if (selectedVideoInfo && selectedVideoInfo.id === videoItem.id) {
74 | extraInfo['genres'] = selectedVideoInfo.genres
75 | if (selectedVideoInfo.runtime) {
76 | extraInfo['runtime'] = selectedVideoInfo.runtime
77 | } else if (selectedVideoInfo.seasons) {
78 | extraInfo['seasons'] = selectedVideoInfo.seasons
79 | }
80 | }
81 | }
82 |
83 | return { mediaType, extraInfo }
84 | }
85 |
86 | export const buildVideoModal = (videoDetailModal, videoInfo, handlers) => {
87 | let detailModalComponent
88 | if (videoDetailModal && videoInfo) {
89 | detailModalComponent = (
90 |
95 | )
96 | }
97 |
98 | return detailModalComponent
99 | }
--------------------------------------------------------------------------------
/src/utils/validation.js:
--------------------------------------------------------------------------------
1 | export const validEmailAndPhoneNumber = input => {
2 | const phoneRegex = /^\d{10}$/ // eslint-disable-next-line
3 | const emailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/
4 | return input.match(phoneRegex) || input.match(emailRegex)
5 | }
--------------------------------------------------------------------------------