├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── images │ ├── no_image.jpg │ ├── reactMovie_logo.png │ └── tmdb_logo.png ├── index.html └── manifest.json └── src ├── components ├── .gitkeep ├── App │ └── App.js ├── Home │ ├── Home.css │ └── Home.js ├── Movie │ ├── Movie.css │ └── Movie.js └── elements │ ├── Actor │ ├── Actor.css │ └── Actor.js │ ├── FourColGrid │ ├── FourColGrid.css │ └── FourColGrid.js │ ├── Header │ ├── Header.css │ └── Header.js │ ├── HeroImage │ ├── HeroImage.css │ └── HeroImage.js │ ├── LoadMoreBtn │ ├── LoadMoreBtn.css │ └── LoadMoreBtn.js │ ├── MovieInfo │ ├── MovieInfo.css │ └── MovieInfo.js │ ├── MovieInfoBar │ ├── MovieInfoBar.css │ └── MovieInfoBar.js │ ├── MovieThumb │ ├── MovieThumb.css │ └── MovieThumb.js │ ├── Navigation │ ├── Navigation.css │ └── Navigation.js │ ├── NotFound │ └── NotFound.js │ ├── SearchBar │ ├── SearchBar.css │ └── SearchBar.js │ └── Spinner │ ├── Spinner.css │ └── Spinner.js ├── config.js ├── helpers.js ├── index.css ├── index.js ├── logo.svg └── serviceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Movie rmdb 2 | [Link to App](http://phobic-heat.surge.sh/) 3 | ![project image](https://oliver-gomes.github.io/images/github%20images/react-movie.png) 4 | 5 | 6 | ## Table of Contents 7 | 8 | * [How to Load the App](#howtoloadtheapp) 9 | * [About the App](#about) 10 | * [How to Use the App](#how-to-use-the-app) 11 | 12 | ### How to Load the App 13 | ``` 14 | git clone https://github.com/oliver-gomes/react-movie.git 15 | npm install 16 | npm start 17 | ``` 18 | ## About 19 | Modern fast movie database web app with React using The Movie DB API. All the new popular movies are populated along with search bar for your own movie search. Clicking on specific movies bring you all data about the movie including actors, directors, time, budget, revenue, rating and many more. 20 | 21 | ## How to Use the App 22 | 23 | - Find the most popular movie at the moment on the front page 24 | - Click on specific movie to view full-blown information 25 | - Use the Search bar to find you favorites movies and their information 26 | - Bookmark and use the App at your own pace! 27 | 28 | # Hope You Enjoy the App ! 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react_movie_db_course", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.5.2", 7 | "react-dom": "^16.5.2", 8 | "react-fontawesome": "^1.6.1", 9 | "react-router-dom": "^4.3.1", 10 | "react-scripts": "1.1.5" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliver-gomes/react-movie/482b96dcb0638b6e8e76710c1e1effcaf24651c7/public/favicon.ico -------------------------------------------------------------------------------- /public/images/no_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliver-gomes/react-movie/482b96dcb0638b6e8e76710c1e1effcaf24651c7/public/images/no_image.jpg -------------------------------------------------------------------------------- /public/images/reactMovie_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliver-gomes/react-movie/482b96dcb0638b6e8e76710c1e1effcaf24651c7/public/images/reactMovie_logo.png -------------------------------------------------------------------------------- /public/images/tmdb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliver-gomes/react-movie/482b96dcb0638b6e8e76710c1e1effcaf24651c7/public/images/tmdb_logo.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 17 | 21 | 25 | 34 | React Movie 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /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 | } 16 | -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | // Must have this so Git won't delete this empty folder -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 3 | import Header from "../elements/Header/Header"; 4 | import Home from "../Home/Home"; 5 | import Movie from "../Movie/Movie"; 6 | import NotFound from "../elements/NotFound/NotFound"; 7 | 8 | class App extends React.Component { 9 | render() { 10 | return ( 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/components/Home/Home.css: -------------------------------------------------------------------------------- 1 | .rmdb-home { 2 | margin-bottom: 100px; 3 | } 4 | 5 | .rmdb-home-grid { 6 | max-width: 1280px; 7 | margin: 0 auto; 8 | padding: 0 20px; 9 | } -------------------------------------------------------------------------------- /src/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./Home.css"; 3 | 4 | import { 5 | API_URL, 6 | API_KEY, 7 | IMAGE_BASE_URL, 8 | POSTER_SIZE, 9 | BACKDROP_SIZE 10 | } from "../../config"; 11 | 12 | import HeroImage from "../elements/HeroImage/HeroImage"; 13 | import SearchBar from "../elements/SearchBar/SearchBar"; 14 | import FourColGrid from "../elements/FourColGrid/FourColGrid"; 15 | import MovieThumb from "../elements/MovieThumb/MovieThumb"; 16 | import LoadMoreBtn from "../elements/LoadMoreBtn/LoadMoreBtn"; 17 | import Spinner from "../elements/Spinner/Spinner"; 18 | 19 | class Home extends Component { 20 | state = { 21 | movies: [], 22 | heroImage: null, 23 | loading: false, 24 | currentPage: 0, 25 | totalPages: 0, 26 | searchTerm: "" 27 | }; 28 | 29 | componentDidMount() { 30 | this.setState({ loading: true }); 31 | const endpoint = `${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`; 32 | this.fetchItems(endpoint); 33 | } 34 | 35 | searchItems = searchTerm => { 36 | console.log(searchTerm); 37 | let endpoint = ""; 38 | this.setState({ 39 | movies: [], 40 | loading: true, 41 | searchTerm 42 | }); 43 | 44 | if (searchTerm === "") { 45 | endpoint = `${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`; 46 | } else { 47 | endpoint = `${API_URL}search/movie?api_key=${API_KEY}&language=en-US&query=${searchTerm}`; 48 | } 49 | this.fetchItems(endpoint); 50 | }; 51 | 52 | loadMoreItems = () => { 53 | let endpoint = ""; 54 | this.setState({ 55 | loading: true 56 | }); 57 | 58 | if (this.state.searchTerm === "") { 59 | endpoint = `${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=${this 60 | .state.currentPage + 1}`; 61 | } else { 62 | endpoint = `${API_URL}search/movie?api_key=${API_KEY}&language=en-US&query${ 63 | this.state.searchTerm 64 | }$page=${this.state.currentPage + 1}`; 65 | } 66 | this.fetchItems(endpoint); 67 | }; 68 | 69 | fetchItems = endpoint => { 70 | fetch(endpoint) 71 | .then(result => result.json()) 72 | .then(result => { 73 | this.setState({ 74 | movies: [...this.state.movies, ...result.results], 75 | heroImage: this.state.heroImage || result.results[0], 76 | loading: false, 77 | currentPage: result.page, 78 | totalPages: result.total_pages 79 | }); 80 | }); 81 | }; 82 | 83 | render() { 84 | return ( 85 |
86 | {this.state.heroImage ? ( 87 |
88 | 95 | 96 |
97 | ) : null} 98 |
99 | 103 | {this.state.movies.map((element, i) => { 104 | return ( 105 | 116 | ); 117 | })} 118 | 119 | {this.state.loading ? : null} 120 | {this.state.currentPage <= this.state.totalPages && 121 | !this.state.loading ? ( 122 | 123 | ) : null} 124 |
125 |
126 | ); 127 | } 128 | } 129 | 130 | export default Home; 131 | -------------------------------------------------------------------------------- /src/components/Movie/Movie.css: -------------------------------------------------------------------------------- 1 | .rmdb-movie { 2 | margin-bottom: 100px; 3 | } 4 | 5 | .rmdb-movie-grid { 6 | max-width: 1280px; 7 | margin: 0 auto; 8 | padding: 0 20px; 9 | } -------------------------------------------------------------------------------- /src/components/Movie/Movie.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { API_URL, API_KEY } from "../../config"; 3 | import Navigation from "../elements/Navigation/Navigation"; 4 | import MovieInfo from "../elements/MovieInfo/MovieInfo"; 5 | import MovieInfoBar from "../elements/MovieInfoBar/MovieInfoBar"; 6 | // import FourColGrid from "../elements/FourColGrid/FourColGrid"; 7 | import Actor from "../elements/Actor/Actor"; 8 | import Spinner from "../elements/Spinner/Spinner"; 9 | import "./Movie.css"; 10 | import FourColGrid from "../elements/FourColGrid/FourColGrid"; 11 | 12 | class Movie extends Component { 13 | state = { 14 | movie: null, 15 | actors: null, 16 | directors: [], 17 | loading: false 18 | }; 19 | 20 | componentDidMount() { 21 | if (localStorage.getItem(`${this.props.match.params.movieId}`)) { 22 | const state = JSON.parse( 23 | localStorage.getItem(`${this.props.match.params.movieId}`) 24 | ); 25 | this.setState({ ...state }); 26 | } else { 27 | this.setState({ loading: true }); 28 | //first fetch the movie data and then actors 29 | const endpoint = `${API_URL}movie/${ 30 | this.props.match.params.movieId 31 | }?api_key=${API_KEY}&language=en-US`; 32 | this.fetchMovieData(endpoint); 33 | } 34 | } 35 | 36 | fetchMovieData = endpoint => { 37 | fetch(endpoint) 38 | .then(result => result.json()) 39 | .then(result => { 40 | if (result.status_code) { 41 | this.setState({ loading: false }); 42 | } else { 43 | this.setState( 44 | { 45 | movie: result 46 | }, 47 | () => { 48 | const endpoint_credit = `${API_URL}movie/${ 49 | this.props.match.params.movieId 50 | }/credits?api_key=${API_KEY}&language=en-US`; 51 | fetch(endpoint_credit) 52 | .then(result => result.json()) 53 | .then(result => { 54 | const directors = result.crew.filter( 55 | member => member.job === "Director" 56 | ); 57 | this.setState( 58 | { 59 | actors: result.cast, 60 | directors, 61 | loading: false 62 | }, 63 | () => { 64 | localStorage.setItem( 65 | `${this.props.match.params.movieId}`, 66 | JSON.stringify(this.state) 67 | ); 68 | } 69 | ); 70 | }); 71 | } 72 | ); 73 | } 74 | }) 75 | .catch(error => console.error("Error: ", error)); 76 | }; 77 | 78 | render() { 79 | return ( 80 |
81 | {this.state.movie ? ( 82 |
83 | 84 | 88 | 93 |
94 | ) : null} 95 | 96 | {this.state.actors ? ( 97 |
98 | 99 | {this.state.actors.map((element, i) => { 100 | return ; 101 | })} 102 | 103 |
104 | ) : null} 105 | 106 | {!this.state.actors && !this.state.loading ? ( 107 |

No Movie Found!

108 | ) : null} 109 | {this.state.loading ? : null} 110 |
111 | ); 112 | } 113 | } 114 | 115 | export default Movie; 116 | -------------------------------------------------------------------------------- /src/components/elements/Actor/Actor.css: -------------------------------------------------------------------------------- 1 | .rmdb-actor { 2 | box-sizing: border-box; 3 | } 4 | 5 | .rmdb-actor img { 6 | width: 40%; 7 | height: auto; 8 | float: left; 9 | box-sizing: border-box; 10 | } 11 | 12 | .rmdb-actor-name { 13 | font-family: 'Abel', sans-serif; 14 | font-size: 22px; 15 | color:#fff; 16 | float: left; 17 | margin: 10px 20px; 18 | width: 40%; 19 | box-sizing: border-box; 20 | } 21 | 22 | .rmdb-actor-character { 23 | font-family: 'Abel', sans-serif; 24 | font-size: 18px; 25 | color:#fff; 26 | float: left; 27 | margin: 0 20px 10px 20px; 28 | width: 40%; 29 | box-sizing: border-box; 30 | } -------------------------------------------------------------------------------- /src/components/elements/Actor/Actor.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IMAGE_BASE_URL } from "../../../config"; 3 | import "./Actor.css"; 4 | 5 | const Actor = props => { 6 | const POSTER_SIZE = "w154"; 7 | return ( 8 |
9 | actorthumb 17 | {props.actor.name} 18 | {props.actor.character} 19 |
20 | ); 21 | }; 22 | 23 | export default Actor; 24 | -------------------------------------------------------------------------------- /src/components/elements/FourColGrid/FourColGrid.css: -------------------------------------------------------------------------------- 1 | .rmdb-grid h1 { 2 | font-family: 'Abel', sans-serif; 3 | font-size:42px; 4 | } 5 | 6 | .rmdb-grid-content { 7 | display: grid; 8 | grid-template-columns: auto auto auto auto; 9 | } 10 | 11 | .rmdb-grid-element { 12 | margin: 0 40px 40px 0; 13 | background: #353535; 14 | max-height: 430px; 15 | animation: animateGrid 0.5s; 16 | overflow: hidden; 17 | } 18 | 19 | @keyframes animateGrid { 20 | from { 21 | opacity:0; 22 | } 23 | to { 24 | opacity:1; 25 | } 26 | } 27 | 28 | /* For a 4-column grid */ 29 | .rmdb-grid-element:nth-child(4n+4) { 30 | margin-right: 0; 31 | } 32 | 33 | @media screen and (max-width: 720px) { 34 | .rmdb-grid-content { 35 | grid-template-columns: auto auto; 36 | } 37 | 38 | .rmdb-grid-element:nth-child(2n+2) { 39 | margin-right: 0; 40 | } 41 | 42 | .rmdb-grid h1 { 43 | font-size:22px; 44 | } 45 | } -------------------------------------------------------------------------------- /src/components/elements/FourColGrid/FourColGrid.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FourColGrid.css"; 3 | 4 | const FourColGrid = props => { 5 | const renderElements = () => { 6 | const gridElements = props.children.map((element, i) => { 7 | return ( 8 |
9 | {element} 10 |
11 | ); 12 | }); 13 | return gridElements; 14 | }; 15 | 16 | return ( 17 |
18 | {props.header && !props.loading ?

{props.header}

: null} 19 |
{renderElements()}
20 |
21 | ); 22 | }; 23 | 24 | export default FourColGrid; 25 | -------------------------------------------------------------------------------- /src/components/elements/Header/Header.css: -------------------------------------------------------------------------------- 1 | .rmdb-header { 2 | width: 100%; 3 | height: auto; 4 | background: #1c1c1c; 5 | padding: 0 20px; 6 | box-sizing: border-box; 7 | } 8 | 9 | .rmdb-header-content { 10 | max-width: 1280px; 11 | min-height: 120px; 12 | height: auto; 13 | padding: 20px 0px; 14 | margin: 0 auto; 15 | box-sizing: border-box; 16 | overflow: hidden; 17 | } 18 | 19 | .rmdb-logo { 20 | width: 300px; 21 | margin-top: 20px; 22 | float: left; 23 | } 24 | 25 | .rmdb-tmdb-logo { 26 | width: 122px; 27 | margin-top: 25px; 28 | float: right; 29 | } 30 | 31 | @media screen and (max-width: 500px) { 32 | .rmdb-header-content { 33 | max-width: 1280px; 34 | min-height: 0px; 35 | } 36 | 37 | .rmdb-tmdb-logo { 38 | display: inline-block; 39 | width:80px; 40 | margin-top: 0px; 41 | } 42 | 43 | .rmdb-logo { 44 | width: 150px; 45 | margin-top: 5px; 46 | } 47 | } -------------------------------------------------------------------------------- /src/components/elements/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import "./Header.css"; 4 | 5 | const Header = () => { 6 | return ( 7 |
8 |
9 | 10 | rmdb-logo 15 | 16 | tmdb-logo 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /src/components/elements/HeroImage/HeroImage.css: -------------------------------------------------------------------------------- 1 | .rmdb-heroimage { 2 | background-size: 100%, cover !important; 3 | background-position: center, center !important; 4 | width: 100%; 5 | height: 600px; 6 | position: relative; 7 | animation: animateHeroimage 1s; 8 | } 9 | 10 | @keyframes animateHeroimage { 11 | from { 12 | opacity:0; 13 | } 14 | to { 15 | opacity:1; 16 | } 17 | } 18 | 19 | .rmdb-heroimage-content { 20 | max-width: 1280px; 21 | padding: 20px; 22 | margin: 0 auto; 23 | } 24 | 25 | .rmdb-heroimage-text { 26 | z-index: 100; 27 | max-width: 700px; 28 | position: absolute; 29 | bottom: 40px; 30 | margin-right: 20px; 31 | min-height: 100px; 32 | background: rgba(0, 0, 0, 0.0); 33 | color: #fff; 34 | } 35 | 36 | .rmdb-heroimage-text h1 { 37 | font-family: 'Abel', sans-serif; 38 | font-size:48px; 39 | color: #fff; 40 | } 41 | 42 | .rmdb-heroimage-text p { 43 | font-family: 'Abel', sans-serif; 44 | font-size: 22px; 45 | line-height: 26px; 46 | color: #fff; 47 | } 48 | 49 | @media screen and (max-width: 720px) { 50 | .rmdb-heroimage-text { 51 | max-width: 100%; 52 | } 53 | 54 | .rmdb-heroimage-text h1 { 55 | font-size: 38px; 56 | color: #fff; 57 | } 58 | 59 | .rmdb-heroimage-text p { 60 | font-size: 16px; 61 | line-height: 20px; 62 | color: #fff; 63 | } 64 | } -------------------------------------------------------------------------------- /src/components/elements/HeroImage/HeroImage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./HeroImage.css"; 3 | 4 | const HeroImage = props => { 5 | return ( 6 |
15 |
16 |
17 |

{props.title}

18 |

{props.text}

19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default HeroImage; 26 | -------------------------------------------------------------------------------- /src/components/elements/LoadMoreBtn/LoadMoreBtn.css: -------------------------------------------------------------------------------- 1 | .rmdb-loadmorebtn { 2 | background: #16d47b; 3 | width: 100%; 4 | min-height: 50px; 5 | text-align: center; 6 | color: #fff; 7 | cursor: pointer; 8 | transition: all 0.3s; 9 | } 10 | 11 | .rmdb-loadmorebtn p { 12 | font-family: 'Abel', sans-serif; 13 | font-size:42px; 14 | padding: 20px; 15 | } 16 | 17 | .rmdb-loadmorebtn:hover { 18 | opacity: 0.8; 19 | } -------------------------------------------------------------------------------- /src/components/elements/LoadMoreBtn/LoadMoreBtn.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./LoadMoreBtn.css"; 3 | 4 | const LoadMoreBtn = props => { 5 | return ( 6 |
7 |

{props.text}

8 |
9 | ); 10 | }; 11 | 12 | export default LoadMoreBtn; 13 | -------------------------------------------------------------------------------- /src/components/elements/MovieInfo/MovieInfo.css: -------------------------------------------------------------------------------- 1 | .rmdb-movieinfo { 2 | background-size: cover !important; 3 | background-position: center !important; 4 | width: 100%; 5 | height: 600px; 6 | padding: 40px 20px; 7 | box-sizing: border-box; 8 | animation: animateMovieinfo 1s; 9 | } 10 | 11 | @keyframes animateMovieinfo { 12 | from { 13 | opacity:0; 14 | } 15 | to { 16 | opacity:1; 17 | } 18 | } 19 | 20 | .rmdb-movieinfo-content { 21 | max-width: 1280px; 22 | width: 100%; 23 | height: 100%; 24 | margin: 0 auto; 25 | background: rgb(0, 0, 0, 0.7); 26 | position: relative; 27 | } 28 | 29 | .rmdb-movieinfo-thumb { 30 | width: 350px; 31 | height: 100%; 32 | overflow: hidden; 33 | position: absolute; 34 | left: 0px; 35 | } 36 | 37 | .rmdb-movieinfo-text { 38 | font-family: Arial, Helvetica, sans-serif; 39 | height: 100%; 40 | width: auto; 41 | padding: 40px; 42 | color: #fff; 43 | overflow: hidden; 44 | box-sizing: border-box; 45 | position: absolute; 46 | left: 360px; 47 | } 48 | 49 | .rmdb-movieinfo-text h1 { 50 | font-family: 'Abel', sans-serif; 51 | font-size:48px; 52 | margin: 0; 53 | } 54 | 55 | .rmdb-movieinfo-text h3 { 56 | font-size: 16px; 57 | line-height: 0; 58 | margin-top: 30px; 59 | } 60 | 61 | .rmdb-movieinfo-text p { 62 | font-family: 'Abel', sans-serif; 63 | font-size: 18px; 64 | line-height: 26px; 65 | } 66 | 67 | .rmdb-rating { 68 | width: 250px; 69 | height: 20px; 70 | margin-top: 20px; 71 | position: relative; 72 | } 73 | 74 | .rmdb-score { 75 | position: absolute; 76 | margin: 0; 77 | right: 0px; 78 | top: -3px; 79 | } 80 | 81 | .rmdb-director { 82 | margin: 0; 83 | } 84 | 85 | .fa-film { 86 | position: absolute; 87 | bottom: 40px; 88 | right: 40px; 89 | color: #fff; 90 | } 91 | 92 | meter::-webkit-meter-bar { 93 | background: #FFF; 94 | width:200px; 95 | } 96 | meter::-webkit-meter-optimum-value { 97 | background: linear-gradient(to bottom, #16d47b); 98 | } 99 | meter::-webkit-meter-suboptimum-value { 100 | background: linear-gradient(to bottom, #fbb450); 101 | } 102 | meter::-webkit-meter-even-less-good-value { 103 | background: linear-gradient(to bottom, #ee5f5b); 104 | } -------------------------------------------------------------------------------- /src/components/elements/MovieInfo/MovieInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IMAGE_BASE_URL, POSTER_SIZE, BACKDROP_SIZE } from "../../../config"; 3 | import FontAwesome from "react-fontawesome"; 4 | import MovieThumb from "../MovieThumb/MovieThumb"; 5 | import "./MovieInfo.css"; 6 | 7 | const MovieInfo = props => { 8 | return ( 9 |
19 |
20 |
21 | 29 |
30 |

{props.movie.title}

31 |

PLOT

32 |

{props.movie.overview}

33 |

IMDB RATING

34 |
35 | 43 |

{props.movie.vote_average}

44 |
45 | {props.directors.length > 1 ? ( 46 |

DIRECTORS

47 | ) : ( 48 |

DIRECTOR

49 | )}{" "} 50 | {props.directors.map((element, i) => { 51 | return ( 52 |

53 | {element.name} 54 |

55 | ); 56 | })} 57 |
58 | 59 |
60 |
61 |
62 | ); 63 | }; 64 | 65 | export default MovieInfo; 66 | -------------------------------------------------------------------------------- /src/components/elements/MovieInfoBar/MovieInfoBar.css: -------------------------------------------------------------------------------- 1 | .rmdb-movieinfobar { 2 | width: 100%; 3 | height: 105px; 4 | background: #1c1c1c; 5 | position: relative; 6 | padding: 25px 20px 0px 20px; 7 | box-sizing: border-box; 8 | font-family: 'Abel', sans-serif; 9 | font-size: 22px; 10 | } 11 | 12 | .rmdb-movieinfobar-content { 13 | max-width: 1280px; 14 | width: 100%; 15 | margin: 0 auto; 16 | color: #fff; 17 | } 18 | 19 | .rmdb-movieinfobar-content-col { 20 | float: left; 21 | width: 33.33%; 22 | box-sizing: border-box; 23 | padding: 10px 20px 0 0; 24 | } 25 | 26 | .rmdb-movieinfobar-info { 27 | padding: 5px 0 0 10px; 28 | float: left; 29 | } 30 | 31 | .fa-time, .fa-revenue { 32 | float: left; 33 | margin-top: -4px; 34 | 35 | } 36 | 37 | .fa-budget { 38 | float: left; 39 | margin-top: -3px; 40 | } -------------------------------------------------------------------------------- /src/components/elements/MovieInfoBar/MovieInfoBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FontAwesome from "react-fontawesome"; 3 | 4 | import { calcTime, convertMoney } from "../../../helpers"; 5 | import "./MovieInfoBar.css"; 6 | 7 | const MovieInfoBar = props => { 8 | return ( 9 |
10 |
11 |
12 | 13 | 14 | Running Time: {calcTime(props.time)} 15 | 16 |
17 |
18 | 19 | 20 | Budget: {convertMoney(props.budget)} 21 | 22 |
23 |
24 | 25 | 26 | Revenue: {convertMoney(props.revenue)} 27 | 28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default MovieInfoBar; 35 | -------------------------------------------------------------------------------- /src/components/elements/MovieThumb/MovieThumb.css: -------------------------------------------------------------------------------- 1 | .rmdb-moviethumb img { 2 | width: 500px; 3 | height: auto; 4 | max-width: 100%; 5 | max-height: 100%; 6 | transition: all 0.3s; 7 | box-sizing: border-box; 8 | } 9 | 10 | .clickable { 11 | cursor: pointer; 12 | } 13 | 14 | .clickable:hover { 15 | opacity: 0.8; 16 | } -------------------------------------------------------------------------------- /src/components/elements/MovieThumb/MovieThumb.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import "./MovieThumb.css"; 5 | 6 | const MovieThumb = props => { 7 | return ( 8 |
9 | {props.clickable ? ( 10 | 16 | moviethumb 17 | 18 | ) : ( 19 | moviethumb 20 | )} 21 |
22 | ); 23 | }; 24 | 25 | MovieThumb.propTypes = { 26 | image: PropTypes.string, 27 | movieId: PropTypes.number, 28 | movieName: PropTypes.string 29 | }; 30 | 31 | export default MovieThumb; 32 | -------------------------------------------------------------------------------- /src/components/elements/Navigation/Navigation.css: -------------------------------------------------------------------------------- 1 | .rmdb-navigation { 2 | width: 100%; 3 | height: 50px; 4 | background: #353535; 5 | color: #fff; 6 | position: relative; 7 | padding: 20px; 8 | box-sizing: border-box; 9 | margin: 0; 10 | padding-top: 10px; 11 | } 12 | 13 | .rmdb-navigation-content { 14 | max-width: 1280px; 15 | margin: 0 auto; 16 | padding: 0 20px; 17 | 18 | } 19 | 20 | .rmdb-navigation-content p { 21 | font-family: 'Abel', sans-serif; 22 | font-size: 22px; 23 | float: left; 24 | color: #fff; 25 | padding-right: 10px; 26 | text-decoration: none; 27 | margin: 0; 28 | } -------------------------------------------------------------------------------- /src/components/elements/Navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import "./Navigation.css"; 4 | 5 | const Navigation = props => { 6 | return ( 7 |
8 |
9 | 10 |

Home

11 | 12 |

/

13 |

{props.movie}

14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Navigation; 20 | -------------------------------------------------------------------------------- /src/components/elements/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotFound = () => { 4 | return ( 5 |
6 |

Whooops. This page doesn't exist

7 |
8 | ); 9 | }; 10 | 11 | export default NotFound; 12 | -------------------------------------------------------------------------------- /src/components/elements/SearchBar/SearchBar.css: -------------------------------------------------------------------------------- 1 | .rmdb-searchbar { 2 | width: 100%; 3 | height: 105px; 4 | background: #1c1c1c; 5 | position: relative; 6 | padding: 25px 20px 0px 20px; 7 | box-sizing: border-box; 8 | color: #fff; 9 | } 10 | 11 | .rmdb-searchbar-content { 12 | max-width: 1280px; 13 | width: 100%; 14 | height: 55px; 15 | background:#353535; 16 | margin: 0 auto; 17 | border-radius: 40px; 18 | position: relative; 19 | color: #fff; 20 | } 21 | 22 | .rmdb-fa-search { 23 | position: absolute; 24 | left: 20px; 25 | top: 12px; 26 | color: #fff; 27 | } 28 | 29 | .rmdb-searchbar-input { 30 | font-family: 'Abel', sans-serif; 31 | font-size: 38px; 32 | position: absolute; 33 | left: 70px; 34 | top: 7px; 35 | border: 0; 36 | background: transparent; 37 | height: 40px; 38 | color: #fff; 39 | } 40 | 41 | .rmdb-searchbar-input:focus { 42 | outline: none; 43 | } 44 | 45 | @media screen and (max-width: 720px) { 46 | .rmdb-searchbar-input { 47 | font-size: 28px; 48 | color: #fff; 49 | } 50 | } -------------------------------------------------------------------------------- /src/components/elements/SearchBar/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import FontAwesome from "react-fontawesome"; 3 | import "./SearchBar.css"; 4 | 5 | class SearchBar extends Component { 6 | state = { 7 | value: "" 8 | }; 9 | 10 | timeout = null; 11 | 12 | doSearch = event => { 13 | this.setState({ value: event.target.value }); 14 | clearTimeout(this.timeout); 15 | 16 | this.timeout = setTimeout(() => { 17 | this.props.callback(this.state.value); 18 | }, 500); 19 | }; 20 | 21 | render() { 22 | return ( 23 |
24 |
25 | 26 | 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | export default SearchBar; 40 | -------------------------------------------------------------------------------- /src/components/elements/Spinner/Spinner.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | border: 5px solid #f3f3f3; /* Light grey */ 3 | border-top: 5px solid #16d47b; /* Blue */ 4 | border-radius: 50%; 5 | width: 50px; 6 | height: 50px; 7 | animation: spin 0.8s linear infinite; 8 | margin: 20px auto; 9 | } 10 | 11 | @keyframes spin { 12 | 0% { transform: rotate(0deg); } 13 | 100% { transform: rotate(360deg); } 14 | } -------------------------------------------------------------------------------- /src/components/elements/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Spinner.css"; 3 | 4 | const Spinner = () => { 5 | return
; 6 | }; 7 | 8 | export default Spinner; 9 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // Configuration for TMDB 2 | // To se the latest configuration fetch it from https://api.themoviedb.org/3/configuration?api_key=019e8f375549e0bbd4a4191862ebc88f 3 | 4 | const API_URL = 'https://api.themoviedb.org/3/'; 5 | const API_KEY = '844dba0bfd8f3a4f3799f6130ef9e335'; 6 | 7 | // Images 8 | // An image URL looks like this example: 9 | // http://image.tmdb.org/t/p/w780/bOGkgRGdhrBYJSLpXaxhXVstddV.jpg 10 | 11 | const IMAGE_BASE_URL ='http://image.tmdb.org/t/p/'; 12 | 13 | //Sizes: w300, w780, w1280, original 14 | const BACKDROP_SIZE = 'w1280'; 15 | 16 | // w92, w154, w185, w342, w500, w780, original 17 | const POSTER_SIZE = 'w500'; 18 | 19 | export { 20 | API_URL, 21 | API_KEY, 22 | IMAGE_BASE_URL, 23 | BACKDROP_SIZE, 24 | POSTER_SIZE 25 | } -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | // Convert time to hours and minutes 2 | export const calcTime = (time) => { 3 | const hours = Math.floor(time / 60); 4 | const mins = time % 60; 5 | return `${hours}h ${mins}m`; 6 | } 7 | 8 | // Convert a number to $ format 9 | export const convertMoney = (money) => { 10 | var formatter = new Intl.NumberFormat('en-US', { 11 | style: 'currency', 12 | currency: 'USD', 13 | minimumFractionDigits: 0, 14 | }); 15 | return formatter.format(money); 16 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./components/App/App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------