├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── actions ├── actionTypes.js ├── miscActions.js ├── movieActions.js ├── reportActions.js ├── request.js ├── userActions.js └── validationActions │ ├── index.js │ ├── validateConnection.js │ ├── validateConnectionPooling.js │ ├── validateCreateUpdateComments.js │ ├── validateDeleteComments.js │ ├── validateErrorHandling.js │ ├── validateFacetedSearch.js │ ├── validateGetComments.js │ ├── validateMigration.js │ ├── validatePOLP.js │ ├── validatePaging.js │ ├── validateProjection.js │ ├── validateTextAndSubfield.js │ ├── validateTicketSevenActions.js │ ├── validateTicketSixActions.js │ ├── validateTimeouts.js │ ├── validateUserManagement.js │ ├── validateUserPreferences.js │ ├── validateUserReport.js │ └── validationHelpers.js ├── assets ├── mongoleaf.png └── pixelatedLeaf.svg ├── components ├── Account.js ├── AccountPanel.js ├── AppDrawer.js ├── CommentCard.js ├── Errors.js ├── ErrorsDiv.js ├── Facets.js ├── Header.js ├── LoginCard.js ├── MovieDetail.js ├── MovieTile.js ├── PostComment.js ├── RatingBar.js ├── SignupCard.js ├── Status.js ├── SubfieldSearch.js ├── TicketValidator.js ├── TicketWaiting.js └── ViewModal.js ├── containers ├── AdminPanel.js ├── CountryResults.js ├── MainContainer.js ├── MovieGrid.js ├── UserReport.js └── normalize.css ├── index.js ├── mock └── movieData.js ├── reducers ├── errorsReducer.js ├── fetchReducer.js ├── miscReducer.js ├── moviesReducer.js ├── reportReducer.js ├── userReducer.js └── validationReducer.js ├── routing ├── AdminRoute.js ├── ConnectedSwitch.js └── PrivateRoute.js └── store ├── configureStore.js └── localStorage.js /README.md: -------------------------------------------------------------------------------- 1 | # M220 MFlix UI Front-End 2 | 3 | Hi there! 4 | 5 | In this repository you can find the [MongoDB University Developer Courses](https://university.mongodb.com/) 6 | front-end application. 7 | 8 | This code is made available so that you can explore how the MFlix application was created and how it is used throughout the M220 online courses. 9 | 10 | All validation codes have been striped out of the source code, so that you can 11 | enjoy the full M220 learning experience! 12 | 13 | The MFlix UI is a React Application that performs backend requests via 14 | a backend exposed Rest API. It will proxy requests to `http://localhost:5000/` 15 | to interact with any of the M220 backends that are listening on that port. 16 | 17 | ## Local build 18 | 19 | If you want to make modifications, debug or simply create a local version of 20 | the MFlix front-end you can do so by building locally. 21 | 22 | ### Dependencies 23 | 24 | To run the MFlix UI application locally you need to have the following 25 | dependencies: 26 | 27 | - ``npm`` 6.4.1 or above 28 | - ``node`` v10.6.0 or above 29 | - Local ``mflix`` backend 30 | 31 | ### Local Installation 32 | 33 | - 1) Install application dependencies 34 | 35 | ```sh 36 | cd mflix-ui 37 | npm install 38 | ``` 39 | 40 | - 2) Run the front-end server 41 | 42 | ```sh 43 | npm start 44 | ``` 45 | 46 | Once you've started the server, a new browser tab or window should open to http://localhost:3000 if one isn't open, otherwise it will refresh the existing window. Since this project uses create-react-app, not reloading is in effect so there's no need to stop and start the front-end when you make a change. 47 | 48 | Enjoy! 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mflix-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^1.3.0", 7 | "@material-ui/icons": "^1.1.0", 8 | "history": "^4.7.2", 9 | "lodash.throttle": "^4.1.1", 10 | "node-sass-chokidar": "^1.3.5", 11 | "normalize": "^0.3.1", 12 | "prettier": "^1.10.2", 13 | "react": "^16.2.0", 14 | "react-copy-to-clipboard": "^5.0.1", 15 | "react-dom": "^16.2.0", 16 | "react-redux": "^5.0.6", 17 | "react-router-dom": "^4.2.2", 18 | "react-router-redux": "^5.0.0-alpha.9", 19 | "react-youtube": "^7.5.0", 20 | "redux": "^3.7.2", 21 | "redux-thunk": "^2.2.0", 22 | "typescript": "^3.5.1" 23 | }, 24 | "devDependencies": { 25 | "react-scripts": "^3.0.1" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test --env=jsdom", 31 | "eject": "react-scripts eject" 32 | }, 33 | "proxy": "http://localhost:5000", 34 | "browserslist": [ 35 | ">0.2%", 36 | "not dead", 37 | "not ie <= 11", 38 | "not op_mini all" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/mflix-ui/45c0daeec2766dcb665871da6fc88ccc12219305/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | mflix 27 | 32 | 33 | 34 | 35 | 38 |
39 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Mflix", 3 | "name": "Mflix", 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/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Movie actions 3 | */ 4 | export const FETCH_MOVIES = "FETCH_MOVIES" 5 | export const FETCH_MOVIES_FAILURE = "FETCH_MOVIES_FAILURE" 6 | export const SEARCH_MOVIES = "SEARCH_MOVIES" 7 | export const SEARCH_MOVIES_FAILURE = "SEARCH_MOVIES_FAILURE" 8 | export const SEARCH_COUNTRIES = "SEARCH_COUNTRIES" 9 | export const SEARCH_COUNTRIES_FAILURE = "SEARCH_COUNTRIES_FAILURE" 10 | export const RECEIVED_COUNTRY_RESULTS = "RECEIVED_COUNTRY_RESULTS" 11 | export const RECEIVED_MOVIES = "RECEIVED_MOVIES" 12 | export const MOVIE_DETAIL = "MOVIE_DETAIL" 13 | export const RECEIVED_SEARCH_RESULTS = "RECEIVED_SEARCH_RESULTS" 14 | export const FETCH_MOVIE_BY_ID = "FETCH_MOVIE_BY_ID" 15 | export const FETCH_MOVIE_BY_ID_FAILURE = "FETCH_MOVIE_BY_ID_FAILURE" 16 | export const RECEIVED_MOVIE_BY_ID = "RECEIVED_MOVIE_BY_ID" 17 | export const VIEW_MOVIE = "VIEW_MOVIE" 18 | export const PAGINATE_MOVIES = "PAGINATE_MOVIES" 19 | export const RECEIVED_PAGINATION = "RECEIVED_PAGINATION" 20 | export const BEGIN_PAGING = "BEGIN_PAGING" 21 | export const PROP_FACET_FILTER = "PROP_FACET_FILTER" 22 | export const SUBMIT_COMMENT = "SUBMIT_COMMENT" 23 | export const SUBMIT_COMMENT_SUCCESS = "SUBMIT_COMMENT_SUCCESS" 24 | export const SUBMIT_COMMENT_FAIL = "SUBMIT_COMMENT_FAIL" 25 | export const UPDATE_COMMENT = "UPDATE_COMMENT" 26 | export const UPDATE_COMMENT_SUCCESS = "UPDATE_COMMENT_SUCCESS" 27 | export const UPDATE_COMMENT_FAIL = "UPDATE_COMMENT_FAIL" 28 | export const DELETE_COMMENT = "DELETE_COMMENT" 29 | export const DELETE_COMMENT_SUCCESS = "DELETE_COMMENT_SUCCESS" 30 | export const DELETE_COMMENT_FAIL = "DELETE_COMMENT_FAIL" 31 | export const CLEAR_ERROR = "CLEAR_ERROR" 32 | 33 | /** 34 | * Report Actions 35 | */ 36 | 37 | export const FETCH_USER_REPORT = "FETCH_USER_REPORT" 38 | export const RECEIVED_USER_REPORT_FAILURE = "RECEIVED_USER_REPORT_FAILURE" 39 | export const RECEIVED_USER_REPORT_SUCCESS = "RECEIVED_USER_REPORT_SUCCESS" 40 | 41 | /** 42 | * Miscellaneous actions 43 | */ 44 | 45 | export const TOGGLE_DRAWER = "TOGGLE_DRAWER" 46 | export const NO_OP = "NO_OP" 47 | 48 | /** 49 | * User actions 50 | */ 51 | 52 | export const LOGIN = "LOGIN" 53 | export const REGISTER = "REGISTER" 54 | export const LOGIN_SUCCESS = "LOGIN_SUCCESS" 55 | export const LOGIN_FAIL = "LOGIN_FAIL" 56 | export const LOGOUT = "LOGOUT" 57 | export const UPDATE_PREFS = "UPDATE_PREFS" 58 | export const SAVE_PREFS = "SAVE_PREFS" 59 | export const SAVE_PREFS_SUCCESS = "SAVE_PREFS_SUCCESS" 60 | export const SAVE_PREFS_FAIL = "SAVE_PREFS_FAIL" 61 | export const CHECK_ADMIN = "CHECK_ADMIN" 62 | export const CHECK_ADMIN_SUCCESS = "CHECK_ADMIN_SUCCESS" 63 | export const CHECK_ADMIN_FAIL = "CHECK_ADMIN_FAIL" 64 | 65 | /** 66 | * Validation Actions 67 | */ 68 | export const VALIDATING_TICKET = "VALIDATING_TICKET" 69 | 70 | export const VALIDATE_CONNECTION = "VALIDATE_CONNECTION" 71 | export const VALIDATE_CONNECTION_SUCCESS = 72 | "VALIDATE_CONNECTION_SUCCESS" 73 | export const VALIDATE_CONNECTION_ERROR = 74 | "VALIDATE_CONNECTION_ERROR" 75 | 76 | export const VALIDATE_PROJECTION = "VALIDATE_PROJECTION" 77 | export const VALIDATE_PROJECTION_SUCCESS = 78 | "VALIDATE_PROJECTION_SUCCESS" 79 | export const VALIDATE_PROJECTION_ERROR = 80 | "VALIDATE_PROJECTION_ERROR" 81 | 82 | export const VALIDATE_TEXT_AND_SUBFIELD = 83 | "VALIDATE_TEXT_AND_SUBFIELD" 84 | export const VALIDATE_TEXT_AND_SUBFIELD_SUCCESS = 85 | "VALIDATE_TEXT_AND_SUBFIELD_SUCCESS" 86 | export const VALIDATE_TEXT_AND_SUBFIELD_ERROR = 87 | "VALIDATE_TEXT_AND_SUBFIELD_ERROR" 88 | 89 | export const VALIDATE_PAGING = "VALIDATE_PAGING" 90 | export const VALIDATE_PAGING_SUCCESS = "VALIDATE_PAGING_SUCCESS" 91 | export const VALIDATE_PAGING_ERROR = "VALIDATE_PAGING_ERROR" 92 | 93 | export const VALIDATE_FACETED_SEARCH = "VALIDATE_FACETED_SEARCH" 94 | export const VALIDATE_FACETED_SEARCH_SUCCESS = 95 | "VALIDATE_FACETED_SEARCH_SUCCESS" 96 | export const VALIDATE_FACETED_SEARCH_ERROR = 97 | "VALIDATE_FACETED_SEARCH_ERROR" 98 | 99 | export const VALIDATE_USER_MANAGEMENT = "VALIDATE_USER_MANAGEMENT" 100 | export const VALIDATE_USER_MANAGEMENT_SUCCESS = 101 | "VALIDATE_USER_MANAGEMENT_SUCCESS" 102 | export const VALIDATE_USER_MANAGEMENT_ERROR = 103 | "VALIDATE_USER_MANAGEMENT_ERROR" 104 | 105 | export const VALIDATE_USER_PREFERENCES = 106 | "VALIDATE_USER_PREFERENCES" 107 | export const VALIDATE_USER_PREFERENCES_SUCCESS = 108 | "VALIDATE_USER_PREFERENCES_SUCCESS" 109 | export const VALIDATE_USER_PREFERENCES_ERROR = 110 | "VALIDATE_USER_PREFERENCES_ERROR" 111 | 112 | export const VALIDATE_GET_COMMENTS = "VALIDATE_GET_COMMENTS" 113 | export const VALIDATE_GET_COMMENTS_SUCCESS = 114 | "VALIDATE_GET_COMMENTS_SUCCESS" 115 | export const VALIDATE_GET_COMMENTS_ERROR = 116 | "VALIDATE_GET_COMMENTS_ERROR" 117 | 118 | export const VALIDATE_CREATE_UPDATE_COMMENTS = 119 | "VALIDATE_CREATE_UPDATE_COMMENTS" 120 | export const VALIDATE_CREATE_UPDATE_COMMENTS_SUCCESS = 121 | "VALIDATE_CREATE_UPDATE_COMMENTS_SUCCESS" 122 | export const VALIDATE_CREATE_UPDATE_COMMENTS_ERROR = 123 | "VALIDATE_CREATE_UPDATE_COMMENTS_ERROR" 124 | 125 | export const VALIDATE_DELETE_COMMENTS = "VALIDATE_DELETE_COMMENTS" 126 | export const VALIDATE_DELETE_COMMENTS_SUCCESS = 127 | "VALIDATE_DELETE_COMMENTS_SUCCESS" 128 | export const VALIDATE_DELETE_COMMENTS_ERROR = 129 | "VALIDATE_DELETE_COMMENTS_ERROR" 130 | 131 | export const VALIDATE_USER_REPORT = "VALIDATE_USER_REPORT" 132 | export const VALIDATE_USER_REPORT_SUCCESS = 133 | "VALIDATE_USER_REPORT_SUCCESS" 134 | export const VALIDATE_USER_REPORT_ERROR = 135 | "VALIDATE_USER_REPORT_ERROR" 136 | 137 | export const VALIDATE_MIGRATION = "VALIDATE_MIGRATION" 138 | export const VALIDATE_MIGRATION_SUCCESS = 139 | "VALIDATE_MIGRATION_SUCCESS" 140 | export const VALIDATE_MIGRATION_ERROR = "VALIDATE_MIGRATION_ERROR" 141 | 142 | export const VALIDATE_CONNECTION_POOLING = 143 | "VALIDATE_CONNECTION_POOLING" 144 | export const VALIDATE_CONNECTION_POOLING_SUCCESS = 145 | "VALIDATE_CONNECTION_POOLING_SUCCESS" 146 | export const VALIDATE_CONNECTION_POOLING_ERROR = 147 | "VALIDATE_CONNECTION_POOLING_ERROR" 148 | 149 | export const VALIDATE_TIMEOUTS = "VALIDATE_TIMEOUTS" 150 | export const VALIDATE_TIMEOUTS_SUCCESS = 151 | "VALIDATE_TIMEOUTS_SUCCESS" 152 | export const VALIDATE_TIMEOUTS_ERROR = "VALIDATE_TIMEOUTS_ERROR" 153 | 154 | export const VALIDATE_ERROR_HANDLING = "VALIDATE_ERROR_HANDLING" 155 | export const VALIDATE_ERROR_HANDLING_SUCCESS = 156 | "VALIDATE_ERROR_HANDLING_SUCCESS" 157 | export const VALIDATE_ERROR_HANDLING_ERROR = 158 | "VALIDATE_ERROR_HANDLING_ERROR" 159 | 160 | export const VALIDATE_POLP = "VALIDATE_POLP" 161 | export const VALIDATE_POLP_SUCCESS = "VALIDATE_POLP_SUCCESS" 162 | export const VALIDATE_POLP_ERROR = "VALIDATE_POLP_ERROR" 163 | -------------------------------------------------------------------------------- /src/actions/miscActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes" 2 | 3 | export function toggleDrawer() { 4 | return { type: types.TOGGLE_DRAWER } 5 | } 6 | 7 | export function clearError(key) { 8 | return { type: types.CLEAR_ERROR, key } 9 | } 10 | -------------------------------------------------------------------------------- /src/actions/movieActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes" 2 | import request from "./request" 3 | 4 | const { useFacets } = window.mflix || { 5 | useFacets: false, 6 | } 7 | 8 | export function viewMovie() { 9 | return { type: types.VIEW_MOVIE } 10 | } 11 | 12 | export function receivedMovies(json) { 13 | return { type: types.RECEIVED_MOVIES, ...json } 14 | } 15 | 16 | export function receivedSearchResults(json) { 17 | return { type: types.RECEIVED_SEARCH_RESULTS, ...json } 18 | } 19 | 20 | export function movieDetail(movie) { 21 | return { type: types.MOVIE_DETAIL, movie: movie } 22 | } 23 | 24 | export function fetchMovies() { 25 | return dispatch => { 26 | return fetch(`/api/v1/movies/`, { 27 | method: "GET", 28 | mode: "cors", 29 | }) 30 | .then(response => response.json()) 31 | .then(json => dispatch(receivedMovies(json))) 32 | .catch(e => dispatch(fetchMoviesError(e.message))) 33 | } 34 | } 35 | 36 | export function searchMovies(subfield, search, history) { 37 | let query 38 | let encodedSearch = encodeURI(search) 39 | switch (subfield) { 40 | case "genre": 41 | query = `genre=${encodedSearch}` 42 | break 43 | case "cast": 44 | query = `cast=${encodedSearch}` 45 | break 46 | default: 47 | query = `text=${encodedSearch}` 48 | } 49 | if (useFacets && subfield === "cast") { 50 | return searchByFacet(query, history) 51 | } 52 | return dispatch => { 53 | return request(`/api/v1/movies/search?${query}`, { 54 | method: "GET", 55 | mode: "cors", 56 | }) 57 | .then(json => dispatch(receivedSearchResults(json))) 58 | .then(() => history.push("/")) 59 | .catch(e => dispatch(searchMoviesError(subfield))) 60 | } 61 | } 62 | 63 | export function searchByFacet(query, history) { 64 | return dispatch => { 65 | return request(`/api/v1/movies/facet-search?${query}`, { 66 | method: "GET", 67 | mode: "cors", 68 | }) 69 | .then(json => dispatch(receivedSearchResults(json))) 70 | .then(() => history.push("/")) 71 | .catch(e => dispatch(searchMoviesError(e.message))) 72 | } 73 | } 74 | 75 | export function searchCountries(search, history) { 76 | return dispatch => { 77 | let countries = search.split(",").map(elem => `countries=${elem.trim()}`) 78 | let uri = `/api/v1/movies/countries?${encodeURI(countries.join("&"))}` 79 | 80 | return request(uri, { 81 | method: "GET", 82 | mode: "cors", 83 | }) 84 | .then(json => dispatch(receivedCountryResults(json.titles))) 85 | .then(() => history.push("/country-results")) 86 | .catch(e => dispatch(searchCountriesError(e.message))) 87 | } 88 | } 89 | 90 | export function receivedCountryResults(titles) { 91 | return { type: types.RECEIVED_COUNTRY_RESULTS, titles } 92 | } 93 | 94 | export function searchCountriesError(e) { 95 | return { 96 | type: types.SEARCH_COUNTRIES_FAILURE, 97 | error: `Unable to fetch movies from this country`, 98 | } 99 | } 100 | 101 | export function receivedMovieByID(json) { 102 | return { type: types.RECEIVED_MOVIE_BY_ID, movie: json.movie } 103 | } 104 | 105 | export function fetchMovieByID(id, history) { 106 | return dispatch => { 107 | return fetch(`/api/v1/movies/id/${id}`, { 108 | method: "GET", 109 | mode: "cors", 110 | }) 111 | .then(response => response.json()) 112 | .then(json => dispatch(receivedMovieByID(json))) 113 | .then(() => history.replace(`/movies/id/${id}`)) 114 | .catch(e => dispatch(fetchMovieByIDError(e.message))) 115 | } 116 | } 117 | 118 | export function fetchMoviesError(e) { 119 | return { 120 | type: types.FETCH_MOVIES_FAILURE, 121 | error: `Unable to fetch movies`, 122 | } 123 | } 124 | 125 | export function fetchMovieByIDError(e) { 126 | return { 127 | type: types.FETCH_MOVIE_BY_ID_FAILURE, 128 | error: `Unable to fetch the movie by _id`, 129 | } 130 | } 131 | 132 | export function searchMoviesError(e) { 133 | return { 134 | type: types.SEARCH_MOVIES_FAILURE, 135 | error: `Unable to search for ` + e + `.`, 136 | } 137 | } 138 | 139 | export function beginPaging() { 140 | return { type: types.BEGIN_PAGING } 141 | } 142 | 143 | export function paginate(currState, currPage, filters) { 144 | return dispatch => { 145 | let query 146 | let url 147 | if (Object.keys(filters).length !== 0) { 148 | query = Object.keys(filters).reduce( 149 | (acc, curr) => [...acc, `${curr}=${filters[curr]}`], 150 | [], 151 | ) 152 | query = "?" + query.join("&") + `&page=${currPage + 1}` 153 | } else { 154 | query = `?page=${currPage + 1}` 155 | } 156 | if (Object.keys(filters).includes("cast") && useFacets) { 157 | url = `/api/v1/movies/facet-search${encodeURI(query)}` 158 | } else { 159 | url = `/api/v1/movies/search${encodeURI(query)}` 160 | } 161 | return request(url, { 162 | method: "GET", 163 | mode: "cors", 164 | }) 165 | .then(json => 166 | dispatch(receivedPagination(currState, currPage, json, dispatch)), 167 | ) 168 | .catch(e => dispatch(fetchMoviesError(e.message))) 169 | } 170 | } 171 | 172 | export function receivedPagination(currState, currPage, json, dispatch) { 173 | let currentMovies = currState.map(elem => elem._id) 174 | let movies = json.movies.filter(movie => !currentMovies.includes(movie._id)) 175 | movies = [...currState, ...movies] 176 | let page = movies.length > currState.length ? json.page : currPage 177 | if (page !== currPage) { 178 | return { 179 | type: types.RECEIVED_PAGINATION, 180 | ...json, 181 | movies, 182 | page, 183 | facets: json.facets, 184 | } 185 | } else { 186 | return { type: types.NO_OP } 187 | } 188 | } 189 | 190 | export function submitComment(movieID, comment, token) { 191 | return dispatch => { 192 | return request(`/api/v1/movies/comment`, { 193 | method: "POST", 194 | mode: "cors", 195 | headers: { 196 | Authorization: `Bearer ${token}`, 197 | "content-type": "application/json", 198 | }, 199 | body: JSON.stringify({ 200 | movie_id: movieID, 201 | comment, 202 | }), 203 | }) 204 | .then(json => dispatch(receivedCommentSubmissionOk(json))) 205 | .catch(e => console.log(e)) 206 | } 207 | } 208 | 209 | export function receivedCommentSubmissionOk(json) { 210 | return { type: types.SUBMIT_COMMENT_SUCCESS, comments: json.comments } 211 | } 212 | 213 | export function editComment(commentID, update, token, movie_id) { 214 | return dispatch => { 215 | return request(`/api/v1/movies/comment`, { 216 | method: "PUT", 217 | mode: "cors", 218 | headers: { 219 | Authorization: `Bearer ${token}`, 220 | "content-type": "application/json", 221 | }, 222 | body: JSON.stringify({ 223 | comment_id: commentID, 224 | updated_comment: update, 225 | movie_id, 226 | }), 227 | }) 228 | .then(json => dispatch(receivedCommentUpdateOk(json))) 229 | .catch(e => console.log(e)) 230 | } 231 | } 232 | 233 | export function receivedCommentUpdateOk(json) { 234 | return { type: types.UPDATE_COMMENT_SUCCESS, comments: json.comments } 235 | } 236 | 237 | export function deleteComment(comment_id, token, movie_id) { 238 | return dispatch => { 239 | return request(`/api/v1/movies/comment`, { 240 | method: "DELETE", 241 | mode: "cors", 242 | headers: { 243 | Authorization: `Bearer ${token}`, 244 | "content-type": "application/json", 245 | }, 246 | body: JSON.stringify({ 247 | comment_id, 248 | movie_id, 249 | }), 250 | }) 251 | .then(json => dispatch(receivedCommentUpdateOk(json))) 252 | .catch(e => console.log(e)) 253 | } 254 | } 255 | 256 | export function applyFacetFilter(facet, key, filter) { 257 | return { type: types.PROP_FACET_FILTER, payload: { facet, key, filter } } 258 | } 259 | -------------------------------------------------------------------------------- /src/actions/reportActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes" 2 | import request from "./request" 3 | 4 | export function fetchReport(user, history) { 5 | return dispatch => { 6 | dispatch(fetchingReport()) 7 | return request(`/api/v1/user/comment-report`, { 8 | method: "GET", 9 | mode: "cors", 10 | headers: { 11 | Authorization: `Bearer ${user.auth_token}`, 12 | }, 13 | }) 14 | .then(json => dispatch(receivedReportSuccess(json))) 15 | .then(() => history.push("/user-report")) 16 | .catch(e => history.push("/login")) 17 | } 18 | } 19 | 20 | export function fetchingReport() { 21 | return { type: types.FETCH_USER_REPORT } 22 | } 23 | 24 | export function receivedReportSuccess({ report }) { 25 | return { type: types.RECEIVED_USER_REPORT_SUCCESS, report } 26 | } 27 | 28 | export function receivedReportFailure(report) { 29 | return { type: types.RECEIVED_USER_REPORT_FAILURE, report } 30 | } 31 | -------------------------------------------------------------------------------- /src/actions/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses the JSON returned by a network request 3 | * 4 | * @param {object} response A response from a network request 5 | * 6 | * @return {object} The parsed JSON, status from the response 7 | */ 8 | function parseJSON(response) { 9 | return new Promise(resolve => 10 | response.json().then(json => 11 | resolve({ 12 | status: response.status, 13 | ok: response.ok, 14 | json 15 | }) 16 | ) 17 | ) 18 | } 19 | 20 | /** 21 | * Requests a URL, returning a promise 22 | * 23 | * @param {string} url The URL we want to request 24 | * @param {object} [options] The options we want to pass to "fetch" 25 | * 26 | * @return {Promise} The request promise 27 | */ 28 | export default function request(url, options) { 29 | return new Promise((resolve, reject) => { 30 | fetch(url, options) 31 | .then(parseJSON) 32 | .then(response => { 33 | if (response.ok) { 34 | return resolve(response.json) 35 | } 36 | // extract the error from the server's json 37 | return reject(response.json) 38 | }) 39 | .catch(error => 40 | reject({ 41 | error 42 | }) 43 | ) 44 | }) 45 | } 46 | 47 | export function requestWithStatus(url, options) { 48 | return new Promise((resolve, reject) => { 49 | fetch(url, options) 50 | .then(parseJSON) 51 | .then(response => { 52 | let { json, status, ok } = response 53 | if (response.ok) { 54 | return resolve({ json, status, ok }) 55 | } 56 | // extract the error from the server's json 57 | return reject({ json, status, ok }) 58 | }) 59 | .catch(error => 60 | reject({ 61 | error 62 | }) 63 | ) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes" 2 | import request from "./request" 3 | 4 | export function login(json, history) { 5 | return dispatch => { 6 | return request(`/api/v1/user/login`, { 7 | method: "POST", 8 | mode: "cors", 9 | headers: { 10 | "content-type": "application/json", 11 | }, 12 | body: JSON.stringify(json), 13 | }) 14 | .then(user => dispatch(loginSuccess(user))) 15 | .then(history.push("/")) 16 | .catch(error => dispatch(loginFail({ error }))) 17 | } 18 | } 19 | 20 | export function register(json, history) { 21 | return dispatch => { 22 | return request(`/api/v1/user/register`, { 23 | method: "POST", 24 | mode: "cors", 25 | headers: { 26 | "content-type": "application/json", 27 | }, 28 | body: JSON.stringify(json), 29 | }) 30 | .then(user => { 31 | dispatch(loginSuccess(user)) 32 | }) 33 | .then(history.push("/")) 34 | .catch(error => { 35 | return dispatch(loginFail({ error })) 36 | }) 37 | } 38 | } 39 | 40 | export function logout(token) { 41 | return dispatch => { 42 | return request(`/api/v1/user/logout`, { 43 | method: "POST", 44 | mode: "cors", 45 | headers: { 46 | Authorization: `Bearer ${token}`, 47 | "content-type": "application/json", 48 | }, 49 | }) 50 | .then(dispatch(loggedOut())) 51 | .catch(dispatch(loggedOut())) 52 | } 53 | } 54 | 55 | export function updatePrefs(preferences, user) { 56 | return dispatch => { 57 | let updatedPreferences = { ...user.info.preferences, ...preferences } 58 | return request(`/api/v1/user/update-preferences`, { 59 | method: "PUT", 60 | mode: "cors", 61 | headers: { 62 | Authorization: `Bearer ${user.auth_token}`, 63 | "content-type": "application/json", 64 | }, 65 | body: JSON.stringify({ preferences: updatedPreferences }), 66 | }) 67 | .then(dispatch(savePrefs(preferences))) 68 | .catch(e => dispatch(failSavePrefs())) 69 | } 70 | } 71 | 72 | export function checkAdminStatus(user) { 73 | console.log("check admin status beginning function") 74 | return dispatch => { 75 | dispatch(beginAdminCheck()) 76 | return request(`/api/v1/user/admin`, { 77 | method: "GET", 78 | mode: "cors", 79 | headers: { 80 | Authorization: `Bearer ${user.auth_token}`, 81 | "content-type": "application/json", 82 | }, 83 | }) 84 | .then(json => checkAdminReturn(json)) 85 | .then(() => dispatch(adminSuccess())) 86 | .catch(() => dispatch(adminFail())) 87 | } 88 | } 89 | 90 | function checkAdminReturn(json) { 91 | if (!json.status === "success") { 92 | throw new Error("not authorized") 93 | } 94 | return json 95 | } 96 | 97 | export function beginAdminCheck() { 98 | return { type: types.CHECK_ADMIN } 99 | } 100 | 101 | export function adminSuccess() { 102 | console.log("admin check ok") 103 | return { type: types.CHECK_ADMIN_SUCCESS } 104 | } 105 | 106 | export function adminFail() { 107 | console.log("admin check fail") 108 | return { type: types.CHECK_ADMIN_FAIL } 109 | } 110 | 111 | export function savePrefs(preferences) { 112 | return { type: types.SAVE_PREFS_SUCCESS, preferences } 113 | } 114 | 115 | export function failSavePrefs() { 116 | return { 117 | type: types.SAVE_PREFS_FAIL, 118 | error: "Failed to save user preference", 119 | } 120 | } 121 | 122 | export function loggedOut() { 123 | return { type: types.LOGOUT } 124 | } 125 | 126 | export function loginSuccess(user) { 127 | return { type: types.LOGIN_SUCCESS, user } 128 | } 129 | 130 | export function loginFail(error) { 131 | return { type: types.LOGIN_FAIL, error } 132 | } 133 | -------------------------------------------------------------------------------- /src/actions/validationActions/index.js: -------------------------------------------------------------------------------- 1 | export * from "./validateConnection" 2 | export * from "./validateProjection" 3 | export * from "./validateTextAndSubfield" 4 | export * from "./validatePaging" 5 | export * from "./validateFacetedSearch" 6 | export * from "./validateUserManagement" 7 | export * from "./validateUserPreferences" 8 | export * from "./validateGetComments" 9 | export * from "./validateCreateUpdateComments" 10 | export * from "./validateDeleteComments" 11 | export * from "./validateUserReport" 12 | export * from "./validateMigration" 13 | export * from "./validateConnectionPooling" 14 | export * from "./validateTimeouts" 15 | export * from "./validateErrorHandling" 16 | export * from "./validatePOLP" 17 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateConnection.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { assert, beginTicketValidation } from "./validationHelpers" 4 | 5 | export function validateConnection() { 6 | return async dispatch => { 7 | dispatch(beginTicketValidation("Connection")) 8 | let response = await getMovies() 9 | let filtersAssertion = assert(0, Object.keys(response.filters).length) 10 | let moviesAssertion = assert(20, response.movies.length) 11 | let resultsAssertion = assert(45993, response.total_results) 12 | let pageAssertion = assert(0, response.page) 13 | if ( 14 | [ 15 | filtersAssertion, 16 | moviesAssertion, 17 | resultsAssertion, 18 | pageAssertion, 19 | ].every(elem => elem) 20 | ) { 21 | return dispatch(validateConnectionSuccess()) 22 | } else { 23 | return dispatch( 24 | validateConnectionError( 25 | new Error("The return from the api was incorrect"), 26 | ), 27 | ) 28 | } 29 | } 30 | } 31 | 32 | export function validateConnectionSuccess() { 33 | return { type: types.VALIDATE_CONNECTION_SUCCESS } 34 | } 35 | 36 | export function validateConnectionError(error) { 37 | return { type: types.VALIDATE_CONNECTION_ERROR, error } 38 | } 39 | 40 | /** 41 | * Ticket internal functions 42 | */ 43 | 44 | const getMovies = () => { 45 | return request(`/api/v1/movies/`, { 46 | method: "GET", 47 | mode: "cors", 48 | }) 49 | .then(res => res) 50 | .catch(error => error) 51 | } 52 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateConnectionPooling.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { assert, beginTicketValidation } from "./validationHelpers" 4 | 5 | export function validateConnectionPooling() { 6 | return async dispatch => { 7 | dispatch(beginTicketValidation("ConnectionPooling")) 8 | let response = await getPoolSize() 9 | let poolAssertion = assert(50, response.pool_size) 10 | if ([poolAssertion].every(elem => elem)) { 11 | return dispatch(validateConnectionPoolingSuccess()) 12 | } else { 13 | return dispatch( 14 | validateConnectionPoolingError( 15 | new Error("The return from the api was incorrect"), 16 | ), 17 | ) 18 | } 19 | } 20 | } 21 | 22 | export function validateConnectionPoolingSuccess() { 23 | return { type: types.VALIDATE_CONNECTION_POOLING_SUCCESS } 24 | } 25 | 26 | export function validateConnectionPoolingError(error) { 27 | return { type: types.VALIDATE_CONNECTION_POOLING_ERROR, error } 28 | } 29 | 30 | /** 31 | * Ticket internal functions 32 | */ 33 | 34 | const getPoolSize = () => { 35 | return request(`/api/v1/movies/config-options`, { 36 | method: "GET", 37 | mode: "cors", 38 | }) 39 | .then(res => res) 40 | .catch(error => error) 41 | } 42 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateCreateUpdateComments.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { 3 | beginTicketValidation, 4 | genRandomUser, 5 | deleteUser, 6 | register, 7 | submitComment, 8 | editComment, 9 | deleteComment, 10 | getMovie, 11 | } from "./validationHelpers" 12 | 13 | // men in black 14 | const movie_id = "573a139af29313caabcf0b0a" 15 | export function validateCreateUpdateComments() { 16 | return async dispatch => { 17 | try { 18 | dispatch(beginTicketValidation("CreateUpdateComments")) 19 | 20 | // create the user who will own this comment 21 | const commentOwner = genRandomUser() 22 | const ownerRegisterResponse = await register(commentOwner) 23 | const ownerAuthToken = ownerRegisterResponse.json.auth_token 24 | 25 | // create another user who will attempt to update the comment 26 | const otherUser = genRandomUser() 27 | const otherRegisterResponse = await register(otherUser) 28 | const otherAuthToken = otherRegisterResponse.json.auth_token 29 | 30 | let postC, badC, goodC 31 | const commentResponse = await submitComment( 32 | movie_id, 33 | "feefee", 34 | ownerAuthToken, 35 | ) 36 | if (!commentResponse.ok) { 37 | throw new Error("Unable to post a comment") 38 | } else { 39 | postC = true 40 | } 41 | 42 | // make sure Get Comments ticket has been completed 43 | let firstCommentID 44 | try { 45 | firstCommentID = commentResponse.json.comments[0]._id 46 | } catch (e) { 47 | throw new Error("Unable to retrieve movie comments") 48 | } 49 | 50 | // try to update comment with a different auth token - this should fail 51 | const badResponse = await editComment( 52 | firstCommentID, 53 | "badCommentText", 54 | otherAuthToken, 55 | movie_id, 56 | ) 57 | 58 | if (badResponse.ok) { 59 | throw new Error("Was able to update a comment that wasn't owned") 60 | } else { 61 | badC = true 62 | } 63 | 64 | // try to update comment with the correct auth token - this should succeed 65 | const newCommentText = "fazzlebizzle" 66 | const goodUpdate = await editComment( 67 | firstCommentID, 68 | newCommentText, 69 | ownerAuthToken, 70 | movie_id, 71 | ) 72 | if (goodUpdate.ok === false) { 73 | throw new Error("Unable to update comment") 74 | } else { 75 | goodC = true 76 | } 77 | 78 | const updatedMovie = await getMovie(movie_id) 79 | let updatedComment 80 | try { 81 | updatedComment = updatedMovie.movie.comments[0] 82 | } catch (e) { 83 | throw new Error("Unable to retrieve movie comments") 84 | } 85 | if (updatedComment.text !== newCommentText) { 86 | throw new Error("Update was performed but unsuccessful") 87 | } 88 | deleteComment(firstCommentID, ownerAuthToken, movie_id) 89 | if (postC && badC && goodC) { 90 | deleteUser(ownerAuthToken, commentOwner) 91 | deleteUser(otherAuthToken, otherUser) 92 | return dispatch(validateCreateUpdateCommentsSuccess()) 93 | } else { 94 | return dispatch( 95 | validateCreateUpdateCommentsError( 96 | new Error("The return from the api was incorrect"), 97 | ), 98 | ) 99 | } 100 | } catch (e) { 101 | return dispatch(validateCreateUpdateCommentsError(new Error(e.message))) 102 | } 103 | } 104 | } 105 | 106 | export function validateCreateUpdateCommentsSuccess() { 107 | return { type: types.VALIDATE_CREATE_UPDATE_COMMENTS_SUCCESS } 108 | } 109 | 110 | export function validateCreateUpdateCommentsError(error) { 111 | return { type: types.VALIDATE_CREATE_UPDATE_COMMENTS_ERROR, error } 112 | } 113 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateDeleteComments.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { 3 | beginTicketValidation, 4 | genRandomUser, 5 | register, 6 | submitComment, 7 | deleteComment, 8 | deleteUser, 9 | getMovie, 10 | } from "./validationHelpers" 11 | 12 | // the martian 13 | const movie_id = "573a13eff29313caabdd82f3" 14 | export function validateDeleteComments() { 15 | return async dispatch => { 16 | try { 17 | dispatch(beginTicketValidation("DeleteComments")) 18 | 19 | // create the user who will own this comment 20 | const commentOwner = genRandomUser() 21 | const ownerRegisterResponse = await register(commentOwner) 22 | const ownerAuthToken = ownerRegisterResponse.json.auth_token 23 | 24 | // create another user who will attempt to delete the comment 25 | const otherUser = genRandomUser() 26 | const otherRegisterResponse = await register(otherUser) 27 | const otherAuthToken = otherRegisterResponse.json.auth_token 28 | 29 | let postC, badC, goodC 30 | const commentResponse = await submitComment( 31 | movie_id, 32 | "feefee", 33 | ownerAuthToken, 34 | ) 35 | if (!commentResponse.ok) { 36 | throw new Error("Unable to post a comment") 37 | } else { 38 | postC = true 39 | } 40 | 41 | // make sure Get Comments ticket has been completed 42 | let firstCommentID 43 | try { 44 | firstCommentID = commentResponse.json.comments[0]._id 45 | } catch (e) { 46 | throw new Error("Unable to retrieve movie comments") 47 | } 48 | 49 | // using otherAuthToken, should not be able to delete this comment 50 | const badResponse = await deleteComment( 51 | firstCommentID, 52 | otherAuthToken, 53 | movie_id, 54 | ) 55 | 56 | try { 57 | let badResponseCommentId 58 | // check to see if the api returns a successful or failure status code 59 | if (badResponse.ok) { 60 | badResponseCommentId = badResponse.json.comments[0]._id 61 | // if the first (latest) comment associated with The Martian has changed, 62 | // then this bad delete was actually successful - this will throw an error 63 | if (badResponseCommentId !== firstCommentID) { 64 | throw new Error("Was able to delete a comment that wasn't owned") 65 | } 66 | } 67 | badC = true 68 | } catch (e) { 69 | throw e 70 | } 71 | // using ownerAuthToken, should be able to successfully delete this comment 72 | await deleteComment(firstCommentID, ownerAuthToken, movie_id) 73 | 74 | const updatedMovie = await getMovie(movie_id) 75 | let newCommentID 76 | try { 77 | newCommentID = updatedMovie.movie.comments[0]._id 78 | } catch (e) { 79 | throw new Error("Unable to retrieve movie comments") 80 | } 81 | 82 | // if the first comment associated with The Martian has NOT changed, then 83 | // this good delete was unsuccessful - this will throw an error 84 | if (newCommentID === firstCommentID) { 85 | throw new Error("Deletion was performed but unsuccessful") 86 | } else { 87 | goodC = true 88 | } 89 | 90 | if (postC && badC && goodC) { 91 | deleteUser(ownerAuthToken, commentOwner) 92 | deleteUser(otherAuthToken, otherUser) 93 | return dispatch(validateDeleteCommentsSuccess()) 94 | } else { 95 | return dispatch( 96 | validateDeleteCommentsError( 97 | new Error("The return from the api was incorrect"), 98 | ), 99 | ) 100 | } 101 | } catch (e) { 102 | return dispatch(validateDeleteCommentsError(new Error(e.message))) 103 | } 104 | } 105 | } 106 | 107 | export function validateDeleteCommentsSuccess() { 108 | return { type: types.VALIDATE_DELETE_COMMENTS_SUCCESS } 109 | } 110 | 111 | export function validateDeleteCommentsError(error) { 112 | return { type: types.VALIDATE_DELETE_COMMENTS_ERROR, error } 113 | } 114 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateErrorHandling.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { beginTicketValidation } from "./validationHelpers" 4 | 5 | export function validateErrorHandling() { 6 | return async dispatch => { 7 | dispatch(beginTicketValidation("ErrorHandling")) 8 | try { 9 | let response = await checkMovieByIDError() 10 | if (response.error !== "Not found") { 11 | throw new Error() 12 | } 13 | return dispatch(validateErrorHandlingSuccess()) 14 | } catch (e) { 15 | return dispatch( 16 | validateErrorHandlingError( 17 | new Error( 18 | "The return from the api was incorrect when providing a bad id to search by", 19 | ), 20 | ), 21 | ) 22 | } 23 | } 24 | } 25 | 26 | export function validateErrorHandlingSuccess() { 27 | return { type: types.VALIDATE_ERROR_HANDLING_SUCCESS } 28 | } 29 | 30 | export function validateErrorHandlingError(error) { 31 | return { type: types.VALIDATE_ERROR_HANDLING_ERROR, error } 32 | } 33 | 34 | /** 35 | * Ticket 15 internal functions 36 | */ 37 | 38 | const checkMovieByIDError = () => { 39 | return request(`/api/v1/movies/id/foobar`, { 40 | method: "GET", 41 | mode: "cors", 42 | }) 43 | .then(res => res) 44 | .catch(error => error) 45 | } 46 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateFacetedSearch.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { 3 | searchByFacetAndPage, 4 | assert, 5 | beginTicketValidation 6 | } from "./validationHelpers" 7 | 8 | export function validateFacetedSearch() { 9 | return async dispatch => { 10 | dispatch(beginTicketValidation("FacetedSearch")) 11 | try { 12 | let facetSearch = await searchFacet() 13 | let facetPagingSearch = await searchFacetPaging() 14 | if ([facetSearch, facetPagingSearch].every(elem => elem)) { 15 | return dispatch(validateFacetedSearchSuccess()) 16 | } 17 | } catch (e) { 18 | return dispatch(validateFacetedSearchError(e)) 19 | } 20 | } 21 | } 22 | 23 | export function validateFacetedSearchSuccess() { 24 | return { type: types.VALIDATE_FACETED_SEARCH_SUCCESS } 25 | } 26 | 27 | export function validateFacetedSearchError(error) { 28 | return { type: types.VALIDATE_FACETED_SEARCH_ERROR, error } 29 | } 30 | 31 | /** 32 | * Ticket 5 internal functions 33 | */ 34 | 35 | const searchFacet = async () => { 36 | try { 37 | let response = await searchByFacetAndPage("Denzel Washington", 0) 38 | let lengthAssertion = assert(20, response.movies.length) 39 | let { rating, runtime } = response.facets 40 | let ratingAssertion = assert(4, rating.length) 41 | let runtimeAssertion = assert(3, runtime.length) 42 | if (lengthAssertion && ratingAssertion && runtimeAssertion) { 43 | return true 44 | } else { 45 | throw new Error( 46 | "Did not receive the proper response when performing a faceted search" 47 | ) 48 | } 49 | } catch (e) { 50 | throw new Error( 51 | "Did not receive the proper response when performing a faceted search" 52 | ) 53 | } 54 | } 55 | 56 | const searchFacetPaging = async () => { 57 | try { 58 | let response = await searchByFacetAndPage("Morgan Freeman", 2) 59 | let lengthAssertion = assert(19, response.movies.length) 60 | let { rating, runtime } = response.facets 61 | let ratingAssertion = assert(3, rating.length) 62 | let runtimeAssertion = assert(4, runtime.length) 63 | if (lengthAssertion && ratingAssertion && runtimeAssertion) { 64 | return true 65 | } else { 66 | throw new Error( 67 | "Did not receive the proper response when performing a faceted search with paging" 68 | ) 69 | } 70 | } catch (e) { 71 | throw new Error( 72 | "Did not receive the proper response when performing a faceted search with paging" 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateGetComments.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { assert, beginTicketValidation, getMovie } from "./validationHelpers" 3 | 4 | // Shrek 2 5 | const movie_id = "573a13a7f29313caabd1aa1f" 6 | 7 | export function validateGetComments() { 8 | return async dispatch => { 9 | try { 10 | dispatch(beginTicketValidation("GetComments")) 11 | let response = await getMovie(movie_id) 12 | let lengthAssertion = assert(response.movie.comments.length, 439) 13 | if (lengthAssertion) { 14 | return dispatch(validateGetCommentsSuccess()) 15 | } else { 16 | return dispatch( 17 | validateGetCommentsError( 18 | new Error("The return from the api was incorrect"), 19 | ), 20 | ) 21 | } 22 | } catch (e) { 23 | return dispatch( 24 | validateGetCommentsError( 25 | new Error("The return from the api was incorrect"), 26 | ), 27 | ) 28 | } 29 | } 30 | } 31 | 32 | export function validateGetCommentsSuccess() { 33 | return { type: types.VALIDATE_GET_COMMENTS_SUCCESS } 34 | } 35 | 36 | export function validateGetCommentsError(error) { 37 | return { type: types.VALIDATE_GET_COMMENTS_ERROR, error } 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateMigration.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { beginTicketValidation, getMovie } from "./validationHelpers" 3 | 4 | let movie_id = "573a1390f29313caabcd4132" 5 | export function validateMigration() { 6 | return async dispatch => { 7 | try { 8 | dispatch(beginTicketValidation("Migration")) 9 | const response = await getMovie(movie_id) 10 | let dateTypes = ["", "java.util.Date", "Date"] 11 | if (dateTypes.indexOf(response.updated_type) > -1) { 12 | return dispatch(validateMigrationSuccess()) 13 | } else { 14 | return dispatch( 15 | validateMigrationError( 16 | new Error( 17 | "It does not appear that you correctly converted the type", 18 | ), 19 | ), 20 | ) 21 | } 22 | } catch (e) { 23 | return dispatch( 24 | validateMigrationError( 25 | new Error("It does not appear that you correctly converted the type"), 26 | ), 27 | ) 28 | } 29 | } 30 | } 31 | 32 | export function validateMigrationSuccess() { 33 | return { type: types.VALIDATE_MIGRATION_SUCCESS } 34 | } 35 | 36 | export function validateMigrationError(error) { 37 | return { type: types.VALIDATE_MIGRATION_ERROR, error } 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/validationActions/validatePOLP.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { beginTicketValidation } from "./validationHelpers" 4 | 5 | export function validatePOLP() { 6 | return async dispatch => { 7 | dispatch(beginTicketValidation("POLP")) 8 | let response = await getUserInfo() 9 | let roleAssertion = response.role === "readWrite" 10 | if (roleAssertion) { 11 | return dispatch(validatePOLPSuccess()) 12 | } else { 13 | return dispatch( 14 | validatePOLPError( 15 | new Error( 16 | "It doesn't appear you have configured the application user", 17 | ), 18 | ), 19 | ) 20 | } 21 | } 22 | } 23 | 24 | export function validatePOLPSuccess() { 25 | return { type: types.VALIDATE_POLP_SUCCESS } 26 | } 27 | 28 | export function validatePOLPError(error) { 29 | return { type: types.VALIDATE_POLP_ERROR, error } 30 | } 31 | 32 | /** 33 | * Ticket 13 internal functions 34 | */ 35 | 36 | const getUserInfo = () => { 37 | return request(`/api/v1/movies/config-options`, { 38 | method: "GET", 39 | mode: "cors", 40 | }) 41 | .then(res => res) 42 | .catch(error => error) 43 | } 44 | -------------------------------------------------------------------------------- /src/actions/validationActions/validatePaging.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { 3 | searchByQueryAndPage, 4 | assert, 5 | beginTicketValidation 6 | } from "./validationHelpers" 7 | 8 | export function validatePaging() { 9 | return async dispatch => { 10 | dispatch(beginTicketValidation("Paging")) 11 | try { 12 | let castPaging0 = await searchByCast() 13 | let castPaging1 = await searchByCastNextPage() 14 | let genrePaging0 = await searchByGenre() 15 | let genrePaging5 = await searchByGenrePage5() 16 | let textPaging0 = await searchByText() 17 | let textPaging7 = await searchByTextPage7() 18 | if ( 19 | [ 20 | castPaging0, 21 | castPaging1, 22 | genrePaging0, 23 | genrePaging5, 24 | textPaging0, 25 | textPaging7 26 | ].every(elem => elem) 27 | ) { 28 | return dispatch(validatePagingSuccess()) 29 | } 30 | } catch (e) { 31 | return dispatch(validatePagingError(e)) 32 | } 33 | } 34 | } 35 | 36 | export function validatePagingSuccess() { 37 | return { type: types.VALIDATE_PAGING_SUCCESS } 38 | } 39 | 40 | export function validatePagingError(error) { 41 | return { type: types.VALIDATE_PAGING_ERROR, error } 42 | } 43 | 44 | /** 45 | * Ticket 6 internal functions 46 | */ 47 | 48 | const searchByCast = async () => { 49 | try { 50 | let response = await searchByQueryAndPage("cast", "Morgan Freeman", 0) 51 | let lengthAssertion = assert(20, response.movies.length) 52 | let movie = response.movies.pop() 53 | let imdb = movie.imdb.id === 428803 54 | let writers = movie.writers.length === 4 55 | let title = movie.title === "March of the Penguins" 56 | if (lengthAssertion && imdb && writers && title) { 57 | return true 58 | } else { 59 | throw new Error("Did not receive the proper response when paging by cast") 60 | } 61 | } catch (e) { 62 | throw new Error("Did not receive the proper response when paging by cast") 63 | } 64 | } 65 | 66 | const searchByCastNextPage = async () => { 67 | try { 68 | let response = await searchByQueryAndPage("cast", "Morgan Freeman", 1) 69 | let lengthAssertion = assert(20, response.movies.length) 70 | let movie = response.movies.pop() 71 | let imdb = movie.imdb.id === 304328 72 | let writers = movie.writers.length === 1 73 | let title = movie.title === "Levity" 74 | if (lengthAssertion && imdb && writers && title) { 75 | return true 76 | } else { 77 | throw new Error("Did not receive the proper response when paging by cast") 78 | } 79 | } catch (e) { 80 | throw new Error("Did not receive the proper response when paging by cast") 81 | } 82 | } 83 | 84 | const searchByGenre = async () => { 85 | try { 86 | let response = await searchByQueryAndPage("genre", "Action", 0) 87 | let lengthAssertion = assert(20, response.movies.length) 88 | let movie = response.movies.pop() 89 | let imdb = movie.imdb.id === 416449 90 | let writers = movie.writers.length === 5 91 | let title = movie.title.toString() === "300" 92 | if (lengthAssertion && imdb && writers && title) { 93 | return true 94 | } else { 95 | throw new Error( 96 | "Did not receive the proper response when paging by genre" 97 | ) 98 | } 99 | } catch (e) { 100 | throw new Error("Did not receive the proper response when paging by genre") 101 | } 102 | } 103 | 104 | const searchByGenrePage5 = async () => { 105 | try { 106 | let response = await searchByQueryAndPage("genre", "Action", 5) 107 | let lengthAssertion = assert(20, response.movies.length) 108 | let movie = response.movies.pop() 109 | let imdb = movie.imdb.id === 1385867 110 | let writers = movie.writers.length === 2 111 | let title = movie.title.toString() === "Cop Out" 112 | if (lengthAssertion && imdb && writers && title) { 113 | return true 114 | } else { 115 | throw new Error( 116 | "Did not receive the proper response when paging by genre" 117 | ) 118 | } 119 | } catch (e) { 120 | throw new Error("Did not receive the proper response when paging by genre") 121 | } 122 | } 123 | 124 | const searchByText = async () => { 125 | try { 126 | let response = await searchByQueryAndPage("text", "Heist", 0) 127 | let lengthAssertion = assert(20, response.movies.length) 128 | let movie = response.movies.pop() 129 | let imdb = movie.imdb.id === 1748197 130 | let writers = movie.writers.length === 2 131 | let title = movie.title.toString() === "Setup" 132 | if (lengthAssertion && imdb && writers && title) { 133 | return true 134 | } else { 135 | throw new Error("Did not receive the proper response when paging by text") 136 | } 137 | } catch (e) { 138 | throw new Error("Did not receive the proper response when paging by text") 139 | } 140 | } 141 | 142 | const searchByTextPage7 = async () => { 143 | try { 144 | let response = await searchByQueryAndPage("text", "Heist", 7) 145 | let lengthAssertion = assert(20, response.movies.length) 146 | let movie = response.movies.pop() 147 | let imdb = movie.imdb.id === 119892 148 | let writers = movie.writers.length === 1 149 | let title = movie.title.toString() === "Phoenix" 150 | if (lengthAssertion && imdb && writers && title) { 151 | return true 152 | } else { 153 | throw new Error("Did not receive the proper response when paging by text") 154 | } 155 | } catch (e) { 156 | throw new Error("Did not receive the proper response when paging by text") 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateProjection.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { assert, beginTicketValidation } from "./validationHelpers" 4 | 5 | export function validateProjection() { 6 | return async dispatch => { 7 | try { 8 | dispatch(beginTicketValidation("Projection")) 9 | let response = await searchByCountry() 10 | let lengthAssertion = assert(710, response.titles.length) 11 | let keysAssertion = assert( 12 | 710, 13 | response.titles.filter(elem => Object.keys(elem).length === 2).length, 14 | ) 15 | if ([lengthAssertion, keysAssertion].every(elem => elem)) { 16 | return dispatch(validateProjectionSuccess()) 17 | } else { 18 | return dispatch( 19 | validateProjectionError( 20 | new Error( 21 | "The return from the api was incorrect when searching by country", 22 | ), 23 | ), 24 | ) 25 | } 26 | } catch (e) { 27 | return dispatch( 28 | validateProjectionError( 29 | new Error( 30 | "The return from the api was incorrect when searching by country", 31 | ), 32 | ), 33 | ) 34 | } 35 | } 36 | } 37 | 38 | export function validateProjectionSuccess() { 39 | return { type: types.VALIDATE_PROJECTION_SUCCESS } 40 | } 41 | 42 | export function validateProjectionError(error) { 43 | return { type: types.VALIDATE_PROJECTION_ERROR, error } 44 | } 45 | 46 | /** 47 | * 2 internal functions 48 | */ 49 | 50 | const searchByCountry = () => { 51 | return request(`/api/v1/movies/countries?countries=Australia`, { 52 | method: "GET", 53 | mode: "cors", 54 | }) 55 | .then(res => res) 56 | .catch(error => error) 57 | } 58 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateTextAndSubfield.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { assert, beginTicketValidation } from "./validationHelpers" 4 | 5 | export function validateTextAndSubfield() { 6 | return async dispatch => { 7 | dispatch(beginTicketValidation("TextAndSubfield")) 8 | try { 9 | let castSearch = await searchGG() 10 | let textSearch = await searchSS() 11 | let genreSearch = await searchReality() 12 | if ([castSearch, textSearch, genreSearch].every(elem => elem)) { 13 | return dispatch(validateTextAndSubfieldSuccess()) 14 | } 15 | } catch (e) { 16 | return dispatch(validateTextAndSubfieldError(e)) 17 | } 18 | } 19 | } 20 | 21 | export function validateTextAndSubfieldSuccess() { 22 | return { type: types.VALIDATE_TEXT_AND_SUBFIELD_SUCCESS } 23 | } 24 | 25 | export function validateTextAndSubfieldError(error) { 26 | return { type: types.VALIDATE_TEXT_AND_SUBFIELD_ERROR, error } 27 | } 28 | 29 | /** 30 | * Ticket 3 internal functions 31 | */ 32 | 33 | const searchByCast = () => { 34 | const griffinGluck = encodeURIComponent("Griffin Gluck") 35 | return request(`/api/v1/movies/search?cast=${griffinGluck}`, { 36 | method: "GET", 37 | mode: "cors", 38 | }) 39 | .then(res => res) 40 | .catch(error => error) 41 | } 42 | 43 | const searchByText = () => { 44 | const shawshank = encodeURI("shawshank") 45 | return request(`/api/v1/movies/search?text=${shawshank}`, { 46 | method: "GET", 47 | mode: "cors", 48 | }) 49 | .then(res => res) 50 | .catch(error => error) 51 | } 52 | 53 | const searchByGenre = () => { 54 | const reality = encodeURI("Reality-TV") 55 | return request(`/api/v1/movies/search?genre=${reality}`, { 56 | method: "GET", 57 | mode: "cors", 58 | }) 59 | .then(res => res) 60 | .catch(error => error) 61 | } 62 | 63 | const searchGG = async () => { 64 | try { 65 | let response = await searchByCast() 66 | let lengthAssertion = assert(1, response.movies.length) 67 | let movie = response.movies.pop() 68 | let imdb = movie.imdb.id === 4981636 69 | let writers = movie.writers.length === 3 70 | let title = movie.title === "Middle School: The Worst Years of My Life" 71 | if (lengthAssertion && imdb && writers && title) { 72 | return true 73 | } else { 74 | throw new Error( 75 | "Did not receive the proper response when searching by cast", 76 | ) 77 | } 78 | } catch (e) { 79 | throw new Error( 80 | "Did not receive the proper response when searching by cast", 81 | ) 82 | } 83 | } 84 | 85 | const searchSS = async () => { 86 | try { 87 | let response = await searchByText() 88 | let lengthAssertion = assert(3, response.movies.length) 89 | let movie = response.movies.pop() 90 | let imdb = movie.imdb.id === 1045642 91 | let writers = movie.writers.length === 3 92 | let title = movie.title === "Tales from the Script" 93 | if (lengthAssertion && imdb && writers && title) { 94 | return true 95 | } else { 96 | throw new Error( 97 | "Did not receive the proper response when searching by text", 98 | ) 99 | } 100 | } catch (e) { 101 | throw new Error( 102 | "Did not receive the proper response when searching by text", 103 | ) 104 | } 105 | } 106 | 107 | const searchReality = async () => { 108 | try { 109 | let response = await searchByGenre() 110 | let lengthAssertion = assert(2, response.movies.length) 111 | let movie = response.movies.pop() 112 | let imdb = movie.imdb.id === 4613322 113 | let writers = movie.writers.length === 1 114 | let title = movie.title === "Louis Theroux: Transgender Kids" 115 | if (lengthAssertion && imdb && writers && title) { 116 | return true 117 | } else { 118 | throw new Error( 119 | "Did not receive the proper response when searching by genre", 120 | ) 121 | } 122 | } catch (e) { 123 | throw new Error( 124 | "Did not receive the proper response when searching by genre", 125 | ) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateTicketSevenActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { beginTicketValidation } from "./validationHelpers" 4 | 5 | const statusOk = status => status === "success" 6 | 7 | export function validateTicketSeven() { 8 | let testUser = genRandomUser() 9 | return async dispatch => { 10 | dispatch(beginTicketValidation("Seven")) 11 | try { 12 | const registerResponse = await register(testUser) 13 | if (!statusOk(registerResponse.status)) { 14 | throw new Error("invalid response to register") 15 | } 16 | const { auth_token } = registerResponse 17 | testUser.preferences = { 18 | favorite_fruit: "watermelon", 19 | favorite_number: "42", 20 | } 21 | 22 | let prefResponse = await updatePreferences(auth_token, testUser) 23 | if (!statusOk(prefResponse.status)) { 24 | throw new Error("invalid response to update preferences") 25 | } 26 | 27 | const { email, password } = testUser 28 | 29 | let loginResponse = await login({ email, password }) 30 | if ( 31 | JSON.stringify(loginResponse.info.preferences) !== 32 | JSON.stringify(testUser.preferences) 33 | ) { 34 | throw new Error("preferences weren't saved correctly") 35 | } 36 | 37 | let deleteResponse = await deleteUser(auth_token, testUser) 38 | if (!statusOk(deleteResponse.status)) { 39 | throw new Error("invalid response to delete") 40 | } 41 | return dispatch(validateTicketSevenSuccess()) 42 | } catch (error) { 43 | return dispatch(validateTicketSevenError(error)) 44 | } 45 | } 46 | } 47 | 48 | export function validateTicketSevenSuccess() { 49 | return { type: types.VALIDATE_TICKET_SEVEN_SUCCESS } 50 | } 51 | 52 | export function validateTicketSevenError(error) { 53 | return { type: types.VALIDATE_TICKET_SEVEN_ERROR, error } 54 | } 55 | 56 | const updatePreferences = (token, user) => { 57 | return request(`/api/v1/user/update-preferences`, { 58 | method: "PUT", 59 | mode: "cors", 60 | headers: { 61 | "content-type": "application/json", 62 | Authorization: `Bearer ${token}`, 63 | }, 64 | body: JSON.stringify({ preferences: user.preferences }), 65 | }) 66 | .then(user => user) 67 | .catch(error => error) 68 | } 69 | 70 | const register = user => { 71 | return request(`/api/v1/user/register`, { 72 | method: "POST", 73 | mode: "cors", 74 | headers: { 75 | "content-type": "application/json", 76 | }, 77 | body: JSON.stringify(user), 78 | }) 79 | .then(user => user) 80 | .catch(error => error) 81 | } 82 | const login = user => { 83 | return request(`/api/v1/user/login`, { 84 | method: "POST", 85 | mode: "cors", 86 | headers: { 87 | "content-type": "application/json", 88 | }, 89 | body: JSON.stringify(user), 90 | }) 91 | .then(user => user) 92 | .catch(error => error) 93 | } 94 | 95 | const deleteUser = (token, user) => { 96 | return request(`/api/v1/user/delete`, { 97 | method: "DELETE", 98 | mode: "cors", 99 | headers: { 100 | "content-type": "application/json", 101 | Authorization: `Bearer ${token}`, 102 | }, 103 | body: JSON.stringify({ password: user.password }), 104 | }) 105 | .then(res => res) 106 | .catch(error => error) 107 | } 108 | 109 | const genRandomUser = () => ({ 110 | name: Math.random() 111 | .toString(36) 112 | .substr(2, 9), 113 | email: `${Math.random() 114 | .toString(36) 115 | .substr(2, 9)}@${Math.random() 116 | .toString(36) 117 | .substr(2, 5)}.${Math.random() 118 | .toString(36) 119 | .substr(2, 3)}`, 120 | password: `${Math.random() 121 | .toString(36) 122 | .substr(2, 9)}`, 123 | }) 124 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateTicketSixActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { beginTicketValidation } from "./validationHelpers" 4 | 5 | const statusOk = status => status === "success" 6 | 7 | export function validateTicketSix() { 8 | let testUser = genRandomUser() 9 | return async dispatch => { 10 | dispatch(beginTicketValidation("Six")) 11 | try { 12 | const registerResponse = await register(testUser) 13 | if (!Object.keys(registerResponse.info).length > 0) { 14 | throw new Error("invalid response to register") 15 | } 16 | const duplicateRegisterResponse = await register(testUser) 17 | if (!statusOk(duplicateRegisterResponse.status)) { 18 | console.log( 19 | `\nHey there! The error response code was expected. 20 | It's us testing if duplicate emails can register. 21 | Great Job!`, 22 | ) 23 | } 24 | if (statusOk(duplicateRegisterResponse.status)) { 25 | throw new Error("duplicate emails should not be allowed") 26 | } 27 | let { auth_token } = registerResponse 28 | let logoutResponse = await logout(auth_token) 29 | if (!statusOk(logoutResponse.status)) { 30 | throw new Error("invalid response to logout") 31 | } 32 | const { email, password } = testUser 33 | const loginResponse = await login({ email, password }) 34 | if (!statusOk(loginResponse.status)) { 35 | throw new Error("invalid response to login") 36 | } 37 | auth_token = loginResponse.auth_token 38 | let deleteResponse = await deleteUser(auth_token, testUser) 39 | if (!statusOk(deleteResponse.status)) { 40 | throw new Error("invalid response to delete") 41 | } 42 | return dispatch(validateTicketSixSuccess()) 43 | } catch (error) { 44 | return dispatch(validateTicketSixError(error)) 45 | } 46 | } 47 | } 48 | 49 | export function validateTicketSixSuccess() { 50 | return { type: types.VALIDATE_TICKET_SIX_SUCCESS } 51 | } 52 | 53 | export function validateTicketSixError(error) { 54 | return { type: types.VALIDATE_TICKET_SIX_ERROR, error } 55 | } 56 | 57 | /** 58 | * Ticket 5 internal functions 59 | */ 60 | 61 | const register = user => { 62 | return request(`/api/v1/user/register`, { 63 | method: "POST", 64 | mode: "cors", 65 | headers: { 66 | "content-type": "application/json", 67 | }, 68 | body: JSON.stringify(user), 69 | }) 70 | .then(user => user) 71 | .catch(error => error) 72 | } 73 | 74 | const login = user => { 75 | return request(`/api/v1/user/login`, { 76 | method: "POST", 77 | mode: "cors", 78 | headers: { 79 | "content-type": "application/json", 80 | }, 81 | body: JSON.stringify(user), 82 | }) 83 | .then(user => user) 84 | .catch(error => error) 85 | } 86 | 87 | const logout = user => { 88 | return request(`/api/v1/user/logout`, { 89 | method: "POST", 90 | mode: "cors", 91 | headers: { 92 | Authorization: `Bearer ${user}`, 93 | "content-type": "application/json", 94 | }, 95 | }) 96 | .then(res => res) 97 | .catch(error => error) 98 | } 99 | 100 | const deleteUser = (token, user) => { 101 | return request(`/api/v1/user/delete`, { 102 | method: "DELETE", 103 | mode: "cors", 104 | headers: { 105 | Authorization: `Bearer ${token}`, 106 | "content-type": "application/json", 107 | }, 108 | body: JSON.stringify({ password: user.password }), 109 | }) 110 | .then(res => res) 111 | .catch(error => error) 112 | } 113 | 114 | const genRandomUser = () => ({ 115 | name: Math.random() 116 | .toString(36) 117 | .substr(2, 9), 118 | email: `${Math.random() 119 | .toString(36) 120 | .substr(2, 9)}@${Math.random() 121 | .toString(36) 122 | .substr(2, 5)}.${Math.random() 123 | .toString(36) 124 | .substr(2, 3)}`, 125 | password: `${Math.random() 126 | .toString(36) 127 | .substr(2, 9)}`, 128 | }) 129 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateTimeouts.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request from "../request" 3 | import { assert, beginTicketValidation } from "./validationHelpers" 4 | 5 | export function validateTimeouts() { 6 | return async dispatch => { 7 | dispatch(beginTicketValidation("Timeouts")) 8 | let response = await getPoolSize() 9 | let timeAssertion = assert(2500, response.wtimeout) 10 | if (timeAssertion) { 11 | return dispatch(validateTimeoutsSuccess()) 12 | } else { 13 | return dispatch( 14 | validateTimeoutsError( 15 | new Error("The return from the api was incorrect"), 16 | ), 17 | ) 18 | } 19 | } 20 | } 21 | 22 | export function validateTimeoutsSuccess() { 23 | return { type: types.VALIDATE_TIMEOUTS_SUCCESS } 24 | } 25 | 26 | export function validateTimeoutsError(error) { 27 | return { type: types.VALIDATE_TIMEOUTS_ERROR, error } 28 | } 29 | 30 | /** 31 | * Ticket 13 internal functions 32 | */ 33 | 34 | const getPoolSize = () => { 35 | return request(`/api/v1/movies/config-options`, { 36 | method: "GET", 37 | mode: "cors", 38 | }) 39 | .then(res => res) 40 | .catch(error => error) 41 | } 42 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateUserManagement.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { 3 | beginTicketValidation, 4 | genRandomUser, 5 | register, 6 | logout, 7 | login, 8 | deleteUser 9 | } from "./validationHelpers" 10 | 11 | export function validateUserManagement() { 12 | let testUser = genRandomUser() 13 | return async dispatch => { 14 | dispatch(beginTicketValidation("UserManagement")) 15 | try { 16 | const registerResponse = await register(testUser) 17 | if (!registerResponse.ok) { 18 | throw new Error("invalid response to register") 19 | } 20 | const duplicateRegisterResponse = await register(testUser) 21 | if (!duplicateRegisterResponse.ok) { 22 | console.log( 23 | `\nHey there! The error response code was expected. 24 | It's us testing if duplicate emails can register. 25 | Great Job!` 26 | ) 27 | } 28 | if (duplicateRegisterResponse.ok) { 29 | throw new Error("duplicate emails should not be allowed") 30 | } 31 | let { auth_token } = registerResponse.json 32 | let logoutResponse = await logout(auth_token) 33 | if (!logoutResponse.ok) { 34 | throw new Error("invalid response to logout") 35 | } 36 | const { email, password } = testUser 37 | const loginResponse = await login({ email, password }) 38 | if (!loginResponse.ok) { 39 | throw new Error("invalid response to login") 40 | } 41 | auth_token = loginResponse.json.auth_token 42 | let deleteResponse = await deleteUser(auth_token, testUser) 43 | if (!deleteResponse.ok) { 44 | throw new Error("invalid response to delete") 45 | } 46 | return dispatch(validateUserManagementSuccess()) 47 | } catch (error) { 48 | return dispatch(validateUserManagementError(error)) 49 | } 50 | } 51 | } 52 | 53 | export function validateUserManagementSuccess() { 54 | return { type: types.VALIDATE_USER_MANAGEMENT_SUCCESS } 55 | } 56 | 57 | export function validateUserManagementError(error) { 58 | return { type: types.VALIDATE_USER_MANAGEMENT_ERROR, error } 59 | } 60 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateUserPreferences.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import { requestWithStatus } from "../request" 3 | import { 4 | beginTicketValidation, 5 | genRandomUser, 6 | register, 7 | login, 8 | deleteUser, 9 | } from "./validationHelpers" 10 | 11 | export function validateUserPreferences() { 12 | let testUser = genRandomUser() 13 | return async dispatch => { 14 | dispatch(beginTicketValidation("UserPreferences")) 15 | try { 16 | const registerResponse = await register(testUser) 17 | if (!registerResponse.ok) { 18 | throw new Error("invalid response to register") 19 | } 20 | const { auth_token } = registerResponse.json 21 | testUser.preferences = { 22 | favorite_fruit: "watermelon", 23 | favorite_number: "42", 24 | } 25 | 26 | let prefResponse = await updatePreferences(auth_token, testUser) 27 | if (!prefResponse.ok) { 28 | throw new Error("invalid response to update preferences") 29 | } 30 | 31 | const { email, password } = testUser 32 | 33 | let loginResponse = await login({ email, password }) 34 | if (!loginResponse.ok){ 35 | throw new Error("invalid response to update preferences - login of user failed") 36 | } 37 | // let's check if the paiload of the response was correctly sent back by the app 38 | if ( 39 | loginResponse.json === undefined || 40 | loginResponse.json.info === undefined ){ 41 | throw new Error("invalid response for user preferences") 42 | } 43 | if ( 44 | JSON.stringify(loginResponse.json.info.preferences) !== 45 | JSON.stringify(testUser.preferences) 46 | ) { 47 | throw new Error("preferences weren't saved correctly") 48 | } 49 | 50 | let deleteResponse = await deleteUser(auth_token, testUser) 51 | if (!deleteResponse.ok) { 52 | throw new Error("invalid response to delete") 53 | } 54 | return dispatch(validateUserPreferencesSuccess()) 55 | } catch (error) { 56 | return dispatch(validateUserPreferencesError(error)) 57 | } 58 | } 59 | } 60 | 61 | export function validateUserPreferencesSuccess() { 62 | return { type: types.VALIDATE_USER_PREFERENCES_SUCCESS } 63 | } 64 | 65 | export function validateUserPreferencesError(error) { 66 | return { type: types.VALIDATE_USER_PREFERENCES_ERROR, error } 67 | } 68 | 69 | const updatePreferences = (token, user) => { 70 | return requestWithStatus(`/api/v1/user/update-preferences`, { 71 | method: "PUT", 72 | mode: "cors", 73 | headers: { 74 | "content-type": "application/json", 75 | Authorization: `Bearer ${token}`, 76 | }, 77 | body: JSON.stringify({ preferences: user.preferences }), 78 | }) 79 | .then(user => user) 80 | .catch(error => error) 81 | } 82 | -------------------------------------------------------------------------------- /src/actions/validationActions/validateUserReport.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes" 2 | import request, { requestWithStatus } from "../request" 3 | import { 4 | beginTicketValidation, 5 | genRandomUser, 6 | deleteUser, 7 | } from "./validationHelpers" 8 | 9 | const invalid_auth_token = 10 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MjIxNzI3NzMsIm5iZiI6MTUyMjE3Mjc3MywianRpIjoiYjFlYmI0ZDQtNjZlZS00MTY4LTg0MWQtZGNhODJkMThmN2NhIiwiZXhwIjoxNTIyMTczNjczLCJpZGVudGl0eSI6eyJlbWFpbCI6ImZvb2JhekBiYXIuY29tIiwibmFtZSI6ImZvbyBiYXIiLCJwYXNzd29yZCI6bnVsbCwicHJlZmVyZW5jZXMiOnsiZmF2b3JpdGVfY2FzdCI6Ik1lZyBSeWFuIiwicHJlZmVycmVkX2xhbmd1YWdlIjoiRW5nbGlzaCJ9fSwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIiwidXNlcl9jbGFpbXMiOnsidXNlciI6eyJlbWFpbCI6ImZvb2JhekBiYXIuY29tIiwibmFtZSI6ImZvbyBiYXIiLCJwYXNzd29yZCI6bnVsbCwicHJlZmVyZW5jZXMiOnsiZmF2b3JpdGVfY2FzdCI6Ik1lZyBSeWFuIiwicHJlZmVycmVkX2xhbmd1YWdlIjoiRW5nbGlzaCJ9fX19.q9z_tG7gEqaRMfrbTpj9Jz52vocqOBWgEpCd3KC6giI" 11 | 12 | // the martian 13 | export function validateUserReport() { 14 | return async dispatch => { 15 | try { 16 | dispatch(beginTicketValidation("UserReport")) 17 | let testUser = genRandomUser() 18 | const registerResponse = await register(testUser) 19 | const { auth_token } = registerResponse 20 | const badReportRequest = await getUserReport(invalid_auth_token) 21 | const goodReportRequest = await getUserReport(auth_token) 22 | if (badReportRequest.ok) { 23 | throw new Error("Invalid response to bad user report request") 24 | } 25 | if ( 26 | !goodReportRequest.ok || 27 | goodReportRequest.json.report.length !== 20 28 | ) { 29 | throw new Error("Invalid response to good user report request") 30 | } 31 | deleteUser(auth_token, testUser) 32 | return dispatch(validateUserReportSuccess()) 33 | } catch (e) { 34 | return dispatch(validateUserReportError(new Error(e.message))) 35 | } 36 | } 37 | } 38 | 39 | export function validateUserReportSuccess() { 40 | return { type: types.VALIDATE_USER_REPORT_SUCCESS } 41 | } 42 | 43 | export function validateUserReportError(error) { 44 | return { type: types.VALIDATE_USER_REPORT_ERROR, error } 45 | } 46 | 47 | /** 48 | * Ticket 11 internal functions 49 | */ 50 | 51 | const getUserReport = token => { 52 | return requestWithStatus(`/api/v1/user/comment-report`, { 53 | method: "GET", 54 | mode: "cors", 55 | headers: { 56 | Authorization: `Bearer ${token}`, 57 | "content-type": "application/json", 58 | }, 59 | }) 60 | .then(res => res) 61 | .catch(error => error) 62 | } 63 | 64 | const register = user => { 65 | return request(`/api/v1/user/make-admin`, { 66 | method: "POST", 67 | mode: "cors", 68 | headers: { 69 | "content-type": "application/json", 70 | }, 71 | body: JSON.stringify(user), 72 | }) 73 | .then(user => user) 74 | .catch(error => error) 75 | } 76 | -------------------------------------------------------------------------------- /src/actions/validationActions/validationHelpers.js: -------------------------------------------------------------------------------- 1 | import request, { requestWithStatus } from "../request" 2 | import * as types from "../actionTypes" 3 | 4 | export const searchByQueryAndPage = (which, query, page) => { 5 | const encodedQuery = encodeURIComponent(query) 6 | return request( 7 | `/api/v1/movies/search?${which}=${encodedQuery}&page=${page}`, 8 | { 9 | method: "GET", 10 | mode: "cors", 11 | }, 12 | ) 13 | .then(res => res) 14 | .catch(error => error) 15 | } 16 | 17 | export const checkMovieByIDError = () => { 18 | return requestWithStatus(`/api/v1/movies/id/foobar`, { 19 | method: "GET", 20 | mode: "cors", 21 | }) 22 | .then(res => res) 23 | .catch(error => error) 24 | } 25 | 26 | export const searchByFacetAndPage = (query, page) => { 27 | const encodedQuery = encodeURIComponent(query) 28 | return request( 29 | `/api/v1/movies/facet-search?cast=${encodedQuery}&page=${page}`, 30 | { 31 | method: "GET", 32 | mode: "cors", 33 | }, 34 | ) 35 | .then(res => res) 36 | .catch(error => error) 37 | } 38 | 39 | export const assert = (expected, actual) => expected === actual 40 | 41 | export function beginTicketValidation(ticket) { 42 | return { type: types.VALIDATING_TICKET, ticket } 43 | } 44 | 45 | export const genRandomUser = () => ({ 46 | name: Math.random() 47 | .toString(36) 48 | .substr(2, 9), 49 | email: `${Math.random() 50 | .toString(36) 51 | .substr(2, 9)}@${Math.random() 52 | .toString(36) 53 | .substr(2, 5)}.${Math.random() 54 | .toString(36) 55 | .substr(2, 3)}`, 56 | password: `${Math.random() 57 | .toString(36) 58 | .substr(2, 9)}`, 59 | }) 60 | 61 | export const deleteUser = (token, user) => { 62 | return requestWithStatus(`/api/v1/user/delete`, { 63 | method: "DELETE", 64 | mode: "cors", 65 | headers: { 66 | Authorization: `Bearer ${token}`, 67 | "content-type": "application/json", 68 | }, 69 | body: JSON.stringify({ password: user.password }), 70 | }) 71 | .then(res => res) 72 | .catch(error => error) 73 | } 74 | 75 | export const logout = user => { 76 | return requestWithStatus(`/api/v1/user/logout`, { 77 | method: "POST", 78 | mode: "cors", 79 | headers: { 80 | Authorization: `Bearer ${user}`, 81 | "content-type": "application/json", 82 | }, 83 | }) 84 | .then(res => res) 85 | .catch(error => error) 86 | } 87 | 88 | export const login = user => { 89 | return requestWithStatus(`/api/v1/user/login`, { 90 | method: "POST", 91 | mode: "cors", 92 | headers: { 93 | "content-type": "application/json", 94 | }, 95 | body: JSON.stringify(user), 96 | }) 97 | .then(user => user) 98 | .catch(error => error) 99 | } 100 | 101 | export const register = user => { 102 | return requestWithStatus(`/api/v1/user/register`, { 103 | method: "POST", 104 | mode: "cors", 105 | headers: { 106 | "content-type": "application/json", 107 | }, 108 | body: JSON.stringify(user), 109 | }) 110 | .then(user => user) 111 | .catch(error => error) 112 | } 113 | 114 | export function getMovie(id) { 115 | return request(`/api/v1/movies/id/${id}`, { 116 | method: "GET", 117 | mode: "cors", 118 | }) 119 | .then(res => res) 120 | .catch(error => error) 121 | } 122 | 123 | export function submitComment(movieID, comment, token) { 124 | return requestWithStatus(`/api/v1/movies/comment`, { 125 | method: "POST", 126 | mode: "cors", 127 | headers: { 128 | Authorization: `Bearer ${token}`, 129 | "content-type": "application/json", 130 | }, 131 | body: JSON.stringify({ 132 | movie_id: movieID, 133 | comment, 134 | }), 135 | }) 136 | .then(json => json) 137 | .catch(e => e) 138 | } 139 | 140 | export function editComment(commentID, update, token, movie_id) { 141 | return requestWithStatus(`/api/v1/movies/comment`, { 142 | method: "PUT", 143 | mode: "cors", 144 | headers: { 145 | Authorization: `Bearer ${token}`, 146 | "content-type": "application/json", 147 | }, 148 | body: JSON.stringify({ 149 | comment_id: commentID, 150 | updated_comment: update, 151 | movie_id, 152 | }), 153 | }) 154 | .then(json => json) 155 | .catch(e => e) 156 | } 157 | 158 | export function deleteComment(comment_id, token, movie_id) { 159 | return requestWithStatus(`/api/v1/movies/comment`, { 160 | method: "DELETE", 161 | mode: "cors", 162 | headers: { 163 | Authorization: `Bearer ${token}`, 164 | "content-type": "application/json", 165 | }, 166 | body: JSON.stringify({ 167 | comment_id, 168 | movie_id, 169 | }), 170 | }) 171 | .then(json => json) 172 | .catch(e => e) 173 | } 174 | -------------------------------------------------------------------------------- /src/assets/mongoleaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb-university/mflix-ui/45c0daeec2766dcb665871da6fc88ccc12219305/src/assets/mongoleaf.png -------------------------------------------------------------------------------- /src/assets/pixelatedLeaf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/Account.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { connect } from "react-redux" 5 | import { bindActionCreators } from "redux" 6 | import * as userActions from "../actions/userActions" 7 | import { compose } from "redux" 8 | import { withRouter } from "react-router-dom" 9 | import green from "@material-ui/core/colors/green" 10 | import Input from "@material-ui/core/Input" 11 | import InputLabel from "@material-ui/core/InputLabel" 12 | import FormControl from "@material-ui/core/FormControl" 13 | import Button from "@material-ui/core/Button" 14 | import { Link } from "react-router-dom" 15 | 16 | const styles = theme => ({ 17 | root: { 18 | display: "flex", 19 | background: "black", 20 | height: "100%", 21 | justifyContent: "space-around", 22 | width: "100vw", 23 | textAlign: "center", 24 | flexDirection: "row", 25 | flexFlow: "wrap", 26 | }, 27 | half: { 28 | marginTop: "65px", 29 | minWidth: "450px", 30 | maxWidth: "45%", 31 | flexDirection: "column", 32 | alignItems: "center", 33 | flex: "0 0 auto", 34 | height: "100vh", 35 | }, 36 | accountDelete: {}, 37 | preferenceSelection: { 38 | display: "inline-flex", 39 | justifyContent: "center", 40 | width: "35vw", 41 | background: "#242424", 42 | padding: "10px", 43 | }, 44 | formControl: { 45 | margin: theme.spacing.unit, 46 | }, 47 | formLabel: { 48 | color: "white", 49 | }, 50 | checked: { 51 | color: green[500], 52 | "& + $bar": { 53 | backgroundColor: green[500], 54 | }, 55 | }, 56 | inputContainer: { 57 | display: "flex", 58 | justifyContent: "center", 59 | background: "#242424", 60 | }, 61 | bar: {}, 62 | buttonSave: { 63 | margin: theme.spacing.unit - 2, 64 | height: "18px", 65 | color: "white", 66 | background: green[500], 67 | }, 68 | }) 69 | 70 | class Account extends Component { 71 | constructor(props) { 72 | super(props) 73 | this.handleSelect = this.handleSelect.bind(this) 74 | this.handleChange = this.handleChange.bind(this) 75 | this.savePrefs = this.savePrefs.bind(this) 76 | this.state = { 77 | ...props.user.info.preferences, 78 | } 79 | } 80 | 81 | preferenceMapping = { 82 | preferred_language: "Preferred Language", 83 | favorite_cast: "Favorite Cast", 84 | } 85 | 86 | textPreferences = ["preferred_language", "favorite_cast"] 87 | 88 | handleSelect = name => event => { 89 | this.props.userActions.updatePrefs( 90 | { [name]: event.target.checked }, 91 | this.props.user 92 | ) 93 | } 94 | 95 | handleChange = event => { 96 | this.setState({ [event.target.id]: event.target.value }) 97 | } 98 | 99 | savePrefs() { 100 | this.props.userActions.updatePrefs(this.state, this.props.user) 101 | } 102 | 103 | loadSelectPrefs() {} 104 | 105 | loadTextPrefs() { 106 | const { classes, user } = this.props 107 | const prefs = Object.keys(user.info.preferences).filter(key => 108 | this.textPreferences.includes(key) 109 | ) 110 | return prefs.map(key => { 111 | return ( 112 |
113 | 114 | 115 | {this.preferenceMapping[key]} 116 | 117 | 123 | 124 |
125 | ) 126 | }) 127 | } 128 | 129 | render() { 130 | const { classes, user } = this.props 131 | return ( 132 |
133 |
134 |

Hello {user.info.name}

135 | {this.loadTextPrefs()} 136 | 141 |
142 |
143 | ) 144 | } 145 | } 146 | 147 | Account.propTypes = { 148 | classes: PropTypes.object.isRequired, 149 | } 150 | 151 | const mapStateToProps = ({ user }) => ({ user }) 152 | 153 | function mapDispatchToProps(dispatch) { 154 | return { 155 | userActions: bindActionCreators(userActions, dispatch), 156 | } 157 | } 158 | 159 | export default compose( 160 | withRouter, 161 | withStyles(styles), 162 | connect( 163 | mapStateToProps, 164 | mapDispatchToProps 165 | ) 166 | )(Account) 167 | -------------------------------------------------------------------------------- /src/components/AccountPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { Link } from "react-router-dom" 5 | import Button from "@material-ui/core/Button" 6 | import grey from "@material-ui/core/colors/grey" 7 | import { connect } from "react-redux" 8 | import { compose } from "redux" 9 | import { bindActionCreators } from "redux" 10 | import * as userActions from "../actions/userActions" 11 | import { withRouter } from "react-router-dom" 12 | 13 | const mongoGrey = grey[900] 14 | 15 | const styles = theme => ({ 16 | buttonStyle: { 17 | margin: theme.spacing.unit - 2, 18 | height: "18px", 19 | color: "white", 20 | background: mongoGrey, 21 | }, 22 | root: { 23 | alignItems: "center", 24 | }, 25 | }) 26 | 27 | class AccountPanel extends Component { 28 | constructor(props) { 29 | super(props) 30 | this.logout = this.logout.bind(this) 31 | } 32 | 33 | logout() { 34 | this.props.userActions.logout( 35 | this.props.user.auth_token, 36 | this.props.history 37 | ) 38 | } 39 | 40 | clickAdmin() { 41 | this.props.userActions.checkAdminStatus(this.props.user) 42 | } 43 | 44 | render() { 45 | const { classes, user } = this.props 46 | const LoginLogout = !user.loggedIn ? ( 47 | 48 | 49 | 50 | ) : ( 51 | 52 | 55 | 56 | ) 57 | 58 | const RegisterName = !user.loggedIn ? ( 59 | 60 | 61 | 62 | ) : ( 63 | 64 | 65 | 66 | ) 67 | const AdminButton = user.loggedIn && 68 | user.info.isAdmin && ( 69 | 70 | 76 | 77 | ) 78 | return ( 79 |
80 | {AdminButton} 81 | 82 | 83 | 84 | {LoginLogout} 85 | {RegisterName} 86 |
87 | ) 88 | } 89 | } 90 | 91 | AccountPanel.propTypes = { 92 | classes: PropTypes.object.isRequired, 93 | } 94 | 95 | const mapStateToProps = ({ user }) => ({ user }) 96 | 97 | const mapDispatchToProps = dispatch => { 98 | return { 99 | userActions: bindActionCreators(userActions, dispatch), 100 | } 101 | } 102 | 103 | export default compose( 104 | withRouter, 105 | withStyles(styles), 106 | connect( 107 | mapStateToProps, 108 | mapDispatchToProps 109 | ) 110 | )(AccountPanel) 111 | -------------------------------------------------------------------------------- /src/components/AppDrawer.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import Drawer from "@material-ui/core/Drawer" 5 | import Divider from "@material-ui/core/Divider" 6 | import SubfieldSearch from "./SubfieldSearch" 7 | import { connect } from "react-redux" 8 | import { bindActionCreators } from "redux" 9 | import * as miscActions from "../actions/miscActions" 10 | import { compose } from "redux" 11 | 12 | const styles = theme => ({ 13 | root: { 14 | display: "flex", 15 | flexDirection: "column", 16 | background: "#262626", 17 | height: "100vh", 18 | }, 19 | divider: { 20 | marginTop: "15px", 21 | }, 22 | }) 23 | 24 | class AppDrawer extends React.Component { 25 | render() { 26 | const { classes } = this.props 27 | 28 | const sideList = ( 29 |
30 | 31 | 32 | 33 |
34 | ) 35 | 36 | return ( 37 | 41 |
42 | {sideList} 43 |
44 |
45 | ) 46 | } 47 | } 48 | 49 | AppDrawer.propTypes = { 50 | classes: PropTypes.object.isRequired, 51 | } 52 | 53 | function mapStateToProps({ misc, movies: { facets, facetFilters } }) { 54 | return { 55 | misc, 56 | } 57 | } 58 | 59 | function mapDispatchToProps(dispatch) { 60 | return { 61 | miscActions: bindActionCreators(miscActions, dispatch), 62 | } 63 | } 64 | 65 | export default compose( 66 | withStyles(styles), 67 | connect( 68 | mapStateToProps, 69 | mapDispatchToProps 70 | ) 71 | )(AppDrawer) 72 | -------------------------------------------------------------------------------- /src/components/CommentCard.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import Card from "@material-ui/core/Card" 5 | import CardHeader from "@material-ui/core/CardHeader" 6 | import CardContent from "@material-ui/core/CardContent" 7 | import Avatar from "@material-ui/core/Avatar" 8 | import green from "@material-ui/core/colors/green" 9 | import red from "@material-ui/core/colors/red" 10 | import Button from "@material-ui/core/Button" 11 | 12 | const styles = theme => ({ 13 | card: { 14 | width: "65vw", 15 | borderRadius: "5px", 16 | margin: "1%", 17 | }, 18 | avatar: { 19 | backgroundColor: green[500], 20 | }, 21 | typography: { 22 | textAlign: "justify", 23 | }, 24 | buttons: { 25 | display: "inline-flex", 26 | flexDirection: "row", 27 | width: "100%", 28 | justifyContent: "flex-end", 29 | }, 30 | buttonSubmit: { 31 | margin: theme.spacing.unit - 2, 32 | height: "18px", 33 | color: "white", 34 | background: green[500], 35 | }, 36 | buttonDelete: { 37 | margin: theme.spacing.unit - 2, 38 | height: "18px", 39 | color: "white", 40 | background: red[500], 41 | }, 42 | }) 43 | 44 | class CommentCard extends React.Component { 45 | state = { 46 | editing: false, 47 | } 48 | 49 | handleUpdate() { 50 | this.props.handleUpdate(this.props.cid, this.divComment.innerText) 51 | } 52 | 53 | handleDelete() { 54 | this.props.handleDelete(this.props.cid) 55 | } 56 | 57 | handleEdit() { 58 | this.setState({ editing: true }) 59 | } 60 | render() { 61 | const { classes } = this.props 62 | return ( 63 |
64 | 65 | 68 | U 69 | 70 | } 71 | title={this.props.name} 72 | subheader={this.props.date} 73 | /> 74 | 75 |
{ 77 | this.divComment = divComment 78 | }} 79 | className={classes.typography} 80 | contentEditable={this.props.editable} 81 | > 82 | {this.props.text} 83 |
84 |
85 | {this.props.editable && ( 86 |
87 | 94 | 100 |
101 | )} 102 |
103 |
104 | ) 105 | } 106 | } 107 | 108 | CommentCard.propTypes = { 109 | classes: PropTypes.object.isRequired, 110 | name: PropTypes.string.isRequired, 111 | date: PropTypes.string.isRequired, 112 | text: PropTypes.string.isRequired, 113 | } 114 | 115 | export default withStyles(styles)(CommentCard) 116 | -------------------------------------------------------------------------------- /src/components/Errors.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { bindActionCreators } from "redux"; 4 | import * as movieActions from "../actions/movieActions"; 5 | import { clearError } from "../actions/miscActions"; 6 | import { compose } from "redux"; 7 | import ErrorsDiv from "./ErrorsDiv"; 8 | 9 | class Errors extends Component { 10 | render() { 11 | const { errors } = this.props; 12 | 13 | let errMsgs = Object.keys(errors) 14 | .filter(key => errors[key] !== "") 15 | .map(key => { 16 | return ( 17 |
25 | 30 |
31 | ); 32 | }); 33 | return {errMsgs}; 34 | } 35 | } 36 | 37 | function mapStateToProps({ errors }) { 38 | return { 39 | errors 40 | }; 41 | } 42 | 43 | function mapDispatchToProps(dispatch) { 44 | return { 45 | movieActions: bindActionCreators(movieActions, dispatch), 46 | clearError: bindActionCreators(clearError, dispatch) 47 | }; 48 | } 49 | 50 | export default compose( 51 | connect( 52 | mapStateToProps, 53 | mapDispatchToProps 54 | ) 55 | )(Errors); 56 | -------------------------------------------------------------------------------- /src/components/ErrorsDiv.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ErrorsDiv = props => { 4 | return ( 5 |
12 | props.dismiss(props.error)} 15 | > 16 | cancel 17 | 18 | {props.msg} 19 |
20 | ); 21 | }; 22 | 23 | export default ErrorsDiv; 24 | -------------------------------------------------------------------------------- /src/components/Facets.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { connect } from "react-redux" 5 | import { bindActionCreators } from "redux" 6 | import * as movieActions from "../actions/movieActions" 7 | import { compose } from "redux" 8 | import FormGroup from "@material-ui/core/FormGroup" 9 | import FormControlLabel from "@material-ui/core/FormControlLabel" 10 | import Checkbox from "@material-ui/core/Checkbox" 11 | import green from "@material-ui/core/colors/green" 12 | 13 | const styles = { 14 | root: { 15 | display: "flex", 16 | flexDirection: "row", 17 | justifyContent: "center", 18 | alignItems: "center", 19 | }, 20 | facets: { 21 | alignItems: "center", 22 | background: "black", 23 | color: "white", 24 | }, 25 | checked: { 26 | color: green[500], 27 | }, 28 | label: { 29 | color: green[500], 30 | }, 31 | } 32 | 33 | class Facets extends Component { 34 | constructor(props) { 35 | super(props) 36 | this.ratingFacet = this.ratingFacet.bind(this) 37 | this.runtimeFacet = this.runtimeFacet.bind(this) 38 | this.handleRatingFacetSelection = this.handleRatingFacetSelection.bind(this) 39 | this.handleRuntimeFacetSelection = this.handleRuntimeFacetSelection.bind( 40 | this 41 | ) 42 | } 43 | formGroup(facet, elem, label, fn) { 44 | return ( 45 | 46 | 55 | } 56 | label={label} 57 | /> 58 | 59 | ) 60 | } 61 | runtimeFacet() { 62 | const { classes } = this.props 63 | const { runtime } = this.props.facets 64 | return ( 65 |
66 |

Runtime:

67 | {runtime.map(elem => { 68 | switch (elem._id + "") { 69 | case "0": 70 | return this.formGroup( 71 | "runtime", 72 | elem, 73 | `0-59 (${elem.count})`, 74 | this.handleRuntimeFacetSelection 75 | ) 76 | case "60": 77 | return this.formGroup( 78 | "runtime", 79 | elem, 80 | `60-89 (${elem.count})`, 81 | this.handleRuntimeFacetSelection 82 | ) 83 | case "90": 84 | return this.formGroup( 85 | "runtime", 86 | elem, 87 | `90-119 (${elem.count})`, 88 | this.handleRuntimeFacetSelection 89 | ) 90 | case "120": 91 | return this.formGroup( 92 | "runtime", 93 | elem, 94 | `120-180 (${elem.count})`, 95 | this.handleRuntimeFacetSelection 96 | ) 97 | case "180": 98 | return this.formGroup( 99 | "runtime", 100 | elem, 101 | `180+ (${elem.count})`, 102 | this.handleRuntimeFacetSelection 103 | ) 104 | default: 105 | return this.formGroup( 106 | "runtime", 107 | elem, 108 | `other (${elem.count})`, 109 | this.handleRuntimeFacetSelection 110 | ) 111 | } 112 | })} 113 |
114 | ) 115 | } 116 | ratingFacet() { 117 | const { classes } = this.props 118 | const { rating } = this.props.facets 119 | return ( 120 |
121 |

Rating:

122 | {rating.map(elem => { 123 | switch (elem._id + "") { 124 | case "0": 125 | return this.formGroup( 126 | "rating", 127 | elem, 128 | `0-49 (${elem.count})`, 129 | this.handleRatingFacetSelection 130 | ) 131 | case "50": 132 | return this.formGroup( 133 | "rating", 134 | elem, 135 | `50-69 (${elem.count})`, 136 | this.handleRatingFacetSelection 137 | ) 138 | case "70": 139 | return this.formGroup( 140 | "rating", 141 | elem, 142 | `70-89 (${elem.count})`, 143 | this.handleRatingFacetSelection 144 | ) 145 | case "90": 146 | return this.formGroup( 147 | "rating", 148 | elem, 149 | `90+ (${elem.count})`, 150 | this.handleRatingFacetSelection 151 | ) 152 | default: 153 | return this.formGroup( 154 | "rating", 155 | elem, 156 | `other (${elem.count})`, 157 | this.handleRatingFacetSelection 158 | ) 159 | } 160 | })} 161 |
162 | ) 163 | } 164 | handleRatingFacetSelection = name => event => { 165 | let filter 166 | switch (name + "") { 167 | case "0": 168 | filter = movie => 169 | movie.metacritic && (movie.metacritic >= 0 && movie.metacritic < 50) 170 | break 171 | case "50": 172 | filter = movie => 173 | movie.metacritic && (movie.metacritic >= 50 && movie.metacritic < 70) 174 | break 175 | 176 | case "70": 177 | filter = movie => 178 | movie.metacritic && (movie.metacritic >= 70 && movie.metacritic < 90) 179 | break 180 | case "90": 181 | filter = movie => movie.metacritic && movie.metacritic >= 90 182 | break 183 | 184 | default: 185 | filter = movie => 186 | !movie.metacritic || typeof movie.metacritic === "string" 187 | } 188 | this.props.movieActions.applyFacetFilter("rating", name, filter) 189 | } 190 | 191 | handleRuntimeFacetSelection = name => event => { 192 | let filter 193 | switch (name + "") { 194 | case "0": 195 | filter = movie => 196 | movie.runtime && (movie.runtime >= 0 && movie.runtime < 60) 197 | break 198 | case "60": 199 | filter = movie => 200 | movie.runtime && (movie.runtime >= 60 && movie.runtime < 90) 201 | break 202 | 203 | case "90": 204 | filter = movie => 205 | movie.runtime && (movie.runtime >= 90 && movie.runtime < 120) 206 | break 207 | case "120": 208 | filter = movie => 209 | movie.runtime && (movie.runtime >= 120 && movie.runtime < 180) 210 | break 211 | 212 | case "180": 213 | filter = movie => movie.runtime && movie.runtime >= 180 214 | break 215 | 216 | default: 217 | filter = movie => 218 | !movie.runtime || (!movie.runtime < 0 && movie.runtime <= Infinity) 219 | } 220 | this.props.movieActions.applyFacetFilter("runtime", name, filter) 221 | } 222 | 223 | render() { 224 | const { classes } = this.props 225 | const ratingFacet = this.ratingFacet() 226 | const runtimeFacet = this.runtimeFacet() 227 | return ( 228 |
229 |
230 | {Object.keys(this.props.facets.rating).length > 0 && ratingFacet} 231 |
232 |
233 | {Object.keys(this.props.facets.runtime).length > 0 && runtimeFacet} 234 |
235 |
236 | ) 237 | } 238 | } 239 | 240 | Facets.propTypes = { 241 | classes: PropTypes.object.isRequired, 242 | } 243 | 244 | function mapStateToProps({ misc, movies: { facets, facetFilters } }) { 245 | return { 246 | facets, 247 | facetFilters, 248 | } 249 | } 250 | 251 | function mapDispatchToProps(dispatch) { 252 | return { 253 | movieActions: bindActionCreators(movieActions, dispatch), 254 | } 255 | } 256 | 257 | export default compose( 258 | withStyles(styles), 259 | connect( 260 | mapStateToProps, 261 | mapDispatchToProps 262 | ) 263 | )(Facets) 264 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import Typography from "@material-ui/core/Typography" 5 | import AccountPanel from "./AccountPanel" 6 | import IconButton from "@material-ui/core/IconButton" 7 | import SearchIcon from "@material-ui/icons/Search" 8 | import { Link } from "react-router-dom" 9 | import green from "@material-ui/core/colors/green" 10 | import leaf from "../assets/mongoleaf.png" 11 | import { connect } from "react-redux" 12 | import { bindActionCreators } from "redux" 13 | import * as miscActions from "../actions/miscActions" 14 | import { compose } from "redux" 15 | 16 | const mongo = green[500] 17 | 18 | const styles = { 19 | root: { 20 | borderBottom: "1px solid gray", 21 | }, 22 | drawer: { 23 | display: "inline-flex", 24 | alignItems: "center", 25 | color: "white", 26 | }, 27 | appbar: { 28 | display: "flex", 29 | height: "120px", 30 | width: "100vw", 31 | background: "#000000", 32 | justifyContent: "space-around", 33 | flexFlow: "wrap", 34 | alignItems: "center", 35 | }, 36 | typography: { 37 | textAlign: "center", 38 | fontSize: "3em", 39 | color: mongo, 40 | fontWeight: "600", 41 | lineHeight: "1.125", 42 | marginLeft: "270px", 43 | fontFamily: 44 | "BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif", 45 | }, 46 | leaf: { 47 | img: { 48 | height: "3em", 49 | }, 50 | }, 51 | } 52 | 53 | function Header(props) { 54 | const { classes } = props 55 | return ( 56 |
57 |
58 | 62 | 63 | 64 | 65 | 66 | mflix 67 | leaf 74 | 75 | 76 | 77 |
78 |
79 | ) 80 | } 81 | 82 | Header.propTypes = { 83 | classes: PropTypes.object.isRequired, 84 | } 85 | 86 | function mapStateToProps({ misc }) { 87 | return { 88 | misc, 89 | } 90 | } 91 | 92 | function mapDispatchToProps(dispatch) { 93 | return { 94 | miscActions: bindActionCreators(miscActions, dispatch), 95 | } 96 | } 97 | 98 | export default compose( 99 | withStyles(styles), 100 | connect( 101 | mapStateToProps, 102 | mapDispatchToProps 103 | ) 104 | )(Header) 105 | -------------------------------------------------------------------------------- /src/components/LoginCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import { connect } from "react-redux"; 5 | import { bindActionCreators } from "redux"; 6 | import * as userActions from "../actions/userActions"; 7 | import { compose } from "redux"; 8 | import IconButton from "@material-ui/core/IconButton"; 9 | import Input from "@material-ui/core/Input"; 10 | import InputLabel from "@material-ui/core/InputLabel"; 11 | import InputAdornment from "@material-ui/core/InputAdornment"; 12 | import FormControl from "@material-ui/core/FormControl"; 13 | import Visibility from "@material-ui/icons/Visibility"; 14 | import VisibilityOff from "@material-ui/icons/VisibilityOff"; 15 | import Email from "@material-ui/icons/Email"; 16 | import { Link } from "react-router-dom"; 17 | import Button from "@material-ui/core/Button"; 18 | import { withRouter } from "react-router-dom"; 19 | 20 | import green from "@material-ui/core/colors/green"; 21 | 22 | const mongo = green[500]; 23 | 24 | const styles = theme => ({ 25 | root: { 26 | justifyContent: "center", 27 | backgroundColor: "black", 28 | alignContent: "center", 29 | width: "100vw", 30 | height: "100vh", 31 | display: "flex" 32 | }, 33 | form: { 34 | display: "inline-flex", 35 | flexDirection: "column", 36 | color: "white", 37 | margin: "3%", 38 | padding: "25px", 39 | background: "#363636", 40 | marginTop: "5%", 41 | borderRadius: "8px", 42 | width: "320px", 43 | height: "450px" 44 | }, 45 | input: { 46 | color: "white", 47 | background: "#e0e0e0" 48 | }, 49 | newUser: { 50 | margin: theme.spacing.unit, 51 | color: "white" 52 | }, 53 | inputStyle: { 54 | fontSize: "18px", 55 | color: "white", 56 | borderRadius: "4px" 57 | }, 58 | buttonOk: { 59 | margin: theme.spacing.unit, 60 | height: "18px", 61 | color: "white", 62 | background: mongo, 63 | alignSelf: "flex-end" 64 | }, 65 | buttonNope: { 66 | margin: theme.spacing.unit, 67 | height: "18px", 68 | color: "white", 69 | background: "red", 70 | alignSelf: "flex-end" 71 | }, 72 | buttonRow: { 73 | margin: theme.spacing.unit, 74 | marginTop: "auto", 75 | display: "inline-flex", 76 | flexDirection: "row", 77 | alignSelf: "flex-end", 78 | justifyContent: "flex-end" 79 | } 80 | }); 81 | 82 | class LoginCard extends Component { 83 | constructor(props) { 84 | super(props); 85 | this.state = { 86 | email: "", 87 | password: "", 88 | showPassword: false, 89 | emailReadOnly: true, 90 | passwordReadOnly: true 91 | }; 92 | 93 | this.handleSubmit = this.handleSubmit.bind(this); 94 | } 95 | 96 | handleSubmit(event) { 97 | event.preventDefault(); 98 | this.props.userActions.login( 99 | { 100 | password: this.state.password, 101 | email: this.state.email 102 | }, 103 | this.props.history 104 | ); 105 | } 106 | 107 | handleChange = prop => event => { 108 | this.setState({ [prop]: event.target.value }); 109 | }; 110 | 111 | handleMouseDownPassword = event => { 112 | event.preventDefault(); 113 | }; 114 | 115 | handleClickShowPasssword = () => { 116 | this.setState({ showPassword: !this.state.showPassword }); 117 | }; 118 | 119 | handleFocusEmail = () => { 120 | this.setState({ emailReadOnly: false }); 121 | }; 122 | 123 | handleFocusPassword = () => { 124 | this.setState({ passwordReadOnly: false }); 125 | }; 126 | 127 | render() { 128 | const { classes } = this.props; 129 | 130 | return ( 131 |
132 |
133 |
134 |

Existing User?

135 |

136 | Sign in below. Don't have an account?{" "} 137 | 141 | Click here 142 | 143 |

144 |
145 | 146 | 147 | 148 | 149 | Email 150 | 151 | 162 | 163 | 164 | 165 | 166 | } 167 | /> 168 | 169 | 170 | 171 | Password 172 | 173 | 184 | 189 | {this.state.showPassword ? ( 190 | 191 | ) : ( 192 | 193 | )} 194 | 195 | 196 | } 197 | /> 198 | 199 |
200 | 205 | 208 |
209 |
210 |
211 | ); 212 | } 213 | } 214 | 215 | LoginCard.propTypes = { 216 | classes: PropTypes.object.isRequired 217 | }; 218 | 219 | function mapStateToProps({ user }) { 220 | return { 221 | user 222 | }; 223 | } 224 | 225 | function mapDispatchToProps(dispatch) { 226 | return { 227 | userActions: bindActionCreators(userActions, dispatch) 228 | }; 229 | } 230 | 231 | export default compose( 232 | withRouter, 233 | withStyles(styles), 234 | connect( 235 | mapStateToProps, 236 | mapDispatchToProps 237 | ) 238 | )(LoginCard); 239 | -------------------------------------------------------------------------------- /src/components/MovieDetail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import green from "@material-ui/core/colors/green" 5 | import { connect } from "react-redux" 6 | import { bindActionCreators } from "redux" 7 | import * as movieActions from "../actions/movieActions" 8 | import { compose } from "redux" 9 | import RatingBar from "./RatingBar" 10 | import Viewer from "./ViewModal" 11 | import Button from "@material-ui/core/Button" 12 | import CommentCard from "./CommentCard" 13 | import PostComment from "./PostComment" 14 | import { withRouter } from "react-router-dom" 15 | const mongo = green[500] 16 | 17 | const getRunTime = runtime => 18 | `${Math.floor(runtime / 60)} hr ${runtime % 60} min` 19 | 20 | const packageRatings = movie => 21 | Object.keys(movie).reduce((acc, key) => { 22 | switch (key) { 23 | case "imdb": 24 | if (movie[key].rating) { 25 | return { 26 | ...acc, 27 | imdb: { 28 | [key]: movie[key].rating, 29 | backgroundColor: "#3273dc", 30 | total: movie[key].votes, 31 | }, 32 | } 33 | } else { 34 | return acc 35 | } 36 | case "metacritic": 37 | return { 38 | ...acc, 39 | metacritic: { 40 | [key]: movie[key], 41 | backgroundColor: mongo, 42 | }, 43 | } 44 | case "tomatoes": 45 | 46 | if (movie[key] && movie[key].viewer && movie[key].viewer.meter) { 47 | return { 48 | ...acc, 49 | tomatoes: { 50 | [key]: movie[key].viewer.meter, 51 | backgroundColor: "red", 52 | total: movie[key].viewer.numReviews, 53 | }, 54 | } 55 | } else { 56 | return acc 57 | } 58 | default: 59 | return acc 60 | } 61 | }, {}) 62 | 63 | const styles = { 64 | root: { 65 | display: "flex", 66 | background: "black", 67 | justifyContent: "space-around", 68 | width: "100vw", 69 | textAlign: "center", 70 | flexDirection: "row", 71 | flexFlow: "wrap", 72 | }, 73 | half: { 74 | marginTop: "65px", 75 | minWidth: "450px", 76 | maxWidth: "45%", 77 | flexDirection: "column", 78 | alignItems: "center", 79 | flex: "0 0 auto", 80 | height: "100vh", 81 | }, 82 | img: { 83 | width: "300px", 84 | height: "444px", 85 | }, 86 | watchButton: { 87 | margin: "8px", 88 | color: "white", 89 | alignItems: "center", 90 | }, 91 | button: { 92 | height: "18px", 93 | color: "white", 94 | background: mongo, 95 | }, 96 | title: { 97 | color: "white", 98 | fontWeight: 320, 99 | lineHeight: 1.125, 100 | fontSize: "2em", 101 | margin: "15px", 102 | }, 103 | runtime: { 104 | color: "black", 105 | fontSize: "12px", 106 | background: "#d5d5d5", 107 | padding: "5px", 108 | margin: "15px", 109 | borderRadius: "4px", 110 | }, 111 | director: { 112 | color: "white", 113 | marginTop: "20px", 114 | margin: "15px", 115 | }, 116 | directorText: { 117 | color: mongo, 118 | marginLeft: "5px", 119 | background: "#474747", 120 | padding: "5px", 121 | borderRadius: "5px", 122 | }, 123 | plotContainer: { 124 | display: "inline-flex", 125 | justifyContent: "center", 126 | background: "#363636", 127 | width: "100%", 128 | padding: "10px 0", 129 | borderRadius: "7px", 130 | marginTop: "15px", 131 | textAlign: "center", 132 | }, 133 | plot: { 134 | margin: "15px", 135 | color: "white", 136 | fontSize: "1rem", 137 | lineHeight: "1.5em", 138 | width: "80%", 139 | height: "80%", 140 | textAlign: "justify", 141 | }, 142 | year: { 143 | borderRadius: "290486px", 144 | background: "#363636", 145 | padding: ".25em .75em", 146 | marginRight: "4px", 147 | color: "#E0E0E0", 148 | fontSize: ".9rem", 149 | }, 150 | rating: { 151 | borderRadius: "290486px", 152 | background: "#ffdd57", 153 | padding: ".25em .75em", 154 | marginLeft: "4px", 155 | color: "black", 156 | fontSize: ".9rem", 157 | }, 158 | cast: { 159 | color: "#E0E0E0", 160 | padding: "0 15px", 161 | fontWeight: 300, 162 | lineHeight: 1.2, 163 | fontSize: "18px", 164 | }, 165 | skittlesHeader: { 166 | color: "white", 167 | marginBottom: "10px", 168 | }, 169 | skittlesContainer: { 170 | display: "flex", 171 | flexDirection: "row", 172 | alignItems: "flex-start", 173 | justifyContent: "center", 174 | color: "white", 175 | }, 176 | genresSkittles: { 177 | color: "white", 178 | fontSize: "12px", 179 | background: "#363636", 180 | padding: "5px", 181 | margin: "0 5px", 182 | borderRadius: "4px", 183 | float: "left", 184 | "&:hover": { 185 | textDecoration: "underline", 186 | cursor: "pointer", 187 | }, 188 | }, 189 | castSkittles: { 190 | color: "white", 191 | fontSize: "12px", 192 | background: mongo, 193 | padding: "5px", 194 | margin: "0 5px", 195 | borderRadius: "4px", 196 | float: "left", 197 | "&:hover": { 198 | textDecoration: "underline", 199 | cursor: "pointer", 200 | }, 201 | }, 202 | writerSkittles: { 203 | color: "white", 204 | fontSize: "12px", 205 | background: "#363636", 206 | padding: "5px", 207 | margin: "0 5px", 208 | borderRadius: "4px", 209 | float: "left", 210 | }, 211 | } 212 | 213 | class MovieDetail extends Component { 214 | constructor(props) { 215 | super(props) 216 | this.handleViewClick = this.handleViewClick.bind(this) 217 | this.handleSearch = this.handleSearch.bind(this) 218 | this.handleUpdate = this.handleUpdate.bind(this) 219 | this.handleDelete = this.handleDelete.bind(this) 220 | this.imgError = this.imgError.bind(this) 221 | this.state = { 222 | matrix: false, 223 | } 224 | } 225 | 226 | rain = null 227 | makeRainTimeout = null 228 | 229 | makeRain() { 230 | // Easter Egg Matrix rain when a user clicks on one of the matrix movies 231 | // getting the 2d context of the canvas element, set through the ref fn 232 | const c = this.canvas 233 | const ctx = c.getContext("2d") 234 | // chinese characters to use for the rain itself 235 | const chinese = Array.from( 236 | "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑" 237 | ) 238 | 239 | let font_size = 10 240 | // columns is the width of the canvas divided by font size 241 | let columns = this.canvas.width / font_size 242 | // drops array, one "drop" per column 243 | let drops = new Array(columns).fill(1) 244 | 245 | function draw() { 246 | // setting the background to black 247 | ctx.fillStyle = "rgba(0, 0, 0, 0.05)" 248 | // filling the canvas with the black background 249 | ctx.fillRect(0, 0, c.width, c.height) 250 | // green color for the drops 251 | ctx.fillStyle = "#0F0" 252 | // setting the font 253 | ctx.font = font_size + "px arial" 254 | // iterate through the drops array 255 | drops.forEach((elem, i) => { 256 | // select a random chinese character for the rain 257 | let text = chinese[Math.floor(Math.random() * chinese.length)] 258 | // set the position of the letter (text, x, y) 259 | // so each iteration, draw the letter in the specific column at an ever 260 | // increasing y position, making it move down the canvas 261 | ctx.fillText(text, i * font_size, elem * font_size) 262 | // begin randomizing the rain after the initial waterfall effect 263 | if (elem * font_size > c.height && Math.random() > 0.975) { 264 | drops[i] = 0 265 | } 266 | // increase y position by 1 for next animation loop to move the letter 267 | drops[i]++ 268 | }) 269 | } 270 | // run the animation loop every 33 milliseconds 271 | this.rain = setInterval(draw, 33) 272 | } 273 | 274 | matrixCheck() { 275 | const matrices = [ 276 | "573a13a2f29313caabd0b8f3", 277 | "573a139bf29313caabcf3d23", 278 | "573a13a3f29313caabd0d923", 279 | "573a13a7f29313caabd1a006", 280 | ] 281 | if (this.props.movie._id && matrices.includes(this.props.movie._id)) { 282 | this.makeRainTimeout = setTimeout(() => { 283 | this.makeRain() 284 | }, 1500) 285 | } 286 | } 287 | 288 | componentDidMount() { 289 | this.props.movieActions.fetchMovieByID(this.props.id, this.props.history) 290 | window.scrollTo(0, 0) 291 | const ctx = this.canvas.getContext("2d") 292 | const img = this.poster 293 | 294 | img.onload = () => { 295 | ctx.drawImage(img, 0, 0, 300, 444) 296 | this.matrixCheck() 297 | } 298 | } 299 | 300 | componentWillUnmount() { 301 | clearTimeout(this.rain) 302 | clearTimeout(this.makeRainTimeout) 303 | } 304 | 305 | imgError(id) { 306 | this.matrixCheck() 307 | let ctx = this.canvas.getContext("2d") 308 | ctx.font = "20pt Calibri" 309 | ctx.textAlign = "center" 310 | ctx.fillStyle = "white" 311 | ctx.fillText("Image failed to load", 150, 222) 312 | } 313 | 314 | handleUpdate(id, text) { 315 | this.props.movieActions.editComment( 316 | id, 317 | text, 318 | this.props.user.auth_token, 319 | this.props.movie._id 320 | ) 321 | } 322 | 323 | handleDelete(id) { 324 | this.props.movieActions.deleteComment( 325 | id, 326 | this.props.user.auth_token, 327 | this.props.movie._id 328 | ) 329 | } 330 | 331 | handleSearch(subfield, e) { 332 | this.props.movieActions.searchMovies( 333 | subfield, 334 | e.target.innerHTML, 335 | this.props.history 336 | ) 337 | } 338 | 339 | handleViewClick() { 340 | this.props.movieActions.viewMovie() 341 | } 342 | 343 | matrixInterval = null 344 | 345 | render() { 346 | const { classes, movie } = this.props 347 | 348 | const comments = movie.comments && ( 349 |
350 |

Comments

351 | 352 | {movie.comments.map(c => { 353 | return ( 354 | 364 | ) 365 | })} 366 |
367 | ) 368 | 369 | const runtime = movie.runtime && ( 370 | 371 | Runtime:{" "} 372 | {getRunTime(movie.runtime)} 373 | 374 | ) 375 | 376 | const directors = movie.directors && ( 377 |
378 | 379 | Directed by{" "} 380 | {movie.directors.map((elem, ix) => ( 381 | 382 | {elem} 383 | 384 | ))} 385 | 386 |
387 | ) 388 | 389 | const plot = 390 | movie.fullplot || movie.plot ? ( 391 |
392 |
{movie.fullplot || movie.plot}
393 |
394 | ) : ( 395 | "" 396 | ) 397 | 398 | const genres = movie.genres ? ( 399 |
400 |

Genres

401 |
402 | {movie.genres.map((elem, ix) => ( 403 | this.handleSearch("genre", e)} 407 | > 408 | {elem} 409 | 410 | ))} 411 |
412 |
413 | ) : ( 414 | "" 415 | ) 416 | 417 | const cast = movie.cast ? ( 418 |
419 |

Cast

420 |
421 | {movie.cast.map((elem, ix) => ( 422 | this.handleSearch("cast", e)} 426 | > 427 | {elem} 428 | 429 | ))} 430 |
431 |
432 | ) : ( 433 | "" 434 | ) 435 | 436 | const writers = movie.writers ? ( 437 |
438 |

Writers

439 |
440 | {movie.writers.map((elem, ix) => ( 441 | 442 | {elem} 443 | 444 | ))} 445 |
446 |
447 | ) : ( 448 | "" 449 | ) 450 | return ( 451 |
452 |
453 | 454 |
455 |

{movie.title}

456 |
457 | {movie.year} 458 | {movie.rated && ( 459 | {movie.rated} 460 | )} 461 |
462 | {directors} 463 | {runtime} 464 | {plot} 465 | 466 |
467 |
468 | { 472 | this.canvas = canvas 473 | }} 474 | > 475 | { 477 | this.poster = poster 478 | }} 479 | src={movie.poster || ""} 480 | alt={movie.title} 481 | onError={() => this.imgError()} 482 | /> 483 | 484 |
485 | 488 |
489 | {genres} 490 | {cast} 491 | {writers} 492 |
493 | {comments} 494 |
495 |
496 | ) 497 | } 498 | } 499 | 500 | MovieDetail.propTypes = { 501 | classes: PropTypes.object.isRequired, 502 | } 503 | 504 | function mapStateToProps({ movies: { movie, viewMovie }, user }, { match }) { 505 | return { 506 | movie, 507 | id: match.params.id, 508 | displayModal: viewMovie, 509 | user, 510 | } 511 | } 512 | 513 | function mapDispatchToProps(dispatch) { 514 | return { 515 | movieActions: bindActionCreators(movieActions, dispatch), 516 | } 517 | } 518 | 519 | export default compose( 520 | withRouter, 521 | withStyles(styles), 522 | connect( 523 | mapStateToProps, 524 | mapDispatchToProps 525 | ) 526 | )(MovieDetail) 527 | -------------------------------------------------------------------------------- /src/components/MovieTile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import green from "@material-ui/core/colors/green" 5 | import { Link } from "react-router-dom" 6 | import { connect } from "react-redux" 7 | import { bindActionCreators } from "redux" 8 | import * as movieActions from "../actions/movieActions" 9 | import { compose } from "redux" 10 | 11 | const mongo = green[500] 12 | 13 | const getScoreBackground = score => { 14 | if (score >= 8) { 15 | return { backgroundColor: mongo } 16 | } 17 | if (score >= 6) { 18 | return { backgroundColor: "#3273dc" } 19 | } 20 | if (score) { 21 | return { backgroundColor: "red" } 22 | } 23 | return { backgroundColor: "rgba(0, 0, 0, 0)" } 24 | } 25 | 26 | const styles = { 27 | tile: { 28 | display: "inline-flex", 29 | background: "#242424", 30 | margin: "1vw", 31 | height: "675px", 32 | width: "320px", 33 | borderRadius: 4, 34 | flexDirection: "column", 35 | alignItems: "center", 36 | textAlign: "center", 37 | }, 38 | img: { 39 | margin: "15px", 40 | alignSelf: "flex-center", 41 | width: "90%", 42 | height: "400px", 43 | }, 44 | title: { 45 | color: mongo, 46 | fontWeight: 320, 47 | lineHeight: 1.125, 48 | fontSize: "1.125em", 49 | margin: "10px", 50 | fontFamily: 51 | "BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif", 52 | }, 53 | infoContainer: { 54 | margin: "15px", 55 | }, 56 | year: { 57 | borderRadius: "100%", 58 | background: "#363636", 59 | padding: ".25em .75em", 60 | marginRight: "4px", 61 | color: "#E0E0E0", 62 | fontSize: ".9rem", 63 | fontFamily: 64 | "Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif", 65 | }, 66 | rating: { 67 | borderRadius: "290486px", 68 | background: "#ffdd57", 69 | padding: ".25em .75em", 70 | marginLeft: "4px", 71 | color: "black", 72 | fontSize: ".9rem", 73 | fontFamily: 74 | "Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif", 75 | }, 76 | cast: { 77 | color: "#E0E0E0", 78 | padding: "0 15px", 79 | fontWeight: 300, 80 | lineHeight: 1.2, 81 | fontSize: "18px", 82 | }, 83 | imdb: { 84 | color: "#e0e0e0", 85 | fontSize: "14px", 86 | }, 87 | scoreBackground: { 88 | color: "#e0e0e0", 89 | padding: "0 10px", 90 | borderRadius: "4px", 91 | fontSize: "14px", 92 | }, 93 | } 94 | 95 | class MovieTile extends Component { 96 | constructor(props) { 97 | super(props) 98 | this.handleClick = this.handleClick.bind(this) 99 | } 100 | 101 | imgEvent({ id, imgError }) { 102 | let img = document.getElementById(id) 103 | let canvas = img.parentNode 104 | let ctx = canvas.getContext("2d") 105 | if (imgError) { 106 | ctx.font = "20pt Calibri" 107 | ctx.textAlign = "center" 108 | ctx.fillStyle = "white" 109 | ctx.fillText("Image failed to load", 150, 222) 110 | } else { 111 | ctx.drawImage(img, 0, 0, 300, 444) 112 | } 113 | } 114 | 115 | handleClick() { 116 | this.props.movieActions.movieDetail(this.props.movie._id) 117 | } 118 | render() { 119 | const { classes, movie } = this.props 120 | const castText = movie.cast ? `Starring: ${movie.cast.join(", ")}` : "" 121 | const imdb = 122 | movie.imdb && movie.imdb.rating ? `IMDB: ${movie.imdb.rating} / 10` : "" 123 | return ( 124 |
125 | 126 | 127 | {movie.title} this.imgEvent({ id: movie._id, imgError: false })} 134 | onError={() => this.imgEvent({ id: movie._id, imgError: true })} 135 | /> 136 | 137 |

{movie.title}

138 |
139 | {movie.year} 140 | {movie.rated && ( 141 | {movie.rated} 142 | )} 143 |
144 |

{castText}

145 |
146 | {imdb && ( 147 | 151 | {imdb} 152 | 153 | )} 154 |
155 | 156 |
157 | ) 158 | } 159 | } 160 | 161 | MovieTile.propTypes = { 162 | movie: PropTypes.object.isRequired, 163 | } 164 | 165 | function mapDispatchToProps(dispatch) { 166 | return { 167 | movieActions: bindActionCreators(movieActions, dispatch), 168 | } 169 | } 170 | 171 | export default compose( 172 | withStyles(styles), 173 | connect( 174 | () => ({}), 175 | mapDispatchToProps 176 | ) 177 | )(MovieTile) 178 | -------------------------------------------------------------------------------- /src/components/PostComment.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { connect } from "react-redux" 5 | import { bindActionCreators } from "redux" 6 | import * as movieActions from "../actions/movieActions" 7 | import { compose } from "redux" 8 | import Card from "@material-ui/core/Card" 9 | import CardHeader from "@material-ui/core/CardHeader" 10 | import CardContent from "@material-ui/core/CardContent" 11 | import green from "@material-ui/core/colors/green" 12 | import Button from "@material-ui/core/Button" 13 | 14 | const styles = theme => ({ 15 | card: { 16 | width: "65vw", 17 | borderRadius: "5px", 18 | margin: "1%", 19 | }, 20 | avatar: { 21 | backgroundColor: green[500], 22 | }, 23 | typography: { 24 | textAlign: "justify", 25 | width: "100%", 26 | height: "100%", 27 | margin: "2% auto", 28 | border: "1px solid blue", 29 | }, 30 | buttonDiv: { 31 | display: "inline-flex", 32 | flexDirection: "row", 33 | width: "100%", 34 | justifyContent: "flex-end", 35 | }, 36 | buttonSubmit: { 37 | margin: theme.spacing.unit - 2, 38 | height: "18px", 39 | color: "white", 40 | background: green[500], 41 | }, 42 | }) 43 | 44 | class PostComment extends React.Component { 45 | constructor(props) { 46 | super(props) 47 | this.handleSubmit = this.handleSubmit.bind(this) 48 | } 49 | 50 | handleSubmit() { 51 | this.props.movieActions.submitComment( 52 | this.props.movieID, 53 | this.divComment.innerText, 54 | this.props.auth_token 55 | ) 56 | this.divComment.innerText = "" 57 | } 58 | 59 | render() { 60 | const { classes } = this.props 61 | 62 | return ( 63 |
64 | 65 | 66 | 67 |
{ 71 | this.divComment = divComment 72 | }} 73 | /> 74 | 75 |
76 | 82 |
83 | 84 |
85 | ) 86 | } 87 | } 88 | 89 | PostComment.propTypes = { 90 | classes: PropTypes.object.isRequired, 91 | } 92 | 93 | function mapStateToProps({ user }) { 94 | return { 95 | auth_token: user.auth_token, 96 | } 97 | } 98 | 99 | function mapDispatchToProps(dispatch) { 100 | return { 101 | movieActions: bindActionCreators(movieActions, dispatch), 102 | } 103 | } 104 | 105 | export default compose( 106 | withStyles(styles), 107 | connect( 108 | mapStateToProps, 109 | mapDispatchToProps 110 | ) 111 | )(PostComment) 112 | -------------------------------------------------------------------------------- /src/components/RatingBar.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | 5 | const getScoreBackground = score => { 6 | let normalized = score 7 | if (score <= 10) { 8 | normalized = score * 10 9 | } 10 | if (normalized >= 80) { 11 | return { 12 | width: `${normalized}%`, 13 | } 14 | } 15 | if (normalized >= 60) { 16 | return { 17 | width: `${normalized}%`, 18 | } 19 | } 20 | if (normalized) { 21 | return { 22 | width: `${normalized}%`, 23 | } 24 | } 25 | } 26 | 27 | const styles = theme => ({ 28 | progressBar: { 29 | marginTop: "15px", 30 | height: "20px", 31 | width: "100%", 32 | background: "#555", 33 | borderRadius: "25px", 34 | boxShadow: "inset 0 -1px 1px rgba(255, 255, 255, 0.3)", 35 | "& > span": { 36 | display: "block", 37 | height: "100%", 38 | borderTopLeftRadius: "20px", 39 | borderBottomLeftRadius: "20px", 40 | backgroundImage: 41 | "linear-gradient(center bottom, rgb(43,194,83) 37%, rgb(84,240,84) 69%)", 42 | boxShadow: 43 | "inset 0 2px 9px rgba(255,255,255,0.3), inset 0 -2px 6px rgba(0,0,0,0.4)", 44 | overflow: "hidden", 45 | }, 46 | }, 47 | }) 48 | 49 | const RatingBar = props => { 50 | const { classes, ratings } = props 51 | const bars = Object.keys(ratings).map((elem, ix) => { 52 | let info = getScoreBackground(ratings[elem][elem]) 53 | let displayName = elem.charAt(0).toUpperCase() + elem.slice(1) 54 | let stats = ratings[elem].total && ratings[elem].total 55 | return ( 56 |
57 |

58 | {displayName} Rating: {ratings[elem][elem].toLocaleString()}{" "} 59 | {stats && `(from ${new Intl.NumberFormat().format(stats)} reviews)`} 60 |

61 |
62 | 68 |

78 |

79 |
80 | ) 81 | }) 82 | 83 | return
{bars}
84 | } 85 | 86 | RatingBar.propTypes = { 87 | classes: PropTypes.object.isRequired, 88 | } 89 | 90 | export default withStyles(styles)(RatingBar) 91 | -------------------------------------------------------------------------------- /src/components/SignupCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import { connect } from "react-redux"; 5 | import { bindActionCreators } from "redux"; 6 | import * as userActions from "../actions/userActions"; 7 | import { compose } from "redux"; 8 | import IconButton from "@material-ui/core/IconButton"; 9 | import Input from "@material-ui/core/Input"; 10 | import InputLabel from "@material-ui/core/InputLabel"; 11 | import InputAdornment from "@material-ui/core/InputAdornment"; 12 | import FormControl from "@material-ui/core/FormControl"; 13 | import Visibility from "@material-ui/icons/Visibility"; 14 | import VisibilityOff from "@material-ui/icons/VisibilityOff"; 15 | import Email from "@material-ui/icons/Email"; 16 | import AccountCircle from "@material-ui/icons/AccountCircle"; 17 | import Button from "@material-ui/core/Button"; 18 | import { Link } from "react-router-dom"; 19 | import { withRouter } from "react-router-dom"; 20 | 21 | import green from "@material-ui/core/colors/green"; 22 | 23 | const mongo = green[500]; 24 | 25 | const styles = theme => ({ 26 | root: { 27 | justifyContent: "center", 28 | backgroundColor: "black", 29 | alignContent: "center", 30 | width: "100vw", 31 | height: "100vh", 32 | display: "flex" 33 | }, 34 | form: { 35 | display: "inline-flex", 36 | flexDirection: "column", 37 | color: "white", 38 | margin: "3%", 39 | padding: "25px", 40 | background: "#363636", 41 | marginTop: "5%", 42 | borderRadius: "8px", 43 | width: "320px", 44 | height: "450px" 45 | }, 46 | input: { 47 | color: "white" 48 | }, 49 | newUser: { 50 | margin: theme.spacing.unit, 51 | color: "white" 52 | }, 53 | inputStyle: { 54 | fontSize: "18px", 55 | color: "white", 56 | borderRadius: "4px" 57 | }, 58 | buttonOk: { 59 | margin: theme.spacing.unit, 60 | height: "18px", 61 | color: "white", 62 | background: mongo, 63 | alignSelf: "flex-end" 64 | }, 65 | buttonNope: { 66 | margin: theme.spacing.unit, 67 | height: "18px", 68 | color: "white", 69 | background: "red", 70 | alignSelf: "flex-end" 71 | }, 72 | buttonRow: { 73 | margin: theme.spacing.unit, 74 | marginTop: "auto", 75 | display: "inline-flex", 76 | flexDirection: "row", 77 | alignSelf: "flex-end", 78 | justifyContent: "flex-end" 79 | } 80 | }); 81 | 82 | class SignupCard extends Component { 83 | state = { 84 | name: "", 85 | email: "", 86 | password: "", 87 | showPassword: false 88 | }; 89 | 90 | handleSubmit = event => { 91 | event.preventDefault(); 92 | this.props.userActions.register( 93 | { 94 | name: this.state.name, 95 | email: this.state.email, 96 | password: this.state.password 97 | }, 98 | this.props.history 99 | ); 100 | }; 101 | 102 | handleChange = prop => event => { 103 | this.setState({ [prop]: event.target.value }); 104 | }; 105 | 106 | handleMouseDownPassword = event => { 107 | event.preventDefault(); 108 | }; 109 | 110 | handleClickShowPasssword = () => { 111 | this.setState({ showPassword: !this.state.showPassword }); 112 | }; 113 | 114 | render() { 115 | const { classes } = this.props; 116 | 117 | return ( 118 |
119 |
120 |
121 |

New User?

122 |

Make an account by filling out the form below.

123 |
124 | 125 | 126 | 127 | Name 128 | 129 | 138 | 139 | 140 | 141 | 142 | } 143 | /> 144 | 145 | 146 | 147 | Email 148 | 149 | 158 | 159 | 160 | 161 | 162 | } 163 | /> 164 | 165 | 166 | 167 | Password 168 | 169 | 178 | 182 | {this.state.showPassword ? ( 183 | 184 | ) : ( 185 | 186 | )} 187 | 188 | 189 | } 190 | /> 191 | 192 |
193 | 198 | 201 |
202 |
203 |
204 | ); 205 | } 206 | } 207 | 208 | SignupCard.propTypes = { 209 | classes: PropTypes.object.isRequired 210 | }; 211 | 212 | function mapStateToProps({ user }) { 213 | return { 214 | user 215 | }; 216 | } 217 | 218 | function mapDispatchToProps(dispatch) { 219 | return { 220 | userActions: bindActionCreators(userActions, dispatch) 221 | }; 222 | } 223 | 224 | export default compose( 225 | withRouter, 226 | withStyles(styles), 227 | connect( 228 | mapStateToProps, 229 | mapDispatchToProps 230 | ) 231 | )(SignupCard); 232 | -------------------------------------------------------------------------------- /src/components/Status.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { connect } from "react-redux" 5 | import { bindActionCreators } from "redux" 6 | import * as validationActions from "../actions/validationActions/index" 7 | import { compose } from "redux" 8 | import pixelLeaf from "../assets/pixelatedLeaf.svg" 9 | import Snackbar from "@material-ui/core/Snackbar" 10 | import TicketValidator from "./TicketValidator" 11 | 12 | const styles = theme => ({ 13 | root: { 14 | flex: 1, 15 | flexWrap: "wrap", 16 | justifyContent: "center", 17 | backgroundColor: "black", 18 | width: "100vw", 19 | minHeight: "100vh", 20 | height: "100%", 21 | flexBasis: 0, 22 | textAlign: "center", 23 | paddingTop: "15px", 24 | alignItems: "center" 25 | }, 26 | inner: { 27 | color: "red", 28 | fontSize: "64px", 29 | fontFamily: "'Press Start 2P', cursive", 30 | textAlign: "center", 31 | textStroke: "1px", 32 | textShadow: 33 | "3px 3px 0 green, -1px -1px 0 blue, 1px -1px 0 blue, -1px 1px 0 blue, 1px 1px 0 blue", 34 | paddingTop: "15px", 35 | animation: "blink 1s linear 3 forwards" 36 | }, 37 | leaf: { 38 | marginTop: "15px", 39 | animation: "spinningLeaf 2s linear 0s infinite" 40 | } 41 | }) 42 | 43 | class Status extends Component { 44 | interval = null 45 | timeout = null 46 | state = { 47 | startValidation: false, 48 | open: false 49 | } 50 | constructor(props) { 51 | super(props) 52 | this.onClickValidate = this.onClickValidate.bind(this) 53 | } 54 | 55 | componentDidMount() { 56 | this.interval = setInterval(() => { 57 | this.leaf.style.opacity -= 0.01 58 | }, 30) 59 | this.timeout = setTimeout(() => { 60 | this.readyName.style.display = "none" 61 | this.leaf.style.display = "none" 62 | clearInterval(this.interval) 63 | this.setState({ startValidation: true }) 64 | }, 3500) 65 | } 66 | 67 | componentWillUnmount() { 68 | clearInterval(this.interval) 69 | clearTimeout(this.timeout) 70 | } 71 | 72 | onClickValidate(ticket) { 73 | this.props.validationActions[`validate${ticket}`]() 74 | } 75 | 76 | copied = () => { 77 | this.setState({ open: true }) 78 | } 79 | 80 | handleClose = () => { 81 | this.setState({ open: false }) 82 | } 83 | 84 | render() { 85 | const playerOne = this.props.user.loggedIn 86 | ? `Ready ${this.props.user.info.name}` 87 | : "Player One" 88 | 89 | const Connection = ( 90 | 100 | ) 101 | const Projection = ( 102 | 112 | ) 113 | const TextAndSubfield = ( 114 | 124 | ) 125 | const Paging = ( 126 | 136 | ) 137 | const FacetedSearch = ( 138 | 148 | ) 149 | 150 | const UserManagement = ( 151 | 161 | ) 162 | 163 | const UserPreferences = ( 164 | 174 | ) 175 | 176 | const GetComments = ( 177 | 187 | ) 188 | 189 | const CreateUpdateComments = ( 190 | 202 | ) 203 | 204 | const DeleteComments = ( 205 | 215 | ) 216 | 217 | const UserReport = ( 218 | 228 | ) 229 | 230 | const Migration = ( 231 | 241 | ) 242 | 243 | const ConnectionPooling = ( 244 | 254 | ) 255 | 256 | const Timeouts = ( 257 | 267 | ) 268 | 269 | const ErrorHandling = ( 270 | 280 | ) 281 | 282 | const POLP = ( 283 | 293 | ) 294 | 295 | const week1Validations = this.state.startValidation ? ( 296 |
297 |
{Connection}
298 |
{Projection}
299 |
{TextAndSubfield}
300 |
{Paging}
301 |
{FacetedSearch}
302 |
303 | ) : ( 304 | "" 305 | ) 306 | const week2Validations = this.state.startValidation ? ( 307 |
308 |
{UserManagement}
309 |
{UserPreferences}
310 |
{GetComments}
311 |
{CreateUpdateComments}
312 |
{DeleteComments}
313 |
{UserReport}
314 |
{Migration}
315 |
{ConnectionPooling}
316 |
{Timeouts}
317 |
{ErrorHandling}
318 |
{POLP}
319 |
320 | ) : ( 321 | "" 322 | ) 323 | //
324 | //
{week1Validations}
325 | //{!this.props.validate.hasWeek1Errors && 326 | //!this.props.validate.week1Validating &&
{week2Validations}
} 327 | //
328 | const validations = this.state.startValidation ? ( 329 |
330 |
{week1Validations}
331 |
{week2Validations}
332 |
333 | ) : ( 334 | "" 335 | ) 336 | return ( 337 |
338 |
{ 340 | this.readyName = readyName 341 | }} 342 | className={this.props.classes.inner} 343 | > 344 | {playerOne} 345 |
346 | (this.leaf = leaf)} 348 | style={{ opacity: 1 }} 349 | className={this.props.classes.leaf} 350 | src={pixelLeaf} 351 | alt="" 352 | /> 353 | {validations} 354 | Copied!} 363 | /> 364 |
365 | ) 366 | } 367 | } 368 | 369 | Status.propTypes = { 370 | classes: PropTypes.object.isRequired 371 | } 372 | 373 | function mapStateToProps({ user, validate }) { 374 | return { 375 | user, 376 | validate 377 | } 378 | } 379 | 380 | function mapDispatchToProps(dispatch) { 381 | return { 382 | validationActions: bindActionCreators(validationActions, dispatch) 383 | } 384 | } 385 | 386 | export default compose( 387 | withStyles(styles), 388 | connect( 389 | mapStateToProps, 390 | mapDispatchToProps 391 | ) 392 | )(Status) 393 | -------------------------------------------------------------------------------- /src/components/SubfieldSearch.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import TextField from "@material-ui/core/TextField" 5 | import purple from "@material-ui/core/colors/purple" 6 | import Button from "@material-ui/core/Button" 7 | import green from "@material-ui/core/colors/green" 8 | import { connect } from "react-redux" 9 | import { bindActionCreators } from "redux" 10 | import * as movieActions from "../actions/movieActions" 11 | import * as miscActions from "../actions/miscActions" 12 | import { compose } from "redux" 13 | import { withRouter } from "react-router-dom" 14 | import FormControl from "@material-ui/core/FormControl" 15 | import FormControlLabel from "@material-ui/core/FormControlLabel" 16 | import Radio from "@material-ui/core/Radio" 17 | import RadioGroup from "@material-ui/core/RadioGroup" 18 | const mongo = green[500] 19 | 20 | const styles = theme => ({ 21 | container: { 22 | display: "inline-flex", 23 | alignItems: "center" 24 | }, 25 | formControl: { 26 | flexDirection: "row" 27 | }, 28 | inputLabelFocused: { 29 | color: purple[500] 30 | }, 31 | inputInkbar: { 32 | "&:after": { 33 | backgroundColor: purple[500] 34 | } 35 | }, 36 | textFieldRoot: { 37 | padding: 0 38 | }, 39 | textFieldInput: { 40 | borderRadius: "4px 0 0 4px", 41 | backgroundColor: theme.palette.common.white, 42 | color: "black", 43 | fontSize: 16, 44 | padding: "10px 12px", 45 | width: "15rem", 46 | transition: theme.transitions.create(["border-color", "box-shadow"]), 47 | "&:focus": { 48 | borderColor: "#80bdff", 49 | boxShadow: "0 0 0 0.2rem rgba(0,123,255,.25)" 50 | }, 51 | height: "20px" 52 | }, 53 | button: { 54 | input: { 55 | display: "none" 56 | }, 57 | borderRadius: "0 4px 4px 0", 58 | color: "white", 59 | padding: "10px 0", 60 | background: mongo, 61 | width: "30px", 62 | display: "inline-flex" 63 | }, 64 | group: { 65 | display: "inline-flex", 66 | flexDirection: "row" 67 | }, 68 | label: { 69 | color: "white" 70 | }, 71 | radio: { 72 | color: "white" 73 | } 74 | }) 75 | 76 | class SubfieldSearch extends Component { 77 | constructor(props) { 78 | super(props) 79 | this.state = { 80 | searchText: "", 81 | selected: false, 82 | defaultValue: "search by parameter", 83 | value: "text" 84 | } 85 | this.handleChange = this.handleChange.bind(this) 86 | this.handleSearch = this.handleSearch.bind(this) 87 | this.handleSelection = this.handleSelection.bind(this) 88 | this.fireSearch = this.fireSearch.bind(this) 89 | } 90 | 91 | handleSelection(e, value) { 92 | this.setState({ value }) 93 | } 94 | 95 | fireSearch(whichType) { 96 | return this.props.movieActions.searchMovies( 97 | whichType, 98 | this.state.searchText, 99 | this.props.history 100 | ) 101 | } 102 | 103 | handleSearch(e) { 104 | this.props.miscActions.toggleDrawer() 105 | switch (this.state.value) { 106 | case "country": 107 | return this.props.movieActions.searchCountries( 108 | this.state.searchText, 109 | this.props.history 110 | ) 111 | 112 | case "genre": 113 | return this.fireSearch("genre") 114 | 115 | case "cast": 116 | return this.fireSearch("cast") 117 | 118 | default: 119 | return this.fireSearch("text") 120 | } 121 | } 122 | 123 | handleChange(e) { 124 | this.setState({ searchText: e.target.value }) 125 | } 126 | 127 | render() { 128 | const { classes } = this.props 129 | return ( 130 |
131 |
132 | 133 | 150 | 153 | 154 |
155 |
156 | 157 | 164 | } 168 | label="Text" 169 | /> 170 | } 174 | label="Country" 175 | /> 176 | } 180 | label="Genre" 181 | /> 182 | } 186 | label="Cast" 187 | /> 188 | 189 | 190 |
191 |
192 | ) 193 | } 194 | } 195 | 196 | SubfieldSearch.propTypes = { 197 | classes: PropTypes.object.isRequired 198 | } 199 | 200 | function mapDispatchToProps(dispatch) { 201 | return { 202 | movieActions: bindActionCreators(movieActions, dispatch), 203 | miscActions: bindActionCreators(miscActions, dispatch) 204 | } 205 | } 206 | 207 | export default compose( 208 | withRouter, 209 | withStyles(styles), 210 | connect( 211 | () => ({}), 212 | mapDispatchToProps 213 | ) 214 | )(SubfieldSearch) 215 | -------------------------------------------------------------------------------- /src/components/TicketValidator.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import Button from "@material-ui/core/Button" 5 | import { CopyToClipboard } from "react-copy-to-clipboard" 6 | import red from "@material-ui/core/colors/red" 7 | import grey from "@material-ui/core/colors/grey" 8 | 9 | const mongoRed = red[900] 10 | const mongoGrey = grey[400] 11 | 12 | const styles = theme => ({ 13 | validationBar: { 14 | display: "inline-flex", 15 | alignItems: "center", 16 | justifyContent: "center", 17 | width: "50vw", 18 | marginTop: "15px", 19 | height: "40px", 20 | }, 21 | validationTicket: { 22 | display: "flex", 23 | padding: "0 15px", 24 | height: "40px", 25 | justifyContent: "center", 26 | alignItems: "center", 27 | width: "30vw", 28 | }, 29 | validationTicketWaiting: { 30 | display: "flex", 31 | padding: "0 15px", 32 | height: "40px", 33 | justifyContent: "center", 34 | alignItems: "center", 35 | width: "30vw", 36 | background: mongoGrey, 37 | }, 38 | ticketLabel: { 39 | display: "flex", 40 | padding: "0 5px", 41 | background: "#e6e6e6", 42 | textAlign: "center", 43 | height: "40px", 44 | justifyContent: "center", 45 | alignItems: "center", 46 | width: "10vw", 47 | }, 48 | copyButton: { 49 | height: "40px", 50 | color: "white", 51 | background: "#6b6b6b", 52 | justifyContent: "center", 53 | borderRadius: 0, 54 | "&:hover": { 55 | background: "#6b6b6b", 56 | }, 57 | width: "10vw", 58 | }, 59 | }) 60 | 61 | class TicketValidator extends React.Component { 62 | state = { 63 | beginValidating: false, 64 | } 65 | 66 | onClickValidate() { 67 | this.setState({beginValidating: true}) 68 | this.props.onClickValidate(this.props.ticketName) 69 | } 70 | 71 | render() { 72 | const props = this.props 73 | if (!this.state.beginValidating) { 74 | return ( 75 |
this.onClickValidate()}> 76 | {props.ticketLabel} 77 |
78 | Click to begin validation 79 |
80 |
81 | ) 82 | } else { 83 | switch (props.ticketValidating) { 84 | case true: 85 | return ( 86 |
87 | 88 | {props.ticketLabel} 89 | 90 |
91 | Currently Validating 92 |
93 |
94 | ) 95 | 96 | default: 97 | return !props.ticketError ? ( 98 |
99 | 100 | {props.ticketLabel} 101 | 102 |
106 | {props.ticketSuccess} 107 |
108 | 109 | 115 | 116 |
117 | ) : ( 118 |
122 | {props.ticketLabel}: {props.ticketErrorMessage} 123 |
124 | ) 125 | } 126 | } 127 | } 128 | } 129 | 130 | TicketValidator.propTypes = { 131 | classes: PropTypes.object.isRequired, 132 | copied: PropTypes.func.isRequired, 133 | onClickValidate: PropTypes.func.isRequired, 134 | ticketError: PropTypes.bool.isRequired, 135 | ticketErrorMessage: PropTypes.string.isRequired, 136 | ticketSuccess: PropTypes.string.isRequired, 137 | ticketLabel: PropTypes.string.isRequired, 138 | ticketValidating: PropTypes.bool.isRequired, 139 | ticketName: PropTypes.string.isRequired, 140 | } 141 | 142 | export default withStyles(styles)(TicketValidator) 143 | -------------------------------------------------------------------------------- /src/components/TicketWaiting.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | 5 | const styles = theme => ({ 6 | validationBar: { 7 | display: "inline-flex", 8 | alignItems: "center", 9 | justifyContent: "center", 10 | width: "50vw", 11 | marginTop: "15px", 12 | height: "40px", 13 | }, 14 | validationTicket: { 15 | display: "flex", 16 | padding: "0 15px", 17 | height: "40px", 18 | justifyContent: "center", 19 | alignItems: "center", 20 | width: "30vw", 21 | }, 22 | ticketLabel: { 23 | display: "flex", 24 | padding: "0 5px", 25 | background: "#e6e6e6", 26 | textAlign: "center", 27 | height: "40px", 28 | justifyContent: "center", 29 | alignItems: "center", 30 | width: "10vw", 31 | }, 32 | }) 33 | 34 | const TicketWaiting = props => { 35 | return ( 36 |
37 | {props.ticketLabel} 38 |
Currently Validating
39 |
40 | ) 41 | } 42 | 43 | TicketWaiting.propTypes = { 44 | classes: PropTypes.object.isRequired, 45 | ticketLabel: PropTypes.string.isRequired, 46 | } 47 | 48 | export default withStyles(styles)(TicketWaiting) 49 | -------------------------------------------------------------------------------- /src/components/ViewModal.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import Modal from "@material-ui/core/Modal" 5 | import YouTube from "react-youtube" 6 | import { connect } from "react-redux" 7 | import { compose } from "redux" 8 | import { bindActionCreators } from "redux" 9 | import * as movieActions from "../actions/movieActions" 10 | 11 | function getModalStyle() { 12 | const top = 50 13 | const left = 50 14 | 15 | return { 16 | top: `${top}%`, 17 | left: `${left}%`, 18 | transform: `translate(-${top}%, -${left}%)`, 19 | } 20 | } 21 | 22 | const styles = theme => ({ 23 | paper: { 24 | position: "absolute", 25 | backgroundColor: theme.palette.background.paper, 26 | boxShadow: theme.shadows[5], 27 | padding: theme.spacing.unit * 4, 28 | }, 29 | }) 30 | 31 | const randomVideo = () => { 32 | let roll = Math.random() 33 | if (roll < 0.5) { 34 | return "6gGXnE1Dbh0" 35 | } else { 36 | return "dQw4w9WgXcQ" 37 | } 38 | } 39 | 40 | class ViewModal extends React.Component { 41 | constructor(props) { 42 | super(props) 43 | this.state = { 44 | open: props.open, 45 | } 46 | this.handleReady = this.handleReady.bind(this) 47 | } 48 | 49 | handleReady(e) { 50 | const video = document.querySelector("video") 51 | if (video) { 52 | video.play() 53 | video.autoplay = true 54 | } 55 | } 56 | 57 | handleClose = () => { 58 | this.props.movieActions.viewMovie() 59 | } 60 | 61 | render() { 62 | const opts = { 63 | height: "390", 64 | width: "640", 65 | } 66 | const { classes } = this.props 67 | 68 | return ( 69 |
70 | 76 |
77 | 82 |
83 |
84 |
85 | ) 86 | } 87 | } 88 | 89 | ViewModal.propTypes = { 90 | classes: PropTypes.object.isRequired, 91 | } 92 | 93 | function mapStateToProps({ movies: { viewMovie } }, { match }) { 94 | return { 95 | displayModal: viewMovie, 96 | } 97 | } 98 | 99 | function mapDispatchToProps(dispatch) { 100 | return { 101 | movieActions: bindActionCreators(movieActions, dispatch), 102 | } 103 | } 104 | 105 | export default compose( 106 | withStyles(styles), 107 | connect( 108 | mapStateToProps, 109 | mapDispatchToProps 110 | ) 111 | )(ViewModal) 112 | -------------------------------------------------------------------------------- /src/containers/AdminPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { connect } from "react-redux" 5 | import { bindActionCreators } from "redux" 6 | import * as reportActions from "../actions/reportActions" 7 | import { compose } from "redux" 8 | import { withRouter } from "react-router-dom" 9 | import Button from "@material-ui/core/Button" 10 | import green from "@material-ui/core/colors/green" 11 | 12 | const styles = theme => ({ 13 | root: { 14 | flex: 1, 15 | flexWrap: "wrap", 16 | justifyContent: "center", 17 | backgroundColor: "black", 18 | alignContent: "center", 19 | width: "100vw", 20 | minHeight: "100vh", 21 | height: "100%", 22 | flexBasis: 0, 23 | }, 24 | button: { 25 | input: { 26 | display: "none", 27 | }, 28 | color: "white", 29 | padding: "10px", 30 | background: green[500], 31 | display: "inline-flex", 32 | margin: theme.spacing.unit - 2, 33 | }, 34 | }) 35 | 36 | class AdminPanel extends Component { 37 | handleClick() { 38 | this.props.reportActions.fetchReport(this.props.user, this.props.history) 39 | } 40 | render() { 41 | const { classes } = this.props 42 | return ( 43 |
44 | 47 |
48 | ) 49 | } 50 | } 51 | 52 | AdminPanel.propTypes = { 53 | classes: PropTypes.object.isRequired, 54 | } 55 | 56 | function mapStateToProps({ user }) { 57 | return { 58 | user, 59 | } 60 | } 61 | 62 | function mapDispatchToProps(dispatch) { 63 | return { 64 | reportActions: bindActionCreators(reportActions, dispatch), 65 | } 66 | } 67 | 68 | export default compose( 69 | withRouter, 70 | withStyles(styles), 71 | connect( 72 | mapStateToProps, 73 | mapDispatchToProps 74 | ) 75 | )(AdminPanel) 76 | -------------------------------------------------------------------------------- /src/containers/CountryResults.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { connect } from "react-redux" 5 | import { bindActionCreators } from "redux" 6 | import * as movieActions from "../actions/movieActions" 7 | import { compose } from "redux" 8 | import { withRouter } from "react-router-dom" 9 | 10 | const styles = theme => ({ 11 | root: { 12 | display: "flex", 13 | flex: 1, 14 | flexWrap: "wrap", 15 | justifyContent: "center", 16 | backgroundColor: "black", 17 | alignContent: "center", 18 | width: "100vw", 19 | minHeight: "100vh", 20 | height: "100%", 21 | flexBasis: 0, 22 | }, 23 | ul: { 24 | listStyle: "none", 25 | textAlign: "center", 26 | }, 27 | li: { 28 | fontSize: "2em", 29 | color: "green", 30 | cursor: "pointer", 31 | }, 32 | }) 33 | 34 | class CountryResults extends Component { 35 | handleClick = id => { 36 | this.props.movieActions.fetchMovieByID(id, this.props.history) 37 | } 38 | render() { 39 | const { 40 | classes, 41 | movies: { titles }, 42 | } = this.props 43 | 44 | let titlesList = titles.map((title, idx) => ( 45 |
  • this.handleClick(title._id)} 49 | > 50 | {title.title} 51 |
  • 52 | )) 53 | return ( 54 |
    55 |
      {titlesList}
    56 |
    57 | ) 58 | } 59 | } 60 | 61 | CountryResults.propTypes = { 62 | classes: PropTypes.object.isRequired, 63 | } 64 | 65 | function mapStateToProps({ movies, errors }) { 66 | return { 67 | movies, 68 | errors, 69 | } 70 | } 71 | 72 | function mapDispatchToProps(dispatch) { 73 | return { 74 | movieActions: bindActionCreators(movieActions, dispatch), 75 | } 76 | } 77 | 78 | export default compose( 79 | withRouter, 80 | withStyles(styles), 81 | connect( 82 | mapStateToProps, 83 | mapDispatchToProps 84 | ) 85 | )(CountryResults) 86 | -------------------------------------------------------------------------------- /src/containers/MainContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | // The following line needs an eslint ignore directive because Router needs to be in scope 3 | import { BrowserRouter as Router, Route } from "react-router-dom" // eslint-disable-line no-unused-vars 4 | import ConnectedSwitch from "../routing/ConnectedSwitch" 5 | import PrivateRoute from "../routing/PrivateRoute" 6 | import Header from "../components/Header" 7 | import Errors from "../components/Errors" 8 | import MovieGrid from "./MovieGrid" 9 | import CountryResults from "./CountryResults" 10 | import LoginCard from "../components/LoginCard" 11 | import SignupCard from "../components/SignupCard" 12 | import MovieDetail from "../components/MovieDetail" 13 | import Account from "../components/Account" 14 | import Status from "../components/Status" 15 | import AppDrawer from "../components/AppDrawer" 16 | import AdminRoute from "../routing/AdminRoute" 17 | import AdminPanel from "../containers/AdminPanel" 18 | import "./normalize.css" 19 | import UserReport from "./UserReport" 20 | 21 | class MainContainer extends Component { 22 | render() { 23 | return ( 24 |
    25 |
    26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
    49 | ) 50 | } 51 | } 52 | export default MainContainer 53 | -------------------------------------------------------------------------------- /src/containers/MovieGrid.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import throttle from "lodash.throttle" 5 | import GridList from "@material-ui/core/GridList" 6 | import MovieTile from "../components/MovieTile" 7 | import { connect } from "react-redux" 8 | import { bindActionCreators } from "redux" 9 | import * as movieActions from "../actions/movieActions" 10 | import { compose } from "redux" 11 | import CircularProgress from "@material-ui/core/CircularProgress" 12 | import Facets from "../components/Facets" 13 | 14 | const styles = theme => ({ 15 | root: { 16 | flex: 1, 17 | flexWrap: "wrap", 18 | justifyContent: "center", 19 | backgroundColor: "black", 20 | alignContent: "center", 21 | width: "100vw", 22 | minHeight: "100vh", 23 | height: "100%", 24 | flexBasis: 0, 25 | }, 26 | gridList: { 27 | height: "100%", 28 | justifyContent: "center", 29 | backgroundColor: "black", 30 | width: "100vw", 31 | flexBasis: 0, 32 | flexGrow: 0, 33 | }, 34 | loading: { 35 | display: "flex", 36 | flexDirection: "column", 37 | justifyContent: "center", 38 | backgroundColor: "black", 39 | alignItems: "center", 40 | width: "100vw", 41 | height: "100vh", 42 | }, 43 | }) 44 | 45 | class MovieGrid extends Component { 46 | constructor(props) { 47 | super(props) 48 | this.state = { 49 | paging: false, 50 | movies: [], 51 | } 52 | this.onScroll = throttle(this.onScroll.bind(this), 1000) 53 | } 54 | componentDidMount() { 55 | if (!this.props.movies || this.props.movies.movies.length === 0) { 56 | this.props.movieActions.fetchMovies() 57 | } 58 | window.addEventListener("scroll", this.onScroll, true) 59 | } 60 | 61 | componentWillUnmount() { 62 | window.removeEventListener("scroll", this.onScroll, true) 63 | this.onScroll.cancel() 64 | } 65 | 66 | componentWillReceiveProps(props) { 67 | if (props.movies.movies.length === props.movies.total_results) { 68 | this.setState({ paging: false }) 69 | this.onScroll.cancel() 70 | window.removeEventListener("scroll", this.onScroll, true) 71 | } 72 | if (!props.movies.paging) { 73 | this.setState({ paging: false }) 74 | this.onScroll.cancel() 75 | } 76 | } 77 | 78 | onScroll() { 79 | const scroll = document.getElementById("root") 80 | if ( 81 | !this.props.movies.paging && 82 | document.body.offsetHeight + window.pageYOffset >= 83 | scroll.scrollHeight - 1500 && 84 | this.props.movies.movies.length !== this.props.movies.total_results 85 | ) { 86 | this.props.movieActions.beginPaging() 87 | this.props.movieActions.paginate( 88 | this.props.movies.movies, 89 | this.props.movies.page, 90 | this.props.movies.filters 91 | ) 92 | } 93 | } 94 | 95 | render() { 96 | const { classes } = this.props 97 | const movies = this.props.movies.shownMovies 98 | if ( 99 | !movies || 100 | (movies.length === 0 && 101 | (!this.props.errors.FetchMovieFailure || 102 | !this.props.searchMovieFailure)) 103 | ) { 104 | return ( 105 |
    106 | 107 |
    108 | ) 109 | } else { 110 | return ( 111 |
    116 | 117 | 122 | {movies.map(movie => )} 123 | 124 |
    125 | ) 126 | } 127 | } 128 | } 129 | 130 | MovieGrid.propTypes = { 131 | classes: PropTypes.object.isRequired, 132 | } 133 | 134 | function mapStateToProps({ movies, errors }) { 135 | return { 136 | movies, 137 | errors, 138 | } 139 | } 140 | 141 | function mapDispatchToProps(dispatch) { 142 | return { 143 | movieActions: bindActionCreators(movieActions, dispatch), 144 | } 145 | } 146 | 147 | export default compose( 148 | withStyles(styles), 149 | connect( 150 | mapStateToProps, 151 | mapDispatchToProps 152 | ) 153 | )(MovieGrid) 154 | -------------------------------------------------------------------------------- /src/containers/UserReport.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { withStyles } from "@material-ui/core/styles" 4 | import { connect } from "react-redux" 5 | import { compose } from "redux" 6 | import { withRouter } from "react-router-dom" 7 | import { bindActionCreators } from "redux" 8 | import * as reportActions from "../actions/reportActions" 9 | 10 | const styles = theme => ({ 11 | root: { 12 | display: "flex", 13 | flex: 1, 14 | flexWrap: "wrap", 15 | justifyContent: "center", 16 | backgroundColor: "black", 17 | alignContent: "center", 18 | width: "100vw", 19 | minHeight: "100vh", 20 | height: "100%", 21 | flexBasis: 0, 22 | }, 23 | ul: { 24 | listStyle: "none", 25 | textAlign: "center", 26 | }, 27 | li: { 28 | fontSize: "1.5em", 29 | color: "white", 30 | }, 31 | }) 32 | 33 | class UserReport extends Component { 34 | componentDidMount() { 35 | if (!this.props.report || this.props.report.length === 0) { 36 | this.props.reportActions.fetchReport(this.props.user, this.props.history) 37 | } 38 | } 39 | render() { 40 | const { report, classes } = this.props 41 | 42 | let userList = report.map((entry, idx) => ( 43 |
  • 44 | {`# ${idx + 1} with ${entry.count} comments: ${entry._id}`} 45 |
  • 46 | )) 47 | return ( 48 |
    49 |
      {userList}
    50 |
    51 | ) 52 | } 53 | } 54 | 55 | UserReport.propTypes = { 56 | classes: PropTypes.object.isRequired, 57 | } 58 | 59 | function mapStateToProps({ report: { report }, user }) { 60 | return { 61 | report, 62 | user, 63 | } 64 | } 65 | 66 | function mapDispatchToProps(dispatch) { 67 | return { 68 | reportActions: bindActionCreators(reportActions, dispatch), 69 | } 70 | } 71 | 72 | export default compose( 73 | withRouter, 74 | withStyles(styles), 75 | connect( 76 | mapStateToProps, 77 | mapDispatchToProps 78 | ) 79 | )(UserReport) 80 | -------------------------------------------------------------------------------- /src/containers/normalize.css: -------------------------------------------------------------------------------- 1 | /* Custom styling (keyframes etc...) */ 2 | 3 | @keyframes spinningLeaf { 4 | from { 5 | transform: rotateY(0deg); 6 | } 7 | to { 8 | transform: rotateY(-360deg); 9 | } 10 | } 11 | 12 | @keyframes blink { 13 | 0% { 14 | opacity: 0; 15 | } 16 | 50% { 17 | opacity: 1; 18 | } 19 | 100% { 20 | opacity: 0; 21 | } 22 | } 23 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 24 | 25 | /* Document 26 | ========================================================================== */ 27 | 28 | /** 29 | * 1. Correct the line height in all browsers. 30 | * 2. Prevent adjustments of font size after orientation changes in 31 | * IE on Windows Phone and in iOS. 32 | */ 33 | 34 | html { 35 | line-height: 1.15; /* 1 */ 36 | -ms-text-size-adjust: 100%; /* 2 */ 37 | -webkit-text-size-adjust: 100%; /* 2 */ 38 | } 39 | 40 | /* Sections 41 | ========================================================================== */ 42 | 43 | /** 44 | * Remove the margin in all browsers (opinionated). 45 | */ 46 | 47 | html, 48 | body { 49 | height: 100%; 50 | margin: 0; 51 | padding: 0; 52 | } 53 | 54 | #full { 55 | background: '#black'; 56 | height: 100%; 57 | } 58 | 59 | body, 60 | button, 61 | input, 62 | select, 63 | textarea, 64 | div, 65 | h1, 66 | h2, 67 | h3, 68 | h4, 69 | p, 70 | span { 71 | font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', 'Roboto', 'Oxygen', 72 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 73 | 'Helvetica', 'Arial', sans-serif; 74 | } 75 | 76 | /** 77 | * Add the correct display in IE 9-. 78 | */ 79 | 80 | article, 81 | aside, 82 | footer, 83 | header, 84 | nav, 85 | section { 86 | display: block; 87 | } 88 | 89 | /** 90 | * Correct the font size and margin on `h1` elements within `section` and 91 | * `article` contexts in Chrome, Firefox, and Safari. 92 | */ 93 | 94 | h1 { 95 | font-size: 2em; 96 | margin: 0.67em 0; 97 | } 98 | 99 | /* Grouping content 100 | ========================================================================== */ 101 | 102 | /** 103 | * Add the correct display in IE 9-. 104 | * 1. Add the correct display in IE. 105 | */ 106 | 107 | figcaption, 108 | figure, 109 | main { 110 | /* 1 */ 111 | display: block; 112 | } 113 | 114 | /** 115 | * Add the correct margin in IE 8. 116 | */ 117 | 118 | figure { 119 | margin: 1em 40px; 120 | } 121 | 122 | /** 123 | * 1. Add the correct box sizing in Firefox. 124 | * 2. Show the overflow in Edge and IE. 125 | */ 126 | 127 | hr { 128 | box-sizing: content-box; /* 1 */ 129 | height: 0; /* 1 */ 130 | overflow: visible; /* 2 */ 131 | } 132 | 133 | /** 134 | * 1. Correct the inheritance and scaling of font size in all browsers. 135 | * 2. Correct the odd `em` font sizing in all browsers. 136 | */ 137 | 138 | pre { 139 | font-family: monospace, monospace; /* 1 */ 140 | font-size: 1em; /* 2 */ 141 | } 142 | 143 | /* Text-level semantics 144 | ========================================================================== */ 145 | 146 | /** 147 | * 1. Remove the gray background on active links in IE 10. 148 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 149 | */ 150 | 151 | a { 152 | background-color: transparent; /* 1 */ 153 | -webkit-text-decoration-skip: objects; /* 2 */ 154 | } 155 | 156 | /** 157 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 158 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 159 | */ 160 | 161 | abbr[title] { 162 | border-bottom: none; /* 1 */ 163 | text-decoration: underline; /* 2 */ 164 | text-decoration: underline dotted; /* 2 */ 165 | } 166 | 167 | /** 168 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 169 | */ 170 | 171 | b, 172 | strong { 173 | font-weight: inherit; 174 | } 175 | 176 | /** 177 | * Add the correct font weight in Chrome, Edge, and Safari. 178 | */ 179 | 180 | b, 181 | strong { 182 | font-weight: bolder; 183 | } 184 | 185 | /** 186 | * 1. Correct the inheritance and scaling of font size in all browsers. 187 | * 2. Correct the odd `em` font sizing in all browsers. 188 | */ 189 | 190 | code, 191 | kbd, 192 | samp { 193 | font-family: monospace, monospace; /* 1 */ 194 | font-size: 1em; /* 2 */ 195 | } 196 | 197 | /** 198 | * Add the correct font style in Android 4.3-. 199 | */ 200 | 201 | dfn { 202 | font-style: italic; 203 | } 204 | 205 | /** 206 | * Add the correct background and color in IE 9-. 207 | */ 208 | 209 | mark { 210 | background-color: #ff0; 211 | color: #000; 212 | } 213 | 214 | /** 215 | * Add the correct font size in all browsers. 216 | */ 217 | 218 | small { 219 | font-size: 80%; 220 | } 221 | 222 | /** 223 | * Prevent `sub` and `sup` elements from affecting the line height in 224 | * all browsers. 225 | */ 226 | 227 | sub, 228 | sup { 229 | font-size: 75%; 230 | line-height: 0; 231 | position: relative; 232 | vertical-align: baseline; 233 | } 234 | 235 | sub { 236 | bottom: -0.25em; 237 | } 238 | 239 | sup { 240 | top: -0.5em; 241 | } 242 | 243 | /* Embedded content 244 | ========================================================================== */ 245 | 246 | /** 247 | * Add the correct display in IE 9-. 248 | */ 249 | 250 | audio, 251 | video { 252 | display: inline-block; 253 | } 254 | 255 | /** 256 | * Add the correct display in iOS 4-7. 257 | */ 258 | 259 | audio:not([controls]) { 260 | display: none; 261 | height: 0; 262 | } 263 | 264 | /** 265 | * Remove the border on images inside links in IE 10-. 266 | */ 267 | 268 | img { 269 | border-style: none; 270 | } 271 | 272 | /** 273 | * Hide the overflow in IE.e 274 | 275 | svg:not(:root) { 276 | overflow: hidden; 277 | } 278 | 279 | /* 280 | * Style the error notifications. 281 | */ 282 | 283 | .material-icons.red { 284 | color: #cd0000; 285 | margin-right: 5px; 286 | vertical-align: sub; 287 | } 288 | .material-icons:hover { 289 | cursor: pointer; 290 | font-size: 28px; 291 | color: #b60000; 292 | } 293 | 294 | /* Forms 295 | ========================================================================== */ 296 | 297 | /** 298 | * 1. Change the font styles in all browsers (opinionated). 299 | * 2. Remove the margin in Firefox and Safari. 300 | */ 301 | 302 | button, 303 | input, 304 | optgroup, 305 | select, 306 | textarea { 307 | font-family: sans-serif; /* 1 */ 308 | font-size: 100%; /* 1 */ 309 | line-height: 1.15; /* 1 */ 310 | margin: 0; /* 2 */ 311 | } 312 | 313 | /** 314 | * Show the overflow in IE. 315 | * 1. Show the overflow in Edge. 316 | */ 317 | 318 | button, 319 | input { 320 | /* 1 */ 321 | overflow: visible; 322 | } 323 | 324 | /** 325 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 326 | * 1. Remove the inheritance of text transform in Firefox. 327 | */ 328 | 329 | button, 330 | select { 331 | /* 1 */ 332 | text-transform: none; 333 | } 334 | 335 | /** 336 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 337 | * controls in Android 4. 338 | * 2. Correct the inability to style clickable types in iOS and Safari. 339 | */ 340 | 341 | button, 342 | html [type="button"], /* 1 */ 343 | [type="reset"], 344 | [type="submit"] { 345 | -webkit-appearance: button; /* 2 */ 346 | } 347 | 348 | /** 349 | * Remove the inner border and padding in Firefox. 350 | */ 351 | 352 | button::-moz-focus-inner, 353 | [type='button']::-moz-focus-inner, 354 | [type='reset']::-moz-focus-inner, 355 | [type='submit']::-moz-focus-inner { 356 | border-style: none; 357 | padding: 0; 358 | } 359 | 360 | /** 361 | * Restore the focus styles unset by the previous rule. 362 | */ 363 | 364 | button:-moz-focusring, 365 | [type='button']:-moz-focusring, 366 | [type='reset']:-moz-focusring, 367 | [type='submit']:-moz-focusring { 368 | outline: 1px dotted ButtonText; 369 | } 370 | 371 | /** 372 | * Correct the padding in Firefox. 373 | */ 374 | 375 | fieldset { 376 | padding: 0.35em 0.75em 0.625em; 377 | } 378 | 379 | /** 380 | * 1. Correct the text wrapping in Edge and IE. 381 | * 2. Correct the color inheritance from `fieldset` elements in IE. 382 | * 3. Remove the padding so developers are not caught out when they zero out 383 | * `fieldset` elements in all browsers. 384 | */ 385 | 386 | legend { 387 | box-sizing: border-box; /* 1 */ 388 | color: inherit; /* 2 */ 389 | display: table; /* 1 */ 390 | max-width: 100%; /* 1 */ 391 | padding: 0; /* 3 */ 392 | white-space: normal; /* 1 */ 393 | } 394 | 395 | /** 396 | * 1. Add the correct display in IE 9-. 397 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 398 | */ 399 | 400 | progress { 401 | display: inline-block; /* 1 */ 402 | vertical-align: baseline; /* 2 */ 403 | } 404 | 405 | /** 406 | * Remove the default vertical scrollbar in IE. 407 | */ 408 | 409 | textarea { 410 | overflow: auto; 411 | } 412 | 413 | /** 414 | * 1. Add the correct box sizing in IE 10-. 415 | * 2. Remove the padding in IE 10-. 416 | */ 417 | 418 | [type='checkbox'], 419 | [type='radio'] { 420 | box-sizing: border-box; /* 1 */ 421 | padding: 0; /* 2 */ 422 | } 423 | 424 | /** 425 | * Correct the cursor style of increment and decrement buttons in Chrome. 426 | */ 427 | 428 | [type='number']::-webkit-inner-spin-button, 429 | [type='number']::-webkit-outer-spin-button { 430 | height: auto; 431 | } 432 | 433 | /** 434 | * 1. Correct the odd appearance in Chrome and Safari. 435 | * 2. Correct the outline style in Safari. 436 | */ 437 | 438 | [type='search'] { 439 | -webkit-appearance: textfield; /* 1 */ 440 | outline-offset: -2px; /* 2 */ 441 | } 442 | 443 | /** 444 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 445 | */ 446 | 447 | [type='search']::-webkit-search-cancel-button, 448 | [type='search']::-webkit-search-decoration { 449 | -webkit-appearance: none; 450 | } 451 | 452 | /** 453 | * 1. Correct the inability to style clickable types in iOS and Safari. 454 | * 2. Change font properties to `inherit` in Safari. 455 | */ 456 | 457 | ::-webkit-file-upload-button { 458 | -webkit-appearance: button; /* 1 */ 459 | font: inherit; /* 2 */ 460 | } 461 | 462 | /* Interactive 463 | ========================================================================== */ 464 | 465 | /* 466 | * Add the correct display in IE 9-. 467 | * 1. Add the correct display in Edge, IE, and Firefox. 468 | */ 469 | 470 | details, /* 1 */ 471 | menu { 472 | display: block; 473 | } 474 | 475 | /* 476 | * Add the correct display in all browsers. 477 | */ 478 | 479 | summary { 480 | display: list-item; 481 | } 482 | 483 | /* Scripting 484 | ========================================================================== */ 485 | 486 | /** 487 | * Add the correct display in IE 9-. 488 | */ 489 | 490 | canvas { 491 | display: inline-block; 492 | } 493 | 494 | /** 495 | * Add the correct display in IE. 496 | */ 497 | 498 | template { 499 | display: none; 500 | } 501 | 502 | /* Hidden 503 | ========================================================================== */ 504 | 505 | /** 506 | * Add the correct display in IE 10-. 507 | */ 508 | 509 | [hidden] { 510 | display: none; 511 | } 512 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import MainContainer from "./containers/MainContainer" 4 | import { Provider } from "react-redux" 5 | import configureStore from "../src/store/configureStore" 6 | import { ConnectedRouter } from "react-router-redux" 7 | import createHistory from "history/createBrowserHistory" 8 | import { saveState } from "./store/localStorage" 9 | import throttle from "lodash.throttle" 10 | 11 | const history = createHistory() 12 | const store = configureStore() 13 | 14 | store.subscribe( 15 | throttle(() => { 16 | saveState(store.getState().user) 17 | }, 1000), 18 | ) 19 | 20 | ReactDOM.render( 21 | 22 | 23 | 24 | 25 | , 26 | document.getElementById("root"), 27 | ) 28 | -------------------------------------------------------------------------------- /src/reducers/errorsReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVED_MOVIES, 3 | RECEIVED_MOVIE_BY_ID, 4 | RECEIVED_SEARCH_RESULTS, 5 | RECEIVED_COUNTRY_RESULTS, 6 | FETCH_MOVIES_FAILURE, 7 | FETCH_MOVIE_BY_ID_FAILURE, 8 | SEARCH_MOVIES_FAILURE, 9 | SEARCH_COUNTRIES_FAILURE, 10 | LOGIN_SUCCESS, 11 | LOGIN_FAIL, 12 | CLEAR_ERROR 13 | } from "../actions/actionTypes"; 14 | 15 | const initialState = { 16 | userErrName: "", 17 | userErrPassword: "", 18 | userErrEmail: "", 19 | fetchMovieErrMsg: "", 20 | searchMovieErrMsg: "", 21 | searchCountriesErrMsg: "", 22 | fetchMovieByIDErrMsg: "" 23 | }; 24 | 25 | export default function errors(state = initialState, action) { 26 | switch (action.type) { 27 | case CLEAR_ERROR: 28 | let newState = { 29 | ...state, 30 | [action.key]: "" 31 | }; 32 | return { ...newState }; 33 | 34 | case RECEIVED_MOVIES: 35 | newState = { 36 | ...state, 37 | fetchMovieErrMsg: "" 38 | }; 39 | return { ...newState }; 40 | 41 | case RECEIVED_SEARCH_RESULTS: 42 | newState = { 43 | ...state, 44 | searchMovieErrMsg: "" 45 | }; 46 | return { ...newState }; 47 | 48 | case RECEIVED_COUNTRY_RESULTS: 49 | newState = { 50 | ...state, 51 | searchCountriesErrMsg: "" 52 | }; 53 | return { ...newState }; 54 | 55 | case RECEIVED_MOVIE_BY_ID: 56 | newState = { 57 | ...state, 58 | fetchMovieByIDErrMsg: "" 59 | }; 60 | return { ...newState }; 61 | 62 | case LOGIN_SUCCESS: 63 | newState = { 64 | ...state, 65 | userErrMsg: "" 66 | }; 67 | return { ...newState }; 68 | 69 | case LOGIN_FAIL: 70 | const error = action.error.error.error; 71 | return { 72 | ...state, 73 | userErrName: error.name || "", 74 | userErrPassword: error.password || "", 75 | userErrEmail: error.email || "", 76 | userErrMsg: 77 | error === "Unauthorized" ? "Invalid username or password" : "" 78 | }; 79 | 80 | case FETCH_MOVIE_BY_ID_FAILURE: 81 | return { 82 | ...state, 83 | fetchMovieByIDErrMsg: action.error 84 | }; 85 | 86 | case FETCH_MOVIES_FAILURE: 87 | return { 88 | ...state, 89 | fetchMovieErrMsg: action.error 90 | }; 91 | 92 | case SEARCH_MOVIES_FAILURE: 93 | console.log("search failure! ", action.error); 94 | return { 95 | ...state, 96 | searchMovieErrMsg: action.error 97 | }; 98 | 99 | case SEARCH_COUNTRIES_FAILURE: 100 | return { 101 | ...state, 102 | searchCountriesErrMsg: action.error 103 | }; 104 | 105 | default: 106 | return state; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/reducers/fetchReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_MOVIES, 3 | SEARCH_MOVIES, 4 | FETCH_MOVIE_BY_ID, 5 | PAGINATE_MOVIES, 6 | } from "../actions/actionTypes" 7 | 8 | const initialState = {} 9 | 10 | export default function movie(state = initialState, action) { 11 | switch (action.type) { 12 | case FETCH_MOVIES: 13 | case SEARCH_MOVIES: 14 | case PAGINATE_MOVIES: 15 | case FETCH_MOVIE_BY_ID: 16 | return action 17 | default: 18 | return state 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/reducers/miscReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | TOGGLE_DRAWER, 3 | CHECK_ADMIN, 4 | CHECK_ADMIN_FAIL, 5 | CHECK_ADMIN_SUCCESS, 6 | } from "../actions/actionTypes" 7 | 8 | const initialState = { 9 | open: false, 10 | checkingAdminStatus: false, 11 | } 12 | 13 | export default function misc(state = initialState, action) { 14 | switch (action.type) { 15 | case TOGGLE_DRAWER: 16 | return { ...state, open: !state.open } 17 | 18 | case CHECK_ADMIN: 19 | console.log("checking admin begin") 20 | return { ...state, checkingAdminStatus: true } 21 | 22 | case CHECK_ADMIN_FAIL: 23 | case CHECK_ADMIN_SUCCESS: 24 | console.log("checking admin end") 25 | return { ...state, checkingAdminStatus: false } 26 | 27 | default: 28 | return state 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/reducers/moviesReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVED_MOVIES, 3 | RECEIVED_MOVIE_BY_ID, 4 | VIEW_MOVIE, 5 | RECEIVED_SEARCH_RESULTS, 6 | RECEIVED_COUNTRY_RESULTS, 7 | MOVIE_DETAIL, 8 | RECEIVED_PAGINATION, 9 | BEGIN_PAGING, 10 | PROP_FACET_FILTER, 11 | SUBMIT_COMMENT_SUCCESS, 12 | UPDATE_COMMENT_SUCCESS 13 | } from "../actions/actionTypes" 14 | 15 | const initialState = { 16 | movies: [], 17 | page: 0, 18 | movie: {}, 19 | filters: {}, 20 | facets: { 21 | rating: [], 22 | runtime: [] 23 | }, 24 | entries_per_page: 0, 25 | total_results: 0, 26 | viewMovie: false, 27 | apiError: false, 28 | fetchMovieFailure: false, 29 | searchMovieFailure: false, 30 | paging: false, 31 | titles: [], 32 | facetFilters: { 33 | rating: {}, 34 | runtime: {} 35 | }, 36 | shownMovies: [] 37 | } 38 | 39 | /** 40 | * @typedef Bucket 41 | * @property {number} _id The lower bound of this statistical bucket 42 | * @property {number} count The count of elements in this bucket 43 | */ 44 | 45 | /** 46 | * 47 | * @param {[Bucket]} left An array of Buckets 48 | * @param {[Bucket]} right An array of Buckets 49 | * @returns {[Bucket]} The combined results of merging the statistical buckets 50 | */ 51 | const mergeStatisticalFacets = (left, right) => { 52 | let combinedBuckets = {} 53 | left.forEach(bucket => { 54 | if (bucket) { 55 | combinedBuckets[bucket._id] = bucket.count 56 | } 57 | }) 58 | right.forEach(bucket => { 59 | if (combinedBuckets[bucket._id] !== undefined) { 60 | combinedBuckets[bucket._id] += bucket.count 61 | } else { 62 | combinedBuckets[bucket._id] = bucket.count 63 | } 64 | }) 65 | return Object.keys(combinedBuckets).map(elem => { 66 | return { 67 | _id: elem, 68 | count: combinedBuckets[elem] 69 | } 70 | }) 71 | } 72 | 73 | const applyFacetFilters = (movies, facetFilters) => { 74 | const { rating, runtime } = facetFilters 75 | let filteredMovies = movies.slice() 76 | if (Object.keys(rating).length || Object.keys(runtime).length) { 77 | const filters = [ 78 | ...Object.keys(rating).map(key => rating[key]), 79 | ...Object.keys(runtime).map(key => runtime[key]) 80 | ] 81 | filteredMovies = filteredMovies.filter(elem => filters.some(fn => fn(elem))) 82 | } 83 | return filteredMovies 84 | } 85 | 86 | export default function movie(state = initialState, action) { 87 | switch (action.type) { 88 | case SUBMIT_COMMENT_SUCCESS: 89 | case UPDATE_COMMENT_SUCCESS: 90 | return { 91 | ...state, 92 | movie: { 93 | ...state.movie, 94 | comments: action.comments 95 | } 96 | } 97 | 98 | case PROP_FACET_FILTER: 99 | let tempFacetFilters = state.facetFilters 100 | let { facet, key, filter } = action.payload 101 | if (tempFacetFilters[facet][key] !== undefined) { 102 | delete tempFacetFilters[facet][key] 103 | } else { 104 | tempFacetFilters[facet][key] = filter 105 | } 106 | return { 107 | ...state, 108 | facetFilters: { 109 | runtime: tempFacetFilters.runtime, 110 | rating: tempFacetFilters.rating 111 | }, 112 | shownMovies: applyFacetFilters(state.movies, tempFacetFilters) 113 | } 114 | 115 | case BEGIN_PAGING: 116 | return { 117 | ...state, 118 | paging: true 119 | } 120 | case MOVIE_DETAIL: 121 | return { 122 | ...state, 123 | movie: state.movies.filter(elem => elem._id === action.movie).pop() 124 | } 125 | case RECEIVED_MOVIES: 126 | return { 127 | ...state, 128 | movies: action.movies, 129 | page: action.page, 130 | filters: action.filters, 131 | entries_per_page: action.entries_per_page, 132 | total_results: action.total_results, 133 | shownMovies: applyFacetFilters(action.movies, state.facetFilters) 134 | } 135 | case RECEIVED_SEARCH_RESULTS: 136 | return { 137 | ...state, 138 | movies: action.movies, 139 | page: action.page, 140 | filters: action.filters, 141 | entries_per_page: action.entries_per_page, 142 | total_results: action.total_results, 143 | facets: { 144 | rating: (action.facets && action.facets.rating) || [], 145 | runtime: (action.facets && action.facets.runtime) || [] 146 | }, 147 | shownMovies: applyFacetFilters(action.movies, state.facetFilters) 148 | } 149 | case RECEIVED_COUNTRY_RESULTS: 150 | return { 151 | ...state, 152 | titles: action.titles 153 | } 154 | case RECEIVED_PAGINATION: 155 | return { 156 | ...state, 157 | movies: action.movies, 158 | page: action.page, 159 | filters: action.filters, 160 | entries_per_page: action.entries_per_page, 161 | paging: false, 162 | facets: { 163 | rating: 164 | (action.facets && 165 | mergeStatisticalFacets( 166 | state.facets.rating, 167 | action.facets.rating 168 | )) || 169 | [], 170 | runtime: 171 | (action.facets && 172 | mergeStatisticalFacets( 173 | state.facets.runtime, 174 | action.facets.runtime 175 | )) || 176 | [] 177 | }, 178 | shownMovies: applyFacetFilters(action.movies, state.facetFilters) 179 | } 180 | case RECEIVED_MOVIE_BY_ID: 181 | return { ...state, movie: action.movie } 182 | case VIEW_MOVIE: 183 | return { ...state, viewMovie: !state.viewMovie } 184 | 185 | default: 186 | return state 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/reducers/reportReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_USER_REPORT, 3 | RECEIVED_USER_REPORT_SUCCESS, 4 | RECEIVED_USER_REPORT_FAILURE, 5 | } from "../actions/actionTypes" 6 | 7 | const initialState = { 8 | fetching: false, 9 | report: [], 10 | } 11 | 12 | export default function misc(state = initialState, action) { 13 | switch (action.type) { 14 | case RECEIVED_USER_REPORT_FAILURE: 15 | return { 16 | report: [], 17 | fetching: false, 18 | } 19 | case RECEIVED_USER_REPORT_SUCCESS: 20 | return { 21 | report: action.report, 22 | fetching: false, 23 | } 24 | case FETCH_USER_REPORT: 25 | return { 26 | ...state, 27 | fetching: true, 28 | } 29 | default: 30 | return state 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_SUCCESS, 3 | LOGOUT, 4 | SAVE_PREFS_SUCCESS, 5 | CHECK_ADMIN_SUCCESS, 6 | CHECK_ADMIN_FAIL, 7 | } from "../actions/actionTypes" 8 | import { loadState } from "../store/localStorage" 9 | 10 | let initialState = { 11 | auth_token: "", 12 | info: { 13 | preferences: { 14 | favorite_cast: "", 15 | preferred_language: "", 16 | }, 17 | }, 18 | loggedIn: false, 19 | isAdmin: false, 20 | } 21 | let localState 22 | try { 23 | localState = { ...initialState, ...loadState() } 24 | } catch (e) { 25 | localState = initialState 26 | } 27 | 28 | export default function user(state = localState, action) { 29 | switch (action.type) { 30 | case LOGIN_SUCCESS: 31 | let loaded_prefs 32 | if (!action.user.info.preferences) { 33 | loaded_prefs = initialState.info.preferences 34 | } else { 35 | loaded_prefs = action.user.info.preferences 36 | } 37 | return { 38 | auth_token: action.user.auth_token, 39 | info: { 40 | ...state.info, 41 | ...action.user.info, 42 | preferences: { ...state.info.preferences, ...loaded_prefs }, 43 | }, 44 | loggedIn: true, 45 | } 46 | case LOGOUT: { 47 | return initialState 48 | } 49 | 50 | case SAVE_PREFS_SUCCESS: 51 | return { 52 | ...state, 53 | info: { 54 | ...state.info, 55 | preferences: { ...state.info.preferences, ...action.preferences }, 56 | }, 57 | } 58 | 59 | case CHECK_ADMIN_FAIL: 60 | return { 61 | ...state, 62 | isAdmin: false, 63 | } 64 | 65 | case CHECK_ADMIN_SUCCESS: 66 | return { 67 | ...state, 68 | isAdmin: true, 69 | } 70 | 71 | default: 72 | return state 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/routing/AdminRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Route, Redirect } from "react-router-dom" 3 | import CircularProgress from "@material-ui/core/CircularProgress" 4 | import { connect } from "react-redux" 5 | import { withStyles } from "@material-ui/core/styles" 6 | import { compose } from "redux" 7 | 8 | const styles = theme => ({ 9 | loading: { 10 | display: "flex", 11 | flexDirection: "column", 12 | justifyContent: "center", 13 | backgroundColor: "black", 14 | alignItems: "center", 15 | width: "100vw", 16 | height: "100vh", 17 | }, 18 | }) 19 | 20 | const AdminRoute = ({ 21 | component: Component, 22 | redirectRoute, 23 | user, 24 | misc, 25 | classes, 26 | ...rest 27 | }) => { 28 | if (misc.checkingAdminStatus) { 29 | return ( 30 |
    31 | 32 |
    33 | ) 34 | } 35 | return ( 36 | 39 | user.isAdmin ? ( 40 | 41 | ) : ( 42 | 43 | ) 44 | } 45 | /> 46 | ) 47 | } 48 | 49 | const mapStateToProps = ({ user, misc }) => ({ user, misc }) 50 | 51 | export default compose( 52 | withStyles(styles), 53 | connect(mapStateToProps) 54 | )(AdminRoute) 55 | -------------------------------------------------------------------------------- /src/routing/ConnectedSwitch.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { Switch } from "react-router-dom" 3 | const ConnectedSwitch = connect(state => ({ 4 | location: state.location, 5 | }))(Switch) 6 | 7 | export default ConnectedSwitch 8 | -------------------------------------------------------------------------------- /src/routing/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Route, Redirect } from "react-router-dom" 3 | import { connect } from "react-redux" 4 | 5 | const PrivateRoute = ({ 6 | component: Component, 7 | redirectRoute, 8 | user, 9 | ...rest 10 | }) => { 11 | return ( 12 | 15 | user.loggedIn ? ( 16 | 17 | ) : ( 18 | 19 | ) 20 | } 21 | /> 22 | ) 23 | } 24 | 25 | const mapStateToProps = ({ user }) => ({ user }) 26 | 27 | export default connect(mapStateToProps)(PrivateRoute) 28 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from "redux" 2 | import thunk from "redux-thunk" 3 | import movies from "../reducers/moviesReducer" 4 | import errors from "../reducers/errorsReducer" 5 | import fetches from "../reducers/fetchReducer" 6 | import user from "../reducers/userReducer" 7 | import misc from "../reducers/miscReducer" 8 | import validate from "../reducers/validationReducer" 9 | import report from "../reducers/reportReducer" 10 | import createHistory from "history/createBrowserHistory" 11 | import { routerReducer, routerMiddleware } from "react-router-redux" 12 | 13 | // Create a history of your choosing (we're using a browser history in this case) 14 | const history = createHistory() 15 | 16 | // Persisted user state 17 | 18 | // Build the middleware for intercepting and dispatching navigation actions 19 | const middleware = routerMiddleware(history) 20 | 21 | export default function configureStore() { 22 | return createStore( 23 | combineReducers({ 24 | report, 25 | misc, 26 | validate, 27 | user, 28 | errors, 29 | movies, 30 | fetches, 31 | router: routerReducer, 32 | }), 33 | window.__REDUX_DEVTOOLS_EXTENSION__ && 34 | window.__REDUX_DEVTOOLS_EXTENSION__(), 35 | applyMiddleware(thunk, middleware) 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/store/localStorage.js: -------------------------------------------------------------------------------- 1 | export const loadState = () => { 2 | try { 3 | const serializedState = localStorage.getItem("state") 4 | if (serializedState === null) { 5 | return undefined 6 | } 7 | return JSON.parse(serializedState) 8 | } catch (err) { 9 | return undefined 10 | } 11 | } 12 | 13 | export const saveState = state => { 14 | try { 15 | const serializedState = JSON.stringify(state) 16 | localStorage.setItem("state", serializedState) 17 | } catch (err) { 18 | // LocalStorage wasn't accessible. This will be a requirement for users, otherwise nothing to do. 19 | } 20 | } 21 | --------------------------------------------------------------------------------