├── 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 | ", "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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------