├── src ├── style.css ├── images │ ├── generic.jpg │ ├── popcorn.png │ ├── screenshot.png │ └── popcorn.svg ├── reducers │ ├── index.js │ ├── score.js │ └── popular.js ├── actions │ └── index.js ├── components │ ├── Search.module.css │ ├── App.js │ ├── Search.js │ ├── Navbar.module.css │ ├── Movie.js │ ├── Movie.module.css │ └── Navbar.js ├── __tests__ │ ├── snapshots │ │ ├── Movie-test.js │ │ └── __snapshots__ │ │ │ └── Movie-test.js.snap │ ├── reducers │ │ ├── score_reducer.test.js │ │ └── popular_reducer.test.js │ └── api-test.js ├── store │ └── store.js ├── index.js ├── containers │ ├── MovieList.module.css │ ├── Details.module.css │ ├── MovieList.js │ └── Detail.js └── API │ └── API.js ├── public ├── favicon.ico └── index.html ├── .babelrc ├── .env ├── .stylelintrc.json ├── .gitignore ├── .eslintrc.json ├── LICENSE ├── .github └── workflows │ └── linters.yml ├── package.json └── README.md /src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanislavkononiuk/popcorn-and-chill/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/generic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanislavkononiuk/popcorn-and-chill/HEAD/src/images/generic.jpg -------------------------------------------------------------------------------- /src/images/popcorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanislavkononiuk/popcorn-and-chill/HEAD/src/images/popcorn.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "plugins": ["@babel/plugin-syntax-jsx"] 6 | } -------------------------------------------------------------------------------- /src/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanislavkononiuk/popcorn-and-chill/HEAD/src/images/screenshot.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_TMDB_API=b8c4eb942e4658a86af0aa43e934c1d3 2 | REACT_APP_MY_NAME=helcio 3 | REACT_APP_PROJECT=popcorn and chill 4 | SKIP_PREFLIGHT_CHECK=true 5 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import popularReducer from './popular'; 3 | import scoreReducer from './score'; 4 | 5 | export default combineReducers({ 6 | popularReducer, scoreReducer, 7 | }); 8 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export const POPULAR = 'POPULAR'; 2 | export const SCORE = 'SCORE'; 3 | 4 | export const popular = (popular) => ({ 5 | type: POPULAR, 6 | popular, 7 | }); 8 | 9 | export const valueScore = (valueScore) => ( 10 | { 11 | type: SCORE, 12 | valueScore, 13 | } 14 | ); 15 | -------------------------------------------------------------------------------- /src/reducers/score.js: -------------------------------------------------------------------------------- 1 | import { SCORE } from '../actions/index'; 2 | 3 | const initialState = { 4 | score: '', 5 | }; 6 | 7 | export default function scoreReducer(state = initialState, action) { 8 | if (action.type === SCORE) { 9 | return { 10 | score: action.valueScore, 11 | }; 12 | } 13 | return state; 14 | } 15 | -------------------------------------------------------------------------------- /src/reducers/popular.js: -------------------------------------------------------------------------------- 1 | import { POPULAR } from '../actions/index'; 2 | 3 | const initialState = { 4 | movies: [], 5 | }; 6 | 7 | export default function popularReducer(state = initialState, action) { 8 | if (action.type === POPULAR) { 9 | return { 10 | movies: [...action.popular], 11 | }; 12 | } 13 | 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": null, 6 | "scss/at-rule-no-unknown": true, 7 | "csstree/validator": true 8 | }, 9 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"] 10 | } -------------------------------------------------------------------------------- /src/components/Search.module.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | background-color: #4a5b8c; 3 | color: white; 4 | padding: 7px 5px; 5 | border: none; 6 | } 7 | 8 | .btn:hover { 9 | background-color: #3a4872; 10 | cursor: pointer; 11 | } 12 | 13 | .search { 14 | padding: 5px 3px; 15 | border: none; 16 | outline: none; 17 | } 18 | 19 | .search:focus { 20 | border: 1px solid #8697a6; 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/snapshots/Movie-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Movie from '../../components/Movie'; 4 | 5 | it('renders correctly the Details component', () => { 6 | const tree = renderer 7 | .create() 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import thunk from 'redux-thunk'; 4 | import reducer from '../reducers/index'; 5 | 6 | const initialState = {}; 7 | 8 | const middleware = [thunk]; 9 | 10 | export default createStore(reducer, initialState, 11 | composeWithDevTools(applyMiddleware(...middleware))); 12 | -------------------------------------------------------------------------------- /.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 | .env -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import Detail from '../containers/Detail'; 4 | 5 | import Navbar from './Navbar'; 6 | import MoviesList from '../containers/MovieList'; 7 | 8 | const App = () => ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import App from './components/App'; 6 | 7 | import store from './store/store'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root'), 18 | ); 19 | -------------------------------------------------------------------------------- /src/__tests__/snapshots/__snapshots__/Movie-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly the Details component 1`] = ` 4 |
7 | popcorn-n-chill 12 |
15 |

18 | popcorn-n-chill 19 |

20 |

23 | score: 24 | 25 | 3 26 |

27 |
28 |
29 | `; 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": ["airbnb", "plugin:react/recommended"], 16 | "plugins": ["react"], 17 | "rules": { 18 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 19 | "react/react-in-jsx-scope": "off", 20 | "import/no-unresolved": "off", 21 | "no-shadow": "off" 22 | }, 23 | "ignorePatterns": [ 24 | "dist/", 25 | "build/" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/components/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SearchCSS from './Search.module.css'; 4 | 5 | const Search = ({ handleSubmit, searchValue, handleSearch }) => ( 6 | 7 |
8 | 15 | 16 |
17 | ); 18 | 19 | Search.propTypes = { 20 | handleSubmit: PropTypes.func.isRequired, 21 | searchValue: PropTypes.string.isRequired, 22 | handleSearch: PropTypes.func.isRequired, 23 | }; 24 | 25 | export default Search; 26 | -------------------------------------------------------------------------------- /src/components/Navbar.module.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: flex; 3 | justify-content: space-around; 4 | align-items: center; 5 | background-color: #dfe7f2; 6 | padding: 10px 5px; 7 | margin-bottom: 23px; 8 | margin-top: 0; 9 | } 10 | 11 | .logo { 12 | height: 60px; 13 | } 14 | 15 | .select { 16 | padding: 5px 3px; 17 | border: 1px solid #8697a6; 18 | outline: none; 19 | } 20 | 21 | @media screen and (min-width: 320px) { 22 | .nav { 23 | flex-direction: column; 24 | } 25 | 26 | .select { 27 | margin-top: 1rem; 28 | width: 80%; 29 | } 30 | } 31 | 32 | @media screen and (min-width: 768px) { 33 | .nav { 34 | flex-direction: row; 35 | align-items: baseline; 36 | } 37 | 38 | .select { 39 | width: 30%; 40 | } 41 | } 42 | 43 | @media screen and (min-width: 1024px) { 44 | .select { 45 | width: auto; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Movie.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import MovieCSS from './Movie.module.css'; 4 | import { movieExtent } from '../API/API'; 5 | import generic from '../images/generic.jpg'; 6 | 7 | const Movie = ({ imageScr, score, title }) => ( 8 |
9 | {title} 14 |
15 |

{title}

16 |

17 | score: 18 | {' '} 19 | {score} 20 |

21 |
22 |
23 | ); 24 | 25 | Movie.propTypes = { 26 | imageScr: PropTypes.string.isRequired, 27 | score: PropTypes.number.isRequired, 28 | title: PropTypes.string.isRequired, 29 | }; 30 | 31 | export default Movie; 32 | -------------------------------------------------------------------------------- /src/__tests__/reducers/score_reducer.test.js: -------------------------------------------------------------------------------- 1 | import scoreReducer from '../../reducers/score'; 2 | import { SCORE } from '../../actions'; 3 | 4 | describe('InitialState', () => { 5 | test('is correct', () => { 6 | const action = { type: 'dummy_action' }; 7 | const initialState = { 8 | score: '', 9 | }; 10 | expect(scoreReducer(undefined, action)).toEqual(initialState); 11 | }); 12 | }); 13 | 14 | describe('popularReducer', () => { 15 | test('return the correct state', () => { 16 | const action = { type: SCORE, valueScore: 6 }; 17 | const expectedState = { 18 | score: action.valueScore, 19 | }; 20 | expect(scoreReducer(undefined, action)).toEqual(expectedState); 21 | }); 22 | }); 23 | 24 | describe('popularReducer', () => { 25 | test('expect returned state to not equal expected state', () => { 26 | const action = { type: SCORE, valueScore: 5 }; 27 | const expectedState = 4; 28 | expect(scoreReducer(undefined, action)).not.toEqual(expectedState); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/__tests__/reducers/popular_reducer.test.js: -------------------------------------------------------------------------------- 1 | import popularReducer from '../../reducers/popular'; 2 | import { POPULAR } from '../../actions'; 3 | 4 | describe('InitialState', () => { 5 | test('is correct', () => { 6 | const action = { type: 'dummy_action' }; 7 | const initialState = { 8 | movies: [], 9 | }; 10 | expect(popularReducer(undefined, action)).toEqual(initialState); 11 | }); 12 | }); 13 | 14 | describe('popularReducer', () => { 15 | test('return the correct state', () => { 16 | const action = { type: POPULAR, popular: [1, 2, 4] }; 17 | const expectedState = { 18 | movies: [...action.popular], 19 | }; 20 | expect(popularReducer(undefined, action)).toEqual(expectedState); 21 | }); 22 | }); 23 | 24 | describe('popularReducer', () => { 25 | test('expect returned state to not equal expected state', () => { 26 | const action = { type: POPULAR, popular: [1, 2, 4] }; 27 | const expectedState = [...action.popular]; 28 | expect(popularReducer(undefined, action)).not.toEqual(expectedState); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 helcio andre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/containers/MovieList.module.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Anton&display=swap"); 2 | 3 | .wrapper { 4 | display: flex; 5 | justify-content: space-between; 6 | flex-wrap: wrap; 7 | } 8 | 9 | .link { 10 | text-decoration: none; 11 | } 12 | 13 | .btnWraper { 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | button { 20 | background-color: #4a5b8c; 21 | padding: 7px 7px; 22 | color: white; 23 | outline: none; 24 | border: none; 25 | cursor: pointer; 26 | } 27 | 28 | .currentBtn { 29 | font-family: "Anton", sans-serif; 30 | } 31 | 32 | .btnLeft { 33 | margin-right: 10px; 34 | } 35 | 36 | .btnRight { 37 | margin-left: 10px; 38 | } 39 | 40 | /* @media screen and (min-width: 375px) { 41 | .wrapper { 42 | flex-direction: column; 43 | align-items: center; 44 | } 45 | } 46 | 47 | @media screen and (min-width: 320px) { 48 | .wrapper { 49 | flex-direction: column; 50 | align-items: center; 51 | } 52 | } */ 53 | 54 | @media screen and (min-width: 768px) { 55 | .wrapper { 56 | flex-direction: row; 57 | align-items: center; 58 | justify-content: space-evenly; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/API/API.js: -------------------------------------------------------------------------------- 1 | const key = process.env.REACT_APP_TMDB_API; 2 | export const movieExtent = 'https://image.tmdb.org/t/p/w342/'; 3 | 4 | export async function popularMovies(page, scoreValue) { 5 | let apiUrl = ''; 6 | if (!scoreValue) { 7 | apiUrl = `https://api.themoviedb.org/3/movie/popular?api_key=${key}&language=en-US&page=${page || 1}`; 8 | } else { 9 | apiUrl = `https://api.themoviedb.org/3/discover/movie?api_key=${key}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page || 1}&vote_average.gte=${scoreValue}&vote_average.lte=${parseInt(scoreValue, 10) + 0.9}&with_watch_monetization_types=free`; 10 | } 11 | 12 | const response = await fetch(apiUrl); 13 | const data = await response.json(); 14 | 15 | return data.results; 16 | } 17 | 18 | export async function movieDetails(id) { 19 | const apiUrl = `https://api.themoviedb.org/3/movie/${id}?api_key=${key}&language=en-US`; 20 | const response = await fetch(apiUrl); 21 | const data = await response.json(); 22 | return data; 23 | } 24 | 25 | export async function searchMovie(movie) { 26 | const api = `https://api.themoviedb.org/3/search/movie?api_key=${key}&language=en-US&query=${movie}&page=&include_adult=false`; 27 | const response = await fetch(api); 28 | const data = await response.json(); 29 | return data.results; 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "12.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@7.x eslint-config-airbnb@18.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@4.x @babel/eslint-parser@7.x @babel/core@7.x @babel/plugin-syntax-jsx@7.x @babel/preset-react@7.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | [ -f .babelrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.babelrc 22 | - name: ESLint Report 23 | run: npx eslint . 24 | stylelint: 25 | name: Stylelint 26 | runs-on: ubuntu-18.04 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: "12.x" 32 | - name: Setup Stylelint 33 | run: | 34 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 35 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 36 | - name: Stylelint Report 37 | run: npx stylelint "**/*.{css,scss}" -------------------------------------------------------------------------------- /src/containers/Details.module.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap'); 2 | 3 | body { 4 | background-color: #8697a6; 5 | color: rgb(233, 232, 232); 6 | font-family: 'Roboto', sans-serif; 7 | } 8 | 9 | .wraper { 10 | width: 90%; 11 | margin: 0 auto; 12 | background-color: #000; 13 | } 14 | 15 | /* @media screen and (min-width: 320px) { 16 | .wraper { 17 | width: 470px; 18 | } 19 | } */ 20 | 21 | ul { 22 | list-style: none; 23 | text-align: left; 24 | } 25 | 26 | h2 { 27 | color: rgb(233, 230, 230); 28 | font-family: 'Anton', sans-serif; 29 | } 30 | 31 | .detailsWrapper { 32 | display: flex; 33 | } 34 | 35 | @media screen and (min-width: 320px) { 36 | .detailsWrapper { 37 | flex-wrap: wrap; 38 | flex-direction: column; 39 | } 40 | 41 | .infoWraper { 42 | border-bottom: 1px solid #bf8756; 43 | width: 90%; 44 | margin: 0 auto; 45 | font-weight: 600; 46 | text-align: center; 47 | } 48 | .infoWraper p { 49 | text-align: center; 50 | } 51 | .poster { 52 | width: 100%; 53 | } 54 | } 55 | 56 | @media screen and (min-width: 768px) { 57 | .detailsWrapper { 58 | flex-wrap: nowrap; 59 | flex-direction: row; 60 | } 61 | .poster { 62 | width: auto; 63 | } 64 | 65 | .infoWraper { 66 | padding: 5px 5px; 67 | text-align: center; 68 | font-weight: 600; 69 | width: 43%; 70 | } 71 | 72 | .infoWraper:not(:last-of-type) { 73 | border-right: 1px solid #bf8756; 74 | } 75 | } 76 | 77 | .overview { 78 | color: rgb(245, 245, 245); 79 | padding: 3px 8px; 80 | font-weight: 800; 81 | } 82 | 83 | a { 84 | text-decoration: none; 85 | color: whitesmoke; 86 | font-weight: bolder; 87 | } 88 | 89 | a:hover { 90 | color: rgb(173, 171, 171); 91 | } 92 | -------------------------------------------------------------------------------- /src/components/Movie.module.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Anton&display=swap"); 2 | 3 | .header { 4 | width: fit-content; 5 | width: -moz-fit-content; 6 | font-family: "Anton", sans-serif; 7 | text-transform: capitalize; 8 | color: #8c4c27; 9 | font-size: 28px; 10 | } 11 | 12 | .movieWraper { 13 | width: 305px; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | margin-bottom: 10px; 18 | transition: all 0.4s; 19 | } 20 | 21 | @media screen and (min-width: 320px) { 22 | .movieWraper { 23 | width: 160px; 24 | } 25 | .image { 26 | width: 100%; 27 | } 28 | } 29 | @media screen and (min-width: 375px) { 30 | .movieWraper { 31 | width: 175px; 32 | } 33 | .image { 34 | object-fit: cover; 35 | } 36 | } 37 | @media screen and (min-width: 425px) { 38 | .movieWraper { 39 | width: 200px; 40 | } 41 | 42 | .image { 43 | object-fit: cover; 44 | } 45 | } 46 | 47 | @media screen and (min-width: 780px) { 48 | .movieWraper { 49 | width: 300px; 50 | } 51 | .image { 52 | object-fit: cover; 53 | } 54 | } 55 | @media screen and (min-width: 1024px) { 56 | .movieWraper { 57 | width: 275px; 58 | } 59 | .image { 60 | object-fit: cover; 61 | } 62 | } 63 | 64 | .image { 65 | object-fit: cover; 66 | } 67 | 68 | .infoWraper { 69 | background-color: #b7bfc7; 70 | padding-left: 10px; 71 | height: 250px; 72 | } 73 | @media screen and (min-width: 320px) { 74 | .infoWraper { 75 | display: none; 76 | } 77 | } 78 | @media screen and (min-width: 780px) { 79 | .infoWraper { 80 | display: block; 81 | } 82 | } 83 | 84 | .movieWraper:hover > .infoWraper { 85 | background-color: black; 86 | transition: all 0.2s; 87 | } 88 | 89 | .score { 90 | font-family: "Anton", sans-serif; 91 | font-weight: bolder; 92 | color: #bf8756; 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "popcorn-and-chill", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "millify": "^4.0.0", 10 | "prop-types": "^15.7.2", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-redux": "^7.2.5", 14 | "react-router-dom": "^5.3.0", 15 | "react-scripts": "4.0.3", 16 | "react-test-render": "^1.1.2", 17 | "redux": "^4.1.1", 18 | "redux-devtools-extension": "^2.13.9", 19 | "redux-thunk": "^2.3.0", 20 | "web-vitals": "^1.1.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.15.5", 48 | "@babel/eslint-parser": "^7.15.4", 49 | "@babel/plugin-syntax-jsx": "^7.14.5", 50 | "@babel/preset-react": "^7.14.5", 51 | "enzyme": "^3.11.0", 52 | "enzyme-adapter-react-17-updated": "^1.0.2", 53 | "eslint": "^7.32.0", 54 | "eslint-config-airbnb": "^18.2.1", 55 | "eslint-plugin-import": "^2.24.2", 56 | "eslint-plugin-jsx-a11y": "^6.4.1", 57 | "eslint-plugin-react": "^7.25.1", 58 | "eslint-plugin-react-hooks": "^4.2.0", 59 | "react-test-renderer": "^17.0.2", 60 | "regenerator-runtime": "^0.13.9", 61 | "stylelint": "^13.13.1", 62 | "stylelint-config-standard": "^21.0.0", 63 | "stylelint-csstree-validator": "^1.9.0", 64 | "stylelint-scss": "^3.20.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 30 | Popcorn and chill 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { popular, valueScore } from '../actions/index'; 5 | import { popularMovies, searchMovie } from '../API/API'; 6 | import Search from './Search'; 7 | import popcorn from '../images/popcorn.png'; 8 | import NavbarCSS from './Navbar.module.css'; 9 | 10 | const Navbar = () => { 11 | const [searchValue, setSearchValue] = useState(''); 12 | const dispatch = useDispatch(); 13 | const rating = useSelector((state) => state.scoreReducer.score); 14 | 15 | const handleRating = (event) => { 16 | const { value } = event.target; 17 | if (value === 'choose a rating') return; 18 | dispatch(valueScore(value)); 19 | popularMovies(1, value).then((movies) => { 20 | dispatch(popular(movies)); 21 | }); 22 | }; 23 | 24 | const handleSubmit = (event) => { 25 | event.preventDefault(); 26 | if (!searchValue) return; 27 | searchMovie(searchValue).then((movies) => { 28 | dispatch(popular(movies)); 29 | }); 30 | setSearchValue(''); 31 | }; 32 | 33 | const handleSearch = (event) => { 34 | const { value } = event.target; 35 | setSearchValue(value); 36 | }; 37 | 38 | return ( 39 | 61 | ); 62 | }; 63 | 64 | export default Navbar; 65 | -------------------------------------------------------------------------------- /src/containers/MovieList.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { popular } from '../actions'; 5 | import Movie from '../components/Movie'; 6 | import { popularMovies } from '../API/API'; 7 | import MovieListCSS from './MovieList.module.css'; 8 | 9 | const MoviesList = () => { 10 | const [nextPage, setNextPage] = useState(2); 11 | const [previousPage, setPreviousPage] = useState(0); 12 | const rating = useSelector((state) => state.scoreReducer.score); 13 | const dispatch = useDispatch(); 14 | const popMovies = useSelector((state) => state.popularReducer.movies); 15 | 16 | useEffect(() => { 17 | popularMovies().then((movies) => { 18 | dispatch(popular(movies)); 19 | }); 20 | }, [dispatch]); 21 | 22 | function moviesNextPage() { 23 | popularMovies(nextPage, rating).then((movies) => { 24 | dispatch(popular(movies)); 25 | }); 26 | setNextPage(nextPage + 1); 27 | setPreviousPage(previousPage + 1); 28 | } 29 | 30 | function moviesPreviousPage() { 31 | popularMovies(previousPage, rating).then((movies) => { 32 | dispatch(popular(movies)); 33 | }); 34 | setNextPage(nextPage - 1); 35 | setPreviousPage(previousPage - 1); 36 | } 37 | return ( 38 | <> 39 |
40 | {popMovies.map((movie) => ( 41 | 46 | 51 | 52 | ))} 53 |
54 |
55 | {previousPage >= 1 && ( 56 | 65 | )} 66 |

67 | current page: 68 | {nextPage - 1} 69 |

70 | 79 |
80 | 81 | ); 82 | }; 83 | 84 | export default MoviesList; 85 | -------------------------------------------------------------------------------- /src/__tests__/api-test.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | import { configure } from 'enzyme'; 3 | import EnzymeAdapter from 'enzyme-adapter-react-17-updated'; 4 | 5 | import { popularMovies, movieDetails } from '../API/API'; 6 | 7 | configure({ 8 | adapter: new EnzymeAdapter(), 9 | }); 10 | 11 | describe('popularMovies()', () => { 12 | it('Returns an array wich first element is an object with popularity propery', () => { 13 | popularMovies().then((movies) => { 14 | expect(movies[0]).toHaveProperty('popularity'); 15 | }); 16 | }); 17 | 18 | it('Returns an array wich first element is an object with title propery', () => { 19 | popularMovies().then((movies) => { 20 | expect(movies[0]).toHaveProperty('title'); 21 | }); 22 | }); 23 | 24 | it('Returns an array wich first element is an object with vote_average propery', () => { 25 | popularMovies().then((movies) => { 26 | expect(movies[0]).toHaveProperty('vote_average'); 27 | }); 28 | }); 29 | 30 | it('Returns an array wich first element is an object that doe not have a corresponding_cast propery', () => { 31 | popularMovies().then((movies) => { 32 | expect(movies[0]).not.toHaveProperty('corresponding_cast'); 33 | }); 34 | }); 35 | 36 | it('Returns an array wich first element is an object that doe not have a leading_actor propery', () => { 37 | popularMovies().then((movies) => { 38 | expect(movies[0]).not.toHaveProperty('leading_actor'); 39 | }); 40 | }); 41 | }); 42 | describe('movieDetails()', () => { 43 | it('Return an object with a property budget', () => { 44 | movieDetails(568620).then((movie) => { 45 | expect(movie).toHaveProperty('budget'); 46 | }); 47 | }); 48 | 49 | it('Return an object with a property genres', () => { 50 | movieDetails(568620).then((movie) => { 51 | expect(movie).toHaveProperty('genres'); 52 | }); 53 | }); 54 | 55 | it('Return an object with a property homepage', () => { 56 | movieDetails(568620).then((movie) => { 57 | expect(movie).toHaveProperty('homepage'); 58 | }); 59 | }); 60 | 61 | it('Return an object with a property original_language', () => { 62 | movieDetails(568620).then((movie) => { 63 | expect(movie).toHaveProperty('original_language'); 64 | }); 65 | }); 66 | 67 | it('Return an object with no property casting_overseas', () => { 68 | movieDetails(568620).then((movie) => { 69 | expect(movie).not.toHaveProperty('casting_overseas'); 70 | }); 71 | }); 72 | 73 | it('Return an object with no property optional_languages', () => { 74 | movieDetails(568620).then((movie) => { 75 | expect(movie).not.toHaveProperty('optional_languages'); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Popcorn-and-chill 2 | ## Table of contents 3 | 4 | - [About](#about) 5 | - [Features](#features) 6 | - [Live Demo](#live-demo) 7 | - [Built with](#built-with) 8 | - [Getting started](#getting-started) 9 | - [Installation instructions](#installation-instructions) 10 | - [Testing](#testing) 11 | - [Author](#author) 12 | - [Show your support](#show-your-support) 13 | - [License](#-license) 14 | 15 | --- 16 | 17 | ## About 18 | 19 | This project consumes data from the TMDB (The Movie Data Base) api, it is a react project built using redux and react hooks 20 | 21 | ![Screenshot-main-page](src/images/screenshot.png) 22 | ## features 23 | Users can see which movies are popular currently, users can also search movies based on their ratings or search for movies based on title, actor, or related words and know details about any particular movie by clicking on it. 24 | ## Live Demo 25 | 26 | This project is hosted on Netlify. 27 | 28 | [Live demo link](https://popcorn-and-chill.netlify.app/) 29 | 30 | ## Built With 31 | 32 | - [![forthebadge](https://forthebadge.com/images/badges/made-with-javascript.svg)](https://forthebadge.com) 33 | - [![forthebadge](https://forthebadge.com/images/badges/uses-html.svg)](https://forthebadge.com) 34 | - React 35 | 36 | ## Getting Started 37 | 38 | ### Installation instructions 39 | In order to try the aplication in your own machine you will have to create an account at [tmdb](https://www.themoviedb.org/) and request an API key. 40 | You can read more about their API here [TMDB API OVERVIEW](https://www.themoviedb.org/documentation/api) 41 | Follow along the steps below to get a copy at your local machine. 42 | 43 | - Navigate to the directory where you want this project to clone and then clone it 44 | 45 | ``` 46 | git clone git@github.com:helciodev/popcorn-and-chill.git 47 | ``` 48 | 49 | - Navigate to the `popcorn-and-chill` directory 50 | 51 | ``` 52 | cd popcorn-and-chill 53 | ``` 54 | 55 | - Install the npm package with `npm install` 56 | - Start the server with `npm start` 57 | - The site should automatically open in your browser at http://localhost:3000/ if it didn't then open your browser with that link. 58 | 59 | ### testing 60 | 61 | - Run npm test to run tests 62 | 63 | ## Author 64 | 65 | 😎 **Helcio André** 66 | 67 | - GitHub: [@helciodev](https://github.com/helciodev) 68 | - Twitter: [@helcio_bruno](https://twitter.com/helcio_bruno) 69 | - Linkedin: [Helcio Andre](https://www.linkedin.com/in/helcio-andre/) 70 | 71 | ## 🤝 Contributing 72 | 73 | Contributions, issues, and feature requests are welcome! 74 | 75 | ## Show your support 76 | 77 | Give a ⭐️ if you like this project, thanks for reading! 78 | 79 | ## 📝 License 80 | 81 | This project is [MIT](./LICENSE) licensed.## Available Scripts 82 | -------------------------------------------------------------------------------- /src/containers/Detail.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import millify from 'millify'; 3 | import { useParams } from 'react-router-dom'; 4 | import { movieDetails, movieExtent } from '../API/API'; 5 | import generic from '../images/generic.jpg'; 6 | import DetailsCSS from './Details.module.css'; 7 | 8 | const Detail = () => { 9 | const [detail, setDetail] = useState({}); 10 | const { id: movieId } = useParams(); 11 | useEffect(() => { 12 | movieDetails(movieId).then((movie) => setDetail(movie)); 13 | }, []); 14 | 15 | const { 16 | poster_path: movieImg, 17 | budget, 18 | homepage: homePage, 19 | overview, 20 | production_companies: productionCompanies, 21 | production_countries: productionCoutries, 22 | release_date: releaseDate, 23 | revenue, 24 | runtime, 25 | spoken_languages: spokenLanguages, 26 | title, 27 | vote_average: score, 28 | genres, 29 | } = detail; 30 | 31 | if (!genres) return 'loading info...'; 32 | return ( 33 |
34 |
35 |
36 | {title} 41 |
42 |
43 |

{title}

44 |

{overview}

45 |
46 |
47 |
48 |

49 | duration: 50 | {' '} 51 | {} 52 | {runtime} 53 | {' '} 54 | minutes 55 |

56 |

57 | average score: 58 | {score} 59 |

60 |

61 | Revenue: 62 | {revenue > 0 ? millify(revenue) : 'not available'} 63 |

64 |
65 |
66 |

Genres:

67 |
    68 | {genres.map((genre) => ( 69 |
  • {genre.name}
  • 70 | ))} 71 |
72 |
73 |
74 |

spoken languages:

75 |
    76 | {spokenLanguages.map((lang) => ( 77 |
  • {lang.english_name}
  • 78 | ))} 79 |
80 |
81 |
82 |

production companies:

83 |
    84 | {productionCompanies.map((company) => ( 85 |
  • {company.name}
  • 86 | ))} 87 |
88 |
89 |
90 |

production countries:

91 |
    92 | {productionCoutries.map((country) => ( 93 |
  • {country.name}
  • 94 | ))} 95 |
96 |
97 |
98 |

Release Date:

99 |

{releaseDate}

100 |

101 | {' '} 102 | Budget: 103 | {millify(budget)} 104 |

105 |

homepage:

106 |

107 | 108 | {title} 109 | 110 |

111 |
112 |
113 |
114 |
115 | ); 116 | }; 117 | 118 | export default Detail; 119 | -------------------------------------------------------------------------------- /src/images/popcorn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 22 | 29 | 33 | 36 | 37 | 39 | 41 | 42 | 44 | 46 | 51 | 54 | 56 | 58 | 61 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 97 | 103 | 109 | 117 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | --------------------------------------------------------------------------------