moveSlideWithArrows('prev')} />
78 |
moveSlideWithArrows('next')} />
79 |
80 | );
81 | };
82 |
83 | const Indicators = (props) => {
84 | const { currentSlide } = props;
85 | const listIndicators = images.map((slide, i) => {
86 | const btnClasses = i === currentSlide ? 'slider-navButton slider-navButton--active' : 'slider-navButton';
87 | return
;
88 | });
89 | return
{listIndicators}
;
90 | };
91 |
92 | return (
93 | <>
94 |
95 |
{images && images.length && slideShow &&
}
96 |
97 | {showArrows ?
: null}
98 |
99 | >
100 | );
101 | };
102 |
103 | Slideshow.propTypes = {
104 | images: PropTypes.array.isRequired,
105 | auto: PropTypes.bool.isRequired,
106 | showArrows: PropTypes.bool.isRequired,
107 | currentSlide: PropTypes.number
108 | };
109 |
110 | export default Slideshow;
111 |
--------------------------------------------------------------------------------
/src/components/content/details/Details.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { connect } from 'react-redux';
3 | import { useParams, useLocation } from 'react-router-dom';
4 | import PropTypes from 'prop-types';
5 |
6 | import './Details.scss';
7 | import Rating from '../rating/Rating';
8 | import Tabs from './tabs/Tabs';
9 | import Overview from './overview/Overview';
10 | import Crew from './crew/Crew';
11 | import Media from './media/Media';
12 | import Reviews from './reviews/Reviews';
13 | import { movieDetails } from '../../../redux/actions/movies';
14 | import { pathURL } from '../../../redux/actions/routes';
15 | import { IMAGE_URL } from '../../../services/movies.service';
16 | import Spinner from '../../spinner/Spinner';
17 |
18 | const Details = (props) => {
19 | const { movieDetails, movie, pathURL } = props;
20 | const [details, setDetails] = useState();
21 | const [loading, setLoading] = useState(false);
22 | const { id } = useParams();
23 | const location = useLocation();
24 |
25 | useEffect(() => {
26 | setLoading(true);
27 | setTimeout(() => {
28 | setLoading(false);
29 | }, 2000);
30 | }, []);
31 |
32 | useEffect(() => {
33 | pathURL(location.pathname, location.pathname);
34 | if (movie.length === 0) {
35 | movieDetails(id);
36 | }
37 | setDetails(movie[0]);
38 | // eslint-disable-next-line
39 | }, [id, movie]);
40 |
41 | return (
42 | <>
43 | {loading ? (
44 |
45 | ) : (
46 | details && (
47 |
48 |
49 |
50 |
51 |
52 |

53 |
54 |
55 |
56 |
57 | {details.title} {details.release_date}
58 |
59 |
60 |
61 | {details.genres.map((genre) => (
62 | - {genre.name}
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 |
{details.vote_average} ({details.vote_count}) reviews
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | )
90 | )}
91 | >
92 | );
93 | };
94 |
95 | Details.propTypes = {
96 | movie: PropTypes.array,
97 | movieDetails: PropTypes.func,
98 | pathURL: PropTypes.func
99 | };
100 |
101 | const mapStateToProps = (state) => ({
102 | movie: state.movies.movie
103 | });
104 |
105 | export default connect(mapStateToProps, { movieDetails, pathURL })(Details);
106 |
--------------------------------------------------------------------------------
/src/redux/actions/movies.js:
--------------------------------------------------------------------------------
1 | import { MOVIE_LIST, SET_ERROR, RESPONSE_PAGE, LOAD_MORE_RESULTS, MOVIE_TYPE, SEARCH_QUERY, SEARCH_RESULT, MOVIE_DETAILS, CLEAR_MOVIE_DETAILS } from '../types';
2 | import { MOVIE_API_URL, SEARCH_API_URL, MOVIE_DETAILS_URL, MOVIE_CREDITS_URL, MOVIE_IMAGES_URL, MOVIE_VIDEOS_URL, MOVIE_REVIEWS_URL } from '../../services/movies.service';
3 |
4 | export const getMovies = (type, pageNumber) => async (dispatch) => {
5 | try {
6 | const response = await getMoviesRequest(type, pageNumber);
7 | const { results, payload } = response;
8 | dispatchMethod(MOVIE_LIST, results, dispatch);
9 | dispatchMethod(RESPONSE_PAGE, payload, dispatch);
10 | } catch (error) {
11 | if (error.response) {
12 | const payload = {
13 | message: error.response.data.message || error.response.data.status_message,
14 | statusCode: error.response.status
15 | };
16 | dispatchMethod(SET_ERROR, payload, dispatch);
17 | }
18 | }
19 | };
20 |
21 | export const loadMoreMovies = (type, pageNumber) => async (dispatch) => {
22 | try {
23 | const response = await getMoviesRequest(type, pageNumber);
24 | const { results, payload } = response;
25 | dispatchMethod(LOAD_MORE_RESULTS, { list: results, page: payload.page, totalPages: payload.totalPages }, dispatch);
26 | } catch (error) {
27 | if (error.response) {
28 | const payload = {
29 | message: error.response.data.message || error.response.data.status_message,
30 | statusCode: error.response.status
31 | };
32 | dispatchMethod(SET_ERROR, payload, dispatch);
33 | }
34 | }
35 | };
36 |
37 | export const searchResult = (query) => async (dispatch) => {
38 | try {
39 | if (query) {
40 | const movies = await SEARCH_API_URL(query);
41 | const { results } = movies.data;
42 | dispatchMethod(SEARCH_RESULT, results, dispatch);
43 | } else {
44 | dispatchMethod(SEARCH_RESULT, [], dispatch);
45 | }
46 | } catch (error) {
47 | if (error.response) {
48 | const payload = {
49 | message: error.response.data.message || error.response.data.status_message,
50 | statusCode: error.response.status
51 | };
52 | dispatchMethod(SET_ERROR, payload, dispatch);
53 | }
54 | }
55 | };
56 |
57 | export const movieDetails = (id) => async (dispatch) => {
58 | try {
59 | const details = await MOVIE_DETAILS_URL(id);
60 | const credits = await MOVIE_CREDITS_URL(id);
61 | const images = await MOVIE_IMAGES_URL(id);
62 | const videos = await MOVIE_VIDEOS_URL(id);
63 | const reviews = await MOVIE_REVIEWS_URL(id);
64 |
65 | const resp = await Promise.all([details, credits, images, videos, reviews])
66 | .then((values) => Promise.all(values.map((value) => value.data)))
67 | .then((response) => response);
68 | dispatchMethod(MOVIE_DETAILS, resp, dispatch);
69 | } catch (error) {
70 | if (error.response) {
71 | dispatchMethod(SET_ERROR, error.response.data.message, dispatch);
72 | }
73 | }
74 | };
75 |
76 | export const clearMovieDetails = () => async (dispatch) => {
77 | dispatchMethod(CLEAR_MOVIE_DETAILS, [], dispatch);
78 | };
79 |
80 | export const setResponsePageNumber = (page, totalPages) => async (dispatch) => {
81 | const payload = { page, totalPages };
82 | dispatchMethod(RESPONSE_PAGE, payload, dispatch);
83 | };
84 |
85 | export const setMovieType = (type) => async (dispatch) => {
86 | dispatchMethod(MOVIE_TYPE, type, dispatch);
87 | };
88 |
89 | export const searchQuery = (query) => async (dispatch) => {
90 | dispatchMethod(SEARCH_QUERY, query, dispatch);
91 | };
92 |
93 | const dispatchMethod = (type, payload, dispatch) => {
94 | dispatch({ type, payload });
95 | };
96 |
97 | const getMoviesRequest = async (type, pageNumber) => {
98 | const movies = await MOVIE_API_URL(type, pageNumber);
99 | const { results, page, total_pages } = movies.data;
100 | const payload = {
101 | page,
102 | totalPages: total_pages
103 | };
104 | return { results, payload };
105 | };
106 |
--------------------------------------------------------------------------------
/src/components/content/details/overview/Overview.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 | import { v4 as uuidv4 } from 'uuid';
5 |
6 | import './Overview.scss';
7 | import { IMAGE_URL } from '../../../../services/movies.service';
8 |
9 | const Overview = (props) => {
10 | const { movie } = props;
11 | const [items, setItems] = useState([]);
12 | const [details] = useState(movie[0]);
13 | const [credits] = useState(movie[1]);
14 |
15 | useEffect(() => {
16 | const detailItems = [
17 | {
18 | id: 0,
19 | name: 'Tagline',
20 | value: `${details.tagline}`
21 | },
22 | {
23 | id: 1,
24 | name: 'Budget',
25 | value: `${numberFormatter(details.budget, 1)}`
26 | },
27 | {
28 | id: 2,
29 | name: 'Revenue',
30 | value: `${numberFormatter(details.revenue, 1)}`
31 | },
32 | {
33 | id: 3,
34 | name: 'Status',
35 | value: `${details.status}`
36 | },
37 | {
38 | id: 4,
39 | name: 'Release Date',
40 | value: `${details.release_date}`
41 | },
42 | {
43 | id: 5,
44 | name: 'Run Time',
45 | value: `${details.runtime} min`
46 | }
47 | ];
48 | setItems(detailItems);
49 |
50 | // eslint-disable-next-line
51 | }, []);
52 |
53 | const numberFormatter = (number, digits) => {
54 | const symbolArray = [
55 | { value: 1, symbol: '' },
56 | { value: 1e3, symbol: 'K' },
57 | { value: 1e6, symbol: 'M' },
58 | { value: 1e9, symbol: 'B' }
59 | ];
60 | const regex = /\.0+$|(\.[0-9]*[1-9])0+$/;
61 | let result = '';
62 |
63 | for (let i = 0; i < symbolArray.length; i++) {
64 | if (number >= symbolArray[i].value) {
65 | result = (number / symbolArray[i].value).toFixed(digits).replace(regex, '$1') + symbolArray[i].symbol;
66 | }
67 | }
68 | return result;
69 | };
70 |
71 | return (
72 |
73 |
74 |
{details.overview}
75 |
76 |
77 |
Cast
78 |
79 | {credits.cast.map((data) => (
80 |
81 |
82 |
83 |
84 | |
85 | {data.name} |
86 | {data.character} |
87 |
88 |
89 | ))}
90 |
91 |
92 |
93 |
94 |
95 |
Production Companies
96 | {details.production_companies.map((prod) => (
97 |
98 |

99 |
{prod.name}
100 |
101 | ))}
102 |
103 |
113 | {items.map((data) => (
114 |
120 | ))}
121 |
122 |
123 | );
124 | };
125 |
126 | Overview.propTypes = {
127 | movie: PropTypes.array
128 | };
129 |
130 | const mapStateToProps = (state) => ({
131 | movie: state.movies.movie
132 | });
133 |
134 | export default connect(mapStateToProps, {})(Overview);
135 |
--------------------------------------------------------------------------------
/src/components/header/Header.scss:
--------------------------------------------------------------------------------
1 | .header-nav-wrapper {
2 | width: 100%;
3 | top: 0;
4 | background-color: #fff;
5 | position: sticky;
6 | z-index: 20;
7 | }
8 |
9 | .header-bar {
10 | width: 100%;
11 | height: 5px;
12 | background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
13 | background-size: 400% 400%;
14 | -webkit-animation: headerbar 15s ease infinite;
15 | -moz-animation: headerbar 15s ease infinite;
16 | animation: headerbar 15s ease infinite;
17 | }
18 |
19 | .header-navbar {
20 | display: grid;
21 | grid-template: "link . . menu search";
22 | grid-template-columns: max-content 1fr 1fr max-content max-content;
23 | height: 50px;
24 | line-height: 50px;
25 | color: #9aa9bb;
26 | }
27 |
28 | .header-image {
29 | grid-area: link;
30 | width: 170px;
31 | height: 170px;
32 | margin-left: 25px;
33 | margin-top: -5px;
34 | }
35 |
36 | .header-nav {
37 | grid-area: menu;
38 | margin-right: 25px;
39 |
40 | .header-nav-item {
41 | .header-list-icon {
42 | padding-right: 5px;
43 | }
44 |
45 | display: inline;
46 | list-style: none;
47 | padding-right: 15px;
48 | }
49 |
50 | .active-item {
51 | color: #3498db;
52 | }
53 | }
54 |
55 | .header-nav-item .header-list-name {
56 | font-size: 0.9rem;
57 | font-weight: 400;
58 | text-decoration: none;
59 | transition: color 0.3s ease-out;
60 | }
61 |
62 | .header-nav-item a:hover {
63 | color: #3498db;
64 | }
65 |
66 | .header-menu-toggle .bar {
67 | width: 25px;
68 | height: 3px;
69 | background-color: #3f3f3f;
70 | margin: 5px auto;
71 | -webkit-transition: all 0.3s ease-in-out;
72 | -o-transition: all 0.3s ease-in-out;
73 | transition: all 0.3s ease-in-out;
74 | }
75 |
76 | .header-menu-toggle {
77 | grid-area: menu;
78 | justify-self: end;
79 | margin-right: 25px;
80 | display: none;
81 | }
82 |
83 | .header-menu-toggle:hover {
84 | cursor: pointer;
85 | }
86 |
87 | #header-mobile-menu.is-active .bar:nth-child(2) {
88 | opacity: 0;
89 | }
90 |
91 | #header-mobile-menu.is-active .bar:nth-child(1) {
92 | -webkit-transform: translateY(8px) rotate(45deg);
93 | -ms-transform: translateY(8px) rotate(45deg);
94 | -o-transform: translateY(8px) rotate(45deg);
95 | transform: translateY(8px) rotate(45deg);
96 | }
97 |
98 | #header-mobile-menu.is-active .bar:nth-child(3) {
99 | -webkit-transform: translateY(-8px) rotate(-45deg);
100 | -ms-transform: translateY(-8px) rotate(-45deg);
101 | -o-transform: translateY(-8px) rotate(-45deg);
102 | transform: translateY(-8px) rotate(-45deg);
103 | }
104 |
105 | .search-input {
106 | grid-area: search;
107 | margin-top: 10px;
108 | width: auto;
109 | border: 1px solid #9aa9bb;
110 | padding: 5px;
111 | height: 36px;
112 | border-radius: 5px;
113 | outline: none;
114 | color: #9dbfaf;
115 | line-height: 36px;
116 | }
117 |
118 | .disabled {
119 | display: none;
120 | }
121 |
122 | ::-webkit-input-placeholder {
123 | color: #9aa9bb;
124 | font-size: 14px;
125 | }
126 |
127 | ::-moz-placeholder {
128 | color: #9aa9bb;
129 | font-size: 14px;
130 | }
131 |
132 | :-ms-input-placeholder {
133 | color: #9aa9bb;
134 | font-size: 14px;
135 | }
136 |
137 | :-moz-placeholder {
138 | color: #9aa9bb;
139 | font-size: 14px;
140 | }
141 |
142 | @-webkit-keyframes headerbar {
143 | 0% {
144 | background-position: 0% 50%;
145 | }
146 | 50% {
147 | background-position: 100% 50%;
148 | }
149 | 100% {
150 | background-position: 0% 50%;
151 | }
152 | }
153 |
154 | @-moz-keyframes headerbar {
155 | 0% {
156 | background-position: 0% 50%;
157 | }
158 | 50% {
159 | background-position: 100% 50%;
160 | }
161 | 100% {
162 | background-position: 0% 50%;
163 | }
164 | }
165 |
166 | @keyframes headerbar {
167 | 0% {
168 | background-position: 0% 50%;
169 | }
170 | 50% {
171 | background-position: 100% 50%;
172 | }
173 | 100% {
174 | background-position: 0% 50%;
175 | }
176 | }
177 |
178 | @media only screen and (max-width: 720px) {
179 | .header-nav-open {
180 | overflow: hidden;
181 | }
182 |
183 | .header-navbar .header-nav {
184 | display: flex;
185 | flex-direction: column;
186 | position: fixed;
187 | top: 55px;
188 | background-color: #fff;
189 | width: 100%;
190 | height: calc(100vh - 55px);
191 | transform: translate(-101%);
192 | text-align: justify;
193 | overflow: hidden;
194 | z-index: 10;
195 | }
196 |
197 | .header-search {
198 | display: flex;
199 | flex-direction: column;
200 | position: fixed;
201 | }
202 |
203 | .header-navbar li {
204 | list-style: none;
205 | }
206 |
207 | .header-navbar li:first-child {
208 | margin-top: 50px;
209 | }
210 |
211 | .header-navbar li .header-list-name {
212 | font-size: 1rem;
213 | }
214 |
215 | .header-nav-item {
216 | .header-list-icon {
217 | display: none;
218 | }
219 | }
220 |
221 | .header-menu-toggle,
222 | .bar {
223 | display: block;
224 | cursor: pointer;
225 | margin-top: 13px;
226 | }
227 |
228 | .header-mobile-nav {
229 | transform: translate(0%) !important;
230 | }
231 |
232 | .search-input {
233 | margin-right: 15px;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/components/content/details/Details.scss:
--------------------------------------------------------------------------------
1 | .movie-container {
2 | position: relative;
3 | }
4 |
5 | .movie-bg, .movie-overlay {
6 | position: absolute;
7 | left: 0;
8 | top: 0;
9 | right: 0;
10 | bottom: 0;
11 | z-index: 1;
12 | }
13 |
14 | .movie-bg {
15 | width: 100%;
16 | height: 515px;
17 | background-position: center;
18 | background-repeat: no-repeat;
19 | background-size: cover;
20 | }
21 |
22 | .movie-overlay {
23 | background: rgba(0, 0, 0, 0.8);
24 | height: 515px;
25 | z-index: 2;
26 | }
27 |
28 | .movie-details {
29 | position: relative;
30 | z-index: 3;
31 | display: grid;
32 | grid-template-areas: "movieImage movieBody";
33 | grid-template-columns: 400px 1fr;
34 | grid-template-rows: 1fr;
35 | grid-gap: 10px;
36 | height: 100vh;
37 | padding-top: 200px;
38 | padding-right: 50px;
39 | padding-left: 50px;
40 | }
41 |
42 | .movie-image {
43 | grid-area: movieImage;
44 | width: 330px;
45 | height: 550px;
46 |
47 | img {
48 | display: inline-block;
49 | max-height: 100%;
50 | max-width: 100%;
51 | background-position: center;
52 | background-repeat: no-repeat;
53 | background-size: cover;
54 | }
55 | }
56 |
57 | .movie-body {
58 | grid-area: movieBody;
59 |
60 | .movie-overview {
61 | height: auto;
62 |
63 | .title {
64 | margin-top: -10px;
65 | font-size: 36px;
66 | font-weight: 700;
67 | color: #ffffff;
68 | text-align: left;
69 | text-transform: uppercase;
70 | margin-bottom: 25px;
71 |
72 | span {
73 | font-size: 24px;
74 | font-weight: 300;
75 | color: #4f5b68;
76 | }
77 | }
78 |
79 | .movie-genres {
80 | .genres {
81 | height: 49px;
82 | line-height: 49px;
83 | display: flex;
84 | margin-bottom: 25px;
85 |
86 | li {
87 | list-style: none;
88 | font-size: 14px;
89 | font-weight: 700;
90 | text-transform: uppercase;
91 | color: #dd003f;
92 | padding-right: 35px;
93 | margin-left: -20px;
94 | }
95 | }
96 | }
97 |
98 | .rating {
99 | height: 49px;
100 | line-height: 49px;
101 | display: flex;
102 | font-size: 25px;
103 | margin-bottom: 90px;
104 |
105 | .fas {
106 | color: #f5b50a;
107 | padding-left: 4px;
108 | }
109 |
110 | span {
111 | color: #9aa9bb;
112 | margin-top: -10px;
113 | }
114 |
115 | p {
116 | color: #dd003f;
117 | margin-left: 10px;
118 | font-size: 18px;
119 | }
120 | }
121 | }
122 |
123 | }
124 |
125 | @media (max-width: 1100px) {
126 | .movie-details {
127 | grid-template-areas: "movieImage" "movieBody";
128 | grid-template-columns: 1fr;
129 | grid-template-rows: 1fr 1fr;
130 | }
131 | }
132 |
133 | @media (max-width: 600px) {
134 | .movie-details {
135 | grid-template-areas: "movieImage" "movieBody";
136 | grid-template-columns: 1fr;
137 | grid-template-rows: 1fr 1fr;
138 | }
139 |
140 | .movie-body {
141 | .movie-overview {
142 | .title {
143 | width: 348px;
144 | font-size: 16px;
145 | overflow-wrap: break-word;
146 | word-wrap: break-word;
147 | hyphens: auto;
148 | span {
149 | display: flex !important;
150 | }
151 | }
152 |
153 | .movie-genres {
154 | text-align: left;
155 | margin-bottom: 50px;
156 |
157 | .genres {
158 | display: inline-block;
159 |
160 | li {
161 | list-style: none;
162 | font-size: 14px;
163 | font-weight: 700;
164 | text-transform: uppercase;
165 | color: #dd003f;
166 | }
167 | }
168 | }
169 | }
170 |
171 | .rating {
172 | display: block !important;
173 |
174 | span {
175 | color: #9aa9bb;
176 | padding-top: 10px;
177 | float: left !important;
178 | margin-left: 3px !important;
179 | }
180 |
181 | p {
182 | color: #dd003f;
183 | float: left !important;
184 | padding-top: 10px;
185 | }
186 | }
187 | }
188 | }
189 |
190 | @media (max-width: 375px) {
191 | .movie-image {
192 | width: 230px;
193 | height: 450px;
194 | }
195 |
196 | .movie-body {
197 | .movie-overview {
198 | .title {
199 | font-size: 16px;
200 | width: auto;
201 | span {
202 | display: flex;
203 | font-size: 18px;
204 | }
205 | }
206 | }
207 | }
208 |
209 | .movie-image {
210 | margin: 0 auto;
211 | }
212 | }
213 |
214 | @media (max-width: 320px) {
215 | .movie-body {
216 | .movie-overview {
217 | .title {
218 | font-size: 14px;
219 | width: 90%;
220 | span {
221 | display: flex;
222 | font-size: 18px;
223 | }
224 | }
225 | }
226 |
227 | .rating {
228 | width: 90%;
229 | .rating-stars {
230 | font-size: 18px !important;
231 | }
232 |
233 | span {
234 | font-size: 80%;
235 | }
236 | }
237 | }
238 |
239 | .movie-image {
240 | margin: 0 auto;
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/src/components/header/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 | import { useNavigate, useLocation, useMatch } from 'react-router-dom';
5 |
6 | import './Header.scss';
7 | import logo from '../../assets/cinema-logo.svg';
8 | import {
9 | getMovies,
10 | setMovieType,
11 | setResponsePageNumber,
12 | searchQuery,
13 | searchResult,
14 | clearMovieDetails
15 | } from '../../redux/actions/movies';
16 | import { pathURL } from '../../redux/actions/routes';
17 | import { setError } from '../../redux/actions/errors';
18 |
19 | const HEADER_LIST = [
20 | {
21 | id: 1,
22 | iconClass: 'fas fa-film',
23 | name: 'Now Playing',
24 | type: 'now_playing'
25 | },
26 | {
27 | id: 2,
28 | iconClass: 'fas fa-fire',
29 | name: 'Popular',
30 | type: 'popular'
31 | },
32 | {
33 | id: 3,
34 | iconClass: 'fas fa-star',
35 | name: 'Top Rated',
36 | type: 'top_rated'
37 | },
38 | {
39 | id: 4,
40 | iconClass: 'fas fa-plus-square',
41 | name: 'Upcoming',
42 | type: 'upcoming'
43 | }
44 | ];
45 |
46 | const Header = (props) => {
47 | const {
48 | getMovies,
49 | setMovieType,
50 | page,
51 | totalPages,
52 | setResponsePageNumber,
53 | searchQuery,
54 | searchResult,
55 | clearMovieDetails,
56 | routesArray,
57 | path,
58 | url,
59 | pathURL,
60 | setError,
61 | errors
62 | } = props;
63 | let [navClass, setNavClass] = useState(false);
64 | let [menuClass, setMenuClass] = useState(false);
65 | const [type, setType] = useState('now_playing');
66 | const [search, setSearch] = useState('');
67 | const [disableSearch, setDisableSearch] = useState(false);
68 | const [hideHeader, setHideHeader] = useState(false);
69 |
70 | const navigate = useNavigate();
71 | const location = useLocation();
72 | const detailsRoute = useMatch('/:id/:name/details');
73 |
74 | useEffect(() => {
75 | if (routesArray.length) {
76 | if (!path && !url) {
77 | pathURL('/', '/');
78 | const error = new Error(`Page with pathname ${location.pathname} not found with status code 404.`);
79 | setError({ message: `Page with pathname ${location.pathname} not found.`, statusCode: 404 });
80 | throw error;
81 | }
82 | }
83 | // eslint-disable-next-line
84 | }, [path, url, routesArray, pathURL]);
85 |
86 | useEffect(() => {
87 | if (errors.message || errors.statusCode) {
88 | pathURL('/', '/');
89 | const error = new Error(`${errors.message} With status code ${errors.statusCode} `);
90 | setError({ message: `Page with pathname ${location.pathname} not found.`, statusCode: 404 });
91 | throw error;
92 | }
93 | // eslint-disable-next-line
94 | }, [errors]);
95 |
96 | useEffect(() => {
97 | if (location.pathname && !errors.message && !errors.statusCode) {
98 | getMovies(type, page);
99 | setResponsePageNumber(page, totalPages);
100 | if (detailsRoute || location.pathname === '/') {
101 | setHideHeader(true);
102 | }
103 |
104 | if (location.pathname !== '/' && location.key) {
105 | setDisableSearch(true);
106 | }
107 | }
108 |
109 | // eslint-disable-next-line
110 | }, [type, disableSearch, location]);
111 |
112 | const setMovieTypeUrl = (type) => {
113 | setDisableSearch(false);
114 | if (location.pathname !== '/') {
115 | clearMovieDetails();
116 | navigate('/');
117 | setType(type);
118 | setMovieType(type);
119 | } else {
120 | setType(type);
121 | setMovieType(type);
122 | }
123 | };
124 |
125 | const onSearchChange = (e) => {
126 | setSearch(e.target.value);
127 | searchQuery(e.target.value);
128 | searchResult(e.target.value);
129 | };
130 |
131 | const navigateToMainPage = () => {
132 | setDisableSearch(false);
133 | clearMovieDetails();
134 | navigate('/');
135 | };
136 |
137 | const toggleMenu = () => {
138 | menuClass = !menuClass;
139 | navClass = !navClass;
140 | setNavClass(navClass);
141 | setMenuClass(menuClass);
142 | if (navClass) {
143 | document.body.classList.add('header-nav-open');
144 | } else {
145 | document.body.classList.remove('header-nav-open');
146 | }
147 | };
148 |
149 | return (
150 | <>
151 | {hideHeader && (
152 |
153 |
154 |
155 |
navigateToMainPage()}>
156 |

157 |
158 |
167 |
189 |
190 |
191 | )}
192 | >
193 | );
194 | };
195 |
196 | Header.propTypes = {
197 | getMovies: PropTypes.func,
198 | setMovieType: PropTypes.func,
199 | searchQuery: PropTypes.func,
200 | searchResult: PropTypes.func,
201 | clearMovieDetails: PropTypes.func,
202 | setResponsePageNumber: PropTypes.func,
203 | page: PropTypes.number,
204 | totalPages: PropTypes.number,
205 | path: PropTypes.string,
206 | url: PropTypes.string,
207 | routesArray: PropTypes.array,
208 | pathURL: PropTypes.func,
209 | setError: PropTypes.func,
210 | errors: PropTypes.object
211 | };
212 |
213 | const mapStateToProps = (state) => ({
214 | page: state.movies.page,
215 | totalPages: state.movies.totalPages,
216 | routesArray: state.routes.routesArray,
217 | path: state.routes.path,
218 | url: state.routes.url,
219 | errors: state.errors
220 | });
221 |
222 | export default connect(mapStateToProps, {
223 | getMovies,
224 | setMovieType,
225 | setResponsePageNumber,
226 | searchQuery,
227 | searchResult,
228 | clearMovieDetails,
229 | pathURL,
230 | setError
231 | })(Header);
232 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | orbs:
2 | slack: circleci/slack@4.2.0
3 | version: 2.1
4 | executors:
5 | app-executor:
6 | docker:
7 | - image: circleci/node:16.17.0
8 | working_directory: ~/repo
9 | slack-executor:
10 | docker:
11 | - image: 'cibuilds/base:latest'
12 | resource_class: small
13 | docker-publisher:
14 | environment:
15 | IMAGE_NAME: uzoeddie/cinemadb
16 | docker:
17 | - image: circleci/buildpack-deps:stretch
18 | terraform-executor:
19 | docker:
20 | - image: hashicorp/terraform:0.14.3
21 |
22 | aliases:
23 | - &show-current-branch-name
24 | run:
25 | name: Show current branch
26 | command: echo ${CIRCLE_BRANCH}
27 | - &restore-cache
28 | restore_cache:
29 | keys:
30 | - app-{{ checksum "package.json" }}
31 | - app-
32 | - &install-dependencies
33 | run:
34 | name: Install dependencies
35 | command: |
36 | mkdir -p artifacts
37 | npm install
38 | - &save-cache
39 | save_cache:
40 | paths:
41 | - node_modules
42 | key: app-{{ checksum "package.json" }}
43 | - &install-aws-cli
44 | run:
45 | name: Installing AWS CLI
46 | working_directory: /
47 | command: |
48 | sudo apt-get -y -qq update
49 | sudo apt-get install -y awscli
50 | sudo apt-get install -y python-pip python-dev build-essential
51 | - &build-project
52 | run:
53 | name: Build Project
54 | command: |
55 | npm install
56 | npm run build
57 | cd build
58 | zip ../build.zip -r * .[^.]*
59 | echo "Build successful"
60 |
61 | jobs:
62 | build:
63 | executor: app-executor
64 | steps:
65 | - checkout
66 | - *show-current-branch-name
67 | - *restore-cache
68 | - *install-dependencies
69 | - *save-cache
70 | - run:
71 | name: Copy Infrastructure Folder
72 | command: |
73 | cp -r infrastructure artifacts/infrastructure
74 | - persist_to_workspace:
75 | root: ./
76 | paths:
77 | - artifacts
78 |
79 | linting:
80 | executor: app-executor
81 | steps:
82 | - checkout
83 | - *show-current-branch-name
84 | - *restore-cache
85 | - run:
86 | name: Run linting
87 | command: npm run lint
88 |
89 | prettier:
90 | executor: app-executor
91 | steps:
92 | - checkout
93 | - *show-current-branch-name
94 | - *restore-cache
95 | - run:
96 | name: Run prettier check
97 | command: npm run prettier:check
98 |
99 | unit-test:
100 | executor: app-executor
101 | steps:
102 | - checkout
103 | - *show-current-branch-name
104 | - *restore-cache
105 | - run:
106 | name: Run unit unit
107 | command: npm run test
108 |
109 | terraform-validate:
110 | executor: terraform-executor
111 | steps:
112 | - checkout
113 | - *show-current-branch-name
114 | - run:
115 | name: Terraform Validate & Format
116 | command: |
117 | cd infrastructure/
118 | terraform init -backend=false
119 | terraform validate
120 | terraform fmt -check
121 |
122 | terraform-plan-and-apply:
123 | executor: terraform-executor
124 | steps:
125 | - checkout
126 | - attach_workspace:
127 | at: ./
128 | - *show-current-branch-name
129 | - run:
130 | name: Terraform Plan
131 | command: |
132 | cd artifacts/infrastructure/
133 | terraform init
134 | terraform workspace select ${CIRCLE_BRANCH} || terraform workspace new ${CIRCLE_BRANCH}
135 | terraform plan
136 | - run:
137 | name: Terraform Apply
138 | command: |
139 | cd artifacts/infrastructure/
140 | terraform workspace select ${CIRCLE_BRANCH}
141 | terraform apply --auto-approve
142 | terraform output cinema_app_bucket_name > ../cinema_bucket_name.txt
143 | terraform output cloudfront_distribution_id > ../cloudfront_distribution_id.txt
144 | - persist_to_workspace:
145 | root: ./
146 | paths:
147 | - artifacts
148 |
149 | publish-to-docker-hub:
150 | executor: docker-publisher
151 | steps:
152 | - checkout
153 | - setup_remote_docker
154 | - *restore-cache
155 | - run: docker login -u "${DOCKER_HUB_USER}" -p "${DOCKER_HUB_PASSWORD}"
156 | - run: docker build --build-arg REACT_APP_API_SECRET=${REACT_APP_API_SECRET} --build-arg REACT_APP_SENTRY_DSN=${REACT_APP_SENTRY_DSN} -t ${IMAGE_NAME}:latest .
157 | - run:
158 | name: Tag and push to docker hub
159 | command: |
160 | if [ "${CIRCLE_BRANCH}" == "master" ]
161 | then
162 | docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:stable-${CIRCLE_BUILD_NUM} && docker push ${IMAGE_NAME}:stable-${CIRCLE_BUILD_NUM}
163 | elif [ "${CIRCLE_BRANCH}" == "staging" ]
164 | then
165 | docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:staging-${CIRCLE_BUILD_NUM} && docker push ${IMAGE_NAME}:staging-${CIRCLE_BUILD_NUM}
166 | else
167 | docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:dev-${CIRCLE_BUILD_NUM} && docker push ${IMAGE_NAME}:dev-${CIRCLE_BUILD_NUM}
168 | fi
169 |
170 | deploy-to-aws-s3:
171 | executor: app-executor
172 | steps:
173 | - checkout
174 | - attach_workspace:
175 | at: ./
176 | - *show-current-branch-name
177 | - *install-aws-cli
178 | - *build-project
179 | - run:
180 | name: Deploy to AWS S3
181 | command: |
182 | AWS_BUCKET_NAME=$(cat artifacts/cinema_bucket_name.txt | sed 's/\"//g')
183 | echo $AWS_BUCKET_NAME
184 | if [ "${CIRCLE_BRANCH}" == "master" ]
185 | then
186 | aws --region ${AWS_REGION} s3 sync ~/repo/build s3://${AWS_BUCKET_NAME} --delete
187 | elif [ "${CIRCLE_BRANCH}" == "staging" ]
188 | then
189 | aws --region ${AWS_REGION} s3 sync ~/repo/build s3://${AWS_BUCKET_NAME} --delete
190 | else
191 | aws --region ${AWS_REGION} s3 sync ~/repo/build s3://${AWS_BUCKET_NAME} --delete
192 | fi
193 |
194 | deploy-to-aws-cloudfront:
195 | executor: app-executor
196 | steps:
197 | - checkout
198 | - attach_workspace:
199 | at: ./
200 | - *show-current-branch-name
201 | - *install-aws-cli
202 | - *build-project
203 | - run:
204 | name: Deploy to AWS Cloudfront
205 | command: |
206 | CLOUDFRONT_DISTRIBUTION_ID=$(cat artifacts/cloudfront_distribution_id.txt | sed 's/\"//g')
207 | echo $CLOUDFRONT_DISTRIBUTION_ID
208 | aws configure set preview.cloudfront true
209 | if [ "${CIRCLE_BRANCH}" == "master" ]
210 | then
211 | aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths /\*
212 | elif [ "${CIRCLE_BRANCH}" == "staging" ]
213 | then
214 | aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths /\*
215 | else
216 | aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths /\*
217 | fi
218 |
219 | notify-via-slack:
220 | executor: slack-executor
221 | steps:
222 | - run: echo "Slack notification"
223 | - slack/notify:
224 | event: 'always'
225 | channel: 'cinema-app-circleci'
226 | template: ''
227 | custom: |
228 | {
229 | "blocks": [
230 | {
231 | "type": "header",
232 | "text": {
233 | "type": "plain_text",
234 | "text": "${CIRCLE_BRANCH} branch deployment to aws s3 and cloudfront is complete. 👍",
235 | "emoji": true
236 | }
237 | },
238 | {
239 | "type": "section",
240 | "fields": [
241 | {
242 | "type": "mrkdwn",
243 | "text": "*Project*:\n$CIRCLE_PROJECT_REPONAME"
244 | },
245 | {
246 | "type": "mrkdwn",
247 | "text": "*Branch*:\n$CIRCLE_BRANCH"
248 | },
249 | {
250 | "type": "mrkdwn",
251 | "text": "*When*:\n$(date +'%m/%d/%Y %T')"
252 | },
253 | {
254 | "type": "mrkdwn",
255 | "text": "*Build*:\n$CIRCLE_BUILD_NUM"
256 | },
257 | {
258 | "type": "mrkdwn",
259 | "text": "*Author*:\n$CIRCLE_PROJECT_USERNAME"
260 | }
261 | ]
262 | },
263 | {
264 | "type": "actions",
265 | "elements": [
266 | {
267 | "type": "button",
268 | "text": {
269 | "type": "plain_text",
270 | "text": "View Job"
271 | },
272 | "url": "${CIRCLE_BUILD_URL}"
273 | }
274 | ]
275 | }
276 | ]
277 | }
278 |
279 | terraform-destroy:
280 | executor: terraform-executor
281 | steps:
282 | - checkout
283 | - *show-current-branch-name
284 | - run:
285 | name: Terraform Destroy
286 | command: |
287 | cd infrastructure/
288 | terraform init
289 | terraform workspace select ${CIRCLE_BRANCH} || terraform workspace new ${CIRCLE_BRANCH}
290 | terraform destroy --auto-approve
291 |
292 | workflows:
293 | build_and_deploy:
294 | jobs:
295 | - build
296 | - linting:
297 | requires:
298 | - build
299 | filters:
300 | branches:
301 | only:
302 | - develop
303 | - staging
304 | - master
305 | - prettier:
306 | requires:
307 | - build
308 | filters:
309 | branches:
310 | only:
311 | - develop
312 | - staging
313 | - master
314 | - unit-test:
315 | requires:
316 | - linting
317 | - prettier
318 | filters:
319 | branches:
320 | only:
321 | - develop
322 | - staging
323 | - master
324 | - terraform-validate:
325 | requires:
326 | - unit-test
327 | filters:
328 | branches:
329 | only:
330 | - develop
331 | - staging
332 | - master
333 | - terraform-plan-and-apply:
334 | requires:
335 | - terraform-validate
336 | filters:
337 | branches:
338 | only:
339 | - develop
340 | - staging
341 | - master
342 | - publish-to-docker-hub:
343 | requires:
344 | - terraform-plan-and-apply
345 | filters:
346 | branches:
347 | only:
348 | - develop
349 | - staging
350 | - master
351 | - deploy-to-aws-s3:
352 | requires:
353 | - publish-to-docker-hub
354 | filters:
355 | branches:
356 | only:
357 | - develop
358 | - staging
359 | - master
360 | - deploy-to-aws-cloudfront:
361 | requires:
362 | - deploy-to-aws-s3
363 | filters:
364 | branches:
365 | only:
366 | - develop
367 | - staging
368 | - master
369 | - notify-via-slack:
370 | requires:
371 | - deploy-to-aws-cloudfront
372 | filters:
373 | branches:
374 | only:
375 | - develop
376 | - staging
377 | - master
378 | - hold:
379 | type: approval
380 | requires:
381 | - deploy-to-aws-cloudfront
382 | - terraform-destroy:
383 | requires:
384 | - hold
385 | filters:
386 | branches:
387 | only:
388 | - develop
389 | - staging
390 | - master
391 |
392 |
--------------------------------------------------------------------------------