From 2cbac5391cdd4a3891309610c8d380c376e01f06 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 3 Oct 2019 09:46:55 +0200 Subject: [PATCH 01/61] react-bootstrap is installed on client --- client/package-lock.json | 280 +++++++++++++++++++++++++++++++++++++++ client/package.json | 1 + client/src/App.js | 2 +- server/routes/movies.js | 1 + 4 files changed, 283 insertions(+), 1 deletion(-) diff --git a/client/package-lock.json b/client/package-lock.json index 440e4a4..7551acf 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1157,6 +1157,39 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@react-bootstrap/react-popper": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@react-bootstrap/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-4l3q7LcZEhrSkI4d3Ie3g4CdrXqqTexXX4PFT45CB0z5z2JUbaxgRwKNq7r5j2bLdVpZm+uvUGqxJw8d9vgbJQ==", + "requires": { + "babel-runtime": "6.x.x", + "create-react-context": "^0.2.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.5", + "warning": "^3.0.0" + }, + "dependencies": { + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, + "@restart/context": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==" + }, + "@restart/hooks": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.14.tgz", + "integrity": "sha512-k57+iyGr6o1XHeWWsGe5aMHKYcw7fukL6mCE+ZrPjtt1gXei5wCUxj71yQYfFbNjg0z5xxX8Els/UmyJiEn4nw==" + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -3273,6 +3306,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", @@ -3649,6 +3687,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz", + "integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3938,6 +3985,11 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", + "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -4241,6 +4293,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", @@ -4398,6 +4458,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -5305,6 +5373,35 @@ "bser": "^2.0.0" } }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + } + } + }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -5769,6 +5866,11 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", @@ -6539,6 +6641,15 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -7752,6 +7863,11 @@ "object.assign": "^4.1.0" } }, + "keycode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz", + "integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -8396,6 +8512,15 @@ "lower-case": "^1.1.1" } }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, "node-forge": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", @@ -9152,6 +9277,11 @@ "ts-pnp": "^1.1.2" } }, + "popper.js": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz", + "integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==" + }, "portfinder": { "version": "1.0.24", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.24.tgz", @@ -10105,6 +10235,25 @@ "react-is": "^16.8.1" } }, + "prop-types-extra": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.0.tgz", + "integrity": "sha512-QFyuDxvMipmIVKD2TwxLVPzMnO4e5oOf1vr3tJIomL8E7d0lr6phTHd5nkPhFIzTD1idBLLEPeylL9g+rrTzRg==", + "requires": { + "react-is": "^16.3.2", + "warning": "^3.0.0" + }, + "dependencies": { + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -10277,6 +10426,33 @@ "whatwg-fetch": "3.0.0" } }, + "react-bootstrap": { + "version": "1.0.0-beta.12", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.12.tgz", + "integrity": "sha512-qBEAthAzqM+OTS2h5ZCfV5/yZUadQcMlaep4iPyPqsu92JzdcznhSDjw6b+asiepsyQgiS33t8OPeLLRiIDh9Q==", + "requires": { + "@babel/runtime": "^7.4.2", + "@react-bootstrap/react-popper": "1.2.1", + "@restart/context": "^2.1.4", + "@restart/hooks": "^0.3.11", + "classnames": "^2.2.6", + "dom-helpers": "^3.4.0", + "invariant": "^2.2.4", + "keycode": "^2.2.0", + "popper.js": "^1.14.7", + "prop-types": "^15.7.2", + "prop-types-extra": "^1.1.0", + "react-overlays": "^1.2.0", + "react-transition-group": "^4.0.0", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + } + }, + "react-context-toolbox": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz", + "integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A==" + }, "react-dev-utils": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.4.tgz", @@ -10352,6 +10528,61 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.1.tgz", "integrity": "sha512-BXUMf9sIOPXXZWqr7+c5SeOKJykyVr2u0UDzEf4LNGc6taGkQe1A9DFD07umCIXz45RLr9oAAwZbAJ0Pkknfaw==" }, + "react-overlays": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.2.0.tgz", + "integrity": "sha512-i/FCV8wR6aRaI+Kz/dpJhOdyx+ah2tN1RhT9InPrexyC4uzf3N4bNayFTGtUeQVacj57j1Mqh1CwV60/5153Iw==", + "requires": { + "classnames": "^2.2.6", + "dom-helpers": "^3.4.0", + "prop-types": "^15.6.2", + "prop-types-extra": "^1.1.0", + "react-context-toolbox": "^2.0.2", + "react-popper": "^1.3.2", + "uncontrollable": "^6.0.0", + "warning": "^4.0.2" + }, + "dependencies": { + "uncontrollable": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.2.3.tgz", + "integrity": "sha512-VgOAoBU2ptCL2bfTG2Mra0I8i1u6Aq84AFonD5tmCAYSfs3hWvr2Rlw0q2ntoxXTHjcQOmZOh3FKaN+UZVyREQ==", + "requires": { + "@babel/runtime": "^7.4.5", + "invariant": "^2.2.4" + } + } + } + }, + "react-popper": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.4.tgz", + "integrity": "sha512-9AcQB29V+WrBKk6X7p0eojd1f25/oJajVdMZkywIoAV6Ag7hzE1Mhyeup2Q1QnvFRtGQFQvtqfhlEoDAPfKAVA==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.3.0", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + }, + "dependencies": { + "create-react-context": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", + "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" + } + }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + } + } + }, "react-scripts": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.1.2.tgz", @@ -10413,6 +10644,28 @@ "workbox-webpack-plugin": "4.3.1" } }, + "react-transition-group": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz", + "integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "dependencies": { + "dom-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.0.tgz", + "integrity": "sha512-zRRYDhpiKuAJHasOqCm7lBnsd22nrM4+OYI4ASWCxen+ocTMl7BIAKgGag97TlLiTl6rrau5aPe1VGUm9jQBng==", + "requires": { + "@babel/runtime": "^7.5.5", + "csstype": "^2.6.6" + } + } + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -12159,11 +12412,21 @@ "mime-types": "~2.1.24" } }, + "typed-styles": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.5.tgz", + "integrity": "sha512-ht+rEe5UsdEBAa3gr64+QjUOqjOLJfWLvl5HZR5Ev9uo/OnD3p43wPeFSB1hNFc13GXQF/JU1Bn0YHLUqBRIlw==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "ua-parser-js": { + "version": "0.7.20", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", + "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" + }, "uglify-js": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", @@ -12185,6 +12448,15 @@ } } }, + "uncontrollable": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.0.1.tgz", + "integrity": "sha512-MGlbii7jczJYfY2GbmZi4j1JmB/6giM0Xc/WcKfxEN5W86KS8NPH/Fq/AD1nKjiFEMq7/MRwTCtzKWCeYgiWMA==", + "requires": { + "@babel/runtime": "^7.4.5", + "invariant": "^2.2.4" + } + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -12480,6 +12752,14 @@ "makeerror": "1.0.x" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", diff --git a/client/package.json b/client/package.json index de8dddd..ff33ac7 100644 --- a/client/package.json +++ b/client/package.json @@ -5,6 +5,7 @@ "dependencies": { "axios": "^0.19.0", "react": "^16.10.1", + "react-bootstrap": "^1.0.0-beta.12", "react-dom": "^16.10.1", "react-scripts": "3.1.2" }, diff --git a/client/src/App.js b/client/src/App.js index d609f55..c114124 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -33,7 +33,7 @@ class App extends Component { return( <div> {this.state.movies.map((movie, index) => ( - <p>{movie.Title} </p> + <p><img src={movie.Poster} />{movie.Title} </p> ))} </div> ) diff --git a/server/routes/movies.js b/server/routes/movies.js index 2a002b6..62102ba 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -3,6 +3,7 @@ var router = express.Router(); const Movies = require('../Schemas/Movies'); +/* GET search result */ router.get('/Search', function(req, res) { Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }, (err, data) => { if (err) return res.json({ success: false, error: err }); -- GitLab From 59357cb50e3f7d4729e9edcfd7e562470e23348b Mon Sep 17 00:00:00 2001 From: asszewcz <asszewcz@stud.ntnu.no> Date: Mon, 7 Oct 2019 14:46:48 +0200 Subject: [PATCH 02/61] Add logo, and give website the name Online Movie Gathering. Ref #6 --- client/public/logo.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 client/public/logo.svg diff --git a/client/public/logo.svg b/client/public/logo.svg new file mode 100644 index 0000000..6fe331d --- /dev/null +++ b/client/public/logo.svg @@ -0,0 +1 @@ +<svg id="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="444.44444444444446" viewBox="0, 0, 400,444.44444444444446"><g id="svgg"><path id="path0" d="M176.000 103.185 C 171.021 104.240,162.837 106.398,162.333 106.789 C 161.891 107.132,161.778 131.277,161.778 224.872 L 161.778 342.524 162.898 343.034 C 165.045 344.013,178.586 347.111,180.714 347.111 C 181.237 347.111,181.348 335.590,181.443 271.341 L 181.556 195.572 184.157 207.008 C 186.293 216.400,196.188 260.733,198.950 273.285 C 199.919 277.691,199.173 280.398,209.384 235.434 L 218.444 195.535 218.557 271.323 C 218.654 337.093,218.754 347.111,219.312 347.111 C 221.460 347.111,234.045 344.217,237.000 343.044 L 238.222 342.559 238.222 224.851 C 238.222 109.094,238.208 107.135,237.366 106.684 C 236.189 106.054,224.289 103.157,221.547 102.832 L 219.317 102.568 210.027 143.395 C 204.917 165.850,200.542 185.122,200.305 186.222 C 199.895 188.118,199.645 187.207,195.500 168.667 C 193.095 157.911,188.789 138.661,185.930 125.889 L 180.733 102.667 179.367 102.714 C 178.615 102.740,177.100 102.952,176.000 103.185 M144.667 113.214 C 142.715 113.702,135.465 117.369,130.000 120.632 C 108.429 133.514,91.142 153.494,81.609 176.561 L 80.000 180.455 80.000 222.408 L 80.000 264.362 82.005 269.148 C 93.427 296.409,117.913 321.090,143.444 331.075 L 145.778 331.987 145.778 222.438 C 145.778 162.186,145.728 112.912,145.667 112.940 C 145.606 112.969,145.156 113.092,144.667 113.214 M251.556 225.110 L 251.556 334.618 253.989 333.541 C 297.226 314.420,324.444 272.437,324.444 224.866 L 324.444 215.556 303.333 215.556 L 282.222 215.556 282.222 225.111 L 282.222 234.667 293.778 234.667 C 300.133 234.667,305.332 234.817,305.331 235.000 C 305.188 253.201,292.099 280.076,275.333 296.594 L 270.444 301.411 270.331 263.115 C 270.268 242.052,270.268 207.693,270.331 186.761 L 270.444 148.703 275.778 154.045 C 284.569 162.850,290.841 171.496,295.816 181.667 L 298.696 187.556 308.661 187.556 L 318.627 187.556 317.577 184.778 C 312.922 172.458,308.175 163.713,300.836 153.936 C 288.749 137.831,267.755 121.272,252.778 116.030 L 251.556 115.602 251.556 225.110 M126.558 260.516 L 126.444 298.881 121.307 293.774 C 112.284 284.803,106.083 276.171,101.089 265.628 L 99.111 261.453 99.118 222.393 L 99.124 183.333 101.265 178.889 C 106.040 168.975,112.640 159.777,120.878 151.557 L 126.444 146.003 126.558 184.078 C 126.621 205.018,126.621 239.416,126.558 260.516 " stroke="none" fill="#000000" fill-rule="evenodd"></path></g></svg> \ No newline at end of file -- GitLab From 833e441ac61f50afe8eb190987d5e758411c064e Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Mon, 7 Oct 2019 15:13:39 +0200 Subject: [PATCH 03/61] Set up basic website layout. Ref. #4 --- client/src/App.css | 55 +++++++++++++++++++++++++++++++++------------- client/src/App.js | 24 ++++++++++++++++---- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index afc3885..a49d57e 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,22 +1,47 @@ -.App { - text-align: center; -} -.App-logo { - height: 40vmin; +.main-container { + display: grid; + text-align: center; + grid-template-areas: + 'header header header header' + 'content content content content'; + grid-template-rows: 2fr, 5fr; + grid-template-columns: 1fr; + grid-gap: 1.5vh; + margin: 1vh 2vh 0 2vh; + } + +.main-header-container { + grid-area: header; + display: grid; + grid-template-columns: 1.5fr 5fr 3fr; + grid-template-rows: 1fr 1fr; + grid-template-areas: + 'Logo Search Filter' + 'Logo Search Sort'; } -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); +.logo-wrapper { + grid-area: Logo; + background-color: black; color: white; } -.App-link { - color: #09d3ac; +.search-wrapper { + grid-area: Search; + background-color: azure; +} + +.filter-wrapper { + grid-area: Filter; + background-color: indigo; } +.sort-wrapper { + grid-area: Sort; + background-color: cyan; +} + +.main-content { + grid-area: content; + + } diff --git a/client/src/App.js b/client/src/App.js index d609f55..4532df4 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -31,10 +31,26 @@ class App extends Component { render() { return( - <div> - {this.state.movies.map((movie, index) => ( - <p>{movie.Title} </p> - ))} + <div className="main-container"> + <div className="main-header-container"> + <div className="logo-wrapper"> + <p>Logo</p> + </div> + <div className="search-wrapper"> + <p>Search</p> + </div> + <div className="filter-wrapper"> + <p>Filter</p> + </div> + <div className="sort-wrapper"> + <p>Sort</p> + </div> + </div> + <div className="main-content"> + {this.state.movies.map((movie, index) => ( + <p>{movie.Title} </p> + ))} + </div> </div> ) } -- GitLab From 17ef65854419682e45db981d76e3246654d8a728 Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Tue, 8 Oct 2019 11:28:22 +0200 Subject: [PATCH 04/61] Adjust the layout. Add search bar. Real update of search results Ref. #3 --- client/package-lock.json | 206 +++++++++++++++++++++++++++++++++++++++ client/package.json | 2 + client/public/index.html | 8 ++ client/src/App.css | 5 + client/src/App.js | 24 ++++- 5 files changed, 240 insertions(+), 5 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 440e4a4..18eea1d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1157,6 +1157,16 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@restart/context": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==" + }, + "@restart/hooks": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.14.tgz", + "integrity": "sha512-k57+iyGr6o1XHeWWsGe5aMHKYcw7fukL6mCE+ZrPjtt1gXei5wCUxj71yQYfFbNjg0z5xxX8Els/UmyJiEn4nw==" + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -1335,11 +1345,25 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==" }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, "@types/q": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==" }, + "@types/react": { + "version": "16.9.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz", + "integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -2393,6 +2417,11 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3273,6 +3302,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", @@ -3649,6 +3683,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", + "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3938,6 +3981,11 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz", + "integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -4241,6 +4289,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", @@ -5769,6 +5825,11 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", @@ -7752,6 +7813,11 @@ "object.assign": "^4.1.0" } }, + "keycode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz", + "integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -9152,6 +9218,11 @@ "ts-pnp": "^1.1.2" } }, + "popper.js": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz", + "integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==" + }, "portfinder": { "version": "1.0.24", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.24.tgz", @@ -10105,6 +10176,25 @@ "react-is": "^16.8.1" } }, + "prop-types-extra": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.0.tgz", + "integrity": "sha512-QFyuDxvMipmIVKD2TwxLVPzMnO4e5oOf1vr3tJIomL8E7d0lr6phTHd5nkPhFIzTD1idBLLEPeylL9g+rrTzRg==", + "requires": { + "react-is": "^16.3.2", + "warning": "^3.0.0" + }, + "dependencies": { + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -10277,6 +10367,33 @@ "whatwg-fetch": "3.0.0" } }, + "react-bootstrap": { + "version": "1.0.0-beta.14", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.14.tgz", + "integrity": "sha512-UGK5f78FE8wAei1YL/oSwFlJZLqxJ/h4S8DCwHyY8hQjFCrjEW5PoEBTOOhQ6PQL6WOsZe1jkiOJG7L5TZWu+w==", + "requires": { + "@babel/runtime": "^7.4.2", + "@restart/context": "^2.1.4", + "@restart/hooks": "^0.3.11", + "@types/react": "^16.8.23", + "classnames": "^2.2.6", + "dom-helpers": "^3.4.0", + "invariant": "^2.2.4", + "keycode": "^2.2.0", + "popper.js": "^1.14.7", + "prop-types": "^15.7.2", + "prop-types-extra": "^1.1.0", + "react-overlays": "^1.2.0", + "react-transition-group": "^4.0.0", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + } + }, + "react-context-toolbox": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz", + "integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A==" + }, "react-dev-utils": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.4.tgz", @@ -10352,6 +10469,50 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.1.tgz", "integrity": "sha512-BXUMf9sIOPXXZWqr7+c5SeOKJykyVr2u0UDzEf4LNGc6taGkQe1A9DFD07umCIXz45RLr9oAAwZbAJ0Pkknfaw==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-overlays": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.2.0.tgz", + "integrity": "sha512-i/FCV8wR6aRaI+Kz/dpJhOdyx+ah2tN1RhT9InPrexyC4uzf3N4bNayFTGtUeQVacj57j1Mqh1CwV60/5153Iw==", + "requires": { + "classnames": "^2.2.6", + "dom-helpers": "^3.4.0", + "prop-types": "^15.6.2", + "prop-types-extra": "^1.1.0", + "react-context-toolbox": "^2.0.2", + "react-popper": "^1.3.2", + "uncontrollable": "^6.0.0", + "warning": "^4.0.2" + }, + "dependencies": { + "uncontrollable": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.2.3.tgz", + "integrity": "sha512-VgOAoBU2ptCL2bfTG2Mra0I8i1u6Aq84AFonD5tmCAYSfs3hWvr2Rlw0q2ntoxXTHjcQOmZOh3FKaN+UZVyREQ==", + "requires": { + "@babel/runtime": "^7.4.5", + "invariant": "^2.2.4" + } + } + } + }, + "react-popper": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.4.tgz", + "integrity": "sha512-9AcQB29V+WrBKk6X7p0eojd1f25/oJajVdMZkywIoAV6Ag7hzE1Mhyeup2Q1QnvFRtGQFQvtqfhlEoDAPfKAVA==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.3.0", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + } + }, "react-scripts": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.1.2.tgz", @@ -10413,6 +10574,28 @@ "workbox-webpack-plugin": "4.3.1" } }, + "react-transition-group": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz", + "integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "dependencies": { + "dom-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.0.tgz", + "integrity": "sha512-zRRYDhpiKuAJHasOqCm7lBnsd22nrM4+OYI4ASWCxen+ocTMl7BIAKgGag97TlLiTl6rrau5aPe1VGUm9jQBng==", + "requires": { + "@babel/runtime": "^7.5.5", + "csstype": "^2.6.6" + } + } + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -12159,6 +12342,11 @@ "mime-types": "~2.1.24" } }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -12185,6 +12373,16 @@ } } }, + "uncontrollable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.0.2.tgz", + "integrity": "sha512-7fa8OBQ5+X4VAcp0os6BD74bCeUPQSHmr4Rqy75Me98NnlD5kNShCqqx4xWo4OmlAMiT2/YSMklLFC4FCuoGYg==", + "requires": { + "@babel/runtime": "^7.4.5", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + } + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -12480,6 +12678,14 @@ "makeerror": "1.0.x" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", diff --git a/client/package.json b/client/package.json index de8dddd..8abf6b2 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,9 @@ "private": true, "dependencies": { "axios": "^0.19.0", + "bootstrap": "^4.3.1", "react": "^16.10.1", + "react-bootstrap": "^1.0.0-beta.14", "react-dom": "^16.10.1", "react-scripts": "3.1.2" }, diff --git a/client/public/index.html b/client/public/index.html index a146b6f..4bb9dcc 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -24,6 +24,14 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> + + <!-- React bootstrap CSS --> + <link + rel="stylesheet" + href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" + integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" + crossorigin="anonymous" + /> <title>React App</title> </head> <body> diff --git a/client/src/App.css b/client/src/App.css index a49d57e..8fabd76 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -35,6 +35,7 @@ .filter-wrapper { grid-area: Filter; background-color: indigo; + color: white; } .sort-wrapper { grid-area: Sort; @@ -45,3 +46,7 @@ grid-area: content; } + +.movie-fakk:hover { + background-color: black; +} diff --git a/client/src/App.js b/client/src/App.js index 4532df4..8f66487 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,6 +1,11 @@ import React, { Component } from 'react'; import axios from 'axios'; import './App.css'; +import {ButtonToolbar, Button, ListGroup, Form} from 'react-bootstrap'; + + +{/*} +dsadadasasas*/} class App extends Component { constructor(props) { @@ -37,19 +42,28 @@ class App extends Component { <p>Logo</p> </div> <div className="search-wrapper"> - <p>Search</p> + <Form.Control size="lg" type="text" placeholder="Large text" onChange={change => this.getMovies(change.target.value, 0, 10)} /> </div> <div className="filter-wrapper"> - <p>Filter</p> + <p>Filter button</p> + <ButtonToolbar> + <Button variant="primary">Filter</Button> + </ButtonToolbar> </div> <div className="sort-wrapper"> <p>Sort</p> </div> </div> <div className="main-content"> - {this.state.movies.map((movie, index) => ( - <p>{movie.Title} </p> - ))} + <ListGroup> + {this.state.movies.map((movie, index) => { + return( + <ListGroup.Item className="movie-fakk" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> + {movie.Title} + </ListGroup.Item> + ) + })} + </ListGroup> </div> </div> ) -- GitLab From d653372420ba8bbd057f1fcf4cfe96838d330f96 Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Tue, 8 Oct 2019 11:34:48 +0200 Subject: [PATCH 05/61] Change ico to be the logo. Ref. #8 --- client/public/favicon.ico | Bin 22382 -> 3902 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/client/public/favicon.ico b/client/public/favicon.ico index c2c86b859eaa20639adf92ff979c2be8d580433e..127c5c6c8baa133b13f1e2fa45d8c12a7b134a71 100644 GIT binary patch literal 3902 zcmeH~O(?8U7{}iU3m?tMFbmCyCS~JVD0WO)AS<OLJ3<mMSWGr|F-bnMr7)C|kCJ>X zkdG+JMzKOEQRE|rW+?u5{?F|lGj547w_CS4(|OOFGjGrDJpbo8&uJ7z<N27970)wS zNz*BcK~a=6Pd9nG#nZXY_EW1U<{!ViEO;IWwU7e;xdJaQFSx$G#?#Z2c*>D;uK$hp z{QQiqtu5r{=3;MePY@h)a&oY_xry7`+n`}-tEZ<2eSLkHnVCUsY%F$mcEmnM&d0~c zQCC+7yWNhPo11U3Pft%#Qc?nqMgyzWirLv&#Kpz=G4*;qCMG6OSXe0U@%^*2vw*ma zQ*m)I5)%_ayUoqbUgq}pw%FvzJ*dp|^mLd^CX|(x;r{;qOI+pzbut(XVlO*88%<43 zA2Rdv^HE)0jj^#YXti4O_xFE^d3=0~l#~?MY&JYTJ|ZhC%a6&N+27xn+)@1`v#P2J z%(e3Ja%5&^;_B+^)7;h7Rnd&2%jLp*KNl7jWIv0Gi=sQVRhfjWtgIj*Apy(F%U@!~ z$H!x1W8)7d*9Qg$#P`9$L2#}zsqMqV0}c)jU^bi4+uIuu)9rTq`N{paw6s8{)1kGs z)f;pASzB8x=PETd6%i2;XlrW=3X^{2<>kq_;OOgTO-&6LkM{O<baZrJeSJM3=KFX? zM@LHxdEN62#l*y5a&poyo}UK!ytuZuCOO1h@YQy1ZVut$;o>9nm9>HzzGp5iEupKc z3wL*SK2|^edUA3C#-y>a5l*Mm8&8!P6%_^Q7#SJq$K=}i`8hH&GEiDtD*OGI^!DrP z>npmuyTw<B!{I%jWQK)>At@;dJSVI<oYT*Of&$@GR#w7jG-7vm_s!rZzfl*SZRRQK zICFJ=eqQvnSS+}_yp%hJ`c6$vp{S?`j5TZ5=;$c8??Bqc#RV!VDnvilc$L}E&>%MJ z>+59?WU|KdOw#A?-lwd6LqkIt85sfV4|8aEcvxJZ?wn6gPs@GAdKk>cz4LDC?Ciwh c;h~s5Iy#d4<a(%u6bLC0d<E41q5o?91sG7bumAu6 literal 22382 zcmeI4_m@>g631uH?hiA>Aq+VTNjPWS;EE`MGJt>?K?MV56hy>`7*J7EOc)R|k_>>b z$<4tHZVqnp59+N?_4~fpbLZao?tAP3ciB0o&waPMLU(m_RdsdWX>0pJ+ZWq9JKOBp z-M02iZEdsK+S<Ci<NHCY-)(h0J@I|Ug0{BBU2SbM491!aBi7p^_iyIOGs3A~o)KDp zf}513%euq0YsQB;vrekkpE>iSaLvu*!^O)cRqLua3I8RFCx>l&hKB3cjSJ6zFeF@g z-Gs36nhD|g14F_Mw~Y%=?H(G=J$G{N{wBO{Tt6;cedBoNG3#s7!}dd+;l&R-!|7k2 z?sd?+X~WpweMtCUw|1QO6L~CO(H*wFH7q>-#;|bd%84$Yt8W_b{pcmH3V-;myMJ_e z^?QTD_Jf^a*T)0Gv+oY^^3E>?gclEYhS$D7D6D^IbhSS&1LJ?`6%)e?*4H)nj&a-v zo*xk|xwJc6v~*Iq@Zw4So4V}>I>XI(j&+#JubSv`IO~jQ;koyRgfqSo<-O^d5#Dcb z5C5wp9R6oVxbUJ$UU&bqBYf<qpEBM1b=K@@f%5EoLjrY-3z)~>9Ok?o-)PVW>NbII z{e>4i;msH>yK<uU>E^q}`n)~3y(=tTZv8hr`oUPd|2D?8HpUlhP8hSs4`1eJ{ez=j zM$8%VduIO-FS|Y-=<-3f$QW5y^|c~@Xx(6Xvubsmf98+5qu>0d9UZ*x)^U-q@RIpQ zH?F*Xg3AFO%n3#O(5)NSk1f<cbQ2v#SJ7E?_X^WFexuXq_L3!&3;81l)(HJ%%}CE! zOV{2qKCrf`<5s^ljuxM}MxlvH>`b?n9xK?VqCcPO7W??0!adbwmeM+w@9;XFg>ly# zFKc|;-l5@@?+gx)yfQp2yR5qqo;Cf@3ti#mZw_{S=e-gARPNc|3wox8_y624y#9kh z;gOxg!`nX_=(dag%$qy4I%n7_Y%+EUo4V!I;chQJ_)EWV&cZ2GxGa6W7mfe!ts}#G zf9x0LoL2E!Cr=NnYz^-HP5-d(xBcCgA)h_J8W8sXu79}RY{+R}F?(rqI{Va<9Ok~K zM-<8n{`KY_`|$L>p?zS^K69G;jRT+d3yg8cw+Dxwv!?ceLnio_dOXwz$s&E<`O%>8 z@Q&e*pKZU)?6mv<_AJ}ZJGTAx9}ddJPvMJxzx=JiVasd7d(j4Vk<ID5zwaMjGCjkW zGe=7n_nqg>FLwpTP()w#Mf3mUJHwoQ+K!JyAMrit-JDs|!dpKb7~cHxK%aAH3LgKd z`4g|SUB8Rw=guvo!hRbk{smp6KX3koHP9Xo{k_A>hyRFA>Y{HP8qhgnmVKtD_dGGO z>G_{GXPW!m$jf2{qrLZ&0pXor4)8f=PVmXMZX6TVTH!Z!=)-PX3-Fb0Ag4PY9Tn!E zU0bh`Pbw3BpK0riSmNN{I=no&Ygj<X?|yuw#{tM0eV%D+EQ?FsOGi3=E{GZKFrLt% zUmutaL0;IVxu?h2B*mvci_V!6-v0T(aNw`~!d;Jz^0@^DzfF8`>wRMavOf1bi!%-W z;!7sEO~NM1&YgGuWQRd6JIr1(_Zk;6djC)T+&-nc3awezUu2DaTfAsWulZu`ktO|l z=*6zc+hPI6!@N)!Cw2&0=sY$P9AX~oRIsP4NBAHk>gnI*SI4zXT*kZV`Av+o$L1a$ zn!`uU7HfXvv)%auzhK_faL8b>k&3TVddP@AuUJ)y^9*N2h5z{%WMUv|g9fzVkBx&z zU;Z6c{=fjc_mloHJ~Mc5RFD&WX3VOC$6F?oH-0oIEHoLV{Y8erV`m>U{Q-x$NB&2w zp2BbG05HIU!@dA`(OYab3LRsvnRB(#P8+_9xx3x=0{9&2(IM>nZ4ZnNhtNNZ6RE!l z{WIOeFT$I;^Gr9<QFIlZC9b3%JH;A8hv1L>q@JQa@Fbpmz+!a9!x$d3I2l>qzB%qq zz$NaTX?_rR6{lnCkF}|>VXqE;+F>?psMCNBYnwGLzQWHwnZpeakE;56WVz?p1H$f# z{bAheHjNHz?j94?+%qQJX0iXH=JSEe+8_GE-U^$R<)im(fAo&+#IIvZv8hk*AL@4h zi0LmfLe{ds=mGYL@iKPi9$ajR=;iUFt*9{f!~kjgZ2XVuDf&9cY#H{9d0}1Sn^IWz zoAf8G%l@WM)TwXj`_7Ln<}>}uwl6Xp_3j_~yDl*2{KkHYhTyTi=&s(g?RqbIGA8zn zdF`?p%sKnaJeY@fbcI)KZ=Khc))n=Su|4tjus;1qm+&>L5!MvH&rbA9^oUcpSLUyX zAFxGb<F*m5L&TE65L*&s5__^9ux0H1u#Xh>hxkEYS!cvW_n0mg(Wjn%>8U>I{hS3; ze2nBLm`mad<h|8of=v-Tu{dL;4d250Bu2vT;&Vh#@Y#3O=RL7)_+a)F_;1<SJlIQY zZvt=jD)a~W<iTa@vi&)h@9;XF1z8-+@n-oI<pqvaJ$Z^AD_g95-t!b$y*<|N$5T(T zdULGbkLMcwJn%l&^8P&XqfaMt`TzPqI$zDci8F_LpB!m<wJzTeaW<8gyK8pMvln2` z!9I^X0l6b`weW0~Rv!G(u1DfU_9NOe$0zxVcoxN(*z(Kcd5qRr8GR#Xz*!FaOY#a? z`s`Qd&5f~Y8G7QKH{P_Zncw6WUit1|A1nQ1%tcSmwurkM(Ps~`<@H(~ridQ(+8Z{a z*{Ho$`LRDg{F#mUv-Ysh_QdEsu{nFVjZcmAy^Pv<vaj;<*t|T0*<f;F<V6n~y(0z> z4*6jAidWq*!E+;=tC6=-d!v0gW3M+p+FB{>*~m#ScIKTk&nz8sY^&{zk(?E9Dv#Ox zrh9te?tgAX)Sn6*d+&9d#`J;{eR6H&jG%{n&_lH~`)nkiqIS-hrSF{YraDF+(s%Mt z?B4{>dS)-jc?bD0av|umbo~j-57Q<ZS$e#a{}8?9E1S13qTT9~b9wc7!{$-dzSg71 znJ#$+^_x94c?`XiUtMi<vw65Ynye%7CkLGeQ?@QozA0{A`^Ie}P5;nA%ZJ*!Z3M6O z3of*CwaWSxzVw)VK5%*bgs*mdpz!gF+4i#U@M@)ykB@$-y?S<9mJWGl=3MyX9sPbs z6MZ4CEPT!b%ks{`NuFipC3y0O(m!(6W$|a*J%^o%&x~unJ-lJ(Y;!C3OXy=GdYq?- zKjUu%U#~rFEBkx%OU!??S=jzEc&B}Jy5DaR9(iPZh~BZI*zz*8xO*Zz;-oS-MKERL zo#Fyd4junY!8d2y^WMp|N1S%IgRBF_K|Ygu)-&;Q*8b)7k9b@BIgifcr@B1+w2k&= z%Pn?BA%D(UMI(G{zt0!(g5A;bdq)+%+&7ay2ZorT5k2lci9hE`jo^#gkppd#N7;Af z=~;usF7@zPtHgd<ubcs8;kgSZedc^AJu}SfA7?RwUsXAe6U{t$weifhr)_0_b7rdX za95#<XR+-<i}N{$L@)4@oF566K68$aJQyd1vv6|%8V^s-<nb}c>KfCX3oPC`<CNMR zOU{r)Gd*{%_QS?m_IFkurQU6voK3K1Isd-T&NMhz;hZz-Wn9nEN5^=c-*KJS-kWLX zOpdc$VoBm-=WYD3tLa!v^}iw?V4E%M<2bj)e&f4U<d2KrneRB)+`Hg>Z<iI~A?Dv> zL$eLV_%&X@z>l*S)`G8>7IFf}t&F_bqg=aYoX;`(DP2Ve<G7<Q<oxOxJ5S|I7`;|I z{=)UQ(#PESvNrsm`xc`MF8hD>f80NHee?Rb=aT-5Ke04%IeV*I`pKN9?e%`oGTkC3 zPyMvUo<2E4>q+je`QErP26VQpU#Bm?5)X+!cVNAbCLin$`~E1`I1hr;lRY&0nc9nb zV=S6~&LEu^KC9xNd2(J)K8k%PI?Q^9x9*XU_d+k-Kbt=LJ=Tis=|>Si^^taBDriCj zTF|7Ob8)|;VfE-5_foj4#Jvc?D}TTpCC-rR@uMwUh(Fj<VQ=JPd2$}i{a)P#1TXG` zuxIUlZ;CqK>)JbYx@VW=!MQr;y4*v-o@Cp3=k9~}6BkkE`#z(~7+Du79(S-gQ;l+n z{E0O<D^B~I;`Esh8$147`o$Cf<@b85Eejv>0NAAtp98-u<u<It<2v$#@YCFKZ<BlN z-bcd`AMVs~rkv6z7Z&e4wfkHUD>4UK?~1!q`rMZ$2gSKEJR<#ezgwiZvKM@!m+;1C z*YG<0-b~$fWHRB7pvJ*k7k+kM&i$O!&x0d8p3-N@iYNYG^ptDl9lxRRr11L9d=nR< z!@|$@N3fCy=l%E~bPgQu8v&#CEWF;aPtlLZ^$I@rQgRbMIooJQ&`s8sO6{GyID&^h zcO>wYf)ze@m&8AXr!IYGjeUqd8ZUgwe6ts6JHJ)1nex5di$K0=tM?>VRm)Se7TN>f zMx;2zXmK85yZpEK=^Yr|tq?x3vD$fJ|L`?xZ}j9F5#b}Z6h4&~_dlXewtKu;M!v%5 z?w<6SxljEa{P;#l?XfRW_o>r*Ry#IUym{i!J%?(rz>q&p+o50Yoo_Hio4co3c;S+F zjQz3ucxG!<U*^f3I;}HecijUNZgv0X<Bj7GeD=wAOTyy`E#1+jf3BNWpTgApP0mf@ zVve)0k_Yy}eYUONbWf*t;bq^^MfqgmvxiOL*#k08wX+`MZ`NiKUl`27`L#R6&{%!P z*oxhVIOIvwdw<#f^m?~7jJ)6lPu6(a&To|-i>-*yy+39%m@~E2dv35b1x{L+FaO3W zq3`Ep6}T9q^u__7^Rko|>xlcKDQ$jZ%Tu_b-`uBk{W2W(oSd1Zu*jEp%~gzBu~Bvh z(qw@D!I!8#3y+P&7fHXkQ>OI{?-ZUmC;Gogzoz%`o%mk1Py9Fo5j|p2*CDfk#3b^6 zY5&C!o57u3!P7r-Hfk6BK2IADF&pqI+;#N(X^EVKr#KZ@@^{>|*0|WCDyJ!UWbmAQ z>nIrT_)T3_?}?o}H)ie31+f+1JqS1V&Ybn1?>4eI2Vc+DrhcT)to)EG=W4``(eK81 zfP2Rtb0u`hy+z&Z@bf?D@*Q5pv%C3~yX{$i#DSb^liNt|m;$H%5J!tYziA`Z7eD-( z;K>1yQzPzEJGjI@9`_`D7JcFJo>#~*az=x{lK({Z=piu=b4kob8#ak=Mxeu<B%bqF z+-fo;9?hd)uZ~zg&U3rRz{F<2v&T|Bc7c6A@jc%v#dQ+<N}uTGQv0T$9v{horwZ`; zjs4-fhuYqobrJPJdI(?c6zj|m{5u{QWpHr~u{R@sAet$C>ay>?|Ft+;_=-b;C4OUH zC*E=WMEfCl;t%ow?A`O|XZfjL^`6*rz6Is`5An`>V)wbD%6GNy6HNxdA&=beTN1g$ zA3Y|Y6USxWAedgU#?t&p9$)InzmcO!WnFJfYRk%_?z^ocVp+yR-Y@#q#DBYAxB2jA z^w+FM`iDHQiOdi7i97rh?!2>(-ZOf>UorST_(rc*S8G?UgSFVoco=Id1)r!~Cvw1N zu-=q2DDv;oHX$#@P+aob@!#x!IY)sf`1sSZ`;@$KBD1>lUC>_J&vf`V;F3M-gm{yE z7w6)flaU+K+C&eSGv&UO|0b7O)9ZG(yNr|X$5*bJ5Wn}V#3A&TSW58NTk&sYJSlG3 z-<&yUO!TuJ9^dx(zS_nPPHNADN8BZP%yXlhKKWI_r{7$OUNg^D<{zKRdZZAar~LBh zac_tD!d|dOs{aqb^oz43?a_hZyPQV!;`v+T&;Ga(d{H}nZKV)T$=|Lp{VYR+Z?N#4 z6yiF*>te0A9dV!9?s@Yvw1{=YpL>U8aPnY~3-v|vDQfo^ucFW70L$RxkBM^?Ki~(5 zE8;jheD8z*E7Kq1Bk4ajrVM|<Wa+EEnWw%>rw{CH%i?ku>NWd@20Ym>p2YQh+f$Yf zcP@mV&4-kwA-JqO%DnsdZ7s8&*_VhG&qnym>^>-c2iJ@JPR|G$;nVK>t_nWBw;BE^ zeSWu6*jvhfvri~XgS~+7fiu3J{8lsj1nyjjKmMmI|14aY`2r^^U-Br@zgsNVnQia$ zZgC^uxM^R6{*x;p4%GUm4cni`FH6sJRYn6k<XW@s&Ag|uWq)(08Qv7){d#!pi?-Sw z9m$I)cOJ0?w2@yBE!xPXvX`t!lU%xR(3^U&joQk{JEZ|UdkjDSwl#?F&bzNc4u-e} zU#D1s@!>nuJqqUp@vJM_1N0idKwLrYGuyxF-q?8KL+a+I8rUOfw_5oB2lD5f;d3X4 zJ5Ah&WdE1FA3)secO`Rp62r5<<g8OPSr?praK=cC#~l#vSYtn;4{fjJGg}=4^O}`! z*>~m_U#9gdeUyLKJ9CbYQqHy>{e|<V__y)k)bNR!YVV@YJwkE?_2`uuU#i>9{^t8& zegB|7>&clAck|M<QI<YFl5YlBTcVdf;hBD`Tb6Df+)>NVbeFrktOvg3z02+fklW+i z75E(uea`*)|61hwI0Glv#osFTd$jcF+lgF`R(}5r_>AF1E=RLI|J&#PL|*%vcRjTR QPNaAH_W=L@JAozeZ|l@fegFUf -- GitLab From f6ef667ffcb37f0c15e27b1fd58c6bd82abb84d3 Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Mon, 7 Oct 2019 15:13:39 +0200 Subject: [PATCH 06/61] Set up basic website layout. Ref. #4 --- client/src/App.css | 55 +++++++++++++++++++++++++++++++++------------- client/src/App.js | 24 ++++++++++++++++---- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index afc3885..a49d57e 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,22 +1,47 @@ -.App { - text-align: center; -} -.App-logo { - height: 40vmin; +.main-container { + display: grid; + text-align: center; + grid-template-areas: + 'header header header header' + 'content content content content'; + grid-template-rows: 2fr, 5fr; + grid-template-columns: 1fr; + grid-gap: 1.5vh; + margin: 1vh 2vh 0 2vh; + } + +.main-header-container { + grid-area: header; + display: grid; + grid-template-columns: 1.5fr 5fr 3fr; + grid-template-rows: 1fr 1fr; + grid-template-areas: + 'Logo Search Filter' + 'Logo Search Sort'; } -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); +.logo-wrapper { + grid-area: Logo; + background-color: black; color: white; } -.App-link { - color: #09d3ac; +.search-wrapper { + grid-area: Search; + background-color: azure; +} + +.filter-wrapper { + grid-area: Filter; + background-color: indigo; } +.sort-wrapper { + grid-area: Sort; + background-color: cyan; +} + +.main-content { + grid-area: content; + + } diff --git a/client/src/App.js b/client/src/App.js index c114124..4532df4 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -31,10 +31,26 @@ class App extends Component { render() { return( - <div> - {this.state.movies.map((movie, index) => ( - <p><img src={movie.Poster} />{movie.Title} </p> - ))} + <div className="main-container"> + <div className="main-header-container"> + <div className="logo-wrapper"> + <p>Logo</p> + </div> + <div className="search-wrapper"> + <p>Search</p> + </div> + <div className="filter-wrapper"> + <p>Filter</p> + </div> + <div className="sort-wrapper"> + <p>Sort</p> + </div> + </div> + <div className="main-content"> + {this.state.movies.map((movie, index) => ( + <p>{movie.Title} </p> + ))} + </div> </div> ) } -- GitLab From 589801c972138bb2721c4ed1c747d470dff561c7 Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Tue, 8 Oct 2019 11:28:22 +0200 Subject: [PATCH 07/61] Adjust the layout. Add search bar. Real update of search results Ref. #3 --- client/package-lock.json | 102 +++++++++++++++++---------------------- client/package.json | 3 +- client/public/index.html | 8 +++ client/src/App.css | 5 ++ client/src/App.js | 24 +++++++-- 5 files changed, 78 insertions(+), 64 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 7551acf..a2d896f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1157,29 +1157,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, - "@react-bootstrap/react-popper": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@react-bootstrap/react-popper/-/react-popper-1.2.1.tgz", - "integrity": "sha512-4l3q7LcZEhrSkI4d3Ie3g4CdrXqqTexXX4PFT45CB0z5z2JUbaxgRwKNq7r5j2bLdVpZm+uvUGqxJw8d9vgbJQ==", - "requires": { - "babel-runtime": "6.x.x", - "create-react-context": "^0.2.1", - "popper.js": "^1.14.4", - "prop-types": "^15.6.1", - "typed-styles": "^0.0.5", - "warning": "^3.0.0" - }, - "dependencies": { - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } - } - } - }, "@restart/context": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", @@ -1368,11 +1345,25 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==" }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, "@types/q": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==" }, + "@types/react": { + "version": "16.9.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz", + "integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -2426,6 +2417,11 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3688,12 +3684,12 @@ } }, "create-react-context": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz", - "integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", + "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", "requires": { - "fbjs": "^0.8.0", - "gud": "^1.0.0" + "gud": "^1.0.0", + "warning": "^4.0.3" } }, "cross-spawn": { @@ -3986,9 +3982,9 @@ } }, "csstype": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", - "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==" + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz", + "integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==" }, "cyclist": { "version": "1.0.1", @@ -10427,14 +10423,14 @@ } }, "react-bootstrap": { - "version": "1.0.0-beta.12", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.12.tgz", - "integrity": "sha512-qBEAthAzqM+OTS2h5ZCfV5/yZUadQcMlaep4iPyPqsu92JzdcznhSDjw6b+asiepsyQgiS33t8OPeLLRiIDh9Q==", + "version": "1.0.0-beta.14", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.14.tgz", + "integrity": "sha512-UGK5f78FE8wAei1YL/oSwFlJZLqxJ/h4S8DCwHyY8hQjFCrjEW5PoEBTOOhQ6PQL6WOsZe1jkiOJG7L5TZWu+w==", "requires": { "@babel/runtime": "^7.4.2", - "@react-bootstrap/react-popper": "1.2.1", "@restart/context": "^2.1.4", "@restart/hooks": "^0.3.11", + "@types/react": "^16.8.23", "classnames": "^2.2.6", "dom-helpers": "^3.4.0", "invariant": "^2.2.4", @@ -10528,6 +10524,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.1.tgz", "integrity": "sha512-BXUMf9sIOPXXZWqr7+c5SeOKJykyVr2u0UDzEf4LNGc6taGkQe1A9DFD07umCIXz45RLr9oAAwZbAJ0Pkknfaw==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-overlays": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.2.0.tgz", @@ -10565,22 +10566,6 @@ "prop-types": "^15.6.1", "typed-styles": "^0.0.7", "warning": "^4.0.2" - }, - "dependencies": { - "create-react-context": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", - "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", - "requires": { - "gud": "^1.0.0", - "warning": "^4.0.3" - } - }, - "typed-styles": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", - "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" - } } }, "react-scripts": { @@ -12413,9 +12398,9 @@ } }, "typed-styles": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.5.tgz", - "integrity": "sha512-ht+rEe5UsdEBAa3gr64+QjUOqjOLJfWLvl5HZR5Ev9uo/OnD3p43wPeFSB1hNFc13GXQF/JU1Bn0YHLUqBRIlw==" + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" }, "typedarray": { "version": "0.0.6", @@ -12449,12 +12434,13 @@ } }, "uncontrollable": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.0.1.tgz", - "integrity": "sha512-MGlbii7jczJYfY2GbmZi4j1JmB/6giM0Xc/WcKfxEN5W86KS8NPH/Fq/AD1nKjiFEMq7/MRwTCtzKWCeYgiWMA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.0.2.tgz", + "integrity": "sha512-7fa8OBQ5+X4VAcp0os6BD74bCeUPQSHmr4Rqy75Me98NnlD5kNShCqqx4xWo4OmlAMiT2/YSMklLFC4FCuoGYg==", "requires": { "@babel/runtime": "^7.4.5", - "invariant": "^2.2.4" + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" } }, "unicode-canonical-property-names-ecmascript": { diff --git a/client/package.json b/client/package.json index ff33ac7..8abf6b2 100644 --- a/client/package.json +++ b/client/package.json @@ -4,8 +4,9 @@ "private": true, "dependencies": { "axios": "^0.19.0", + "bootstrap": "^4.3.1", "react": "^16.10.1", - "react-bootstrap": "^1.0.0-beta.12", + "react-bootstrap": "^1.0.0-beta.14", "react-dom": "^16.10.1", "react-scripts": "3.1.2" }, diff --git a/client/public/index.html b/client/public/index.html index a146b6f..4bb9dcc 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -24,6 +24,14 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> + + <!-- React bootstrap CSS --> + <link + rel="stylesheet" + href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" + integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" + crossorigin="anonymous" + /> <title>React App</title> </head> <body> diff --git a/client/src/App.css b/client/src/App.css index a49d57e..8fabd76 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -35,6 +35,7 @@ .filter-wrapper { grid-area: Filter; background-color: indigo; + color: white; } .sort-wrapper { grid-area: Sort; @@ -45,3 +46,7 @@ grid-area: content; } + +.movie-fakk:hover { + background-color: black; +} diff --git a/client/src/App.js b/client/src/App.js index 4532df4..8f66487 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,6 +1,11 @@ import React, { Component } from 'react'; import axios from 'axios'; import './App.css'; +import {ButtonToolbar, Button, ListGroup, Form} from 'react-bootstrap'; + + +{/*} +dsadadasasas*/} class App extends Component { constructor(props) { @@ -37,19 +42,28 @@ class App extends Component { <p>Logo</p> </div> <div className="search-wrapper"> - <p>Search</p> + <Form.Control size="lg" type="text" placeholder="Large text" onChange={change => this.getMovies(change.target.value, 0, 10)} /> </div> <div className="filter-wrapper"> - <p>Filter</p> + <p>Filter button</p> + <ButtonToolbar> + <Button variant="primary">Filter</Button> + </ButtonToolbar> </div> <div className="sort-wrapper"> <p>Sort</p> </div> </div> <div className="main-content"> - {this.state.movies.map((movie, index) => ( - <p>{movie.Title} </p> - ))} + <ListGroup> + {this.state.movies.map((movie, index) => { + return( + <ListGroup.Item className="movie-fakk" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> + {movie.Title} + </ListGroup.Item> + ) + })} + </ListGroup> </div> </div> ) -- GitLab From a568b399ceed1bc47414eb69614ea34b6cbf9408 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Tue, 8 Oct 2019 12:35:10 +0200 Subject: [PATCH 08/61] Added user rating fields in DB. Ref. #10 --- server/Schemas/Movies.js | 4 +++- server/routes/movies.js | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/Schemas/Movies.js b/server/Schemas/Movies.js index a24ce06..ea78a18 100644 --- a/server/Schemas/Movies.js +++ b/server/Schemas/Movies.js @@ -26,7 +26,9 @@ const MoviesSchema = new Schema({ BoxOffice: String, Production: String, Website: String, - Response: String + Response: String, + UserRatings: Array, + UserRating: String }); module.exports = mongoose.model("Movies", MoviesSchema); diff --git a/server/routes/movies.js b/server/routes/movies.js index 62102ba..f45ad48 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -47,4 +47,9 @@ function stripReturnObject(result, from, to) { return returnResult; } +/* POST user ratings */ +router.post('/UserRating', function(req, res) { + +}) + module.exports = router; -- GitLab From 10643aeab63becd5ee5d19587a64a418175902d7 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Tue, 8 Oct 2019 13:13:36 +0200 Subject: [PATCH 09/61] Added user rating route in backend. Not tested. Ref. #11 --- server/routes/movies.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/server/routes/movies.js b/server/routes/movies.js index f45ad48..917f063 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -32,6 +32,27 @@ router.get('/GetMovies', function(req, res, next) { }) }); +/* POST user ratings */ +router.post('/UserRating', function(req, res) { + Movies.find({ "Title" : req.query.Title }, function(err, data) { + if (err) return res.json({ success: false, error: err }); + + let totalRating = 0; + + data.UserRatings.map((rating) => { + totalRating += rating; + }); + + let averageRating = totalRating / data.UserRatings.length; + + Movies.update( { "Title": req.query.Title }, { $set: { "UserRating": averageRating } } , (err, data) => { + if (err) return res.json({ success: false, error: err }); + + return res.json({ success: true }); + }); + }); +}); + function stripReturnObject(result, from, to) { returnResult = []; @@ -47,9 +68,4 @@ function stripReturnObject(result, from, to) { return returnResult; } -/* POST user ratings */ -router.post('/UserRating', function(req, res) { - -}) - module.exports = router; -- GitLab From 74de570c40dc14f82c351bbf8b7455c9b484ff1a Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Tue, 8 Oct 2019 14:00:29 +0200 Subject: [PATCH 10/61] Finish basic layout, including search bar. Refactor logo.svg and grid layout. Ref. #4 Ref. #5 --- client/public/index.html | 4 +-- client/public/logo.svg | 1 - client/public/manifest.json | 10 ------ client/src/App.css | 69 +++++++++++++++++++++++++++---------- client/src/App.js | 18 +++++----- client/src/images/logo.svg | 1 + 6 files changed, 61 insertions(+), 42 deletions(-) delete mode 100644 client/public/logo.svg create mode 100644 client/src/images/logo.svg diff --git a/client/public/index.html b/client/public/index.html index 4bb9dcc..0ce8a59 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -3,13 +3,13 @@ <head> <meta charset="utf-8" /> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> + <link href="https://fonts.googleapis.com/css?family=Righteous&display=swap" rel="stylesheet"> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> - <link rel="apple-touch-icon" href="logo192.png" /> <!-- manifest.json provides metadata used when your web app is installed on a user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - + <!-- React bootstrap CSS --> <link rel="stylesheet" diff --git a/client/public/logo.svg b/client/public/logo.svg deleted file mode 100644 index 6fe331d..0000000 --- a/client/public/logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg id="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="444.44444444444446" viewBox="0, 0, 400,444.44444444444446"><g id="svgg"><path id="path0" d="M176.000 103.185 C 171.021 104.240,162.837 106.398,162.333 106.789 C 161.891 107.132,161.778 131.277,161.778 224.872 L 161.778 342.524 162.898 343.034 C 165.045 344.013,178.586 347.111,180.714 347.111 C 181.237 347.111,181.348 335.590,181.443 271.341 L 181.556 195.572 184.157 207.008 C 186.293 216.400,196.188 260.733,198.950 273.285 C 199.919 277.691,199.173 280.398,209.384 235.434 L 218.444 195.535 218.557 271.323 C 218.654 337.093,218.754 347.111,219.312 347.111 C 221.460 347.111,234.045 344.217,237.000 343.044 L 238.222 342.559 238.222 224.851 C 238.222 109.094,238.208 107.135,237.366 106.684 C 236.189 106.054,224.289 103.157,221.547 102.832 L 219.317 102.568 210.027 143.395 C 204.917 165.850,200.542 185.122,200.305 186.222 C 199.895 188.118,199.645 187.207,195.500 168.667 C 193.095 157.911,188.789 138.661,185.930 125.889 L 180.733 102.667 179.367 102.714 C 178.615 102.740,177.100 102.952,176.000 103.185 M144.667 113.214 C 142.715 113.702,135.465 117.369,130.000 120.632 C 108.429 133.514,91.142 153.494,81.609 176.561 L 80.000 180.455 80.000 222.408 L 80.000 264.362 82.005 269.148 C 93.427 296.409,117.913 321.090,143.444 331.075 L 145.778 331.987 145.778 222.438 C 145.778 162.186,145.728 112.912,145.667 112.940 C 145.606 112.969,145.156 113.092,144.667 113.214 M251.556 225.110 L 251.556 334.618 253.989 333.541 C 297.226 314.420,324.444 272.437,324.444 224.866 L 324.444 215.556 303.333 215.556 L 282.222 215.556 282.222 225.111 L 282.222 234.667 293.778 234.667 C 300.133 234.667,305.332 234.817,305.331 235.000 C 305.188 253.201,292.099 280.076,275.333 296.594 L 270.444 301.411 270.331 263.115 C 270.268 242.052,270.268 207.693,270.331 186.761 L 270.444 148.703 275.778 154.045 C 284.569 162.850,290.841 171.496,295.816 181.667 L 298.696 187.556 308.661 187.556 L 318.627 187.556 317.577 184.778 C 312.922 172.458,308.175 163.713,300.836 153.936 C 288.749 137.831,267.755 121.272,252.778 116.030 L 251.556 115.602 251.556 225.110 M126.558 260.516 L 126.444 298.881 121.307 293.774 C 112.284 284.803,106.083 276.171,101.089 265.628 L 99.111 261.453 99.118 222.393 L 99.124 183.333 101.265 178.889 C 106.040 168.975,112.640 159.777,120.878 151.557 L 126.444 146.003 126.558 184.078 C 126.621 205.018,126.621 239.416,126.558 260.516 " stroke="none" fill="#000000" fill-rule="evenodd"></path></g></svg> \ No newline at end of file diff --git a/client/public/manifest.json b/client/public/manifest.json index 080d6c7..1f2f141 100644 --- a/client/public/manifest.json +++ b/client/public/manifest.json @@ -6,16 +6,6 @@ "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": ".", diff --git a/client/src/App.css b/client/src/App.css index 8fabd76..b484e69 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -2,51 +2,82 @@ .main-container { display: grid; text-align: center; + justify-items: center; grid-template-areas: - 'header header header header' - 'content content content content'; - grid-template-rows: 2fr, 5fr; - grid-template-columns: 1fr; + 'Logo Logo' + 'Title Title' + 'Search Search' + 'Filter Sort' + 'Content Content'; + grid-template-columns: 1fr 1fr; + grid-template-rows: 2fr 0.3fr 0.3fr 0.3fr 5fr; grid-gap: 1.5vh; - margin: 1vh 2vh 0 2vh; + margin: 0vh 15vh 0 15vh; } - +/* .main-header-container { grid-area: header; display: grid; - grid-template-columns: 1.5fr 5fr 3fr; - grid-template-rows: 1fr 1fr; + justify-items: strech; + + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; grid-template-areas: - 'Logo Search Filter' - 'Logo Search Sort'; -} + 'Logo Logo' + 'Search Search' + 'Filter Sort'; + grid-gap: 5px; + } */ .logo-wrapper { grid-area: Logo; - background-color: black; - color: white; + font-family: 'Righteous', cursive; + width: 30vw; + +} +.title-wrapper { + grid-area: Title; + font-family: 'Righteous', cursive; + width: 50vw; + font-size: 5vh; + } .search-wrapper { grid-area: Search; - background-color: azure; + padding-top: 2vh; + width: 70vw; } .filter-wrapper { grid-area: Filter; - background-color: indigo; - color: white; + display: auto; + padding-left: 10vw; + padding-right: 3vw; + width: 40vw; + + } .sort-wrapper { grid-area: Sort; - background-color: cyan; + display: auto; + padding-left: 3vw; + padding-right: 10vw; + width: 40vw; + } .main-content { - grid-area: content; + grid-area: Content; + width: 70vw; + } +.search-bar { + +} + .movie-fakk:hover { - background-color: black; + background-color: grey; } diff --git a/client/src/App.js b/client/src/App.js index 8f66487..2a695a7 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import axios from 'axios'; import './App.css'; import {ButtonToolbar, Button, ListGroup, Form} from 'react-bootstrap'; - +const logo = require('./images/logo.svg'); {/*} dsadadasasas*/} @@ -37,23 +37,21 @@ class App extends Component { render() { return( <div className="main-container"> - <div className="main-header-container"> <div className="logo-wrapper"> - <p>Logo</p> + <img src={logo} alt="logo" /> + </div> + <div className="title-wrapper"> + <p> Online Movie Gathering </p> </div> <div className="search-wrapper"> - <Form.Control size="lg" type="text" placeholder="Large text" onChange={change => this.getMovies(change.target.value, 0, 10)} /> + <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 10)} /> </div> <div className="filter-wrapper"> - <p>Filter button</p> - <ButtonToolbar> - <Button variant="primary">Filter</Button> - </ButtonToolbar> + <Button variant="outline-secondary" size="lg" block>Filter</Button> </div> <div className="sort-wrapper"> - <p>Sort</p> + <Button variant="outline-secondary" size="lg" block>Sort</Button> </div> - </div> <div className="main-content"> <ListGroup> {this.state.movies.map((movie, index) => { diff --git a/client/src/images/logo.svg b/client/src/images/logo.svg new file mode 100644 index 0000000..4336b53 --- /dev/null +++ b/client/src/images/logo.svg @@ -0,0 +1 @@ +<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0, 0, 400,400"><g id="svgg"><path id="path0" d="M154.962 38.556 C 149.625 39.881,144.825 41.397,144.296 41.926 C 143.766 42.455,143.333 113.749,143.333 200.356 L 143.333 357.825 150.000 359.553 C 165.784 363.645,169.060 363.850,169.879 360.799 C 170.292 359.260,170.683 315.400,170.748 263.333 L 170.866 168.667 181.978 217.767 C 188.090 244.773,193.428 267.206,193.841 267.619 C 195.192 268.970,196.872 262.589,207.503 215.741 L 218.000 169.482 218.697 266.407 C 219.080 319.717,219.530 363.406,219.697 363.495 C 220.387 363.862,233.165 361.164,239.723 359.267 L 246.779 357.226 246.390 199.897 C 246.065 68.699,245.698 42.382,244.180 41.444 C 240.305 39.050,221.035 35.658,220.081 37.202 C 219.572 38.026,213.796 62.391,207.245 91.348 C 200.693 120.305,194.984 143.998,194.556 143.998 C 193.863 144.000,187.770 117.829,174.224 56.667 C 169.145 33.734,170.241 34.765,154.962 38.556 M113.333 53.434 C 82.911 67.823,55.784 94.819,40.514 125.903 L 34.667 137.806 34.667 196.768 L 34.667 255.729 40.720 267.821 C 55.293 296.932,81.619 323.845,108.716 337.332 C 114.610 340.266,120.160 342.667,121.050 342.667 C 122.307 342.667,122.667 310.036,122.667 196.000 C 122.667 115.333,122.517 49.334,122.333 49.334 C 122.150 49.335,118.100 51.180,113.333 53.434 M262.667 200.000 L 262.667 346.846 265.667 345.919 C 317.507 329.890,361.333 261.989,361.333 197.702 L 361.333 186.667 332.667 186.667 L 304.000 186.667 304.000 200.000 L 304.000 213.333 319.333 213.333 C 330.723 213.333,334.661 213.762,334.644 215.000 C 334.365 235.057,315.309 273.773,296.766 291.954 L 289.333 299.242 289.333 200.005 L 289.333 100.767 296.103 107.383 C 305.414 116.484,314.606 128.972,320.670 140.759 L 325.767 150.667 339.550 150.667 C 347.131 150.667,353.333 150.172,353.333 149.568 C 353.333 143.107,336.746 113.135,325.695 99.628 C 310.551 81.117,280.823 58.543,265.667 54.044 L 262.667 53.154 262.667 200.000 M95.956 197.000 L 95.911 296.667 87.435 287.809 C 78.185 278.142,70.329 267.209,64.851 256.378 L 61.333 249.422 61.333 196.667 L 61.333 143.911 64.851 136.956 C 71.138 124.525,92.162 97.333,95.486 97.333 C 95.769 97.333,95.980 142.183,95.956 197.000 " stroke="none" fill="#000000" fill-rule="evenodd"></path></g></svg> -- GitLab From e83c7bc6dcc52b730c6c91bba94de7d8540c1027 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Tue, 8 Oct 2019 15:42:53 +0200 Subject: [PATCH 11/61] Added /UpdateUserRating route. Now updates or sets user rating on selected title. Ref. #13 --- server/routes/movies.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/server/routes/movies.js b/server/routes/movies.js index 917f063..c3dec0a 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -33,19 +33,32 @@ router.get('/GetMovies', function(req, res, next) { }); /* POST user ratings */ -router.post('/UserRating', function(req, res) { +router.post('/UpdateUserRating', function(req, res) { Movies.find({ "Title" : req.query.Title }, function(err, data) { if (err) return res.json({ success: false, error: err }); let totalRating = 0; + let averageRating = 0; - data.UserRatings.map((rating) => { - totalRating += rating; - }); + let tempUserRatings = data[0].UserRatings; + + if (data[0].UserRatings.length !== 0) { + data[0].UserRatings.map((rating) => { + totalRating += Number(rating); + }); + + averageRating = totalRating / data[0].UserRatings.length; + } else { + totalRating++; + averageRating = req.query.UserRating; + } - let averageRating = totalRating / data.UserRatings.length; + tempUserRatings.push(req.query.UserRating); - Movies.update( { "Title": req.query.Title }, { $set: { "UserRating": averageRating } } , (err, data) => { + Movies.updateOne( { "Title": req.query.Title }, { $set: { + "UserRating": averageRating, + "UserRatings": tempUserRatings + }}, (err, data) => { if (err) return res.json({ success: false, error: err }); return res.json({ success: true }); -- GitLab From 86b278a7ff98e4940c8cfc086a5bd831e21fe444 Mon Sep 17 00:00:00 2001 From: Rolf Erik Sesseng Aas <reaas@stud.ntnu.no> Date: Tue, 8 Oct 2019 15:48:19 +0200 Subject: [PATCH 12/61] Update README.md --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b9f7d38..923e2f4 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,11 @@ PORT=3001 node server.js Username: gruppe29 Password: sniperxXx42069 -| **Route** | **Method** | **Path** | **Description** | **Parameters** | **Returns** | -| --------- | ---------- | -------------- | ------------------------------------------------------------ | ---------------------- | ------------------------------ | -| movies.js | GET | /api/GetMovies | Get movies from "from" to "to" that mathces the searchString | searchString, from, to | { success: Boolean, data: {} } | -| movies.js | GET | /api/Search | Get the 5 first result of searchString | searchString | { data: {} } | +| **Route** | **Method** | **Path** | **Description** | **Parameters** | **Returns** | +| --------- | ---------- | --------------------- | ------------------------------------------------------------ | ---------------------- | ------------------------------ | +| movies.js | GET | /api/GetMovies | Get movies from "from" to "to" that mathces the searchString | searchString, from, to | { success: Boolean, data: {} } | +| movies.js | GET | /api/Search | Get the 5 first result of searchString | searchString | { data: {} } | +| movies.js | POST | /api/UpdateUserRating | Updates or sets the user ratings | Title, UserRating | { success: Boolean } | ### How to use `axios` In general @@ -52,6 +53,12 @@ axios.get('/api/GetMovies', params: { searchString: 'somestring', from: 0, to: 5 }); ``` +Since `axios.post` takes the parameters as the third argument, `null` has to be passed as the second. +UpdateUserRating +```javascript +axios.post('/api/UpdateUserRating', null, { params: { Title: 'something', UserRating: N }}); +``` + ### How to use data Use `data.Title` to get the title etc. To iterate through a result set use ```javascript @@ -86,5 +93,7 @@ DVD: String, BoxOffice: String, Production: String, Website: String, -Response: String +Response: String, +UserRatings: Array, +UserRating: Number ``` -- GitLab From 179dd4f5ebd4643ab7280f411f0f1414fb058d8c Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Wed, 9 Oct 2019 09:07:05 +0200 Subject: [PATCH 13/61] Refactor logo.svg. Fix bug in layout. Adjust text alignment in the listgroup. Ref. #5 --- client/src/App.css | 32 ++++++++++++++++++++++---------- client/src/App.js | 13 ++++++------- client/src/images/logo.svg | 2 +- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index b484e69..a20f09a 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -10,8 +10,8 @@ 'Filter Sort' 'Content Content'; grid-template-columns: 1fr 1fr; - grid-template-rows: 2fr 0.3fr 0.3fr 0.3fr 5fr; - grid-gap: 1.5vh; + grid-template-rows: 1fr 0.3fr 0.3fr 0.3fr 2.5fr; + grid-gap: 1vh; margin: 0vh 15vh 0 15vh; } /* @@ -33,12 +33,14 @@ grid-area: Logo; font-family: 'Righteous', cursive; width: 30vw; + height: 50%; } .title-wrapper { grid-area: Title; font-family: 'Righteous', cursive; - width: 50vw; + width: 90%; + height: 90%; font-size: 5vh; } @@ -69,15 +71,25 @@ .main-content { grid-area: Content; - width: 70vw; - - + width: 100%; + padding-left: 5%; + padding-right: 5%; + /* opacity: 0.7; + filter: alpha(opacity=70); /* For IE8 and earlier */ + } + .list-group-item { + background-color: white; + padding-left: 5%; + padding-right: 5%; + font-size: 2vh; + text-align: left; + opacity: 0.90; + filter: alpha(opacity=90); /* For IE8 and earlier */ + } + .list-group-item:hover { + background-color: grey; } .search-bar { } - -.movie-fakk:hover { - background-color: grey; -} diff --git a/client/src/App.js b/client/src/App.js index 2a695a7..b9c34ea 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,11 +1,9 @@ import React, { Component } from 'react'; import axios from 'axios'; import './App.css'; -import {ButtonToolbar, Button, ListGroup, Form} from 'react-bootstrap'; +import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-bootstrap'; const logo = require('./images/logo.svg'); -{/*} -dsadadasasas*/} class App extends Component { constructor(props) { @@ -20,7 +18,7 @@ class App extends Component { } componentDidMount() { - this.getMovies("", 10, 20); + this.getMovies("", 0, 20); } getMovies = (searchInput, fromIndex, toIndex) => { @@ -40,11 +38,11 @@ class App extends Component { <div className="logo-wrapper"> <img src={logo} alt="logo" /> </div> - <div className="title-wrapper"> + <div className="title-wrapper"> <p> Online Movie Gathering </p> </div> <div className="search-wrapper"> - <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 10)} /> + <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 20)} /> </div> <div className="filter-wrapper"> <Button variant="outline-secondary" size="lg" block>Filter</Button> @@ -56,7 +54,7 @@ class App extends Component { <ListGroup> {this.state.movies.map((movie, index) => { return( - <ListGroup.Item className="movie-fakk" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> + <ListGroup.Item className="list-group-item" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> {movie.Title} </ListGroup.Item> ) @@ -68,4 +66,5 @@ class App extends Component { } } + export default App; diff --git a/client/src/images/logo.svg b/client/src/images/logo.svg index 4336b53..59d715e 100644 --- a/client/src/images/logo.svg +++ b/client/src/images/logo.svg @@ -1 +1 @@ -<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0, 0, 400,400"><g id="svgg"><path id="path0" d="M154.962 38.556 C 149.625 39.881,144.825 41.397,144.296 41.926 C 143.766 42.455,143.333 113.749,143.333 200.356 L 143.333 357.825 150.000 359.553 C 165.784 363.645,169.060 363.850,169.879 360.799 C 170.292 359.260,170.683 315.400,170.748 263.333 L 170.866 168.667 181.978 217.767 C 188.090 244.773,193.428 267.206,193.841 267.619 C 195.192 268.970,196.872 262.589,207.503 215.741 L 218.000 169.482 218.697 266.407 C 219.080 319.717,219.530 363.406,219.697 363.495 C 220.387 363.862,233.165 361.164,239.723 359.267 L 246.779 357.226 246.390 199.897 C 246.065 68.699,245.698 42.382,244.180 41.444 C 240.305 39.050,221.035 35.658,220.081 37.202 C 219.572 38.026,213.796 62.391,207.245 91.348 C 200.693 120.305,194.984 143.998,194.556 143.998 C 193.863 144.000,187.770 117.829,174.224 56.667 C 169.145 33.734,170.241 34.765,154.962 38.556 M113.333 53.434 C 82.911 67.823,55.784 94.819,40.514 125.903 L 34.667 137.806 34.667 196.768 L 34.667 255.729 40.720 267.821 C 55.293 296.932,81.619 323.845,108.716 337.332 C 114.610 340.266,120.160 342.667,121.050 342.667 C 122.307 342.667,122.667 310.036,122.667 196.000 C 122.667 115.333,122.517 49.334,122.333 49.334 C 122.150 49.335,118.100 51.180,113.333 53.434 M262.667 200.000 L 262.667 346.846 265.667 345.919 C 317.507 329.890,361.333 261.989,361.333 197.702 L 361.333 186.667 332.667 186.667 L 304.000 186.667 304.000 200.000 L 304.000 213.333 319.333 213.333 C 330.723 213.333,334.661 213.762,334.644 215.000 C 334.365 235.057,315.309 273.773,296.766 291.954 L 289.333 299.242 289.333 200.005 L 289.333 100.767 296.103 107.383 C 305.414 116.484,314.606 128.972,320.670 140.759 L 325.767 150.667 339.550 150.667 C 347.131 150.667,353.333 150.172,353.333 149.568 C 353.333 143.107,336.746 113.135,325.695 99.628 C 310.551 81.117,280.823 58.543,265.667 54.044 L 262.667 53.154 262.667 200.000 M95.956 197.000 L 95.911 296.667 87.435 287.809 C 78.185 278.142,70.329 267.209,64.851 256.378 L 61.333 249.422 61.333 196.667 L 61.333 143.911 64.851 136.956 C 71.138 124.525,92.162 97.333,95.486 97.333 C 95.769 97.333,95.980 142.183,95.956 197.000 " stroke="none" fill="#000000" fill-rule="evenodd"></path></g></svg> +<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0, 0, 400, 400"><g id="svgg"><path id="path0" d="M154.962 38.556 C 149.625 39.881,144.825 41.397,144.296 41.926 C 143.766 42.455,143.333 113.749,143.333 200.356 L 143.333 357.825 150.000 359.553 C 165.784 363.645,169.060 363.850,169.879 360.799 C 170.292 359.260,170.683 315.400,170.748 263.333 L 170.866 168.667 181.978 217.767 C 188.090 244.773,193.428 267.206,193.841 267.619 C 195.192 268.970,196.872 262.589,207.503 215.741 L 218.000 169.482 218.697 266.407 C 219.080 319.717,219.530 363.406,219.697 363.495 C 220.387 363.862,233.165 361.164,239.723 359.267 L 246.779 357.226 246.390 199.897 C 246.065 68.699,245.698 42.382,244.180 41.444 C 240.305 39.050,221.035 35.658,220.081 37.202 C 219.572 38.026,213.796 62.391,207.245 91.348 C 200.693 120.305,194.984 143.998,194.556 143.998 C 193.863 144.000,187.770 117.829,174.224 56.667 C 169.145 33.734,170.241 34.765,154.962 38.556 M113.333 53.434 C 82.911 67.823,55.784 94.819,40.514 125.903 L 34.667 137.806 34.667 196.768 L 34.667 255.729 40.720 267.821 C 55.293 296.932,81.619 323.845,108.716 337.332 C 114.610 340.266,120.160 342.667,121.050 342.667 C 122.307 342.667,122.667 310.036,122.667 196.000 C 122.667 115.333,122.517 49.334,122.333 49.334 C 122.150 49.335,118.100 51.180,113.333 53.434 M262.667 200.000 L 262.667 346.846 265.667 345.919 C 317.507 329.890,361.333 261.989,361.333 197.702 L 361.333 186.667 332.667 186.667 L 304.000 186.667 304.000 200.000 L 304.000 213.333 319.333 213.333 C 330.723 213.333,334.661 213.762,334.644 215.000 C 334.365 235.057,315.309 273.773,296.766 291.954 L 289.333 299.242 289.333 200.005 L 289.333 100.767 296.103 107.383 C 305.414 116.484,314.606 128.972,320.670 140.759 L 325.767 150.667 339.550 150.667 C 347.131 150.667,353.333 150.172,353.333 149.568 C 353.333 143.107,336.746 113.135,325.695 99.628 C 310.551 81.117,280.823 58.543,265.667 54.044 L 262.667 53.154 262.667 200.000 M95.956 197.000 L 95.911 296.667 87.435 287.809 C 78.185 278.142,70.329 267.209,64.851 256.378 L 61.333 249.422 61.333 196.667 L 61.333 143.911 64.851 136.956 C 71.138 124.525,92.162 97.333,95.486 97.333 C 95.769 97.333,95.980 142.183,95.956 197.000 " stroke="none" fill="#000000" fill-rule="evenodd"></path></g></svg> -- GitLab From 6eb9342273899ffd473763b231e1c822139b763c Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Wed, 9 Oct 2019 09:46:59 +0200 Subject: [PATCH 14/61] Added pagination support in backend. Ref. #14 Comments are added to backend. --- server/Schemas/Movies.js | 3 ++ server/routes/movies.js | 78 +++++++++++++++++++--------------------- server/server.js | 3 ++ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/server/Schemas/Movies.js b/server/Schemas/Movies.js index ea78a18..cec8d5b 100644 --- a/server/Schemas/Movies.js +++ b/server/Schemas/Movies.js @@ -1,6 +1,9 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; +/* + * How the object stored in MongoDB collection is structured + */ const MoviesSchema = new Schema({ Title: String, Year: Number, diff --git a/server/routes/movies.js b/server/routes/movies.js index c3dec0a..4913074 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -3,36 +3,48 @@ var router = express.Router(); const Movies = require('../Schemas/Movies'); -/* GET search result */ -router.get('/Search', function(req, res) { - Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }, (err, data) => { - if (err) return res.json({ success: false, error: err }); - - returnResult = []; - - index = 0; - data.some(function(movie) { - returnResult.push(movie); - index++; - return index == 5; - }); - - return res.json({ success: true, data: returnResult }); - }) -}) - -/* GET movies */ +/* GET movies + * + * Params: { + * searchString: searches for titles containing this string, + * from: start index of search, + * to: end index of search + * } + * Returns: { + * success: Boolean, + * data: Array of results + * } + * + * Gets all movie titles containing the searchString in title. + * Only returns the number of hits wanted + */ router.get('/GetMovies', function(req, res, next) { - Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }, (err, data) => { - if (err) return res.json({ success: false, error: err }); + const numberOfHits = Number(req.query.to) - Number(req.query.from); + var query = Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }) + .skip(Number(req.query.from)).limit(Number(numberOfHits)); - data = stripReturnObject(data, req.query.from, req.query.to); + query.exec(function(err, data) { + if (err) return res.json({ success: false, error: err }); return res.json({ success: true, data: data }); - }) + }); }); -/* POST user ratings */ +/* POST user ratings + * + * Params: { + * Title: title of movie, + * UserRating: the given rating to add + * } + * Returns: { success: Boolean } - if the update was successful or not + * + * Updates or sets the user rating for the given movie. + * First get the current user ratings from DB, add them + * all together with the new rating and calculate the + * average. If no ratings exists in DB, add only the new. + * + * Update the collection in DB with new ratings and average + */ router.post('/UpdateUserRating', function(req, res) { Movies.find({ "Title" : req.query.Title }, function(err, data) { if (err) return res.json({ success: false, error: err }); @@ -47,9 +59,8 @@ router.post('/UpdateUserRating', function(req, res) { totalRating += Number(rating); }); - averageRating = totalRating / data[0].UserRatings.length; + averageRating = (totalRating + Number(req.query.UserRating)) / (data[0].UserRatings.length + 1); } else { - totalRating++; averageRating = req.query.UserRating; } @@ -66,19 +77,4 @@ router.post('/UpdateUserRating', function(req, res) { }); }); -function stripReturnObject(result, from, to) { - returnResult = []; - - index = 0; - result.some(function(movie) { - if (index >= from && index < to) { - returnResult.push(movie); - } - index++; - return index == to; - }); - - return returnResult; -} - module.exports = router; diff --git a/server/server.js b/server/server.js index 4928cb9..9b7babd 100644 --- a/server/server.js +++ b/server/server.js @@ -3,16 +3,19 @@ const mongoose = require('mongoose'); var movieRouter = require('./routes/movies'); +// Initial DB connection const API_PORT = 3001; const app = express(); const router = express.Router(); const dbRoute = 'mongodb://gruppe29:sniperxXx42069@it2810-29.idi.ntnu.no:27017/project3'; +// Connect to DB mongoose.connect(dbRoute, { useUnifiedTopology: true, useNewUrlParser: true }); let db = mongoose.connection; +// Logging to console just to clarify if the connection was successful db.once('open', () => console.log('Connected to database')); db.on('error', console.error.bind(console, 'MongoDB connection error: ')); -- GitLab From c0fe2ba0863ca44aa922f43d230a40dff4be4efe Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Wed, 9 Oct 2019 11:46:45 +0200 Subject: [PATCH 15/61] Implement basic redux with reducer and actions for filter and sort. Ref. #15 --- client/package-lock.json | 95 +++++++++++++----------------------- client/package.json | 4 +- client/src/App.js | 7 ++- client/src/actions/index.js | 47 ++++++++++++++++++ client/src/index.js | 13 ++++- client/src/reducers/index.js | 6 +++ client/src/reducers/sort.js | 4 ++ 7 files changed, 113 insertions(+), 63 deletions(-) create mode 100644 client/src/actions/index.js create mode 100644 client/src/reducers/index.js create mode 100644 client/src/reducers/sort.js diff --git a/client/package-lock.json b/client/package-lock.json index a2d896f..134f01c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4454,14 +4454,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -5369,35 +5361,6 @@ "bser": "^2.0.0" } }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.18" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "~2.0.3" - } - } - } - }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -6025,6 +5988,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", + "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "requires": { + "react-is": "^16.7.0" + } + }, "hosted-git-info": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", @@ -6637,15 +6608,6 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" - } - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8508,15 +8470,6 @@ "lower-case": "^1.1.1" } }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - }, "node-forge": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", @@ -10568,6 +10521,19 @@ "warning": "^4.0.2" } }, + "react-redux": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.1.tgz", + "integrity": "sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + } + }, "react-scripts": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.1.2.tgz", @@ -10730,6 +10696,15 @@ "minimatch": "3.0.4" } }, + "redux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", + "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -12111,6 +12086,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -12407,11 +12387,6 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "ua-parser-js": { - "version": "0.7.20", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", - "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" - }, "uglify-js": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", diff --git a/client/package.json b/client/package.json index 8abf6b2..864fdae 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,9 @@ "react": "^16.10.1", "react-bootstrap": "^1.0.0-beta.14", "react-dom": "^16.10.1", - "react-scripts": "3.1.2" + "react-redux": "^7.1.1", + "react-scripts": "3.1.2", + "redux": "^4.0.4" }, "scripts": { "start": "react-scripts start", diff --git a/client/src/App.js b/client/src/App.js index b9c34ea..7fcdd43 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,7 +1,9 @@ import React, { Component } from 'react'; +import { connect } from 'react-redux'; import axios from 'axios'; import './App.css'; import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-bootstrap'; + const logo = require('./images/logo.svg'); @@ -33,6 +35,7 @@ class App extends Component { } render() { + // console.log(this.props.store.getState()); return( <div className="main-container"> <div className="logo-wrapper"> @@ -67,4 +70,6 @@ class App extends Component { } -export default App; + + +export default connect()(App); diff --git a/client/src/actions/index.js b/client/src/actions/index.js new file mode 100644 index 0000000..b903271 --- /dev/null +++ b/client/src/actions/index.js @@ -0,0 +1,47 @@ + +export const sortYear = order => ({ + type: 'SORT_YEAR', + order +}); + +export const sortBoxOffice = order => ({ + type: 'SORT_BOX_OFFICE', + order +}); + +export const sortUserRating = order => ({ + type: 'SORT_USER_RATING', + order +}); + +export const sortIMDB = order => ({ + type: 'SORT_IMDB', + order +}); + + +export const filterRated = (chosen) => ({ + type: 'FILTER_RATED', + chosen +}); + +export const filterCountry = (chosen) => ({ + type: 'FILTER_COUNTRY', + chosen +}); + +export const filterLanguage = (chosen) => ({ + type: 'FILTER_LANGUAGE', + chosen +}); + +export const filterIMDB = (from, to) => ({ + type: 'FILTER_IMDB', + from, + to +}); + +export const chosenMovie = (id) => ({ + type: "CHOSEN_MOVIE", + id +}); diff --git a/client/src/index.js b/client/src/index.js index 87d1be5..91046a4 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -3,8 +3,19 @@ import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; +import { Provider } from 'react-redux'; +import { createStore} from 'redux'; +import rootReducer from './reducers' -ReactDOM.render(<App />, document.getElementById('root')); + +const store = createStore(rootReducer) + + ReactDOM.render( + <Provider store={store}> + <App store={store} /> + </Provider>, + document.getElementById('root') + ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js new file mode 100644 index 0000000..9c767e0 --- /dev/null +++ b/client/src/reducers/index.js @@ -0,0 +1,6 @@ +import {combineReducers } from 'redux'; +import sort from './sort'; + +export default combineReducers({ + sort +}) diff --git a/client/src/reducers/sort.js b/client/src/reducers/sort.js new file mode 100644 index 0000000..1b33315 --- /dev/null +++ b/client/src/reducers/sort.js @@ -0,0 +1,4 @@ +const sort = (order = '', action = '') => { + return({ type: action.type, order: action.order }); +} +export default sort; -- GitLab From 950a3480dcbb6bb350690f4949982e45c3830561 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Wed, 9 Oct 2019 15:46:45 +0200 Subject: [PATCH 16/61] Sort now works in backend. Extra parameters required for /GetMovies-route --- server/routes/movies.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/server/routes/movies.js b/server/routes/movies.js index 4913074..63791ee 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -3,12 +3,20 @@ var router = express.Router(); const Movies = require('../Schemas/Movies'); +// Global variables for dafault settings +let FROM = 0; +let NUMBER_OF_HITS = 10; +let SORT_BY = ""; + /* GET movies * * Params: { * searchString: searches for titles containing this string, * from: start index of search, - * to: end index of search + * to: end index of search, + * sort: boolean if result should be sorted. Default is alphabetically + * sortType: the sort type. Get the current type from state + * sortOrder: the sort order. Get the current order from state * } * Returns: { * success: Boolean, @@ -19,9 +27,29 @@ const Movies = require('../Schemas/Movies'); * Only returns the number of hits wanted */ router.get('/GetMovies', function(req, res, next) { - const numberOfHits = Number(req.query.to) - Number(req.query.from); + let numberOfHits = Number(req.query.from) - Number(req.query.to); + + let sortOrder = 0; + let sortBy = {}; + + if (req.query.sort === "true") { + // Selecting ascending or descending sort type + if (req.query.sortOrder === 'ASC') sortOrder = 1; + else if (req.query.sortOrder === 'DESC') sortOrder = -1; + else return res.json({ success: false, error: "Order has to be ASC or DESC"}); + + // Selecting what to sort on + if (req.query.sortType === 'SORT_YEAR') sortBy = { "Year": sortOrder }; + else if (req.query.sortType === 'SORT_BOX_OFFICE') sortBy = { "BoxOffice": sortOrder }; + else if (req.query.sortType === 'SORT_USER_RATING') sortBy = { "UserRating": sortOrder }; + else if (req.query.sortType === 'SORT_IMDB') sortBy = { "imdbRating": sortOrder }; + else return res.json({ success: false, error: "Wrong sort type" }); + } else { + sortBy = { "Title": 1 } + } + var query = Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }) - .skip(Number(req.query.from)).limit(Number(numberOfHits)); + .sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); query.exec(function(err, data) { if (err) return res.json({ success: false, error: err }); -- GitLab From 12b8ac89df19a3ca165e31728714c1719babfa64 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 10 Oct 2019 20:58:41 +0200 Subject: [PATCH 17/61] Filter is now up and running in backend. /GetMovies now have even 3 more parameters. Check comments. Ref. #19 --- server/routes/movies.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/server/routes/movies.js b/server/routes/movies.js index 63791ee..4bbbf11 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -3,11 +3,6 @@ var router = express.Router(); const Movies = require('../Schemas/Movies'); -// Global variables for dafault settings -let FROM = 0; -let NUMBER_OF_HITS = 10; -let SORT_BY = ""; - /* GET movies * * Params: { @@ -17,6 +12,12 @@ let SORT_BY = ""; * sort: boolean if result should be sorted. Default is alphabetically * sortType: the sort type. Get the current type from state * sortOrder: the sort order. Get the current order from state + * filter: boolean if filter should be active + * filters: string of all filters structured: + * [ + * { FILTER_TYPE_1: VALUE_1 }, + * { FILTER_TYPE_2: VALUE_2 } + * ] * } * Returns: { * success: Boolean, @@ -27,11 +28,13 @@ let SORT_BY = ""; * Only returns the number of hits wanted */ router.get('/GetMovies', function(req, res, next) { + var query; + let numberOfHits = Number(req.query.from) - Number(req.query.to); + // Sorting logic let sortOrder = 0; let sortBy = {}; - if (req.query.sort === "true") { // Selecting ascending or descending sort type if (req.query.sortOrder === 'ASC') sortOrder = 1; @@ -43,13 +46,24 @@ router.get('/GetMovies', function(req, res, next) { else if (req.query.sortType === 'SORT_BOX_OFFICE') sortBy = { "BoxOffice": sortOrder }; else if (req.query.sortType === 'SORT_USER_RATING') sortBy = { "UserRating": sortOrder }; else if (req.query.sortType === 'SORT_IMDB') sortBy = { "imdbRating": sortOrder }; - else return res.json({ success: false, error: "Wrong sort type" }); + else return res.json({ success: false, error: "Sort type not supported" }); } else { sortBy = { "Title": 1 } } - var query = Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }) + // Filter logic. As you cannot use $and, $or with empty values, different + // queries has to be created based on if filter is active or not + let filters = ""; + if (req.query.filter === "true") { + filters = JSON.parse(req.query.filters); + query = Movies.find({ $and:[ + { $or: filters }, + { "Title": { $regex: req.query.searchString, $options: "i" } } + ]}).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); + } else { + query = Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }) .sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); + } query.exec(function(err, data) { if (err) return res.json({ success: false, error: err }); @@ -82,6 +96,7 @@ router.post('/UpdateUserRating', function(req, res) { let tempUserRatings = data[0].UserRatings; + // Calculates the average rating if (data[0].UserRatings.length !== 0) { data[0].UserRatings.map((rating) => { totalRating += Number(rating); -- GitLab From cb4e56133cee1cb7b04eb19b9931180043535401 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Mon, 14 Oct 2019 12:04:17 +0200 Subject: [PATCH 18/61] Google Trends chart component is working as intended. Takes one argument, the movie title. It is not styled yet.. Ref. #22 --- client/package-lock.json | 18 ++++++++ client/package.json | 2 + client/src/components/InterestOverTime.js | 53 +++++++++++++++++++++++ server/package-lock.json | 5 +++ server/package.json | 1 + server/routes/movies.js | 24 ++++++++++ 6 files changed, 103 insertions(+) create mode 100644 client/src/components/InterestOverTime.js diff --git a/client/package-lock.json b/client/package-lock.json index 134f01c..b49672e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -5815,6 +5815,11 @@ } } }, + "google-trends-api": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/google-trends-api/-/google-trends-api-4.9.0.tgz", + "integrity": "sha512-ujOLHM98bE7Igy8YA4Lrb5naDpaY1wGt215zgHXPznCDwHmMjMK1FW8yW//dHQ9CKyG/HzaNBDfF7jEAUk8Kkw==" + }, "graceful-fs": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", @@ -10472,6 +10477,14 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.2.tgz", "integrity": "sha512-DHRuRk3K4Lg9obI6J4Y+nKvtwjasYRU9CFL3ud42x9YJG1HbQjSNublapC/WBJOA726gNUbqbj0U2df9+uzspQ==" }, + "react-google-charts": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/react-google-charts/-/react-google-charts-3.0.15.tgz", + "integrity": "sha512-78s5xOQOJvL+jIewrWQZEHtlVk+5Yh4zZy+ODA1on1o1FaRjKWXxoo4n4JQl1XuqkF/A9NWque3KqM6pMggjzQ==", + "requires": { + "react-load-script": "^0.0.6" + } + }, "react-is": { "version": "16.10.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.1.tgz", @@ -10482,6 +10495,11 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-load-script": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/react-load-script/-/react-load-script-0.0.6.tgz", + "integrity": "sha512-aRGxDGP9VoLxcsaYvKWIW+LRrMOzz2eEcubTS4NvQPPugjk2VvMhow0wWTkSl7RxookomD1MwcP4l5UStg5ShQ==" + }, "react-overlays": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.2.0.tgz", diff --git a/client/package.json b/client/package.json index 864fdae..b09a007 100644 --- a/client/package.json +++ b/client/package.json @@ -5,9 +5,11 @@ "dependencies": { "axios": "^0.19.0", "bootstrap": "^4.3.1", + "google-trends-api": "^4.9.0", "react": "^16.10.1", "react-bootstrap": "^1.0.0-beta.14", "react-dom": "^16.10.1", + "react-google-charts": "^3.0.15", "react-redux": "^7.1.1", "react-scripts": "3.1.2", "redux": "^4.0.4" diff --git a/client/src/components/InterestOverTime.js b/client/src/components/InterestOverTime.js new file mode 100644 index 0000000..a17b196 --- /dev/null +++ b/client/src/components/InterestOverTime.js @@ -0,0 +1,53 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { Chart } from 'react-google-charts'; + +/** + * Component to get and show the Google search trend over the last week. + * Param: title - movie title to search for + * return: the component HTML as an Area chart. + */ +const InterestOverTime = ({ title }) => { + const [currentTitle, setTitle] = useState(title); + const [chartData, setChartData] = useState(0); + + // Called each time a new title is passed to the component + useEffect(() => { + axios.get('/api/MovieTrend', { params: { Title: title }}) + .then(res => { + let data = JSON.parse(res.data.data); + + let tempData = []; + let tempTitles = ['Date', 'Interest']; + tempData.push(tempTitles); + + // Since the data recieved from Google is not on the form we want, + // some magic has to happen. Created the data structure that + // react-google-charts wants + data.default.timelineData.map(time => { + let tempValues = []; + tempValues.push(time.formattedTime); + tempValues.push(Number(time.formattedValue[0])); + tempData.push(tempValues); + }); + + setChartData(tempData); + }); + }, [title]); + + return( + <Chart + width={300} + height={300} + chartType="AreaChart" + loader={<div>Loading chart</div>} + data={chartData} + options={{ + title: title, + vAxis: { title: 'Interest over time' } + }} + /> + ) +} + +export default InterestOverTime; diff --git a/server/package-lock.json b/server/package-lock.json index f1df13b..6632d14 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -311,6 +311,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "google-trends-api": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/google-trends-api/-/google-trends-api-4.9.0.tgz", + "integrity": "sha512-ujOLHM98bE7Igy8YA4Lrb5naDpaY1wGt215zgHXPznCDwHmMjMK1FW8yW//dHQ9CKyG/HzaNBDfF7jEAUk8Kkw==" + }, "graceful-readlink": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", diff --git a/server/package.json b/server/package.json index 0b5d96f..aafcbc6 100644 --- a/server/package.json +++ b/server/package.json @@ -9,6 +9,7 @@ "cookie-parser": "~1.4.3", "debug": "~2.6.9", "express": "~4.16.0", + "google-trends-api": "^4.9.0", "http-errors": "~1.6.2", "jade": "~1.11.0", "mongoose": "^5.7.3", diff --git a/server/routes/movies.js b/server/routes/movies.js index 4bbbf11..8855bbd 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -1,6 +1,8 @@ var express = require('express'); var router = express.Router(); +const googleTrends = require('google-trends-api'); + const Movies = require('../Schemas/Movies'); /* GET movies @@ -120,4 +122,26 @@ router.post('/UpdateUserRating', function(req, res) { }); }); +router.get('/MovieTrend', function(req, res) { + var days = 7; + var date = new Date(); + var lastWeek = new Date(date.getTime() - (days * 24 * 60 * 60 * 1000)); + var lastDay = lastWeek.getDate(); + var lastMonth = lastWeek.getMonth() + 1; + var lastYear = lastWeek.getFullYear(); + + var startTime = new Date(lastYear + "-" + lastMonth + "-" + lastDay); + + googleTrends.interestOverTime({ + keyword: req.query.Title, + startTime: startTime + }) + .then(function(result) { + return res.json({ success: true, data: result }); + }) + .catch(function(error) { + return res.json({ success: false, error: error }); + }); +}) + module.exports = router; -- GitLab From 45c321ac9f7da0b8f0f002705beb04b730403ac8 Mon Sep 17 00:00:00 2001 From: asszewcz <asszewcz@stud.ntnu.no> Date: Tue, 15 Oct 2019 11:56:26 +0200 Subject: [PATCH 19/61] Add buttons for sort type and order. Ref #18 --- client/src/App.js | 3 +- client/src/actions/index.js | 34 +++++++----- client/src/components/Filter.js | 0 client/src/components/SortButton.css | 18 +++++++ client/src/components/SortButton.js | 77 ++++++++++++++++++++++++++++ client/src/containers/SortLink.js | 14 +++++ 6 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 client/src/components/Filter.js create mode 100644 client/src/components/SortButton.css create mode 100644 client/src/components/SortButton.js create mode 100644 client/src/containers/SortLink.js diff --git a/client/src/App.js b/client/src/App.js index 7fcdd43..df838ea 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import axios from 'axios'; import './App.css'; import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-bootstrap'; +import SortButton from './components/SortButton' const logo = require('./images/logo.svg'); @@ -51,7 +52,7 @@ class App extends Component { <Button variant="outline-secondary" size="lg" block>Filter</Button> </div> <div className="sort-wrapper"> - <Button variant="outline-secondary" size="lg" block>Sort</Button> + <SortButton /> {/*<Button variant="outline-secondary" size="lg" block>Sort</Button>*/} </div> <div className="main-content"> <ListGroup> diff --git a/client/src/actions/index.js b/client/src/actions/index.js index b903271..9ad045b 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -1,24 +1,32 @@ - -export const sortYear = order => ({ - type: 'SORT_YEAR', - order +export const SORT_YEAR = 'SORT_YEAR'; +export function sortYear = order => ({ + type: SORT_YEAR, + order: order }); -export const sortBoxOffice = order => ({ - type: 'SORT_BOX_OFFICE', - order +export const SORT_BOX_OFFICE = 'SORT_BOX_OFFICE'; +export function sortBoxOffice = order => ({ + type: SORT_BOX_OFFICE, + order: order }); -export const sortUserRating = order => ({ - type: 'SORT_USER_RATING', - order +export const SORT_USER_RATING = 'SORT_USER_RATING'; +export function sortUserRating = order => ({ + type: SORT_USER_RATING, + order: order }); -export const sortIMDB = order => ({ - type: 'SORT_IMDB', - order +export const SORT_IMDB = 'SORT_IMDB'; +export function sortIMDB = order => ({ + type: SORT_IMDB, + order: order }); +export const SORT_TITLE = 'SORT_TITLE'; +export function sortTitle = order => ({ + type: SORT_TITLE, + order: order +}); export const filterRated = (chosen) => ({ type: 'FILTER_RATED', diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js new file mode 100644 index 0000000..e69de29 diff --git a/client/src/components/SortButton.css b/client/src/components/SortButton.css new file mode 100644 index 0000000..8ddf46d --- /dev/null +++ b/client/src/components/SortButton.css @@ -0,0 +1,18 @@ +.sort-button { + float: left; + width: 20vw; + height: 48px; + font-size: 20px; +} + +.sort-button-drop { + width: 20vw; +} + +.sort-order-button { + float: right; + margin-left: auto; + width: 5vw; + height: 48px; + font-size: 20px; +} diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js new file mode 100644 index 0000000..64d6eb6 --- /dev/null +++ b/client/src/components/SortButton.js @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import { Dropdown, ButtonToolbar, Button } from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import './SortButton.css' + +const SortButton = () => { + const [sortingBy, setSortingBy] = useState('Title'); + const [order, setOrder] = useState('ASC'); + + + function ChangeOrder() { + if (order == 'ASC') { + setOrder('DESC') + } + else { + setOrder('ASC') + } + } + function ChangeType(sorting){ + setSortingBy(sorting) + + } + + return( + <div> + <Dropdown > + <Dropdown.Toggle className="sort-button" variant="outline-secondary" id="dropdown-basic"> + {sortingBy} + </Dropdown.Toggle> + + <Dropdown.Menu className="sort-button-drop"> + <Dropdown.Item onClick={() => ChangeType('Title')}>Title</Dropdown.Item> + <Dropdown.Item onClick={() => ChangeType('Year')}>Year</Dropdown.Item> + <Dropdown.Item onClick={() => ChangeType('Box')}>Box office</Dropdown.Item> + <Dropdown.Item onClick={() => ChangeType('Imdb')}>Imdb rating</Dropdown.Item> + <Dropdown.Item onClick={() => ChangeType('User')}>User rating</Dropdown.Item> + </Dropdown.Menu> + </Dropdown> + + <ButtonToolbar> + <Button className="sort-order-button" variant="outline-secondary" onClick={() => ChangeOrder()}> + {order == "ASC" ? '↑' : '↓'} + </Button> + </ButtonToolbar> + </div> +) + +} + +// function ChangeType(sorting){ +// this.setSortingBy(sorting) +// // switch(sorting){ +// // case 'Title': +// // e => dispatch(sortTitle('ASC') +// // break +// // case 'Year': +// // e => dispatch(sort('ASC') +// // } +// +// } +// +// function ChangeOrder() { +// if (order == 'ASC') { +// setOrder('DESC') +// } +// else { +// setOrder('ASC') +// } +// } + +// SortButton.propTypes = { +// onClick: +// } + + +export default SortButton diff --git a/client/src/containers/SortLink.js b/client/src/containers/SortLink.js new file mode 100644 index 0000000..1199f0b --- /dev/null +++ b/client/src/containers/SortLink.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { sortTitle, sortYear, sortIMDB, sortBoxOffice, sortUserRating} from './actions'; +import SortButton from './components'; + +const mapStateToProps = (state, ownProps) => ({ + sort: ownProps.type === state.type && ownProps.order === state.order +}) + +const mapDispathToProps = (dispatch, ownProps) => ({ + onClick +}) + + +export default connect(mapStateToProps, mapDispathToProps)(SortButton) -- GitLab From b5d3a9e5125ddeb0ffcbf02ed9d43b4d54625e78 Mon Sep 17 00:00:00 2001 From: Burgurrd <35113972+Burgurrd@users.noreply.github.com> Date: Wed, 16 Oct 2019 12:19:55 +0200 Subject: [PATCH 20/61] Changing harddrives --- client/src/App.css | 51 +++++++- client/src/App.js | 56 ++++++++- client/src/actions/index.js | 42 +++++-- client/src/components/Filter.js | 199 ++++++++++++++++++++++++++++++++ client/src/index.js | 8 +- client/src/reducers/filter.js | 95 +++++++++++++++ client/src/reducers/index.js | 6 +- client/src/reducers/sort.js | 2 +- client/src/store/store.js | 5 + 9 files changed, 440 insertions(+), 24 deletions(-) create mode 100644 client/src/components/Filter.js create mode 100644 client/src/reducers/filter.js create mode 100644 client/src/store/store.js diff --git a/client/src/App.css b/client/src/App.css index a20f09a..6621a4e 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -8,9 +8,10 @@ 'Title Title' 'Search Search' 'Filter Sort' + 'FilterOptions FilterOptions' 'Content Content'; grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr 0.3fr 0.3fr 0.3fr 2.5fr; + grid-template-rows: 1fr 0.3fr 0.3fr 0.3fr 0.1fr 2.5fr; grid-gap: 1vh; margin: 0vh 15vh 0 15vh; } @@ -90,6 +91,54 @@ background-color: grey; } + .filter_category{ + /* width: 50vw; */ + width: 40vw; + } + + + +.filter-options { + grid-area: FilterOptions; + text-align: left; + height: 0; + /* height: 0; */ + /* width: 0; */ + /* display: none; */ + opacity: 0; + filter: alpha(opacity=0); /* For IE8 and earlier */ + /* TODO: Transform opacity, height and/or toggle display = none + for activation of the filter options */ +} +.filter-options-active { + grid-area: FilterOptions; + text-align: left; + width: 70vw; + height: auto; + /* height: 0; */ + /* width: 0; */ + /* display: none; */ + opacity: 1; + filter: alpha(opacity=1); /* For IE8 and earlier */ + /* TODO: Transform opacity, height and/or toggle display = none + for activation of the filter options */ +} + + + + + +.filter-from-rating { + width: 5vw; + align-self: center; + +} + +.filter-to-rating { + width: 5vw; + align-self: center; +} + .search-bar { } diff --git a/client/src/App.js b/client/src/App.js index 7fcdd43..0464e6d 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -3,6 +3,9 @@ import { connect } from 'react-redux'; import axios from 'axios'; import './App.css'; import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-bootstrap'; +import { filterRated, filterState } from './actions'; +import store from './store/store'; +import {Filter} from './components/Filter'; const logo = require('./images/logo.svg'); @@ -11,12 +14,29 @@ class App extends Component { constructor(props) { super(props); + + this.state = { movies: [], error: "" } this.getMovies = this.getMovies.bind(this); + console.log('Before:\t', store.getState()); + store.dispatch(filterState(true)) + store.dispatch(filterRated([false, false, false, false, false, false, false, false])) + console.log('After:\t', store.getState()); + store.dispatch(filterState(false)) + store.dispatch(filterRated([false, false, false, false, false, false, false, false])) + console.log('After1:\t', store.getState()); + + + // store.dispatch(filterRated([true, false, false, false, false])); + // console.log('After filterRated([true, false, false, false, false]):\t', store.getState()); + // store.dispatch(filterRated([false, false, false, false, false])); + // console.log('After1 filterRated([false, false, false, false, false]):\t', store.getState()); + // store.dispatch(filterState(true)); + // console.log('After2:\t', store.getState()); } componentDidMount() { @@ -34,8 +54,8 @@ class App extends Component { }); } + render() { - // console.log(this.props.store.getState()); return( <div className="main-container"> <div className="logo-wrapper"> @@ -48,16 +68,19 @@ class App extends Component { <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 20)} /> </div> <div className="filter-wrapper"> - <Button variant="outline-secondary" size="lg" block>Filter</Button> + <Button variant="outline-secondary" size="lg" block >Filter</Button> </div> <div className="sort-wrapper"> <Button variant="outline-secondary" size="lg" block>Sort</Button> </div> + <div className={"filter-options-active"}> + <Filter/> + </div> <div className="main-content"> <ListGroup> {this.state.movies.map((movie, index) => { return( - <ListGroup.Item className="list-group-item" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> + <ListGroup.Item key={index} className="list-group-item" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> {movie.Title} </ListGroup.Item> ) @@ -68,8 +91,33 @@ class App extends Component { ) } } +// +// function toggleFilterState(){ +// console.log('toggleFilterState', store.getState()) +// store.dispatch(filterState(!getState())) +// } +// function getState(){ +// // let fstate = store.getState().filterReducer.filter[4].filter_state; +// console.log('store.getState()', store.getState()); +// return !store.getState().filterReducer[3].filter_state; +// } +function mapStateToProps(state) { + // console.log(state.filter) + const {filter} = state; + return { filters : filter} +}; + + +const mapDispatchToProps = dispatch => { + return { + filterState: () => dispatch({type : 'FILTER_STATE'}) + } +}; + +export default connect(mapStateToProps, null)(App); + -export default connect()(App); +// <div className={"filter-options-active" + getState() === true ? '' : '-active'}> diff --git a/client/src/actions/index.js b/client/src/actions/index.js index b903271..5a24289 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -20,28 +20,50 @@ export const sortIMDB = order => ({ }); +export const FILTER_STATE = 'FILTER_STATE'; + +export const filterState = (bool) => ({ + type: FILTER_STATE, + bool: bool +}); + +export const FILTER_RATED = 'FILTER_RATED'; +// params::chosen - a list of booleans corresponding to the following ratings: +// G - General audiences +// PG - Parental Guidance Suggested +// PG-13 - Parents Strongly Cautioned +// R - Restricted +// NC-17 - Adults Only export const filterRated = (chosen) => ({ - type: 'FILTER_RATED', + type: FILTER_RATED, chosen }); +export const FILTER_COUNTRY = 'FILTER_COUNTRY'; + export const filterCountry = (chosen) => ({ - type: 'FILTER_COUNTRY', - chosen + type: FILTER_COUNTRY, + chosen: chosen }); +export const FILTER_LANGUAGE = 'FILTER_LANGUAGE'; + export const filterLanguage = (chosen) => ({ - type: 'FILTER_LANGUAGE', - chosen + type: FILTER_LANGUAGE, + chosen: chosen }); +export const FILTER_IMDB = 'FILTER_IMDB'; + export const filterIMDB = (from, to) => ({ - type: 'FILTER_IMDB', - from, - to + type: FILTER_IMDB, + from: from, + to: to }); +export const CHOSEN_MOVIE = 'CHOSEN_MOVIE'; + export const chosenMovie = (id) => ({ - type: "CHOSEN_MOVIE", - id + type: CHOSEN_MOVIE, + id: id }); diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js new file mode 100644 index 0000000..9c69073 --- /dev/null +++ b/client/src/components/Filter.js @@ -0,0 +1,199 @@ +import React, { Component } from 'react'; +import { Table, Form, Row, Col} from 'react-bootstrap'; +import { connect } from 'react-redux'; +import store from '../store/store' +import { filterRated, filterCountry, filterLanguage, filterIMDB } from '../actions'; + + + +function mapDispatchToProps(dispatch) { + return { + filterRated: chosen => dispatch(filterRated(chosen)), + filterCountry: chosen => dispatch(filterCountry(chosen)), + filterLanguage: chosen => dispatch(filterLanguage(chosen)), + filterIMDB: chosen => dispatch(filterIMDB(chosen)) + }; +} + + +export function Filter (dispatch) { + // console.log('Filter_store',store.getState()); + // let filter = store.getState() + // let rating = filter.filterReducer[0].filter_rated; + let rating = [true, false, false, false, true, false, false, false] + // let chosenPG = filter.filterReducer[0].filter_rated; + // console.log('store.getState().filter.filterReducer[0].filter', rating); + // onChange={filterRated(chosenPG)} + return ( + <Form as={Row}> + <Form.Group as={Col}> + <Form.Label column sm="15"> + Rating + </Form.Label> + <Col sm="15"> + <Form.Check type={'checkbox'} defaultChecked={rating[0]} id={"PG1"} label={"G"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[1]} id={"PG2"} label={"PG"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[2]} id={"PG3"} label={"PG-13"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[3]} id={"PG4"} label={"R"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[4]} id={"PG5"} label={"NC-17"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[5]} id={"PG6"} label={"Not Rated"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[6]} id={"PG7"} label={"Passed"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[7]} id={"PG8"} label={"Approved"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> + <Form.Label column sm="15"> + Language + </Form.Label> + <Col sm="15"> + <Form.Check type={'checkbox'} defaultChecked={rating[1]} id={"PG2"} label={"PG"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[2]} id={"PG3"} label={"PG-13"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[3]} id={"PG4"} label={"R"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[4]} id={"PG5"} label={"NC-17"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[5]} id={"PG6"} label={"Not Rated"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[6]} id={"PG7"} label={"Passed"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[7]} id={"PG8"} label={"Approved"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[1]} id={"PG2"} label={"PG"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[2]} id={"PG3"} label={"PG-13"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[3]} id={"PG4"} label={"R"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[4]} id={"PG5"} label={"NC-17"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[5]} id={"PG6"} label={"Not Rated"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[6]} id={"PG7"} label={"Passed"}/> + <Form.Check type={'checkbox'} defaultChecked={rating[7]} id={"PG8"} label={"Approved"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> + <Form.Label column sm="15"> + IMDB-Rating + </Form.Label> + <Col sm="15"> + <p> + TODO: IMDBRating fields (and/or sliders) + </p> + </Col> + </Form.Group> + </Form> + ); +}; + +// function setPG(){} + +// function updateRatedFilter(){ +// +// } + +function mapStateToProps(state){ + const filter = state + return { filter } +} + +export default connect(mapStateToProps , mapDispatchToProps)(Filter) + + + + + + +// +// <ButtonToolbar> +// {[DropdownButton].map((DropdownType, idx) => ( +// <DropdownType +// variant="secondary" +// drop="down" +// size="lg" +// title="Filter" +// id={`dropdown-button-drop-${idx}`} +// key={idx} +// > +// <Container> +// <Row> +// <Col> +// <Dropdown.Item className="filter_category" eventKey="1"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="2"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="3"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="4"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="5"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// </Col> +// <Col> +// <Dropdown.Item className="filter_category" eventKey="1"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="2"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="3"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="4"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// <Dropdown.Item className="filter_category" eventKey="5"> +// <Form.Check type="checkbox" label="Check me out" /> +// </Dropdown.Item> +// </Col> +// </Row> +// </Container> +// </DropdownType> +// ))} +// </ButtonToolbar> +// +// +// <Table className="table-contents" bordered hover > +// <thead> +// <tr> +// <th>Rated</th> +// <th>Country</th> +// <th>Language</th> +// <th colSpan="2">IMDB Rating</th> +// </tr> +// </thead> +// <tbody> +// <tr> +// <td><Form.Check type="checkbox" label="G - General audiences" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td> +// From: +// <Form.Control className="filter-from-rating" size="md" type="number" placeholder="0"/> +// </td> +// <td> +// To: +// <Form.Control className="filter-to-rating" size="md" type="number" placeholder="100"/> +// </td> +// </tr> +// <tr> +// <td><Form.Check type="checkbox" label="PG - Parental Guidance Suggested" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// </tr> +// <tr> +// <td><Form.Check type="checkbox" label="PG-13 - Parents Strongly Cautioned" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td> <Form.Check type="checkbox" label="Check me out" /></td> +// </tr> +// <tr> +// <td><Form.Check type="checkbox" label="R - Restricted" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td> <Form.Check type="checkbox" label="Check me out" /></td> +// </tr> +// <tr> +// <td><Form.Check type="checkbox" label="NC-17 - Adults Only" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td><Form.Check type="checkbox" label="Check me out" /></td> +// <td> <Form.Check type="checkbox" label="Check me out" /></td> +// </tr> +// </tbody> +// </Table> diff --git a/client/src/index.js b/client/src/index.js index 91046a4..4ee8b4c 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -4,15 +4,11 @@ import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import { Provider } from 'react-redux'; -import { createStore} from 'redux'; -import rootReducer from './reducers' - - -const store = createStore(rootReducer) +import store from './store/store'; ReactDOM.render( <Provider store={store}> - <App store={store} /> + <App /> </Provider>, document.getElementById('root') ); diff --git a/client/src/reducers/filter.js b/client/src/reducers/filter.js new file mode 100644 index 0000000..ba58c8a --- /dev/null +++ b/client/src/reducers/filter.js @@ -0,0 +1,95 @@ +import { FILTER_IMDB, FILTER_RATED, FILTER_COUNTRY, FILTER_LANGUAGE, FILTER_STATE} from '../actions'; + +const initialState = { + filter_state : false, + filter_rated : [true, true, true, true, false, false, false, false] +} + + +// [ +// {"filter_rated": [ +// {"G":true}, +// {"PG":true}, +// {"PG-13":true}, +// {"R":false}, +// {"NC-17":false}, +// {"Not Rated":false}, +// {"Passed":false}, +// {"Approved":false} +// ] +// }, +// {"filter_language": [ +// {"English":false}, +// {"Japanese":false}, +// {"Malaysian":false}, +// {"Pakistani":false}, +// {"Finnish":false}, +// {"Swedish":false}, +// {"Danish":false} , +// {"Norwegian":false}, +// {"Mandarin":false}, +// {"Hindi":false}, +// {"Urdu":false} +// ] +// }, +// {"filter_imdb": [ +// {"from" : 0}, {"to" : 10} +// ] +// }, +// {"filter_state": false} +// ]; + +// TODO: find the number of different ratings, languages and countries +// and order them. find somewhere to save the orderings. +// implement the filter function. + + + +function filterReducer(state = initialState, action) { + switch(action.type){ + case FILTER_RATED: + return{ + ...state, filter_rated: action.chosen + }; + case FILTER_LANGUAGE: + const key3 = "filter_language" + return{ + ...state, //copy state + filter: { + ...state.filter, //copy filter + [key3]: { //update one spesific filter + ...state.filter.key3, //copy that spesific filters properties + key3: action.chosen //set the + // values to be the new selected true/false for this filter + } + } + + }; + case FILTER_IMDB: + const key4 = "filter_imdb" + return{ + ...state, //copy state + filter: { + ...state.filter, //copy filter + filter_imdb: { //update one spesific filter + ...state.filter.filter_imdb, //copy that spesific filters properties + filter_imdb: [{"from" : action.from}, {"to" : action.to}] //set the + // values to be the new selected from/to values for this filter + } + } + }; + case FILTER_STATE: + const key5 = "filter_state" + // console.log(action.bool == (true || false) ? action.bool : 'bool is not defined'); + return{ + ...state, filter_state: action.bool + }; + + default: + return state; + +}; +} + + +export default filterReducer; diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 9c767e0..81ddb71 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -1,6 +1,8 @@ import {combineReducers } from 'redux'; -import sort from './sort'; +import sortReducer from './sort'; +import filterReducer from './filter' export default combineReducers({ - sort + sortReducer, + filterReducer }) diff --git a/client/src/reducers/sort.js b/client/src/reducers/sort.js index 1b33315..c5bbed0 100644 --- a/client/src/reducers/sort.js +++ b/client/src/reducers/sort.js @@ -1,4 +1,4 @@ const sort = (order = '', action = '') => { - return({ type: action.type, order: action.order }); + return({ type: '', order: '' }); } export default sort; diff --git a/client/src/store/store.js b/client/src/store/store.js new file mode 100644 index 0000000..2236f31 --- /dev/null +++ b/client/src/store/store.js @@ -0,0 +1,5 @@ +import { createStore } from 'redux'; +import rootReducer from '../reducers'; + + +export default createStore(rootReducer); -- GitLab From 569b3ebbb5de3cac09baca8f7a8e2f1aec1f9ed6 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Wed, 16 Oct 2019 18:32:06 +0200 Subject: [PATCH 21/61] Movie component done. Ref. #9. Lacks Redux, doing later. Google Trends chart added Ref. #22. User rating set up and working. Ref. #12 --- client/public/index.html | 7 +- client/src/App.css | 1 + client/src/components/InterestOverTime.js | 5 +- client/src/components/MovieDetail.css | 83 ++++++++++++++++++ client/src/components/MovieDetail.js | 60 +++++++++++++ client/src/components/MovieDetailBody.js | 101 ++++++++++++++++++++++ 6 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 client/src/components/MovieDetail.css create mode 100644 client/src/components/MovieDetail.js create mode 100644 client/src/components/MovieDetailBody.js diff --git a/client/public/index.html b/client/public/index.html index 0ce8a59..1557bec 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -3,7 +3,6 @@ <head> <meta charset="utf-8" /> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> - <link href="https://fonts.googleapis.com/css?family=Righteous&display=swap" rel="stylesheet"> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta @@ -25,6 +24,12 @@ Learn how to configure a non-root public URL by running `npm run build`. --> + <!-- Google imports --> + <link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" + rel="stylesheet"> + <link href="https://fonts.googleapis.com/css?family=Righteous&display=swap" + rel="stylesheet"> + <!-- React bootstrap CSS --> <link rel="stylesheet" diff --git a/client/src/App.css b/client/src/App.css index a20f09a..f48935d 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -88,6 +88,7 @@ } .list-group-item:hover { background-color: grey; + cursor: default; } .search-bar { diff --git a/client/src/components/InterestOverTime.js b/client/src/components/InterestOverTime.js index a17b196..87850bb 100644 --- a/client/src/components/InterestOverTime.js +++ b/client/src/components/InterestOverTime.js @@ -9,7 +9,7 @@ import { Chart } from 'react-google-charts'; */ const InterestOverTime = ({ title }) => { const [currentTitle, setTitle] = useState(title); - const [chartData, setChartData] = useState(0); + const [chartData, setChartData] = useState(['Date', 'Intereset'], ['Temp', 10]); // Called each time a new title is passed to the component useEffect(() => { @@ -43,8 +43,7 @@ const InterestOverTime = ({ title }) => { loader={<div>Loading chart</div>} data={chartData} options={{ - title: title, - vAxis: { title: 'Interest over time' } + title: 'Interest over time on Google' }} /> ) diff --git a/client/src/components/MovieDetail.css b/client/src/components/MovieDetail.css new file mode 100644 index 0000000..5a4306f --- /dev/null +++ b/client/src/components/MovieDetail.css @@ -0,0 +1,83 @@ +.list-group-item-title { + display: inline; + float: left; +} + +.list-group-item-grade { + display: inline; + float: right; +} + +.inline-icon { + vertical-align: bottom; +} + +.material-icons-outlined.active:hover { + color: gold; + cursor: pointer; +} + +.material-icons.active:hover { + color: gold; + cursor: pointer; +} + +.movie-details-body-info { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, .1fr); + grid-gap: 2vw; + + text-align: left; +} + +.movie-details-body-info-poster { + grid-area: 1 / 1 / 2 / 2; +} + +img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.movie-details-body-info-text { + grid-area: 1 / 2 / 2 / 3; +} + +.movie-details-body-chart { + grid-area: 1 / 3 / 2 / 4; + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.movie-details-body-text-plot { + grid-area: 2 / 1 / 3 / 4; +} + +@media only screen and (max-width: 768px) { + .movie-details-body-info { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: repeat(4, .1fr); + } + + .movie-details-body-poster { + grid-area: 1 / 1 / 2 / 2; + text-align: center; + } + + .movie-details-body-info-text { + grid-area: 2 / 1 / 3 / 2; + } + + .movie-details-body-text-plot { + grid-area: 3 / 1 / 4 / 2; + } + + .movie-details-body-chart { + grid-area: 4 / 1 / 5 / 2; + justify-self: center; + } +} diff --git a/client/src/components/MovieDetail.js b/client/src/components/MovieDetail.js new file mode 100644 index 0000000..fbdafab --- /dev/null +++ b/client/src/components/MovieDetail.js @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { Accordion, Card } from 'react-bootstrap'; + +import InterestOverTime from './InterestOverTime'; +import MovieDetailBody from './MovieDetailBody'; + +import './MovieDetail.css'; + +const MovieDetail = ({ title, index }) => { + const [movieDetail, setMovieDetail] = useState(0); + const [selected, setSelected] = useState(false); + + const userGrade = [1, 2, 3, 4, 5]; + + useEffect(() => { + axios.get('/api/GetMovies', { + params: { + searchString: title, + from: 0, + to: 1, + sort: false, + filter: false + } + }).then(res => { + if (res.data.success) { + setMovieDetail(res.data.data[0]); + } + }); + }, [title]); + + return( + <Card className="list-group"> + <Accordion.Toggle + as={Card.Header} + eventKey={movieDetail.Title} + onClick={e => setSelected(!selected)} + className="list-group-item" + > + <div className="list-group-item-title"> + {movieDetail.Title} + </div> + <div className="list-group-item-grade"> + {userGrade.map(index => { + if (movieDetail.UserRating >= index) { + return (<i key={index} className="material-icons">grade</i>) + } else { + return (<i key={index} className="material-icons-outlined">grade</i>) + } + })} + </div> + </Accordion.Toggle> + <Accordion.Collapse eventKey={movieDetail.Title}> + <MovieDetailBody movieDetail={movieDetail} selected={selected} /> + </Accordion.Collapse> + </Card> + ) +} + +export default MovieDetail; diff --git a/client/src/components/MovieDetailBody.js b/client/src/components/MovieDetailBody.js new file mode 100644 index 0000000..a297c80 --- /dev/null +++ b/client/src/components/MovieDetailBody.js @@ -0,0 +1,101 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { Card } from 'react-bootstrap'; + +import './MovieDetail.css'; +import InterestOverTime from './InterestOverTime'; + +const MovieDetailBody = ({ movieDetail, selected }) => { + const [rating, setRating] = useState(0); + const userGrade = [1, 2, 3, 4, 5]; + + function handleRating(rating) { + axios.post('/api/UpdateUserRating', null, { + params: { + Title: movieDetail.Title, + UserRating: rating + } + }).then(res => { + localStorage.setItem(movieDetail.Title + "-Rating", rating); + setRating(rating); + + axios.get('/api/GetMovies', { + params: { + searchString: movieDetail.Title, + from: 0, + to: 1, + sort: false, + filter: false + } + }).then(res => { + movieDetail.UserRating = res.data.data[0].UserRating; + }) + }); + } + + useEffect(() => { + let userRate = localStorage.getItem(movieDetail.Title + "-Rating"); + + if (userRate !== null) { + setRating(userRate); + } + }, [selected, movieDetail.Title]); + + return( + <Card.Body className="movie-details-body"> + {selected ? + <> + <div className="movie-details-body-info"> + <div className="movie-details-body-poster"> + <img src={movieDetail.Poster} alt="Poster not available" /> + </div> + <div className="movie-details-body-text"> + <b>Title:</b> {movieDetail.Title} <br /> + <b>Year:</b> {movieDetail.Year} <br /> + <b>Actors:</b> {movieDetail.Actors} <br /> + <b>Director:</b> {movieDetail.Director} <br /> + <b>Country:</b> {movieDetail.Country} <br /> + <b>Your rating:</b> + {userGrade.map(index => { + if (rating !== null) { + if (rating >= index) { + return( + <i + key={movieDetail.Title + "-normal-active-" + index} + className="inline-icon material-icons active" + onClick={() => handleRating(index)} + >grade</i> + ) + } else { + return( + <i + key={movieDetail.Title + "-outline-active-" + index} + className="inline-icon material-icons-outlined active" + onClick={() => handleRating(index)} + >grade</i> + ) + } + } else { + return( + <i + key={movieDetail.Title + "-outline-not-rated-active-" + index} + className="inline-icon material-icons-outlined active" + onClick={() => handleRating(index)} + >grade</i>) + } + })} + </div> + <div className="movie-details-body-chart"> + <InterestOverTime title={movieDetail.Title} /> + </div> + <div className="movie-details-body-text-plot"> + {movieDetail.Plot} + </div> + </div> + </> + : <></> } + </Card.Body> + ) +} + +export default MovieDetailBody; -- GitLab From 8fbf71268b914d7042fa5a41616ba6ba932658e8 Mon Sep 17 00:00:00 2001 From: asszewcz <asszewcz@stud.ntnu.no> Date: Wed, 16 Oct 2019 22:21:43 +0200 Subject: [PATCH 22/61] Make sort and order use redux --- client/src/actions/index.js | 55 --------------------- client/src/components/SortButton.js | 76 +++++++---------------------- client/src/constants.js | 12 +++++ client/src/containers/SortLink.js | 14 ------ client/src/index.js | 8 +-- client/src/reducers/index.js | 6 --- client/src/reducers/sort.js | 4 -- client/src/redux/actionTypes.js | 2 + client/src/redux/actions.js | 30 ++++++++++++ client/src/redux/reducers/index.js | 5 ++ client/src/redux/reducers/order.js | 17 +++++++ client/src/redux/reducers/sort.js | 17 +++++++ client/src/redux/selectors.js | 4 ++ client/src/redux/store.js | 4 ++ 14 files changed, 113 insertions(+), 141 deletions(-) delete mode 100644 client/src/actions/index.js create mode 100644 client/src/constants.js delete mode 100644 client/src/containers/SortLink.js delete mode 100644 client/src/reducers/index.js delete mode 100644 client/src/reducers/sort.js create mode 100644 client/src/redux/actionTypes.js create mode 100644 client/src/redux/actions.js create mode 100644 client/src/redux/reducers/index.js create mode 100644 client/src/redux/reducers/order.js create mode 100644 client/src/redux/reducers/sort.js create mode 100644 client/src/redux/selectors.js create mode 100644 client/src/redux/store.js diff --git a/client/src/actions/index.js b/client/src/actions/index.js deleted file mode 100644 index 9ad045b..0000000 --- a/client/src/actions/index.js +++ /dev/null @@ -1,55 +0,0 @@ -export const SORT_YEAR = 'SORT_YEAR'; -export function sortYear = order => ({ - type: SORT_YEAR, - order: order -}); - -export const SORT_BOX_OFFICE = 'SORT_BOX_OFFICE'; -export function sortBoxOffice = order => ({ - type: SORT_BOX_OFFICE, - order: order -}); - -export const SORT_USER_RATING = 'SORT_USER_RATING'; -export function sortUserRating = order => ({ - type: SORT_USER_RATING, - order: order -}); - -export const SORT_IMDB = 'SORT_IMDB'; -export function sortIMDB = order => ({ - type: SORT_IMDB, - order: order -}); - -export const SORT_TITLE = 'SORT_TITLE'; -export function sortTitle = order => ({ - type: SORT_TITLE, - order: order -}); - -export const filterRated = (chosen) => ({ - type: 'FILTER_RATED', - chosen -}); - -export const filterCountry = (chosen) => ({ - type: 'FILTER_COUNTRY', - chosen -}); - -export const filterLanguage = (chosen) => ({ - type: 'FILTER_LANGUAGE', - chosen -}); - -export const filterIMDB = (from, to) => ({ - type: 'FILTER_IMDB', - from, - to -}); - -export const chosenMovie = (id) => ({ - type: "CHOSEN_MOVIE", - id -}); diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js index 64d6eb6..4ec00e8 100644 --- a/client/src/components/SortButton.js +++ b/client/src/components/SortButton.js @@ -1,77 +1,37 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; -import { Dropdown, ButtonToolbar, Button } from 'react-bootstrap'; -import PropTypes from 'prop-types'; -import './SortButton.css' - -const SortButton = () => { - const [sortingBy, setSortingBy] = useState('Title'); - const [order, setOrder] = useState('ASC'); - - function ChangeOrder() { - if (order == 'ASC') { - setOrder('DESC') - } - else { - setOrder('ASC') - } - } - function ChangeType(sorting){ - setSortingBy(sorting) +import { Dropdown, ButtonToolbar, Button } from 'react-bootstrap'; +import './SortButton.css'; - } +import { selectSort, selectOrder } from "../redux/actions"; +import { SORT, ORDER } from "../constants"; +const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder }) => { return( <div> <Dropdown > <Dropdown.Toggle className="sort-button" variant="outline-secondary" id="dropdown-basic"> - {sortingBy} + {activeSort} </Dropdown.Toggle> - <Dropdown.Menu className="sort-button-drop"> - <Dropdown.Item onClick={() => ChangeType('Title')}>Title</Dropdown.Item> - <Dropdown.Item onClick={() => ChangeType('Year')}>Year</Dropdown.Item> - <Dropdown.Item onClick={() => ChangeType('Box')}>Box office</Dropdown.Item> - <Dropdown.Item onClick={() => ChangeType('Imdb')}>Imdb rating</Dropdown.Item> - <Dropdown.Item onClick={() => ChangeType('User')}>User rating</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.TITLE)}}>Title</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.YEAR)}}>Year</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.BOX_OFFICE)}}>Box office</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.IMDB_RATING)}}>Imdb rating</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.USER_RATING)}}>User rating</Dropdown.Item> </Dropdown.Menu> </Dropdown> - <ButtonToolbar> - <Button className="sort-order-button" variant="outline-secondary" onClick={() => ChangeOrder()}> - {order == "ASC" ? '↑' : '↓'} + <Button className="sort-order-button" variant="outline-secondary" onClick={() => {selectOrder(activeOrder)}}> + {activeOrder == "ASC" ? '↑' : '↓'} </Button> </ButtonToolbar> </div> ) - } - -// function ChangeType(sorting){ -// this.setSortingBy(sorting) -// // switch(sorting){ -// // case 'Title': -// // e => dispatch(sortTitle('ASC') -// // break -// // case 'Year': -// // e => dispatch(sort('ASC') -// // } -// -// } -// -// function ChangeOrder() { -// if (order == 'ASC') { -// setOrder('DESC') -// } -// else { -// setOrder('ASC') -// } -// } - -// SortButton.propTypes = { -// onClick: -// } - - -export default SortButton +const mapStateToProps = state => { + console.log(state); + return { activeSort: state.sort, activeOrder: state.order }; +}; +export default connect(mapStateToProps, { selectSort, selectOrder })(SortButton); diff --git a/client/src/constants.js b/client/src/constants.js new file mode 100644 index 0000000..3d95d3f --- /dev/null +++ b/client/src/constants.js @@ -0,0 +1,12 @@ +export const SORT = { + TITLE: "Title", + YEAR: "Year", + BOX_OFFICE: "Box office", + IMDB_RATING: "IMDB rating", + USER_RATING: "User rating" +} + +export const ORDER = { + ASC: "ASC", + DESC: "DESC" +} diff --git a/client/src/containers/SortLink.js b/client/src/containers/SortLink.js deleted file mode 100644 index 1199f0b..0000000 --- a/client/src/containers/SortLink.js +++ /dev/null @@ -1,14 +0,0 @@ -import { connect } from 'react-redux'; -import { sortTitle, sortYear, sortIMDB, sortBoxOffice, sortUserRating} from './actions'; -import SortButton from './components'; - -const mapStateToProps = (state, ownProps) => ({ - sort: ownProps.type === state.type && ownProps.order === state.order -}) - -const mapDispathToProps = (dispatch, ownProps) => ({ - onClick -}) - - -export default connect(mapStateToProps, mapDispathToProps)(SortButton) diff --git a/client/src/index.js b/client/src/index.js index 91046a4..428e117 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -4,15 +4,15 @@ import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import { Provider } from 'react-redux'; -import { createStore} from 'redux'; -import rootReducer from './reducers' +import store from "./redux/store"; + + -const store = createStore(rootReducer) ReactDOM.render( <Provider store={store}> - <App store={store} /> + <App store={store}/> </Provider>, document.getElementById('root') ); diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js deleted file mode 100644 index 9c767e0..0000000 --- a/client/src/reducers/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import {combineReducers } from 'redux'; -import sort from './sort'; - -export default combineReducers({ - sort -}) diff --git a/client/src/reducers/sort.js b/client/src/reducers/sort.js deleted file mode 100644 index 1b33315..0000000 --- a/client/src/reducers/sort.js +++ /dev/null @@ -1,4 +0,0 @@ -const sort = (order = '', action = '') => { - return({ type: action.type, order: action.order }); -} -export default sort; diff --git a/client/src/redux/actionTypes.js b/client/src/redux/actionTypes.js new file mode 100644 index 0000000..cec9302 --- /dev/null +++ b/client/src/redux/actionTypes.js @@ -0,0 +1,2 @@ +export const SELECT_SORT = "SELECT_SORT"; +export const SELECT_ORDER = "SELECT_ORDER"; diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js new file mode 100644 index 0000000..3b86568 --- /dev/null +++ b/client/src/redux/actions.js @@ -0,0 +1,30 @@ +import { SELECT_SORT, SELECT_ORDER } from "./actionTypes"; +import { ORDER } from "../constants"; + +export const selectSort = sortType => ({ + type: SELECT_SORT, + payload: { + sortType: sortType + } +}); + +export const selectOrder = orderType => { + if (orderType == ORDER.ASC){ + return ({ + type: SELECT_ORDER, + payload: { + orderType: ORDER.DESC + } + }) + } + else { + return ({ + type: SELECT_ORDER, + payload: { + orderType: ORDER.ASC, + } + }) + } + + +}; diff --git a/client/src/redux/reducers/index.js b/client/src/redux/reducers/index.js new file mode 100644 index 0000000..916aa10 --- /dev/null +++ b/client/src/redux/reducers/index.js @@ -0,0 +1,5 @@ +import { combineReducers } from "redux"; +import sort from "./sort"; +import order from "./order"; + +export default combineReducers({ sort, order }); diff --git a/client/src/redux/reducers/order.js b/client/src/redux/reducers/order.js new file mode 100644 index 0000000..ba98610 --- /dev/null +++ b/client/src/redux/reducers/order.js @@ -0,0 +1,17 @@ +import { SELECT_ORDER } from "../actionTypes"; +import { ORDER } from "../../constants"; + +const initialState = ORDER.ASC; + +const order = (state = initialState, action) => { + switch (action.type) { + case SELECT_ORDER: { + return action.payload.orderType + } + default: { + return state; + } + } +} + +export default order; diff --git a/client/src/redux/reducers/sort.js b/client/src/redux/reducers/sort.js new file mode 100644 index 0000000..d680a02 --- /dev/null +++ b/client/src/redux/reducers/sort.js @@ -0,0 +1,17 @@ +import { SELECT_SORT } from "../actionTypes"; +import { SORT } from "../../constants"; + +const initialState = SORT.TITLE; + +const sort = (state = initialState, action) => { + switch (action.type) { + case SELECT_SORT: { + return action.payload.sortType; + } + default: { + return state; + } + } +} + +export default sort; diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js new file mode 100644 index 0000000..94c95bd --- /dev/null +++ b/client/src/redux/selectors.js @@ -0,0 +1,4 @@ +import { SORT, ORDER } from "../constants"; + +export const selectSortState = store => store.sort; +export const selectOrderState = store => store.order; diff --git a/client/src/redux/store.js b/client/src/redux/store.js new file mode 100644 index 0000000..f02d47a --- /dev/null +++ b/client/src/redux/store.js @@ -0,0 +1,4 @@ +import { createStore } from "redux"; +import rootReducer from "./reducers"; + +export default createStore(rootReducer); -- GitLab From b250c6a809ee8e2f6c4f68769f9fb6d3c2387291 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 17 Oct 2019 14:45:56 +0200 Subject: [PATCH 23/61] Added comments to code. Movie detail component is now responsive. Ref. #9 --- client/src/components/InterestOverTime.js | 48 ++++++-------- client/src/components/MovieDetail.css | 7 +++ client/src/components/MovieDetail.js | 17 +++-- client/src/components/MovieDetailBody.js | 77 ++++++++++++----------- 4 files changed, 77 insertions(+), 72 deletions(-) diff --git a/client/src/components/InterestOverTime.js b/client/src/components/InterestOverTime.js index 87850bb..93a62e7 100644 --- a/client/src/components/InterestOverTime.js +++ b/client/src/components/InterestOverTime.js @@ -8,44 +8,36 @@ import { Chart } from 'react-google-charts'; * return: the component HTML as an Area chart. */ const InterestOverTime = ({ title }) => { - const [currentTitle, setTitle] = useState(title); const [chartData, setChartData] = useState(['Date', 'Intereset'], ['Temp', 10]); + const [error, setError] = useState(false); // Called each time a new title is passed to the component useEffect(() => { axios.get('/api/MovieTrend', { params: { Title: title }}) .then(res => { - let data = JSON.parse(res.data.data); - - let tempData = []; - let tempTitles = ['Date', 'Interest']; - tempData.push(tempTitles); - - // Since the data recieved from Google is not on the form we want, - // some magic has to happen. Created the data structure that - // react-google-charts wants - data.default.timelineData.map(time => { - let tempValues = []; - tempValues.push(time.formattedTime); - tempValues.push(Number(time.formattedValue[0])); - tempData.push(tempValues); - }); - - setChartData(tempData); + if (res.data.success === true) { + setChartData(res.data.data); + } else { + setError(true); + } }); }, [title]); return( - <Chart - width={300} - height={300} - chartType="AreaChart" - loader={<div>Loading chart</div>} - data={chartData} - options={{ - title: 'Interest over time on Google' - }} - /> + <> + {!error ? + <Chart + width={300} + height={300} + chartType="AreaChart" + loader={<div>Loading chart</div>} + data={chartData} + options={{ + title: 'Interest over time on Google' + }} + /> + : <b>No data found on Google Trends</b> } + </> ) } diff --git a/client/src/components/MovieDetail.css b/client/src/components/MovieDetail.css index 5a4306f..329d0bc 100644 --- a/client/src/components/MovieDetail.css +++ b/client/src/components/MovieDetail.css @@ -45,6 +45,13 @@ img { grid-area: 1 / 2 / 2 / 3; } + +.movie-details-body-text-header h1 { + display: inline; + font-size: 16px; + font-weight: bold; +} + .movie-details-body-chart { grid-area: 1 / 3 / 2 / 4; max-width: 100%; diff --git a/client/src/components/MovieDetail.js b/client/src/components/MovieDetail.js index fbdafab..61ffc44 100644 --- a/client/src/components/MovieDetail.js +++ b/client/src/components/MovieDetail.js @@ -2,29 +2,28 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { Accordion, Card } from 'react-bootstrap'; -import InterestOverTime from './InterestOverTime'; import MovieDetailBody from './MovieDetailBody'; import './MovieDetail.css'; -const MovieDetail = ({ title, index }) => { +/** + * Renders one search result with movie title and average user rating in header. + * Uses Bootstraps Accordion to get nice animations. + */ +const MovieDetail = ({ title }) => { const [movieDetail, setMovieDetail] = useState(0); const [selected, setSelected] = useState(false); const userGrade = [1, 2, 3, 4, 5]; useEffect(() => { - axios.get('/api/GetMovies', { + axios.get('/api/GetMovieDetail', { params: { - searchString: title, - from: 0, - to: 1, - sort: false, - filter: false + searchString: title } }).then(res => { if (res.data.success) { - setMovieDetail(res.data.data[0]); + setMovieDetail(res.data.data); } }); }, [title]); diff --git a/client/src/components/MovieDetailBody.js b/client/src/components/MovieDetailBody.js index a297c80..d3f0dcb 100644 --- a/client/src/components/MovieDetailBody.js +++ b/client/src/components/MovieDetailBody.js @@ -5,10 +5,18 @@ import { Card } from 'react-bootstrap'; import './MovieDetail.css'; import InterestOverTime from './InterestOverTime'; +/** + * Component shown when selecting a movie in the search result list. + * Renders all relevant information about the selected movie. + * This component also handles user rating input. The user rating + * is stored in localStorage of the user. + */ const MovieDetailBody = ({ movieDetail, selected }) => { const [rating, setRating] = useState(0); const userGrade = [1, 2, 3, 4, 5]; + // Stores the given user rating in the DB and updates the data in + // frontend function handleRating(rating) { axios.post('/api/UpdateUserRating', null, { params: { @@ -19,20 +27,17 @@ const MovieDetailBody = ({ movieDetail, selected }) => { localStorage.setItem(movieDetail.Title + "-Rating", rating); setRating(rating); - axios.get('/api/GetMovies', { + axios.get('/api/GetMovieDetail', { params: { - searchString: movieDetail.Title, - from: 0, - to: 1, - sort: false, - filter: false + searchString: movieDetail.Title } }).then(res => { - movieDetail.UserRating = res.data.data[0].UserRating; + movieDetail.UserRating = res.data.data.UserRating; }) }); } + // Hook to update the selected user rating useEffect(() => { let userRate = localStorage.getItem(movieDetail.Title + "-Rating"); @@ -50,40 +55,42 @@ const MovieDetailBody = ({ movieDetail, selected }) => { <img src={movieDetail.Poster} alt="Poster not available" /> </div> <div className="movie-details-body-text"> - <b>Title:</b> {movieDetail.Title} <br /> - <b>Year:</b> {movieDetail.Year} <br /> - <b>Actors:</b> {movieDetail.Actors} <br /> - <b>Director:</b> {movieDetail.Director} <br /> - <b>Country:</b> {movieDetail.Country} <br /> - <b>Your rating:</b> - {userGrade.map(index => { - if (rating !== null) { - if (rating >= index) { - return( - <i - key={movieDetail.Title + "-normal-active-" + index} - className="inline-icon material-icons active" - onClick={() => handleRating(index)} - >grade</i> - ) + <div className="movie-details-body-text-header"><h1>Title:</h1> {movieDetail.Title}</div> + <div className="movie-details-body-text-header"><h1>Year:</h1> {movieDetail.Year}</div> + <div className="movie-details-body-text-header"><h1>Actors:</h1> {movieDetail.Actors}</div> + <div className="movie-details-body-text-header"><h1>Director:</h1> {movieDetail.Director}</div> + <div className="movie-details-body-text-header"><h1>Country:</h1> {movieDetail.Country}</div> + <div className="movie-details-body-text-header"><h1>imdbRating:</h1> {movieDetail.imdbRating}</div> + <div className="movie-details-body-text-header"><h1>Your rating:</h1> + {userGrade.map(index => { + if (rating !== null) { + if (rating >= index) { + return( + <i + key={movieDetail.Title + "-normal-active-" + index} + className="inline-icon material-icons active" + onClick={() => handleRating(index)} + >grade</i> + ) + } else { + return( + <i + key={movieDetail.Title + "-outline-active-" + index} + className="inline-icon material-icons-outlined active" + onClick={() => handleRating(index)} + >grade</i> + ) + } } else { return( <i - key={movieDetail.Title + "-outline-active-" + index} + key={movieDetail.Title + "-outline-not-rated-active-" + index} className="inline-icon material-icons-outlined active" onClick={() => handleRating(index)} - >grade</i> - ) + >grade</i>) } - } else { - return( - <i - key={movieDetail.Title + "-outline-not-rated-active-" + index} - className="inline-icon material-icons-outlined active" - onClick={() => handleRating(index)} - >grade</i>) - } - })} + })} + </div> </div> <div className="movie-details-body-chart"> <InterestOverTime title={movieDetail.Title} /> -- GitLab From 5c58ce0fbaabf86c773213b4a3832bbc1598c937 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 17 Oct 2019 14:47:58 +0200 Subject: [PATCH 24/61] Refactored backend with more endpoits. Cleaned up code and made the logic more understandable. Less data is sent to frontend. Ref. #25 --- server/routes/movies.js | 163 ++++++++++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 46 deletions(-) diff --git a/server/routes/movies.js b/server/routes/movies.js index 8855bbd..f0930d9 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -5,53 +5,36 @@ const googleTrends = require('google-trends-api'); const Movies = require('../Schemas/Movies'); -/* GET movies - * - * Params: { - * searchString: searches for titles containing this string, - * from: start index of search, - * to: end index of search, - * sort: boolean if result should be sorted. Default is alphabetically - * sortType: the sort type. Get the current type from state - * sortOrder: the sort order. Get the current order from state - * filter: boolean if filter should be active - * filters: string of all filters structured: - * [ - * { FILTER_TYPE_1: VALUE_1 }, - * { FILTER_TYPE_2: VALUE_2 } - * ] - * } - * Returns: { - * success: Boolean, - * data: Array of results - * } - * - * Gets all movie titles containing the searchString in title. - * Only returns the number of hits wanted - */ -router.get('/GetMovies', function(req, res, next) { + /* GET movie titles + * + * Params: { + * searchString: searches for titles containing this string, + * from: start index of search, + * to: end index of search, + * sort: boolean if result should be sorted. Default is alphabetically + * sortType: the sort type. Get the current type from state + * sortOrder: the sort order. Get the current order from state + * filter: boolean if filter should be active + * filters: string of all filters structured: + * [ + * { FILTER_TYPE_1: VALUE_1 }, + * { FILTER_TYPE_2: VALUE_2 } + * ] + * } + * Returns: { + * success: Boolean, + * data: Array of results + * } + * + * Gets all movie titles containing the searchString in title. + * Only returns the number of hits wanted + */ +router.get('/GetMovieTitles', function(req, res) { var query; let numberOfHits = Number(req.query.from) - Number(req.query.to); - // Sorting logic - let sortOrder = 0; - let sortBy = {}; - if (req.query.sort === "true") { - // Selecting ascending or descending sort type - if (req.query.sortOrder === 'ASC') sortOrder = 1; - else if (req.query.sortOrder === 'DESC') sortOrder = -1; - else return res.json({ success: false, error: "Order has to be ASC or DESC"}); - - // Selecting what to sort on - if (req.query.sortType === 'SORT_YEAR') sortBy = { "Year": sortOrder }; - else if (req.query.sortType === 'SORT_BOX_OFFICE') sortBy = { "BoxOffice": sortOrder }; - else if (req.query.sortType === 'SORT_USER_RATING') sortBy = { "UserRating": sortOrder }; - else if (req.query.sortType === 'SORT_IMDB') sortBy = { "imdbRating": sortOrder }; - else return res.json({ success: false, error: "Sort type not supported" }); - } else { - sortBy = { "Title": 1 } - } + let sortBy = sorting(req.query.sortOrder, req.query.sortType); // Filter logic. As you cannot use $and, $or with empty values, different // queries has to be created based on if filter is active or not @@ -61,9 +44,11 @@ router.get('/GetMovies', function(req, res, next) { query = Movies.find({ $and:[ { $or: filters }, { "Title": { $regex: req.query.searchString, $options: "i" } } - ]}).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); + ]}, { "Title": 1 }).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); } else { - query = Movies.find({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }) + query = Movies.find({ + "Title": {'$regex': req.query.searchString, '$options': 'i'} + }, { "Title": 1 }) .sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); } @@ -74,6 +59,61 @@ router.get('/GetMovies', function(req, res, next) { }); }); +/* GET movie details + * + * Params: { + * searchString: title to find details of + * } + * Returns: returns the details of the given movie + * + * Used to get the details of one givem movie. This to reduse inital load on + * search. + */ +router.get('/GetMovieDetail', function(req, res, next) { + var query = Movies.findOne({ "Title": {'$regex': req.query.searchString, '$options': 'i'} }); + + query.exec(function(err, data) { + if (err) return res.json({ success: false, error: err }); + + return res.json({ success: true, data: data }); + }); +}); + +// Sorting logic +function sorting(sortOrder, sortType) { + let sortBy = {}; + + switch(sortOrder) { + case 'ASC': + sortOrder = -1; + break; + case 'DESC': + sortOrder = 1; + break; + default: + sortOrder = 1; + } + + switch(sortType) { + case 'SORT_YEAR': + sortBy = { "Year": sortOrder }; + break; + case 'SORT_BOX_OFFICE': + sortBy = { "BoxOffice": sortOrder }; + break; + case 'SORT_USER_RATING': + sortBy = { "UserRating": sortOrder }; + break; + case 'SORT_IMDB': + sortBy = { "imdbRating": sortOrder }; + break; + default: + sortBy = { "Title": sortOrder }; + } + + return sortBy; +} + /* POST user ratings * * Params: { @@ -122,7 +162,19 @@ router.post('/UpdateUserRating', function(req, res) { }); }); +/* + * GET MovieTrends + * + * Params: { + * Title: the movie title to get trends on + * } + * Returns: { success: Boolean, data: the result from Google } + * + * Uses the Google Trends API to get the interest over time on the + * selected movie title. Used to get the graph view on each movie. + */ router.get('/MovieTrend', function(req, res) { + // Get the date 7 days ago. var days = 7; var date = new Date(); var lastWeek = new Date(date.getTime() - (days * 24 * 60 * 60 * 1000)); @@ -137,7 +189,26 @@ router.get('/MovieTrend', function(req, res) { startTime: startTime }) .then(function(result) { - return res.json({ success: true, data: result }); + result = JSON.parse(result) + let tempData = []; + let tempTitles = ['Date', 'Interest']; + tempData.push(tempTitles); + + // Since the data recieved from Google is not on the form we want, + // some magic has to happen. Created the data structure that + // react-google-charts wants + result.default.timelineData.map(time => { + let tempValues = []; + tempValues.push(time.formattedTime); + tempValues.push(Number(time.formattedValue[0])); + tempData.push(tempValues); + return ""; + }); + if (tempData.length > 1) { + return res.json({ success: true, data: tempData }); + } else { + return res.json({ success: false, error: 'No data found' }) + } }) .catch(function(error) { return res.json({ success: false, error: error }); -- GitLab From 6122d9f33eacd938923b792bcfee6f122c64ea3b Mon Sep 17 00:00:00 2001 From: asszewcz <asszewcz@stud.ntnu.no> Date: Thu, 17 Oct 2019 14:58:57 +0200 Subject: [PATCH 25/61] Get ready to merge redux implementations. Ref. #23, #16, #17 --- client/src/components/Filter.js | 0 client/src/components/SortButton.js | 10 +++++----- client/src/index.js | 2 +- client/src/redux/actionTypes.js | 2 -- client/src/redux/actions.js | 5 ++++- client/src/redux/reducers/order.js | 2 +- client/src/redux/reducers/sort.js | 2 +- client/src/redux/selectors.js | 4 ---- 8 files changed, 12 insertions(+), 15 deletions(-) delete mode 100644 client/src/components/Filter.js delete mode 100644 client/src/redux/actionTypes.js delete mode 100644 client/src/redux/selectors.js diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js index 4ec00e8..8854ae3 100644 --- a/client/src/components/SortButton.js +++ b/client/src/components/SortButton.js @@ -15,11 +15,11 @@ const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder }) => { {activeSort} </Dropdown.Toggle> <Dropdown.Menu className="sort-button-drop"> - <Dropdown.Item onClick={() => {selectSort(SORT.TITLE)}}>Title</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.YEAR)}}>Year</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.BOX_OFFICE)}}>Box office</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.IMDB_RATING)}}>Imdb rating</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.USER_RATING)}}>User rating</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.TITLE)}}>{SORT.TITLE}</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.YEAR)}}>{SORT.YEAR}</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.BOX_OFFICE)}}>{SORT.BOX_OFFICE}</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.IMDB_RATING)}}>{SORT.IMDB_RATING}</Dropdown.Item> + <Dropdown.Item onClick={() => {selectSort(SORT.USER_RATING)}}>{SORT.USER_RATING}</Dropdown.Item> </Dropdown.Menu> </Dropdown> <ButtonToolbar> diff --git a/client/src/index.js b/client/src/index.js index 428e117..6c32782 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -12,7 +12,7 @@ import store from "./redux/store"; ReactDOM.render( <Provider store={store}> - <App store={store}/> + <App /> </Provider>, document.getElementById('root') ); diff --git a/client/src/redux/actionTypes.js b/client/src/redux/actionTypes.js deleted file mode 100644 index cec9302..0000000 --- a/client/src/redux/actionTypes.js +++ /dev/null @@ -1,2 +0,0 @@ -export const SELECT_SORT = "SELECT_SORT"; -export const SELECT_ORDER = "SELECT_ORDER"; diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js index 3b86568..39533bc 100644 --- a/client/src/redux/actions.js +++ b/client/src/redux/actions.js @@ -1,6 +1,9 @@ -import { SELECT_SORT, SELECT_ORDER } from "./actionTypes"; import { ORDER } from "../constants"; +export const SELECT_SORT = "SELECT_SORT"; +export const SELECT_ORDER = "SELECT_ORDER"; + + export const selectSort = sortType => ({ type: SELECT_SORT, payload: { diff --git a/client/src/redux/reducers/order.js b/client/src/redux/reducers/order.js index ba98610..1f09cb7 100644 --- a/client/src/redux/reducers/order.js +++ b/client/src/redux/reducers/order.js @@ -1,4 +1,4 @@ -import { SELECT_ORDER } from "../actionTypes"; +import { SELECT_ORDER } from "../actions"; import { ORDER } from "../../constants"; const initialState = ORDER.ASC; diff --git a/client/src/redux/reducers/sort.js b/client/src/redux/reducers/sort.js index d680a02..75467e6 100644 --- a/client/src/redux/reducers/sort.js +++ b/client/src/redux/reducers/sort.js @@ -1,4 +1,4 @@ -import { SELECT_SORT } from "../actionTypes"; +import { SELECT_SORT } from "../actions"; import { SORT } from "../../constants"; const initialState = SORT.TITLE; diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js deleted file mode 100644 index 94c95bd..0000000 --- a/client/src/redux/selectors.js +++ /dev/null @@ -1,4 +0,0 @@ -import { SORT, ORDER } from "../constants"; - -export const selectSortState = store => store.sort; -export const selectOrderState = store => store.order; -- GitLab From 53bf7cd145b09993f66f68d4fe72bd085c5f2454 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 17 Oct 2019 15:03:35 +0200 Subject: [PATCH 26/61] Make filter component. Implement redux. Tweak bugs in layout. Prepare for merge with sort-button branch. Ref. #16, #17, #21 --- .gitignore | 1 + client/src/App.css | 29 +-- client/src/App.js | 66 +---- client/src/actions/index.js | 69 ------ client/src/components/Filter.css | 7 + client/src/components/Filter.js | 300 +++++++++-------------- client/src/index.js | 4 +- client/src/reducers/filter.js | 95 ------- client/src/redux/actions.js | 61 +++++ client/src/redux/reducers/current.js | 17 ++ client/src/redux/reducers/filter.js | 58 +++++ client/src/{ => redux}/reducers/index.js | 4 +- client/src/{ => redux}/reducers/sort.js | 0 client/src/{store => redux}/store.js | 2 +- 14 files changed, 284 insertions(+), 429 deletions(-) delete mode 100644 client/src/actions/index.js create mode 100644 client/src/components/Filter.css delete mode 100644 client/src/reducers/filter.js create mode 100644 client/src/redux/actions.js create mode 100644 client/src/redux/reducers/current.js create mode 100644 client/src/redux/reducers/filter.js rename client/src/{ => redux}/reducers/index.js (69%) rename client/src/{ => redux}/reducers/sort.js (100%) rename client/src/{store => redux}/store.js (67%) diff --git a/.gitignore b/.gitignore index 175dd57..e9785a2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ .env.development.local .env.test.local .env.production.local +/client/src/List.txt npm-debug.log* yarn-debug.log* diff --git a/client/src/App.css b/client/src/App.css index 6621a4e..9694465 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -11,7 +11,7 @@ 'FilterOptions FilterOptions' 'Content Content'; grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr 0.3fr 0.3fr 0.3fr 0.1fr 2.5fr; + grid-template-rows: 0.5fr 0.1fr 0.1fr 0.1fr 0.1fr 2.5fr; grid-gap: 1vh; margin: 0vh 15vh 0 15vh; } @@ -107,38 +107,15 @@ /* display: none; */ opacity: 0; filter: alpha(opacity=0); /* For IE8 and earlier */ - /* TODO: Transform opacity, height and/or toggle display = none - for activation of the filter options */ + } .filter-options-active { grid-area: FilterOptions; text-align: left; width: 70vw; height: auto; - /* height: 0; */ - /* width: 0; */ - /* display: none; */ + padding-bottom: 2vw; opacity: 1; filter: alpha(opacity=1); /* For IE8 and earlier */ - /* TODO: Transform opacity, height and/or toggle display = none - for activation of the filter options */ -} - - - - - -.filter-from-rating { - width: 5vw; - align-self: center; - -} - -.filter-to-rating { - width: 5vw; - align-self: center; -} - -.search-bar { } diff --git a/client/src/App.js b/client/src/App.js index 0464e6d..83a19e4 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -2,10 +2,10 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import axios from 'axios'; import './App.css'; -import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-bootstrap'; -import { filterRated, filterState } from './actions'; -import store from './store/store'; -import {Filter} from './components/Filter'; +import {ButtonToolbar, Button, ListGroup, Form, Accordion} from 'react-bootstrap'; +import { filterState } from './redux/actions'; +import store from './redux/store'; +import Filter from './components/Filter'; const logo = require('./images/logo.svg'); @@ -14,30 +14,11 @@ class App extends Component { constructor(props) { super(props); - - this.state = { movies: [], error: "" } - - this.getMovies = this.getMovies.bind(this); - console.log('Before:\t', store.getState()); - store.dispatch(filterState(true)) - store.dispatch(filterRated([false, false, false, false, false, false, false, false])) - console.log('After:\t', store.getState()); - store.dispatch(filterState(false)) - store.dispatch(filterRated([false, false, false, false, false, false, false, false])) - console.log('After1:\t', store.getState()); - - - // store.dispatch(filterRated([true, false, false, false, false])); - // console.log('After filterRated([true, false, false, false, false]):\t', store.getState()); - // store.dispatch(filterRated([false, false, false, false, false])); - // console.log('After1 filterRated([false, false, false, false, false]):\t', store.getState()); - // store.dispatch(filterState(true)); - // console.log('After2:\t', store.getState()); - } +} componentDidMount() { this.getMovies("", 0, 20); @@ -56,6 +37,7 @@ class App extends Component { render() { + console.log(this.props.filter_state) return( <div className="main-container"> <div className="logo-wrapper"> @@ -68,12 +50,12 @@ class App extends Component { <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 20)} /> </div> <div className="filter-wrapper"> - <Button variant="outline-secondary" size="lg" block >Filter</Button> + <Button variant="outline-secondary" size="lg" onClick={() => this.props.filterState(this.props.filter_state)} block >Filter</Button> </div> <div className="sort-wrapper"> <Button variant="outline-secondary" size="lg" block>Sort</Button> </div> - <div className={"filter-options-active"}> + <div className={this.props.filter_state ? "filter-options-active" : "filter-options"}> <Filter/> </div> <div className="main-content"> @@ -81,7 +63,7 @@ class App extends Component { {this.state.movies.map((movie, index) => { return( <ListGroup.Item key={index} className="list-group-item" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> - {movie.Title} + {movie.Title} -- {movie.imdbRating} </ListGroup.Item> ) })} @@ -91,33 +73,9 @@ class App extends Component { ) } } -// -// function toggleFilterState(){ -// console.log('toggleFilterState', store.getState()) -// store.dispatch(filterState(!getState())) -// } -// function getState(){ -// // let fstate = store.getState().filterReducer.filter[4].filter_state; -// console.log('store.getState()', store.getState()); -// return !store.getState().filterReducer[3].filter_state; -// } - - -function mapStateToProps(state) { - // console.log(state.filter) - const {filter} = state; - return { filters : filter} +const mapStateToProps = state => { + return { filter_state : state.filterReducer.filter_state} }; - -const mapDispatchToProps = dispatch => { - return { - filterState: () => dispatch({type : 'FILTER_STATE'}) - } -}; - -export default connect(mapStateToProps, null)(App); - - -// <div className={"filter-options-active" + getState() === true ? '' : '-active'}> +export default connect(mapStateToProps, {filterState})(App); diff --git a/client/src/actions/index.js b/client/src/actions/index.js deleted file mode 100644 index 5a24289..0000000 --- a/client/src/actions/index.js +++ /dev/null @@ -1,69 +0,0 @@ - -export const sortYear = order => ({ - type: 'SORT_YEAR', - order -}); - -export const sortBoxOffice = order => ({ - type: 'SORT_BOX_OFFICE', - order -}); - -export const sortUserRating = order => ({ - type: 'SORT_USER_RATING', - order -}); - -export const sortIMDB = order => ({ - type: 'SORT_IMDB', - order -}); - - -export const FILTER_STATE = 'FILTER_STATE'; - -export const filterState = (bool) => ({ - type: FILTER_STATE, - bool: bool -}); - -export const FILTER_RATED = 'FILTER_RATED'; -// params::chosen - a list of booleans corresponding to the following ratings: -// G - General audiences -// PG - Parental Guidance Suggested -// PG-13 - Parents Strongly Cautioned -// R - Restricted -// NC-17 - Adults Only -export const filterRated = (chosen) => ({ - type: FILTER_RATED, - chosen -}); - -export const FILTER_COUNTRY = 'FILTER_COUNTRY'; - -export const filterCountry = (chosen) => ({ - type: FILTER_COUNTRY, - chosen: chosen -}); - -export const FILTER_LANGUAGE = 'FILTER_LANGUAGE'; - -export const filterLanguage = (chosen) => ({ - type: FILTER_LANGUAGE, - chosen: chosen -}); - -export const FILTER_IMDB = 'FILTER_IMDB'; - -export const filterIMDB = (from, to) => ({ - type: FILTER_IMDB, - from: from, - to: to -}); - -export const CHOSEN_MOVIE = 'CHOSEN_MOVIE'; - -export const chosenMovie = (id) => ({ - type: CHOSEN_MOVIE, - id: id -}); diff --git a/client/src/components/Filter.css b/client/src/components/Filter.css new file mode 100644 index 0000000..8a90fa3 --- /dev/null +++ b/client/src/components/Filter.css @@ -0,0 +1,7 @@ + + + +.col-class { + overflow-y: scroll; + height: 40vh; +} diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js index 9c69073..1a6e112 100644 --- a/client/src/components/Filter.js +++ b/client/src/components/Filter.js @@ -1,199 +1,137 @@ -import React, { Component } from 'react'; -import { Table, Form, Row, Col} from 'react-bootstrap'; +import React, { useState, useEffect } from 'react'; +import { Table, Form, Row, Col, Button} from 'react-bootstrap'; import { connect } from 'react-redux'; -import store from '../store/store' -import { filterRated, filterCountry, filterLanguage, filterIMDB } from '../actions'; +import store from '../redux/store' +import { filterRated, filterGenre, filterLanguage } from '../redux/actions'; +const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { -function mapDispatchToProps(dispatch) { - return { - filterRated: chosen => dispatch(filterRated(chosen)), - filterCountry: chosen => dispatch(filterCountry(chosen)), - filterLanguage: chosen => dispatch(filterLanguage(chosen)), - filterIMDB: chosen => dispatch(filterIMDB(chosen)) - }; -} +// TODO: If we have the time, consider imporoving the checkboxes with an select/ +// deselect all button. + // const [PGClear, setPGClear] = useState(false); + // <Button onClick={ () => setPGClear(!PGClear)}>Unselect/select all</Button> + +// TODO: make constants instead of fuckings hard coded searchInput +// <Form.Check type={'checkbox'} onClick={ () =>filterRated(FILTER.PG[0])} id={"PG1"} label={"G"}/> -export function Filter (dispatch) { - // console.log('Filter_store',store.getState()); - // let filter = store.getState() - // let rating = filter.filterReducer[0].filter_rated; - let rating = [true, false, false, false, true, false, false, false] - // let chosenPG = filter.filterReducer[0].filter_rated; - // console.log('store.getState().filter.filterReducer[0].filter', rating); - // onChange={filterRated(chosenPG)} return ( <Form as={Row}> - <Form.Group as={Col}> + <Form.Group as={Col}> + <Form.Label column sm="15"> + <b>PG-Rating</b> + </Form.Label> + <Col sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("G")} id={"PG1"} label={"G"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("PG")} id={"PG2"} label={"PG"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("PG-13")} id={"PG3"} label={"PG-13"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("R")} id={"PG4"} label={"R"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("NC-17")} id={"PG5"} label={"NC-17"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("Not Rated")} id={"PG6"} label={"Not Rated"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("Passed")} id={"PG7"} label={"Passed"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("Approved")} id={"PG8"} label={"Approved"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> + <Form.Label column sm="15"> + <b>Genre</b> + </Form.Label> + <Col className="col-class" sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Action")} id={"G1"} label={"Action"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Animation")} id={"G2"} label={"Animation"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Biography")} id={"G3"} label={"Biography"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Comedy")} id={"G4"} label={"Comedy"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Crime")} id={"G5"} label={"Crime"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Drama")} id={"G6"} label={"Drama"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Family")} id={"G8"} label={"Family"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Fantasy")} id={"G9"} label={"Fantasy"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Film-Noir")} id={"G10"} label={"Film-Noir"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Horror")} id={"G11"} label={"Horror"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("History")} id={"G12"} label={"History"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Musical")} id={"G13"} label={"Musical"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Mystery")} id={"G14"} label={"Mystery"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Romance")} id={"G15"} label={"Romance"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Sci-Fi")} id={"G16"} label={"Sci-Fi"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Short")} id={"G17"} label={"Short"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Sport")} id={"G18"} label={"Sport"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Thriller")} id={"G19"} label={"Thriller"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("War")} id={"G20"} label={"War"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Western")} id={"G21"} label={"Western"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> <Form.Label column sm="15"> - Rating + <b>Language</b> </Form.Label> - <Col sm="15"> - <Form.Check type={'checkbox'} defaultChecked={rating[0]} id={"PG1"} label={"G"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[1]} id={"PG2"} label={"PG"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[2]} id={"PG3"} label={"PG-13"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[3]} id={"PG4"} label={"R"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[4]} id={"PG5"} label={"NC-17"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[5]} id={"PG6"} label={"Not Rated"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[6]} id={"PG7"} label={"Passed"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[7]} id={"PG8"} label={"Approved"}/> - </Col> - </Form.Group> - <Form.Group as={Col}> - <Form.Label column sm="15"> - Language - </Form.Label> - <Col sm="15"> - <Form.Check type={'checkbox'} defaultChecked={rating[1]} id={"PG2"} label={"PG"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[2]} id={"PG3"} label={"PG-13"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[3]} id={"PG4"} label={"R"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[4]} id={"PG5"} label={"NC-17"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[5]} id={"PG6"} label={"Not Rated"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[6]} id={"PG7"} label={"Passed"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[7]} id={"PG8"} label={"Approved"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[1]} id={"PG2"} label={"PG"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[2]} id={"PG3"} label={"PG-13"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[3]} id={"PG4"} label={"R"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[4]} id={"PG5"} label={"NC-17"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[5]} id={"PG6"} label={"Not Rated"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[6]} id={"PG7"} label={"Passed"}/> - <Form.Check type={'checkbox'} defaultChecked={rating[7]} id={"PG8"} label={"Approved"}/> - </Col> - </Form.Group> - <Form.Group as={Col}> + <Col className="col-class" sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Arabic")} id={"L1"} label={"Arabic"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("American Sign Language")} id={"L2"} label={"American Sign Language"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Belarusian")} id={"L3"} label={"Belarusian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Cantonese")} id={"L4"} label={"Cantonese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Chinese")} id={"L5"} label={"Chinese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Czech")} id={"L6"} label={"Czech"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Danish")} id={"L7"} label={"Danish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("English")} id={"L8"} label={"English"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Esperanto")} id={"L9"} label={"Esperanto"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("French")} id={"L10"} label={"French"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("German")} id={"L11"} label={"German"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Greek")} id={"L12"} label={"Greek"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hebrew")} id={"L13"} label={"Hebrew"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hindi")} id={"L14"} label={"Hindi"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hmong")} id={"L15"} label={"Hmong"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hungarian")} id={"L16"} label={"Hungarian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Italian")} id={"L17"} label={"Italian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Japanese")} id={"L18"} label={"Japanese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Korean")} id={"L19"} label={"Korean"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Kurdish")} id={"L20"} label={"Kurdish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Latin")} id={"L21"} label={"Latin"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Mandarin")} id={"L22"} label={"Mandarin"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Nepali")} id={"L23"} label={"Nepali"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Norwegian")} id={"L24"} label={"Norwegian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("North American Indian")} id={"L25"} label={"North American Indian"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> <Form.Label column sm="15"> - IMDB-Rating </Form.Label> - <Col sm="15"> - <p> - TODO: IMDBRating fields (and/or sliders) - </p> - </Col> - </Form.Group> + <Col className="col-class" sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Old English")} id={"L26"} label={"Old English"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Persian")} id={"L27"} label={"Persian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Polish")} id={"L28"} label={"Polish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Portuguese")} id={"L29"} label={"Portuguese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Punjabi")} id={"L30"} label={"Punjabi"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Quenya")} id={"L31"} label={"Quenya"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Russian")} id={"L32"} label={"Russian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Scottish Gaelic")} id={"L33"} label={"Scottish Gaelic"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Shanghainese")} id={"L34"} label={"Shanghainese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Sicilian")} id={"L35"} label={"Sicilian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Sindarin")} id={"L36"} label={"Sindarin"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Spanish")} id={"L37"} label={"Spanish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Swedish")} id={"L38"} label={"Swedish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Swahili")} id={"L39"} label={"Swahili"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Tamil")} id={"L40"} label={"Tamil"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Telugu")} id={"L41"} label={"Telugu"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Thai")} id={"L42"} label={"Thai"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Turkish")} id={"L43"} label={"Turkish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Urdu")} id={"L44"} label={"Urdu"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Vietnamese")} id={"L45"} label={"Vietnamese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Xhosa")} id={"L46"} label={"Xhosa"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Yiddish")} id={"L47"} label={"Yiddish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Zulu")} id={"L48"} label={"Zulu"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("None")} id={"L49"} label={"None (sunset boulevard)"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("N/A")} id={"L50"} label={"N/A (your name)"}/> + </Col> + </Form.Group> </Form> ); }; -// function setPG(){} - -// function updateRatedFilter(){ -// -// } - -function mapStateToProps(state){ - const filter = state - return { filter } -} - -export default connect(mapStateToProps , mapDispatchToProps)(Filter) - - - - +// const mapStateToProps = state => { + // console.log(state.filterReducer.filter_rated); + // console.log(state.filterReducer.filter_genre); + // console.log(state.filterReducer.filter_language); +// return { PGActive : state.filterReducer.filter_rated, LanguageActive : state.filterReducer.filter_language , GenreActive : state.filterReducer.filter_genre}} -// -// <ButtonToolbar> -// {[DropdownButton].map((DropdownType, idx) => ( -// <DropdownType -// variant="secondary" -// drop="down" -// size="lg" -// title="Filter" -// id={`dropdown-button-drop-${idx}`} -// key={idx} -// > -// <Container> -// <Row> -// <Col> -// <Dropdown.Item className="filter_category" eventKey="1"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="2"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="3"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="4"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="5"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// </Col> -// <Col> -// <Dropdown.Item className="filter_category" eventKey="1"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="2"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="3"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="4"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// <Dropdown.Item className="filter_category" eventKey="5"> -// <Form.Check type="checkbox" label="Check me out" /> -// </Dropdown.Item> -// </Col> -// </Row> -// </Container> -// </DropdownType> -// ))} -// </ButtonToolbar> -// -// -// <Table className="table-contents" bordered hover > -// <thead> -// <tr> -// <th>Rated</th> -// <th>Country</th> -// <th>Language</th> -// <th colSpan="2">IMDB Rating</th> -// </tr> -// </thead> -// <tbody> -// <tr> -// <td><Form.Check type="checkbox" label="G - General audiences" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td> -// From: -// <Form.Control className="filter-from-rating" size="md" type="number" placeholder="0"/> -// </td> -// <td> -// To: -// <Form.Control className="filter-to-rating" size="md" type="number" placeholder="100"/> -// </td> -// </tr> -// <tr> -// <td><Form.Check type="checkbox" label="PG - Parental Guidance Suggested" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// </tr> -// <tr> -// <td><Form.Check type="checkbox" label="PG-13 - Parents Strongly Cautioned" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td> <Form.Check type="checkbox" label="Check me out" /></td> -// </tr> -// <tr> -// <td><Form.Check type="checkbox" label="R - Restricted" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td> <Form.Check type="checkbox" label="Check me out" /></td> -// </tr> -// <tr> -// <td><Form.Check type="checkbox" label="NC-17 - Adults Only" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td><Form.Check type="checkbox" label="Check me out" /></td> -// <td> <Form.Check type="checkbox" label="Check me out" /></td> -// </tr> -// </tbody> -// </Table> +export default connect(null, { filterRated, filterLanguage, filterGenre} )(Filter) diff --git a/client/src/index.js b/client/src/index.js index 4ee8b4c..93fc5d5 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -4,11 +4,11 @@ import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import { Provider } from 'react-redux'; -import store from './store/store'; +import store from './redux/store'; ReactDOM.render( <Provider store={store}> - <App /> + <App /> </Provider>, document.getElementById('root') ); diff --git a/client/src/reducers/filter.js b/client/src/reducers/filter.js deleted file mode 100644 index ba58c8a..0000000 --- a/client/src/reducers/filter.js +++ /dev/null @@ -1,95 +0,0 @@ -import { FILTER_IMDB, FILTER_RATED, FILTER_COUNTRY, FILTER_LANGUAGE, FILTER_STATE} from '../actions'; - -const initialState = { - filter_state : false, - filter_rated : [true, true, true, true, false, false, false, false] -} - - -// [ -// {"filter_rated": [ -// {"G":true}, -// {"PG":true}, -// {"PG-13":true}, -// {"R":false}, -// {"NC-17":false}, -// {"Not Rated":false}, -// {"Passed":false}, -// {"Approved":false} -// ] -// }, -// {"filter_language": [ -// {"English":false}, -// {"Japanese":false}, -// {"Malaysian":false}, -// {"Pakistani":false}, -// {"Finnish":false}, -// {"Swedish":false}, -// {"Danish":false} , -// {"Norwegian":false}, -// {"Mandarin":false}, -// {"Hindi":false}, -// {"Urdu":false} -// ] -// }, -// {"filter_imdb": [ -// {"from" : 0}, {"to" : 10} -// ] -// }, -// {"filter_state": false} -// ]; - -// TODO: find the number of different ratings, languages and countries -// and order them. find somewhere to save the orderings. -// implement the filter function. - - - -function filterReducer(state = initialState, action) { - switch(action.type){ - case FILTER_RATED: - return{ - ...state, filter_rated: action.chosen - }; - case FILTER_LANGUAGE: - const key3 = "filter_language" - return{ - ...state, //copy state - filter: { - ...state.filter, //copy filter - [key3]: { //update one spesific filter - ...state.filter.key3, //copy that spesific filters properties - key3: action.chosen //set the - // values to be the new selected true/false for this filter - } - } - - }; - case FILTER_IMDB: - const key4 = "filter_imdb" - return{ - ...state, //copy state - filter: { - ...state.filter, //copy filter - filter_imdb: { //update one spesific filter - ...state.filter.filter_imdb, //copy that spesific filters properties - filter_imdb: [{"from" : action.from}, {"to" : action.to}] //set the - // values to be the new selected from/to values for this filter - } - } - }; - case FILTER_STATE: - const key5 = "filter_state" - // console.log(action.bool == (true || false) ? action.bool : 'bool is not defined'); - return{ - ...state, filter_state: action.bool - }; - - default: - return state; - -}; -} - - -export default filterReducer; diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js new file mode 100644 index 0000000..01aad7d --- /dev/null +++ b/client/src/redux/actions.js @@ -0,0 +1,61 @@ + +export const sortYear = order => ({ + type: 'SORT_YEAR', + order +}); + +export const sortBoxOffice = order => ({ + type: 'SORT_BOX_OFFICE', + order +}); + +export const sortUserRating = order => ({ + type: 'SORT_USER_RATING', + order +}); + +export const sortIMDB = order => ({ + type: 'SORT_IMDB', + order +}); + + +export const FILTER_STATE = 'FILTER_STATE'; +// TODO: move logic for state to reducer +export const filterState = (bool) => { + let new_bool = !bool; + return ({ + type: FILTER_STATE, + bool: new_bool +})} + +export const FILTER_RATED = 'FILTER_RATED'; + +export const filterRated = (rating) => { + return({ + type: FILTER_RATED, + rating: rating + })}; + +export const FILTER_GENRE = 'FILTER_GENRE'; + +export const filterGenre = (genre) => { + return ({ + type: FILTER_GENRE, + genre: genre +})}; + +export const FILTER_LANGUAGE = 'FILTER_LANGUAGE'; + +export const filterLanguage = (language) => { + return ({ + type: FILTER_LANGUAGE, + language: language +})}; + +export const CURRENT_MOVIE = 'CURRENT_MOVIE'; + +export const currentMovie = (title) => ({ + type: CURRENT_MOVIE, + title: title +}); diff --git a/client/src/redux/reducers/current.js b/client/src/redux/reducers/current.js new file mode 100644 index 0000000..5371daa --- /dev/null +++ b/client/src/redux/reducers/current.js @@ -0,0 +1,17 @@ +import { CURRENT_MOVIE } from '../actions'; + +const initialState = { + current_title : "" +} + +function currentReducer(state = initialState, action) { + if (action.type === CURRENT_MOVIE) { + return{ + ...state, current_title: action.title + }; + } else { + return state; + } +} + +export default currentReducer; diff --git a/client/src/redux/reducers/filter.js b/client/src/redux/reducers/filter.js new file mode 100644 index 0000000..d10a83e --- /dev/null +++ b/client/src/redux/reducers/filter.js @@ -0,0 +1,58 @@ +import { FILTER_IMDB, FILTER_RATED, FILTER_GENRE, FILTER_LANGUAGE, FILTER_STATE} from '../actions'; + + +// Set initialState for the filter +const initialState = { + filter_state : false, + filter_rated : [], + filter_language: [], + filter_genre:[] +} +// TODO: find the number of different ratings, languages and countries +// and order them. find somewhere to save the orderings. +// implement the filter function. + +function filterReducer(state = initialState, action) { + switch(action.type){ + case FILTER_RATED: + if (state.filter_rated.includes(action.rating)){ + let rated = state.filter_rated; + rated.splice(rated.indexOf(action.rating), 1); + return ({...state, filter_rated : rated}); + }else { + let rated = state.filter_rated; + rated.push(action.rating); + return ({...state, filter_rated : rated}); + } + case FILTER_LANGUAGE: + if (state.filter_language.includes(action.language)){ + let language = state.filter_language; + language.splice(language.indexOf(action.language), 1); + return ({...state, filter_language : language}); + }else { + let language = state.filter_language; + language.push(action.language); + return ({...state, filter_language : language}); + } + case FILTER_GENRE: + if (state.filter_genre.includes(action.genre)){ + let genre = state.filter_genre; + genre.splice(genre.indexOf(action.genre), 1); + return ({...state, filter_genre : genre}); + }else { + let genre = state.filter_genre; + genre.push(action.genre); + return ({...state, filter_genre : genre}); + } + case FILTER_STATE: + return{ + ...state, filter_state: action.bool + }; + default: + return state; + +}; +} + + +export default filterReducer; diff --git a/client/src/reducers/index.js b/client/src/redux/reducers/index.js similarity index 69% rename from client/src/reducers/index.js rename to client/src/redux/reducers/index.js index 81ddb71..9fbdaaf 100644 --- a/client/src/reducers/index.js +++ b/client/src/redux/reducers/index.js @@ -1,8 +1,10 @@ import {combineReducers } from 'redux'; import sortReducer from './sort'; import filterReducer from './filter' +import currentReducer from './current' export default combineReducers({ sortReducer, - filterReducer + filterReducer, + currentReducer }) diff --git a/client/src/reducers/sort.js b/client/src/redux/reducers/sort.js similarity index 100% rename from client/src/reducers/sort.js rename to client/src/redux/reducers/sort.js diff --git a/client/src/store/store.js b/client/src/redux/store.js similarity index 67% rename from client/src/store/store.js rename to client/src/redux/store.js index 2236f31..dd23eff 100644 --- a/client/src/store/store.js +++ b/client/src/redux/store.js @@ -1,5 +1,5 @@ import { createStore } from 'redux'; -import rootReducer from '../reducers'; +import rootReducer from './reducers'; export default createStore(rootReducer); -- GitLab From 67f06094b787b480bd0a5560fa3ad358bcacc8a0 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 17 Oct 2019 15:11:18 +0200 Subject: [PATCH 27/61] Merge branches --- .gitignore | 1 + client/src/components/Filter.js | 137 +++++++++++++++++++++++++++ client/src/redux/reducers/current.js | 17 ++++ client/src/redux/reducers/filter.js | 58 ++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 client/src/components/Filter.js create mode 100644 client/src/redux/reducers/current.js create mode 100644 client/src/redux/reducers/filter.js diff --git a/.gitignore b/.gitignore index 175dd57..e9785a2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ .env.development.local .env.test.local .env.production.local +/client/src/List.txt npm-debug.log* yarn-debug.log* diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js new file mode 100644 index 0000000..1a6e112 --- /dev/null +++ b/client/src/components/Filter.js @@ -0,0 +1,137 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Form, Row, Col, Button} from 'react-bootstrap'; +import { connect } from 'react-redux'; +import store from '../redux/store' +import { filterRated, filterGenre, filterLanguage } from '../redux/actions'; + + +const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { + +// TODO: If we have the time, consider imporoving the checkboxes with an select/ +// deselect all button. + // const [PGClear, setPGClear] = useState(false); + // <Button onClick={ () => setPGClear(!PGClear)}>Unselect/select all</Button> + +// TODO: make constants instead of fuckings hard coded searchInput +// <Form.Check type={'checkbox'} onClick={ () =>filterRated(FILTER.PG[0])} id={"PG1"} label={"G"}/> + + + return ( + <Form as={Row}> + <Form.Group as={Col}> + <Form.Label column sm="15"> + <b>PG-Rating</b> + </Form.Label> + <Col sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("G")} id={"PG1"} label={"G"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("PG")} id={"PG2"} label={"PG"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("PG-13")} id={"PG3"} label={"PG-13"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("R")} id={"PG4"} label={"R"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("NC-17")} id={"PG5"} label={"NC-17"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("Not Rated")} id={"PG6"} label={"Not Rated"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("Passed")} id={"PG7"} label={"Passed"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterRated("Approved")} id={"PG8"} label={"Approved"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> + <Form.Label column sm="15"> + <b>Genre</b> + </Form.Label> + <Col className="col-class" sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Action")} id={"G1"} label={"Action"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Animation")} id={"G2"} label={"Animation"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Biography")} id={"G3"} label={"Biography"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Comedy")} id={"G4"} label={"Comedy"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Crime")} id={"G5"} label={"Crime"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Drama")} id={"G6"} label={"Drama"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Family")} id={"G8"} label={"Family"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Fantasy")} id={"G9"} label={"Fantasy"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Film-Noir")} id={"G10"} label={"Film-Noir"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Horror")} id={"G11"} label={"Horror"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("History")} id={"G12"} label={"History"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Musical")} id={"G13"} label={"Musical"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Mystery")} id={"G14"} label={"Mystery"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Romance")} id={"G15"} label={"Romance"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Sci-Fi")} id={"G16"} label={"Sci-Fi"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Short")} id={"G17"} label={"Short"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Sport")} id={"G18"} label={"Sport"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Thriller")} id={"G19"} label={"Thriller"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("War")} id={"G20"} label={"War"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Western")} id={"G21"} label={"Western"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> + <Form.Label column sm="15"> + <b>Language</b> + </Form.Label> + <Col className="col-class" sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Arabic")} id={"L1"} label={"Arabic"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("American Sign Language")} id={"L2"} label={"American Sign Language"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Belarusian")} id={"L3"} label={"Belarusian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Cantonese")} id={"L4"} label={"Cantonese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Chinese")} id={"L5"} label={"Chinese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Czech")} id={"L6"} label={"Czech"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Danish")} id={"L7"} label={"Danish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("English")} id={"L8"} label={"English"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Esperanto")} id={"L9"} label={"Esperanto"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("French")} id={"L10"} label={"French"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("German")} id={"L11"} label={"German"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Greek")} id={"L12"} label={"Greek"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hebrew")} id={"L13"} label={"Hebrew"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hindi")} id={"L14"} label={"Hindi"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hmong")} id={"L15"} label={"Hmong"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hungarian")} id={"L16"} label={"Hungarian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Italian")} id={"L17"} label={"Italian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Japanese")} id={"L18"} label={"Japanese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Korean")} id={"L19"} label={"Korean"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Kurdish")} id={"L20"} label={"Kurdish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Latin")} id={"L21"} label={"Latin"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Mandarin")} id={"L22"} label={"Mandarin"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Nepali")} id={"L23"} label={"Nepali"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Norwegian")} id={"L24"} label={"Norwegian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("North American Indian")} id={"L25"} label={"North American Indian"}/> + </Col> + </Form.Group> + <Form.Group as={Col}> + <Form.Label column sm="15"> + </Form.Label> + <Col className="col-class" sm="15"> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Old English")} id={"L26"} label={"Old English"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Persian")} id={"L27"} label={"Persian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Polish")} id={"L28"} label={"Polish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Portuguese")} id={"L29"} label={"Portuguese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Punjabi")} id={"L30"} label={"Punjabi"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Quenya")} id={"L31"} label={"Quenya"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Russian")} id={"L32"} label={"Russian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Scottish Gaelic")} id={"L33"} label={"Scottish Gaelic"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Shanghainese")} id={"L34"} label={"Shanghainese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Sicilian")} id={"L35"} label={"Sicilian"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Sindarin")} id={"L36"} label={"Sindarin"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Spanish")} id={"L37"} label={"Spanish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Swedish")} id={"L38"} label={"Swedish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Swahili")} id={"L39"} label={"Swahili"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Tamil")} id={"L40"} label={"Tamil"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Telugu")} id={"L41"} label={"Telugu"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Thai")} id={"L42"} label={"Thai"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Turkish")} id={"L43"} label={"Turkish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Urdu")} id={"L44"} label={"Urdu"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Vietnamese")} id={"L45"} label={"Vietnamese"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Xhosa")} id={"L46"} label={"Xhosa"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Yiddish")} id={"L47"} label={"Yiddish"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Zulu")} id={"L48"} label={"Zulu"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("None")} id={"L49"} label={"None (sunset boulevard)"}/> + <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("N/A")} id={"L50"} label={"N/A (your name)"}/> + </Col> + </Form.Group> + </Form> + ); +}; + +// const mapStateToProps = state => { + // console.log(state.filterReducer.filter_rated); + // console.log(state.filterReducer.filter_genre); + // console.log(state.filterReducer.filter_language); +// return { PGActive : state.filterReducer.filter_rated, LanguageActive : state.filterReducer.filter_language , GenreActive : state.filterReducer.filter_genre}} + + +export default connect(null, { filterRated, filterLanguage, filterGenre} )(Filter) diff --git a/client/src/redux/reducers/current.js b/client/src/redux/reducers/current.js new file mode 100644 index 0000000..5371daa --- /dev/null +++ b/client/src/redux/reducers/current.js @@ -0,0 +1,17 @@ +import { CURRENT_MOVIE } from '../actions'; + +const initialState = { + current_title : "" +} + +function currentReducer(state = initialState, action) { + if (action.type === CURRENT_MOVIE) { + return{ + ...state, current_title: action.title + }; + } else { + return state; + } +} + +export default currentReducer; diff --git a/client/src/redux/reducers/filter.js b/client/src/redux/reducers/filter.js new file mode 100644 index 0000000..d10a83e --- /dev/null +++ b/client/src/redux/reducers/filter.js @@ -0,0 +1,58 @@ +import { FILTER_IMDB, FILTER_RATED, FILTER_GENRE, FILTER_LANGUAGE, FILTER_STATE} from '../actions'; + + +// Set initialState for the filter +const initialState = { + filter_state : false, + filter_rated : [], + filter_language: [], + filter_genre:[] +} +// TODO: find the number of different ratings, languages and countries +// and order them. find somewhere to save the orderings. +// implement the filter function. + +function filterReducer(state = initialState, action) { + switch(action.type){ + case FILTER_RATED: + if (state.filter_rated.includes(action.rating)){ + let rated = state.filter_rated; + rated.splice(rated.indexOf(action.rating), 1); + return ({...state, filter_rated : rated}); + }else { + let rated = state.filter_rated; + rated.push(action.rating); + return ({...state, filter_rated : rated}); + } + case FILTER_LANGUAGE: + if (state.filter_language.includes(action.language)){ + let language = state.filter_language; + language.splice(language.indexOf(action.language), 1); + return ({...state, filter_language : language}); + }else { + let language = state.filter_language; + language.push(action.language); + return ({...state, filter_language : language}); + } + case FILTER_GENRE: + if (state.filter_genre.includes(action.genre)){ + let genre = state.filter_genre; + genre.splice(genre.indexOf(action.genre), 1); + return ({...state, filter_genre : genre}); + }else { + let genre = state.filter_genre; + genre.push(action.genre); + return ({...state, filter_genre : genre}); + } + case FILTER_STATE: + return{ + ...state, filter_state: action.bool + }; + default: + return state; + +}; +} + + +export default filterReducer; -- GitLab From c98a03a07ee9df30ddcc818e786ffc4cbf887c58 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 17 Oct 2019 15:26:40 +0200 Subject: [PATCH 28/61] Fix problems with merge --- client/src/components/SortButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js index 8854ae3..c4580d8 100644 --- a/client/src/components/SortButton.js +++ b/client/src/components/SortButton.js @@ -12,7 +12,7 @@ const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder }) => { <div> <Dropdown > <Dropdown.Toggle className="sort-button" variant="outline-secondary" id="dropdown-basic"> - {activeSort} + {/*activeSort*/} </Dropdown.Toggle> <Dropdown.Menu className="sort-button-drop"> <Dropdown.Item onClick={() => {selectSort(SORT.TITLE)}}>{SORT.TITLE}</Dropdown.Item> -- GitLab From 44de546739ca2abb32dd754c72b2ac420065ab16 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 17 Oct 2019 15:45:52 +0200 Subject: [PATCH 29/61] Last fix after merge problems --- client/src/App.js | 60 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/client/src/App.js b/client/src/App.js index 0e25b1e..1392e83 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -2,13 +2,15 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import axios from 'axios'; import './App.css'; -import {ButtonToolbar, Button, ListGroup, Form, Accordion} from 'react-bootstrap'; +import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-bootstrap'; +import { Chart } from 'react-google-charts'; +import InterestOverTime from './components/InterestOverTime'; +import MovieDetail from './components/MovieDetail'; import { filterState } from './redux/actions'; import store from './redux/store'; import Filter from './components/Filter'; import SortButton from './components/SortButton' - const logo = require('./images/logo.svg'); @@ -18,17 +20,53 @@ class App extends Component { this.state = { movies: [], - error: "" + error: "", + currentMovie: "The Godfather" } -} + + this.sort = false; + this.filter = false; + + this.searchString = ""; + + this.getMovies = this.getMovies.bind(this); + } componentDidMount() { - this.getMovies("", 0, 20); + /* + axios.get('/api/MovieTrend', { params: { Title: 'The Godfather' }}) + .then(res => { + let data = JSON.parse(res.data.data); + + let tempTitles = ['Date', 'Interest']; + let tempData = []; + tempData.push(tempTitles); + + data.default.timelineData.map(time => { + let tempValues = []; + tempValues.push(time.formattedTime); + tempValues.push(Number(time.formattedValue[0])); + tempData.push(tempValues); + }); + + this.setState({ chartData: tempData }); + }); + */ } getMovies = (searchInput, fromIndex, toIndex) => { - axios.get('/api/GetMovies', { params: { searchString: searchInput, from: fromIndex, to: toIndex }}) - .then((res) => { + axios.get('/api/GetMovieTitles', { + params: { + searchString: searchInput, + from: fromIndex, + to: toIndex, + sort: this.sort, + sortType: 'SORT_IMDB', + sortOrder: 'DESC', + filter: this.filter, + filters: '[{ "Country": "India" }]' + } + }).then((res) => { if (res.data.success) { this.setState({ movies: res.data.data }); } else { @@ -61,15 +99,13 @@ class App extends Component { <Filter/> </div> <div className="main-content"> - <ListGroup> + <Accordion> {this.state.movies.map((movie, index) => { return( - <ListGroup.Item key={index} className="list-group-item" name={movie.Title} onClick={e => console.log("click ", e.target.attributes["name"])}> - {movie.Title} -- {movie.imdbRating} - </ListGroup.Item> + <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} index={index} /> ) })} - </ListGroup> + </Accordion> </div> </div> ) -- GitLab From b42842bbe6156d208b4c3876afd41fce4324b0f8 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 17 Oct 2019 15:48:14 +0200 Subject: [PATCH 30/61] Tweak sort after merge --- client/src/redux/reducers/sort.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/client/src/redux/reducers/sort.js b/client/src/redux/reducers/sort.js index c5bbed0..75467e6 100644 --- a/client/src/redux/reducers/sort.js +++ b/client/src/redux/reducers/sort.js @@ -1,4 +1,17 @@ -const sort = (order = '', action = '') => { - return({ type: '', order: '' }); +import { SELECT_SORT } from "../actions"; +import { SORT } from "../../constants"; + +const initialState = SORT.TITLE; + +const sort = (state = initialState, action) => { + switch (action.type) { + case SELECT_SORT: { + return action.payload.sortType; + } + default: { + return state; + } + } } + export default sort; -- GitLab From aefe35842282d67e176964fe73115c2242859cc1 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 17 Oct 2019 15:54:36 +0200 Subject: [PATCH 31/61] Fix redux bug in sort button Ref. #23 --- client/src/components/SortButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js index c4580d8..8854ae3 100644 --- a/client/src/components/SortButton.js +++ b/client/src/components/SortButton.js @@ -12,7 +12,7 @@ const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder }) => { <div> <Dropdown > <Dropdown.Toggle className="sort-button" variant="outline-secondary" id="dropdown-basic"> - {/*activeSort*/} + {activeSort} </Dropdown.Toggle> <Dropdown.Menu className="sort-button-drop"> <Dropdown.Item onClick={() => {selectSort(SORT.TITLE)}}>{SORT.TITLE}</Dropdown.Item> -- GitLab From 7ff5c4e91303740da0381ce5f1dfe7d74b6d5b79 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 17 Oct 2019 21:06:20 +0200 Subject: [PATCH 32/61] Redux partially implemented. Need help from Sigurd --- client/src/App.js | 8 +++--- client/src/components/InterestOverTime.js | 11 ++++++++- client/src/components/MovieDetail.js | 30 ++++++++++++++++++----- client/src/components/MovieDetailBody.js | 20 ++++++++++----- client/src/redux/actions.js | 11 ++++++--- client/src/redux/reducers/current.js | 4 ++- server/routes/movies.js | 14 +++++++++++ 7 files changed, 77 insertions(+), 21 deletions(-) diff --git a/client/src/App.js b/client/src/App.js index 1392e83..d1d4d7b 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -6,7 +6,7 @@ import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-boo import { Chart } from 'react-google-charts'; import InterestOverTime from './components/InterestOverTime'; import MovieDetail from './components/MovieDetail'; -import { filterState } from './redux/actions'; +import { filterState, currentMovie } from './redux/actions'; import store from './redux/store'; import Filter from './components/Filter'; import SortButton from './components/SortButton' @@ -55,6 +55,8 @@ class App extends Component { } getMovies = (searchInput, fromIndex, toIndex) => { + //this.props.currentMovie(searchInput); + axios.get('/api/GetMovieTitles', { params: { searchString: searchInput, @@ -102,7 +104,7 @@ class App extends Component { <Accordion> {this.state.movies.map((movie, index) => { return( - <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} index={index} /> + <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} /> ) })} </Accordion> @@ -116,4 +118,4 @@ const mapStateToProps = state => { return { filter_state : state.filterReducer.filter_state} }; -export default connect(mapStateToProps, {filterState})(App); +export default connect(mapStateToProps, {filterState, currentMovie})(App); diff --git a/client/src/components/InterestOverTime.js b/client/src/components/InterestOverTime.js index 93a62e7..2f02fcf 100644 --- a/client/src/components/InterestOverTime.js +++ b/client/src/components/InterestOverTime.js @@ -1,5 +1,10 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; + +import store from '../redux/store'; +import { currentMovie } from '../redux/actions'; +import { connect } from 'react-redux'; + import { Chart } from 'react-google-charts'; /** @@ -41,4 +46,8 @@ const InterestOverTime = ({ title }) => { ) } -export default InterestOverTime; +const mapStateToProps = state => { + return { title: state.currentReducer.current_title } +} + +export default connect(mapStateToProps, { currentMovie })(InterestOverTime); diff --git a/client/src/components/MovieDetail.js b/client/src/components/MovieDetail.js index 61ffc44..3b8e7ff 100644 --- a/client/src/components/MovieDetail.js +++ b/client/src/components/MovieDetail.js @@ -2,6 +2,10 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { Accordion, Card } from 'react-bootstrap'; +import store from '../redux/store'; +import { currentMovie } from '../redux/actions'; +import { connect } from 'react-redux'; + import MovieDetailBody from './MovieDetailBody'; import './MovieDetail.css'; @@ -10,16 +14,16 @@ import './MovieDetail.css'; * Renders one search result with movie title and average user rating in header. * Uses Bootstraps Accordion to get nice animations. */ -const MovieDetail = ({ title }) => { +const MovieDetail = ({ currentTitle, title }) => { const [movieDetail, setMovieDetail] = useState(0); const [selected, setSelected] = useState(false); const userGrade = [1, 2, 3, 4, 5]; useEffect(() => { - axios.get('/api/GetMovieDetail', { + axios.get('/api/GetMovieHeaders', { params: { - searchString: title + Title: title } }).then(res => { if (res.data.success) { @@ -28,12 +32,22 @@ const MovieDetail = ({ title }) => { }); }, [title]); + const selectMovie = (title) => { + setSelected(!selected); + + console.log("Before: ", currentTitle) + + currentMovie(title); + + console.log("After: ", currentTitle) + } + return( <Card className="list-group"> <Accordion.Toggle as={Card.Header} eventKey={movieDetail.Title} - onClick={e => setSelected(!selected)} + onClick={e => selectMovie(movieDetail.Title)} className="list-group-item" > <div className="list-group-item-title"> @@ -50,10 +64,14 @@ const MovieDetail = ({ title }) => { </div> </Accordion.Toggle> <Accordion.Collapse eventKey={movieDetail.Title}> - <MovieDetailBody movieDetail={movieDetail} selected={selected} /> + <MovieDetailBody movieDetail={movieDetail} /> </Accordion.Collapse> </Card> ) } -export default MovieDetail; + const mapStateToProps = state => { + return { currentTitle: state.currentReducer.current_title } + } + +export default connect(mapStateToProps, { currentMovie })(MovieDetail); diff --git a/client/src/components/MovieDetailBody.js b/client/src/components/MovieDetailBody.js index d3f0dcb..4b1622e 100644 --- a/client/src/components/MovieDetailBody.js +++ b/client/src/components/MovieDetailBody.js @@ -2,6 +2,10 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { Card } from 'react-bootstrap'; +import store from '../redux/store'; +import { currentMovie } from '../redux/actions'; +import { connect } from 'react-redux'; + import './MovieDetail.css'; import InterestOverTime from './InterestOverTime'; @@ -11,7 +15,7 @@ import InterestOverTime from './InterestOverTime'; * This component also handles user rating input. The user rating * is stored in localStorage of the user. */ -const MovieDetailBody = ({ movieDetail, selected }) => { +const MovieDetailBody = ({ movieDetail, title }) => { const [rating, setRating] = useState(0); const userGrade = [1, 2, 3, 4, 5]; @@ -20,7 +24,7 @@ const MovieDetailBody = ({ movieDetail, selected }) => { function handleRating(rating) { axios.post('/api/UpdateUserRating', null, { params: { - Title: movieDetail.Title, + Title: title, UserRating: rating } }).then(res => { @@ -39,16 +43,16 @@ const MovieDetailBody = ({ movieDetail, selected }) => { // Hook to update the selected user rating useEffect(() => { - let userRate = localStorage.getItem(movieDetail.Title + "-Rating"); + let userRate = localStorage.getItem(title + "-Rating"); if (userRate !== null) { setRating(userRate); } - }, [selected, movieDetail.Title]); + }, [rating]); return( <Card.Body className="movie-details-body"> - {selected ? + {title === movieDetail.Title ? <> <div className="movie-details-body-info"> <div className="movie-details-body-poster"> @@ -105,4 +109,8 @@ const MovieDetailBody = ({ movieDetail, selected }) => { ) } -export default MovieDetailBody; +const mapStateToProps = state => { + return { title: state.currentReducer.current_title } +} + +export default connect(mapStateToProps, { currentMovie })(MovieDetailBody); diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js index d276608..0188d72 100644 --- a/client/src/redux/actions.js +++ b/client/src/redux/actions.js @@ -67,7 +67,10 @@ export const filterLanguage = (language) => { export const CURRENT_MOVIE = 'CURRENT_MOVIE'; -export const currentMovie = (title) => ({ - type: CURRENT_MOVIE, - title: title -}); +export const currentMovie = (title) => { + console.log("CUUUURRRENT: ", title) + return ({ + type: CURRENT_MOVIE, + title: title + }); +} diff --git a/client/src/redux/reducers/current.js b/client/src/redux/reducers/current.js index 5371daa..ae2e8de 100644 --- a/client/src/redux/reducers/current.js +++ b/client/src/redux/reducers/current.js @@ -5,9 +5,11 @@ const initialState = { } function currentReducer(state = initialState, action) { + console.log("REDUCER-STATE: ", state) + console.log("REDUCER-ACTION: ", action) if (action.type === CURRENT_MOVIE) { return{ - ...state, current_title: action.title + ...state, current_title: action.current_title }; } else { return state; diff --git a/server/routes/movies.js b/server/routes/movies.js index f0930d9..84d9d39 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -59,6 +59,20 @@ router.get('/GetMovieTitles', function(req, res) { }); }); +/* + * GET movie headers + * + */ +router.get('/GetMovieHeaders', function(req, res) { + var query = Movies.findOne({ "Title": req.query.Title }, { "Title": 1, "UserRating": 1 }); + + query.exec(function(err, data) { + if (err) return res.json({ success: false, error: err }); + + return res.json({ success: true, data: data }); + }) +}) + /* GET movie details * * Params: { -- GitLab From 916094ba1f7bdf0dc05187662004b71f3faa1402 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 17 Oct 2019 21:48:02 +0200 Subject: [PATCH 33/61] Fix bug with naming of variable/method in redux for MovieDetail.js. Ref. #26 --- client/src/components/MovieDetail.js | 8 ++++---- client/src/redux/reducers/current.js | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/client/src/components/MovieDetail.js b/client/src/components/MovieDetail.js index 3b8e7ff..5cee7b4 100644 --- a/client/src/components/MovieDetail.js +++ b/client/src/components/MovieDetail.js @@ -14,7 +14,7 @@ import './MovieDetail.css'; * Renders one search result with movie title and average user rating in header. * Uses Bootstraps Accordion to get nice animations. */ -const MovieDetail = ({ currentTitle, title }) => { +const MovieDetail = ({ currentMovie, title }) => { const [movieDetail, setMovieDetail] = useState(0); const [selected, setSelected] = useState(false); @@ -35,11 +35,11 @@ const MovieDetail = ({ currentTitle, title }) => { const selectMovie = (title) => { setSelected(!selected); - console.log("Before: ", currentTitle) + console.log("Before: ", currentMovie) currentMovie(title); - console.log("After: ", currentTitle) + console.log("After: ", currentMovie) } return( @@ -71,7 +71,7 @@ const MovieDetail = ({ currentTitle, title }) => { } const mapStateToProps = state => { - return { currentTitle: state.currentReducer.current_title } + return { title_state: state.currentReducer.current_title } } export default connect(mapStateToProps, { currentMovie })(MovieDetail); diff --git a/client/src/redux/reducers/current.js b/client/src/redux/reducers/current.js index ae2e8de..590244d 100644 --- a/client/src/redux/reducers/current.js +++ b/client/src/redux/reducers/current.js @@ -5,11 +5,14 @@ const initialState = { } function currentReducer(state = initialState, action) { - console.log("REDUCER-STATE: ", state) - console.log("REDUCER-ACTION: ", action) + // console.log("currentReducer : state: ", state) + // console.log("currentReducer : action.type:", action.type) + // console.log("currentReducer : action.title: ", action.title) if (action.type === CURRENT_MOVIE) { + console.log("action.type is CURRENT_MOVIE"); + console.log("title to be set as current:", action.title); return{ - ...state, current_title: action.current_title + ...state, current_title: action.title }; } else { return state; -- GitLab From aefebd8c5166474282a19f05181db57c4aefcac3 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Tue, 22 Oct 2019 10:19:37 +0200 Subject: [PATCH 34/61] MovieDetails is now working properly with redux. Ref. #16, #26 --- client/src/components/InterestOverTime.js | 3 +- client/src/components/MovieDetail.js | 11 ++---- client/src/components/MovieDetailBody.js | 41 ++++++++++++++--------- client/src/redux/actions.js | 1 - client/src/redux/reducers/current.js | 5 --- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/client/src/components/InterestOverTime.js b/client/src/components/InterestOverTime.js index 2f02fcf..ff6bea6 100644 --- a/client/src/components/InterestOverTime.js +++ b/client/src/components/InterestOverTime.js @@ -14,7 +14,7 @@ import { Chart } from 'react-google-charts'; */ const InterestOverTime = ({ title }) => { const [chartData, setChartData] = useState(['Date', 'Intereset'], ['Temp', 10]); - const [error, setError] = useState(false); + const [error, setError] = useState(true); // Called each time a new title is passed to the component useEffect(() => { @@ -22,6 +22,7 @@ const InterestOverTime = ({ title }) => { .then(res => { if (res.data.success === true) { setChartData(res.data.data); + setError(false); } else { setError(true); } diff --git a/client/src/components/MovieDetail.js b/client/src/components/MovieDetail.js index 5cee7b4..3963bda 100644 --- a/client/src/components/MovieDetail.js +++ b/client/src/components/MovieDetail.js @@ -14,9 +14,8 @@ import './MovieDetail.css'; * Renders one search result with movie title and average user rating in header. * Uses Bootstraps Accordion to get nice animations. */ -const MovieDetail = ({ currentMovie, title }) => { +const MovieDetail = ({ currentMovie, title, title_state }) => { const [movieDetail, setMovieDetail] = useState(0); - const [selected, setSelected] = useState(false); const userGrade = [1, 2, 3, 4, 5]; @@ -33,13 +32,7 @@ const MovieDetail = ({ currentMovie, title }) => { }, [title]); const selectMovie = (title) => { - setSelected(!selected); - - console.log("Before: ", currentMovie) - currentMovie(title); - - console.log("After: ", currentMovie) } return( @@ -64,7 +57,7 @@ const MovieDetail = ({ currentMovie, title }) => { </div> </Accordion.Toggle> <Accordion.Collapse eventKey={movieDetail.Title}> - <MovieDetailBody movieDetail={movieDetail} /> + {movieDetail.Title === title_state ? <MovieDetailBody /> : <></>} </Accordion.Collapse> </Card> ) diff --git a/client/src/components/MovieDetailBody.js b/client/src/components/MovieDetailBody.js index 4b1622e..690d14d 100644 --- a/client/src/components/MovieDetailBody.js +++ b/client/src/components/MovieDetailBody.js @@ -15,7 +15,8 @@ import InterestOverTime from './InterestOverTime'; * This component also handles user rating input. The user rating * is stored in localStorage of the user. */ -const MovieDetailBody = ({ movieDetail, title }) => { +const MovieDetailBody = ({ title }) => { + const [movieDetail, setMovieDetail] = useState(0); const [rating, setRating] = useState(0); const userGrade = [1, 2, 3, 4, 5]; @@ -28,16 +29,19 @@ const MovieDetailBody = ({ movieDetail, title }) => { UserRating: rating } }).then(res => { - localStorage.setItem(movieDetail.Title + "-Rating", rating); - setRating(rating); + if (res.data.success) { + localStorage.setItem(movieDetail.Title + "-Rating", rating); + setRating(rating); - axios.get('/api/GetMovieDetail', { - params: { - searchString: movieDetail.Title - } - }).then(res => { - movieDetail.UserRating = res.data.data.UserRating; - }) + axios.get('/api/GetMovieDetail', { + params: { + searchString: movieDetail.Title + } + }).then(res => { + if (res.data.success) + movieDetail.UserRating = res.data.data.UserRating; + }); + } }); } @@ -50,16 +54,25 @@ const MovieDetailBody = ({ movieDetail, title }) => { } }, [rating]); + useEffect(() => { + console.log("Calling GetMovieDetail") + axios.get('/api/GetMovieDetail', { + params: { + searchString: title + } + }).then(res => { + setMovieDetail(res.data.data); + }); + }, [title]); + return( <Card.Body className="movie-details-body"> - {title === movieDetail.Title ? - <> <div className="movie-details-body-info"> <div className="movie-details-body-poster"> <img src={movieDetail.Poster} alt="Poster not available" /> </div> <div className="movie-details-body-text"> - <div className="movie-details-body-text-header"><h1>Title:</h1> {movieDetail.Title}</div> + <div className="movie-details-body-text-header"><h1>Title:</h1> {title}</div> <div className="movie-details-body-text-header"><h1>Year:</h1> {movieDetail.Year}</div> <div className="movie-details-body-text-header"><h1>Actors:</h1> {movieDetail.Actors}</div> <div className="movie-details-body-text-header"><h1>Director:</h1> {movieDetail.Director}</div> @@ -103,8 +116,6 @@ const MovieDetailBody = ({ movieDetail, title }) => { {movieDetail.Plot} </div> </div> - </> - : <></> } </Card.Body> ) } diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js index 0188d72..9ac059c 100644 --- a/client/src/redux/actions.js +++ b/client/src/redux/actions.js @@ -68,7 +68,6 @@ export const filterLanguage = (language) => { export const CURRENT_MOVIE = 'CURRENT_MOVIE'; export const currentMovie = (title) => { - console.log("CUUUURRRENT: ", title) return ({ type: CURRENT_MOVIE, title: title diff --git a/client/src/redux/reducers/current.js b/client/src/redux/reducers/current.js index 590244d..5371daa 100644 --- a/client/src/redux/reducers/current.js +++ b/client/src/redux/reducers/current.js @@ -5,12 +5,7 @@ const initialState = { } function currentReducer(state = initialState, action) { - // console.log("currentReducer : state: ", state) - // console.log("currentReducer : action.type:", action.type) - // console.log("currentReducer : action.title: ", action.title) if (action.type === CURRENT_MOVIE) { - console.log("action.type is CURRENT_MOVIE"); - console.log("title to be set as current:", action.title); return{ ...state, current_title: action.title }; -- GitLab From f86296ae9db2526ae96c34c63b70b7aa4468e500 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Tue, 22 Oct 2019 11:16:01 +0200 Subject: [PATCH 35/61] Optimized backend with added sort and filter logic. Ref. #29 --- server/routes/movies.js | 79 ++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/server/routes/movies.js b/server/routes/movies.js index 84d9d39..72cafd2 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -36,21 +36,16 @@ router.get('/GetMovieTitles', function(req, res) { let sortBy = sorting(req.query.sortOrder, req.query.sortType); - // Filter logic. As you cannot use $and, $or with empty values, different - // queries has to be created based on if filter is active or not - let filters = ""; - if (req.query.filter === "true") { - filters = JSON.parse(req.query.filters); - query = Movies.find({ $and:[ - { $or: filters }, - { "Title": { $regex: req.query.searchString, $options: "i" } } - ]}, { "Title": 1 }).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); - } else { - query = Movies.find({ - "Title": {'$regex': req.query.searchString, '$options': 'i'} - }, { "Title": 1 }) - .sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); - } + let filters = filter( + req.query.filterLanguages, + req.query.filterGenres, + req.query.filterRatings + ); + + query = Movies.find({ $and:[ + { $or: filters }, + { "Title": { $regex: req.query.searchString, $options: "i" } } + ]}, { "Title": 1 }).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); query.exec(function(err, data) { if (err) return res.json({ success: false, error: err }); @@ -59,20 +54,6 @@ router.get('/GetMovieTitles', function(req, res) { }); }); -/* - * GET movie headers - * - */ -router.get('/GetMovieHeaders', function(req, res) { - var query = Movies.findOne({ "Title": req.query.Title }, { "Title": 1, "UserRating": 1 }); - - query.exec(function(err, data) { - if (err) return res.json({ success: false, error: err }); - - return res.json({ success: true, data: data }); - }) -}) - /* GET movie details * * Params: { @@ -93,6 +74,38 @@ router.get('/GetMovieDetail', function(req, res, next) { }); }); +// Filer logic. Converts the filters to something +// MongoDB accepts +function filter(languages, genres, rated) { + let languageFilter = languages.map(language => { + return({ 'Language': language }); + }); + + let genreFilter = genres.map(genre => { + return({ 'Genre': genre }); + }); + + let ratedFilter = rated.map(rating => { + return({ 'Rated': rating }); + }); + + let filters = []; + + // Since MongoDB does not allow empty arrays in + // $and or $or, an empty object has to be passed. + if(languageFilter.length == 0 && + genreFilter.length == 0 && + ratedFilter.length == 0 + ) { + filters.push({}); + } else { + filters = languageFilter.concat(genreFilter).concat(ratedFilter); + filters = JSON.stringify(filters); + } + + return (filters); +} + // Sorting logic function sorting(sortOrder, sortType) { let sortBy = {}; @@ -109,16 +122,16 @@ function sorting(sortOrder, sortType) { } switch(sortType) { - case 'SORT_YEAR': + case 'Year': sortBy = { "Year": sortOrder }; break; - case 'SORT_BOX_OFFICE': + case 'Box office': sortBy = { "BoxOffice": sortOrder }; break; - case 'SORT_USER_RATING': + case 'User rating': sortBy = { "UserRating": sortOrder }; break; - case 'SORT_IMDB': + case 'IMDB rating': sortBy = { "imdbRating": sortOrder }; break; default: -- GitLab From bb3eccd3f5e6f97ffa1742e8f0d8fb5e256d5ca9 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Tue, 22 Oct 2019 11:18:39 +0200 Subject: [PATCH 36/61] Set up passing of filter and sort states from the store to the backend. Functionality Ref. #30 --- .gitignore | 2 ++ client/src/App.css | 4 +-- client/src/App.js | 48 ++++++++++++++++++++++++++------- client/src/components/Filter.js | 7 ----- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index e9785a2..73e8888 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ .env.test.local .env.production.local /client/src/List.txt +/client/src/redux/reducers/frontpage.js + npm-debug.log* yarn-debug.log* diff --git a/client/src/App.css b/client/src/App.css index ab18136..dc7fda6 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -33,8 +33,8 @@ .logo-wrapper { grid-area: Logo; font-family: 'Righteous', cursive; - width: 30vw; - height: 50%; + width: 25vw; + height: 100%; } .title-wrapper { diff --git a/client/src/App.js b/client/src/App.js index 1392e83..c398828 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -6,7 +6,7 @@ import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-boo import { Chart } from 'react-google-charts'; import InterestOverTime from './components/InterestOverTime'; import MovieDetail from './components/MovieDetail'; -import { filterState } from './redux/actions'; +import { filterState} from './redux/actions'; import store from './redux/store'; import Filter from './components/Filter'; import SortButton from './components/SortButton' @@ -25,7 +25,6 @@ class App extends Component { } this.sort = false; - this.filter = false; this.searchString = ""; @@ -54,17 +53,41 @@ class App extends Component { */ } + + + /* GET movie titles + * + * Params: { + * searchString: searches for titles containing this string, + * from: start index of search, + * to: end index of search, + * sort: boolean if result should be sorted. Default is alphabetically + * sortType: the sort type. Get the current type from state + * sortOrder: the sort order. Get the current order from state + * filterLanguages: the selected language filters. + * filterRatings: the selected rating filters. + * filterGenres: the selected genre filters. + * } + * Returns: { + * success: Boolean, + * data: Array of results + * } + * + * Gets all movie titles containing the searchString in title. + * Only returns the number of hits wanted + */ + getMovies = (searchInput, fromIndex, toIndex) => { axios.get('/api/GetMovieTitles', { params: { searchString: searchInput, from: fromIndex, to: toIndex, - sort: this.sort, - sortType: 'SORT_IMDB', - sortOrder: 'DESC', - filter: this.filter, - filters: '[{ "Country": "India" }]' + sortType: this.props.sort_type, + sortOrder: this.props.sort_order, + filterLanguages: this.props.filters_languages, + filterRatings: this.props.filter_ratings, + filterGenres: this.props.filter_genres } }).then((res) => { if (res.data.success) { @@ -77,7 +100,7 @@ class App extends Component { render() { - console.log(this.props.filter_state) + console.log('this.props.filter_state') return( <div className="main-container"> <div className="logo-wrapper"> @@ -113,7 +136,14 @@ class App extends Component { } const mapStateToProps = state => { - return { filter_state : state.filterReducer.filter_state} + return { + filter_state : state.filterReducer.filter_state, + filter_languages : state.filterReducer.filter_language, + filter_ratings : state.filterReducer.filter_rated, + filter_genres : state.filterReducer.filter_genre, + sort_type : state.sort, + sort_order : state.order + } }; export default connect(mapStateToProps, {filterState})(App); diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js index 1a6e112..de5f504 100644 --- a/client/src/components/Filter.js +++ b/client/src/components/Filter.js @@ -127,11 +127,4 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { ); }; -// const mapStateToProps = state => { - // console.log(state.filterReducer.filter_rated); - // console.log(state.filterReducer.filter_genre); - // console.log(state.filterReducer.filter_language); -// return { PGActive : state.filterReducer.filter_rated, LanguageActive : state.filterReducer.filter_language , GenreActive : state.filterReducer.filter_genre}} - - export default connect(null, { filterRated, filterLanguage, filterGenre} )(Filter) -- GitLab From 40c5c47e02ad1fcd1e9e6f044709cbcb4260c16f Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Wed, 23 Oct 2019 10:41:53 +0200 Subject: [PATCH 37/61] Filter and sort is now working properly, both front- and backend. Ref. #30, #29, #19, #20 --- client/src/App.css | 4 +-- client/src/App.js | 58 +++++--------------------------------- server/routes/movies.js | 62 +++++++++++++++++++++++++++++------------ 3 files changed, 53 insertions(+), 71 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index dc7fda6..f7c0678 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -11,7 +11,7 @@ 'FilterOptions FilterOptions' 'Content Content'; grid-template-columns: 1fr 1fr; - grid-template-rows: 0.5fr 0.1fr 0.1fr 0.1fr 0.1fr 2.5fr; + grid-template-rows: 0.1fr 0.1fr 0.1fr 0.1fr 0.1fr 2.5fr; grid-gap: 1vh; margin: 0vh 15vh 0 15vh; } @@ -33,7 +33,7 @@ .logo-wrapper { grid-area: Logo; font-family: 'Righteous', cursive; - width: 25vw; + width: 30vw; height: 100%; } diff --git a/client/src/App.js b/client/src/App.js index 3604f8a..984410f 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -31,65 +31,22 @@ class App extends Component { this.getMovies = this.getMovies.bind(this); } - componentDidMount() { - /* - axios.get('/api/MovieTrend', { params: { Title: 'The Godfather' }}) - .then(res => { - let data = JSON.parse(res.data.data); - - let tempTitles = ['Date', 'Interest']; - let tempData = []; - tempData.push(tempTitles); - - data.default.timelineData.map(time => { - let tempValues = []; - tempValues.push(time.formattedTime); - tempValues.push(Number(time.formattedValue[0])); - tempData.push(tempValues); - }); - - this.setState({ chartData: tempData }); - }); - */ - } - - - - /* GET movie titles - * - * Params: { - * searchString: searches for titles containing this string, - * from: start index of search, - * to: end index of search, - * sort: boolean if result should be sorted. Default is alphabetically - * sortType: the sort type. Get the current type from state - * sortOrder: the sort order. Get the current order from state - * filterLanguages: the selected language filters. - * filterRatings: the selected rating filters. - * filterGenres: the selected genre filters. - * } - * Returns: { - * success: Boolean, - * data: Array of results - * } - * - * Gets all movie titles containing the searchString in title. - * Only returns the number of hits wanted - */ - getMovies = (searchInput, fromIndex, toIndex) => { //this.props.currentMovie(searchInput); + let filterLanguagesArr = Array.from(this.props.filter_languages); + let filterRatingsArr = Array.from(this.props.filter_ratings); + let filterGenresArr = Array.from(this.props.filter_genres); - axios.get('/api/GetMovieTitles', { + axios.post('/api/GetMovieTitles', null, { params: { searchString: searchInput, from: fromIndex, to: toIndex, sortType: this.props.sort_type, sortOrder: this.props.sort_order, - filterLanguages: this.props.filters_languages, - filterRatings: this.props.filter_ratings, - filterGenres: this.props.filter_genres + filterLanguages: filterLanguagesArr, + filterRatings: filterRatingsArr, + filterGenres: filterGenresArr } }).then((res) => { if (res.data.success) { @@ -102,7 +59,6 @@ class App extends Component { render() { - console.log('this.props.filter_state') return( <div className="main-container"> <div className="logo-wrapper"> diff --git a/server/routes/movies.js b/server/routes/movies.js index 72cafd2..1432cd9 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -11,7 +11,6 @@ const Movies = require('../Schemas/Movies'); * searchString: searches for titles containing this string, * from: start index of search, * to: end index of search, - * sort: boolean if result should be sorted. Default is alphabetically * sortType: the sort type. Get the current type from state * sortOrder: the sort order. Get the current order from state * filter: boolean if filter should be active @@ -29,7 +28,14 @@ const Movies = require('../Schemas/Movies'); * Gets all movie titles containing the searchString in title. * Only returns the number of hits wanted */ -router.get('/GetMovieTitles', function(req, res) { +router.post('/GetMovieTitles', function(req, res) { + if(req.query.filterLanguages === undefined) + req.query.filterLanguages = []; + if(req.query.filterGenres === undefined) + req.query.filterGenres = []; + if(req.query.filterRatings === undefined) + req.query.filterRatings = []; + var query; let numberOfHits = Number(req.query.from) - Number(req.query.to); @@ -42,10 +48,16 @@ router.get('/GetMovieTitles', function(req, res) { req.query.filterRatings ); - query = Movies.find({ $and:[ - { $or: filters }, - { "Title": { $regex: req.query.searchString, $options: "i" } } - ]}, { "Title": 1 }).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); + //filters = JSON.parse(filters) + query = Movies.find({ + //filters, + $and: [ + {$or: filters[0]}, + {$or: filters[1]}, + {$or: filters[2]} + ], + "Title": { $regex: req.query.searchString, $options: "i" } + }, { "Title": 1 }).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); query.exec(function(err, data) { if (err) return res.json({ success: false, error: err }); @@ -78,30 +90,29 @@ router.get('/GetMovieDetail', function(req, res, next) { // MongoDB accepts function filter(languages, genres, rated) { let languageFilter = languages.map(language => { - return({ 'Language': language }); + return({ Language: { $regex: language, $options: "i" } }); }); let genreFilter = genres.map(genre => { - return({ 'Genre': genre }); + return({ Genre: { $regex: genre, $options: "i" } }); }); let ratedFilter = rated.map(rating => { - return({ 'Rated': rating }); + return({ Rated: rating }); }); let filters = []; // Since MongoDB does not allow empty arrays in // $and or $or, an empty object has to be passed. - if(languageFilter.length == 0 && - genreFilter.length == 0 && - ratedFilter.length == 0 - ) { - filters.push({}); - } else { - filters = languageFilter.concat(genreFilter).concat(ratedFilter); - filters = JSON.stringify(filters); - } + if(languageFilter.length == 0) + languageFilter = [{}]; + if(genreFilter.length == 0) + genreFilter = [{}]; + if(ratedFilter.length == 0) + ratedFilter = [{}]; + + filters.push(languageFilter, genreFilter, ratedFilter); return (filters); } @@ -141,6 +152,21 @@ function sorting(sortOrder, sortType) { return sortBy; } +/* + * GET movie headers + * + */ +router.get('/GetMovieHeaders', function(req, res) { + var query = Movies.findOne({ "Title": req.query.Title }, { "Title": 1, "UserRating": 1 }); + + query.exec(function(err, data) { + if (err) return res.json({ success: false, error: err }); + + return res.json({ success: true, data: data }); + }) +}) + + /* POST user ratings * * Params: { -- GitLab From 1214da350eed6af0d3e79d95875b06868fe7022e Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Wed, 23 Oct 2019 13:49:12 +0200 Subject: [PATCH 38/61] Fix layout bugs and setup live update on every change in sort and filter functionality. Ref. #31, #33 --- client/src/App.css | 85 +++++------- client/src/App.js | 25 ++-- client/src/components/Filter.css | 2 +- client/src/components/Filter.js | 178 ++++++++++++++------------ client/src/components/MovieDetail.css | 2 + client/src/components/SortButton.css | 12 +- client/src/components/SortButton.js | 42 ++++-- client/src/constants.js | 12 +- client/src/redux/reducers/order.js | 2 +- 9 files changed, 201 insertions(+), 159 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index f7c0678..0d334bd 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -11,39 +11,26 @@ 'FilterOptions FilterOptions' 'Content Content'; grid-template-columns: 1fr 1fr; - grid-template-rows: 0.1fr 0.1fr 0.1fr 0.1fr 0.1fr 2.5fr; - grid-gap: 1vh; - margin: 0vh 15vh 0 15vh; - } -/* -.main-header-container { - grid-area: header; - display: grid; - justify-items: strech; - - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr 1fr 1fr; - grid-template-areas: - 'Logo Logo' - 'Search Search' - 'Filter Sort'; - grid-gap: 5px; - } */ + grid-template-rows: 0.1fr 0.1fr 0.1fr 0.1fr 0.01fr 1.5fr; + grid-gap: 1%; + margin: 2% 5% 0 5%; +} .logo-wrapper { grid-area: Logo; font-family: 'Righteous', cursive; width: 30vw; height: 100%; - } + .title-wrapper { grid-area: Title; font-family: 'Righteous', cursive; - width: 90%; - height: 90%; + width: 90vw; + height: 100%; + /* width: auto; + height: 90%; */ font-size: 5vh; - } .search-wrapper { @@ -58,65 +45,57 @@ padding-left: 10vw; padding-right: 3vw; width: 40vw; +} - +.filter-button { + font-size: 120%; } + .sort-wrapper { grid-area: Sort; display: auto; - padding-left: 3vw; padding-right: 10vw; + padding-left: 3vw; width: 40vw; - } .main-content { grid-area: Content; width: 100%; - padding-left: 5%; - padding-right: 5%; - /* opacity: 0.7; - filter: alpha(opacity=70); /* For IE8 and earlier */ - } - .list-group-item { - background-color: white; - padding-left: 5%; - padding-right: 5%; - font-size: 2vh; - text-align: left; - opacity: 0.90; - filter: alpha(opacity=90); /* For IE8 and earlier */ - } - .list-group-item:hover { - background-color: grey; - cursor: default; - } - - .filter_category{ - /* width: 50vw; */ - width: 40vw; - } + padding-bottom: 5%; +} +.list-group-item { + background-color: white; + padding-left: 5%; + padding-right: 5%; + font-size: 2vh; + text-align: left; + opacity: 0.90; + filter: alpha(opacity=90); /* For IE8 and earlier */ +} +.list-group-item:hover { + background-color: grey; + cursor: default; +} .filter-options { grid-area: FilterOptions; text-align: left; + font-size: 100%; height: 0; - /* height: 0; */ - /* width: 0; */ - /* display: none; */ opacity: 0; filter: alpha(opacity=0); /* For IE8 and earlier */ - } + .filter-options-active { grid-area: FilterOptions; text-align: left; + font-size: 100%; width: 70vw; height: auto; padding-bottom: 2vw; opacity: 1; filter: alpha(opacity=1); /* For IE8 and earlier */ - } diff --git a/client/src/App.js b/client/src/App.js index 984410f..dbd6a4b 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -27,11 +27,20 @@ class App extends Component { this.sort = false; this.searchString = ""; + this.from = 0; + this.to = 10; this.getMovies = this.getMovies.bind(this); + this.searchChange = this.searchChange.bind(this); } - getMovies = (searchInput, fromIndex, toIndex) => { + searchChange(searchInput) { + this.searchString = searchInput; + + this.getMovies(); + } + + getMovies = () => { //this.props.currentMovie(searchInput); let filterLanguagesArr = Array.from(this.props.filter_languages); let filterRatingsArr = Array.from(this.props.filter_ratings); @@ -39,9 +48,9 @@ class App extends Component { axios.post('/api/GetMovieTitles', null, { params: { - searchString: searchInput, - from: fromIndex, - to: toIndex, + searchString: this.searchString, + from: this.from, + to: this.to, sortType: this.props.sort_type, sortOrder: this.props.sort_order, filterLanguages: filterLanguagesArr, @@ -68,16 +77,16 @@ class App extends Component { <p> Online Movie Gathering </p> </div> <div className="search-wrapper"> - <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 20)} /> + <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.searchChange(change.target.value)} /> </div> <div className="filter-wrapper"> - <Button variant="outline-secondary" size="lg" onClick={() => this.props.filterState(this.props.filter_state)} block >Filter</Button> + <Button className= "filter-button" variant="outline-secondary" size="lg" onClick={() => this.props.filterState(this.props.filter_state)} block >Filter</Button> </div> <div className="sort-wrapper"> - <SortButton /> {/*<Button variant="outline-secondary" size="lg" block>Sort</Button>*/} + <SortButton getMovies = {this.getMovies} /> {/*<Button variant="outline-secondary" size="lg" block>Sort</Button>*/} </div> <div className={this.props.filter_state ? "filter-options-active" : "filter-options"}> - <Filter/> + <Filter getMovies = {this.getMovies}/> </div> <div className="main-content"> <Accordion> diff --git a/client/src/components/Filter.css b/client/src/components/Filter.css index 8a90fa3..deb93fc 100644 --- a/client/src/components/Filter.css +++ b/client/src/components/Filter.css @@ -3,5 +3,5 @@ .col-class { overflow-y: scroll; - height: 40vh; + height: 20vh; } diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js index de5f504..2ce6517 100644 --- a/client/src/components/Filter.js +++ b/client/src/components/Filter.js @@ -3,9 +3,10 @@ import { Table, Form, Row, Col, Button} from 'react-bootstrap'; import { connect } from 'react-redux'; import store from '../redux/store' import { filterRated, filterGenre, filterLanguage } from '../redux/actions'; +import { FILTER_TYPE } from '../constants' -const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { +const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies}) => { // TODO: If we have the time, consider imporoving the checkboxes with an select/ // deselect all button. @@ -16,6 +17,25 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { // <Form.Check type={'checkbox'} onClick={ () =>filterRated(FILTER.PG[0])} id={"PG1"} label={"G"}/> + const combinedFunction = filter => { + switch (filter[0]) { + case FILTER_TYPE.PG: + filterRated(filter[1]); + getMovies(); + break; + case FILTER_TYPE.LANGUAGE: + filterLanguage(filter[1]); + getMovies(); + break; + case FILTER_TYPE.GENRE: + filterGenre(filter[1]); + getMovies(); + break; + default: + break; + } + } + return ( <Form as={Row}> <Form.Group as={Col}> @@ -23,14 +43,14 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { <b>PG-Rating</b> </Form.Label> <Col sm="15"> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("G")} id={"PG1"} label={"G"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("PG")} id={"PG2"} label={"PG"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("PG-13")} id={"PG3"} label={"PG-13"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("R")} id={"PG4"} label={"R"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("NC-17")} id={"PG5"} label={"NC-17"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("Not Rated")} id={"PG6"} label={"Not Rated"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("Passed")} id={"PG7"} label={"Passed"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterRated("Approved")} id={"PG8"} label={"Approved"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG,"G"])} id={"PG1"} label={"G"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "PG"])} id={"PG2"} label={"PG"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "PG-13"])} id={"PG3"} label={"PG-13"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG,"R"])} id={"PG4"} label={"R"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "NC-17"])} id={"PG5"} label={"NC-17"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "Not Rated"])} id={"PG6"} label={"Not Rated"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "Passed"])} id={"PG7"} label={"Passed"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "Approved"])} id={"PG8"} label={"Approved"}/> </Col> </Form.Group> <Form.Group as={Col}> @@ -38,26 +58,26 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { <b>Genre</b> </Form.Label> <Col className="col-class" sm="15"> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Action")} id={"G1"} label={"Action"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Animation")} id={"G2"} label={"Animation"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Biography")} id={"G3"} label={"Biography"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Comedy")} id={"G4"} label={"Comedy"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Crime")} id={"G5"} label={"Crime"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Drama")} id={"G6"} label={"Drama"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Family")} id={"G8"} label={"Family"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Fantasy")} id={"G9"} label={"Fantasy"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Film-Noir")} id={"G10"} label={"Film-Noir"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Horror")} id={"G11"} label={"Horror"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("History")} id={"G12"} label={"History"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Musical")} id={"G13"} label={"Musical"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Mystery")} id={"G14"} label={"Mystery"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Romance")} id={"G15"} label={"Romance"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Sci-Fi")} id={"G16"} label={"Sci-Fi"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Short")} id={"G17"} label={"Short"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Sport")} id={"G18"} label={"Sport"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Thriller")} id={"G19"} label={"Thriller"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("War")} id={"G20"} label={"War"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterGenre("Western")} id={"G21"} label={"Western"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Action"])} id={"G1"} label={"Action"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Animation"])} id={"G2"} label={"Animation"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Biography"])} id={"G3"} label={"Biography"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Comedy"])} id={"G4"} label={"Comedy"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Crime"])} id={"G5"} label={"Crime"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Drama"])} id={"G6"} label={"Drama"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Family"])} id={"G8"} label={"Family"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Fantasy"])} id={"G9"} label={"Fantasy"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Film-Noir"])} id={"G10"} label={"Film-Noir"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Horror"])} id={"G11"} label={"Horror"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "History"])} id={"G12"} label={"History"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Musical"])} id={"G13"} label={"Musical"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Mystery"])} id={"G14"} label={"Mystery"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Romance"])} id={"G15"} label={"Romance"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Sci-Fi"])} id={"G16"} label={"Sci-Fi"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Short"])} id={"G17"} label={"Short"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Sport"])} id={"G18"} label={"Sport"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Thriller"])} id={"G19"} label={"Thriller"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "War"])} id={"G20"} label={"War"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Western"])} id={"G21"} label={"Western"}/> </Col> </Form.Group> <Form.Group as={Col}> @@ -65,62 +85,62 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre}) => { <b>Language</b> </Form.Label> <Col className="col-class" sm="15"> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Arabic")} id={"L1"} label={"Arabic"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("American Sign Language")} id={"L2"} label={"American Sign Language"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Belarusian")} id={"L3"} label={"Belarusian"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Cantonese")} id={"L4"} label={"Cantonese"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Chinese")} id={"L5"} label={"Chinese"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Czech")} id={"L6"} label={"Czech"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Danish")} id={"L7"} label={"Danish"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("English")} id={"L8"} label={"English"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Esperanto")} id={"L9"} label={"Esperanto"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("French")} id={"L10"} label={"French"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("German")} id={"L11"} label={"German"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Greek")} id={"L12"} label={"Greek"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hebrew")} id={"L13"} label={"Hebrew"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hindi")} id={"L14"} label={"Hindi"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hmong")} id={"L15"} label={"Hmong"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Hungarian")} id={"L16"} label={"Hungarian"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Italian")} id={"L17"} label={"Italian"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Japanese")} id={"L18"} label={"Japanese"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Korean")} id={"L19"} label={"Korean"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Kurdish")} id={"L20"} label={"Kurdish"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Latin")} id={"L21"} label={"Latin"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Mandarin")} id={"L22"} label={"Mandarin"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Nepali")} id={"L23"} label={"Nepali"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Norwegian")} id={"L24"} label={"Norwegian"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("North American Indian")} id={"L25"} label={"North American Indian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Arabic"])} id={"L1"} label={"Arabic"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "American Sign Language"])} id={"L2"} label={"American Sign Language"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Belarusian"])} id={"L3"} label={"Belarusian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Cantonese"])} id={"L4"} label={"Cantonese"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Chinese"])} id={"L5"} label={"Chinese"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Czech"])} id={"L6"} label={"Czech"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Danish"])} id={"L7"} label={"Danish"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "English"])} id={"L8"} label={"English"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Esperanto"])} id={"L9"} label={"Esperanto"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "French"])} id={"L10"} label={"French"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "German"])} id={"L11"} label={"German"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Greek"])} id={"L12"} label={"Greek"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Hebrew"])} id={"L13"} label={"Hebrew"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Hindi"])} id={"L14"} label={"Hindi"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Hmong"])} id={"L15"} label={"Hmong"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Hungarian"])} id={"L16"} label={"Hungarian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Italian"])} id={"L17"} label={"Italian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Japanese"])} id={"L18"} label={"Japanese"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Korean"])} id={"L19"} label={"Korean"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Kurdish"])} id={"L20"} label={"Kurdish"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Latin"])} id={"L21"} label={"Latin"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Mandarin"])} id={"L22"} label={"Mandarin"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Nepali"])} id={"L23"} label={"Nepali"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Norwegian"])} id={"L24"} label={"Norwegian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "North American Indian"])} id={"L25"} label={"North American Indian"}/> </Col> </Form.Group> <Form.Group as={Col}> <Form.Label column sm="15"> </Form.Label> <Col className="col-class" sm="15"> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Old English")} id={"L26"} label={"Old English"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Persian")} id={"L27"} label={"Persian"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Polish")} id={"L28"} label={"Polish"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Portuguese")} id={"L29"} label={"Portuguese"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Punjabi")} id={"L30"} label={"Punjabi"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Quenya")} id={"L31"} label={"Quenya"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Russian")} id={"L32"} label={"Russian"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Scottish Gaelic")} id={"L33"} label={"Scottish Gaelic"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Shanghainese")} id={"L34"} label={"Shanghainese"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Sicilian")} id={"L35"} label={"Sicilian"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Sindarin")} id={"L36"} label={"Sindarin"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Spanish")} id={"L37"} label={"Spanish"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Swedish")} id={"L38"} label={"Swedish"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Swahili")} id={"L39"} label={"Swahili"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Tamil")} id={"L40"} label={"Tamil"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Telugu")} id={"L41"} label={"Telugu"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Thai")} id={"L42"} label={"Thai"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Turkish")} id={"L43"} label={"Turkish"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Urdu")} id={"L44"} label={"Urdu"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Vietnamese")} id={"L45"} label={"Vietnamese"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Xhosa")} id={"L46"} label={"Xhosa"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Yiddish")} id={"L47"} label={"Yiddish"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("Zulu")} id={"L48"} label={"Zulu"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("None")} id={"L49"} label={"None (sunset boulevard)"}/> - <Form.Check type={'checkbox'} onClick={ () =>filterLanguage("N/A")} id={"L50"} label={"N/A (your name)"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Old English"])} id={"L26"} label={"Old English"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Persian"])} id={"L27"} label={"Persian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Polish"])} id={"L28"} label={"Polish"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Portuguese"])} id={"L29"} label={"Portuguese"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Punjabi"])} id={"L30"} label={"Punjabi"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Quenya"])} id={"L31"} label={"Quenya"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Russian"])} id={"L32"} label={"Russian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Scottish Gaelic"])} id={"L33"} label={"Scottish Gaelic"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Shanghainese"])} id={"L34"} label={"Shanghainese"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Sicilian"])} id={"L35"} label={"Sicilian"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Sindarin"])} id={"L36"} label={"Sindarin"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Spanish"])} id={"L37"} label={"Spanish"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Swedish"])} id={"L38"} label={"Swedish"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Swahili"])} id={"L39"} label={"Swahili"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Tamil"])} id={"L40"} label={"Tamil"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Telugu"])} id={"L41"} label={"Telugu"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Thai"])} id={"L42"} label={"Thai"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Turkish"])} id={"L43"} label={"Turkish"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Urdu"])} id={"L44"} label={"Urdu"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Vietnamese"])} id={"L45"} label={"Vietnamese"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Xhosa"])} id={"L46"} label={"Xhosa"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Yiddish"])} id={"L47"} label={"Yiddish"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Zulu"])} id={"L48"} label={"Zulu"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "None"])} id={"L49"} label={"None (sunset boulevard)"}/> + <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "N/A"])} id={"L50"} label={"N/A (your name)"}/> </Col> </Form.Group> </Form> diff --git a/client/src/components/MovieDetail.css b/client/src/components/MovieDetail.css index 329d0bc..eb04b36 100644 --- a/client/src/components/MovieDetail.css +++ b/client/src/components/MovieDetail.css @@ -1,6 +1,8 @@ .list-group-item-title { display: inline; float: left; + font-size: 100%; + /* font-size: 2.5vmax; */ } .list-group-item-grade { diff --git a/client/src/components/SortButton.css b/client/src/components/SortButton.css index 8ddf46d..f67610c 100644 --- a/client/src/components/SortButton.css +++ b/client/src/components/SortButton.css @@ -1,18 +1,18 @@ .sort-button { float: left; - width: 20vw; + width: 70%; height: 48px; - font-size: 20px; + font-size: 120%; } .sort-button-drop { - width: 20vw; + width: 70%; } .sort-order-button { float: right; - margin-left: auto; - width: 5vw; + margin-left: 10%; + width: 100%; height: 48px; - font-size: 20px; + font-size: 120%; } diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js index 8854ae3..9251654 100644 --- a/client/src/components/SortButton.js +++ b/client/src/components/SortButton.js @@ -7,7 +7,29 @@ import './SortButton.css'; import { selectSort, selectOrder } from "../redux/actions"; import { SORT, ORDER } from "../constants"; -const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder }) => { +const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder, getMovies }) => { + + + const combinedFunction = input => { + switch (input[0]) { + case SORT.SORT: + selectSort(input[1]); + setTimeout(function() { + getMovies(); + }, 250); + break; + case ORDER.ORDER: + selectOrder(input[1]); + setTimeout(function() { + getMovies(); + }, 250); + getMovies(); + break; + default: + break; + } + } + return( <div> <Dropdown > @@ -15,23 +37,25 @@ const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder }) => { {activeSort} </Dropdown.Toggle> <Dropdown.Menu className="sort-button-drop"> - <Dropdown.Item onClick={() => {selectSort(SORT.TITLE)}}>{SORT.TITLE}</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.YEAR)}}>{SORT.YEAR}</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.BOX_OFFICE)}}>{SORT.BOX_OFFICE}</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.IMDB_RATING)}}>{SORT.IMDB_RATING}</Dropdown.Item> - <Dropdown.Item onClick={() => {selectSort(SORT.USER_RATING)}}>{SORT.USER_RATING}</Dropdown.Item> + <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.TITLE])}}>{SORT.TITLE}</Dropdown.Item> + <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.YEAR])}}>{SORT.YEAR}</Dropdown.Item> + <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.BOX_OFFICE])}}>{SORT.BOX_OFFICE}</Dropdown.Item> + <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.IMDB_RATING])}}>{SORT.IMDB_RATING}</Dropdown.Item> + <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.USER_RATING])}}>{SORT.USER_RATING}</Dropdown.Item> </Dropdown.Menu> </Dropdown> <ButtonToolbar> - <Button className="sort-order-button" variant="outline-secondary" onClick={() => {selectOrder(activeOrder)}}> - {activeOrder == "ASC" ? '↑' : '↓'} + <Button className="sort-order-button" variant="outline-secondary" onClick={() => {combinedFunction([ORDER.ORDER, activeOrder])}}> + {activeOrder == "DESC" ? '↑' : '↓'} </Button> </ButtonToolbar> </div> ) } + const mapStateToProps = state => { - console.log(state); + console.log('state.sort', state.sort, 'state.order', state.sort); return { activeSort: state.sort, activeOrder: state.order }; }; + export default connect(mapStateToProps, { selectSort, selectOrder })(SortButton); diff --git a/client/src/constants.js b/client/src/constants.js index 3d95d3f..dcb57b1 100644 --- a/client/src/constants.js +++ b/client/src/constants.js @@ -3,10 +3,18 @@ export const SORT = { YEAR: "Year", BOX_OFFICE: "Box office", IMDB_RATING: "IMDB rating", - USER_RATING: "User rating" + USER_RATING: "User rating", + SORT: "SORT" } export const ORDER = { ASC: "ASC", - DESC: "DESC" + DESC: "DESC", + ORDER: "ORDER" +} + +export const FILTER_TYPE = { + LANGUAGE: "LANGUAGE", + GENRE: "GENRE", + PG: "PG" } diff --git a/client/src/redux/reducers/order.js b/client/src/redux/reducers/order.js index 1f09cb7..4efb994 100644 --- a/client/src/redux/reducers/order.js +++ b/client/src/redux/reducers/order.js @@ -1,7 +1,7 @@ import { SELECT_ORDER } from "../actions"; import { ORDER } from "../../constants"; -const initialState = ORDER.ASC; +const initialState = ORDER.DESC; const order = (state = initialState, action) => { switch (action.type) { -- GitLab From 06184e170489b0b4c201d5ecbc9d60850f43b508 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Wed, 23 Oct 2019 13:51:46 +0200 Subject: [PATCH 39/61] Started on pagination. Ref. #14 --- client/src/App.js | 73 ++++++++++++++----------- client/src/components/PagePagination.js | 51 +++++++++++++++++ server/routes/movies.js | 43 +++++++++++---- 3 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 client/src/components/PagePagination.js diff --git a/client/src/App.js b/client/src/App.js index 984410f..3a56cde 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,18 +1,18 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { Button, Form, Accordion } from 'react-bootstrap'; import axios from 'axios'; -import './App.css'; -import {ButtonToolbar, Button, ListGroup, Form, Accordion, Card} from 'react-bootstrap'; -import { Chart } from 'react-google-charts'; -import InterestOverTime from './components/InterestOverTime'; -import MovieDetail from './components/MovieDetail'; + import { filterState, currentMovie } from './redux/actions'; -import store from './redux/store'; + +import MovieDetail from './components/MovieDetail'; import Filter from './components/Filter'; import SortButton from './components/SortButton' +import PagePagination from './components/PagePagination.js'; -const logo = require('./images/logo.svg'); +import './App.css'; +const logo = require('./images/logo.svg'); class App extends Component { constructor(props) { @@ -21,18 +21,15 @@ class App extends Component { this.state = { movies: [], error: "", - currentMovie: "The Godfather" + noOfPages: 0, + activePage: 20 } - this.sort = false; - - this.searchString = ""; - + this.changePage = this.changePage.bind(this); this.getMovies = this.getMovies.bind(this); } - getMovies = (searchInput, fromIndex, toIndex) => { - //this.props.currentMovie(searchInput); + getMovies = async(searchInput, fromIndex, toIndex) => { let filterLanguagesArr = Array.from(this.props.filter_languages); let filterRatingsArr = Array.from(this.props.filter_ratings); let filterGenresArr = Array.from(this.props.filter_genres); @@ -50,35 +47,38 @@ class App extends Component { } }).then((res) => { if (res.data.success) { - this.setState({ movies: res.data.data }); + this.setState({ movies: res.data.data, noOfPages: res.data.pages }); } else { this.setState({ error: res.data.error }); } }); } + changePage = (newPage) => { + this.setState({ activePage: newPage }); + } render() { return( <div className="main-container"> - <div className="logo-wrapper"> - <img src={logo} alt="logo" /> - </div> - <div className="title-wrapper"> - <p> Online Movie Gathering </p> - </div> - <div className="search-wrapper"> - <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 20)} /> - </div> - <div className="filter-wrapper"> - <Button variant="outline-secondary" size="lg" onClick={() => this.props.filterState(this.props.filter_state)} block >Filter</Button> - </div> - <div className="sort-wrapper"> - <SortButton /> {/*<Button variant="outline-secondary" size="lg" block>Sort</Button>*/} - </div> - <div className={this.props.filter_state ? "filter-options-active" : "filter-options"}> - <Filter/> - </div> + <div className="logo-wrapper"> + <img src={logo} alt="logo" /> + </div> + <div className="title-wrapper"> + <p> Online Movie Gathering {this.state.noOfPages} </p> + </div> + <div className="search-wrapper"> + <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.getMovies(change.target.value, 0, 10)} /> + </div> + <div className="filter-wrapper"> + <Button variant="outline-secondary" size="lg" onClick={() => this.props.filterState(this.props.filter_state)} block >Filter</Button> + </div> + <div className="sort-wrapper"> + <SortButton /> + </div> + <div className={this.props.filter_state ? "filter-options-active" : "filter-options"}> + <Filter/> + </div> <div className="main-content"> <Accordion> {this.state.movies.map((movie, index) => { @@ -88,6 +88,13 @@ class App extends Component { })} </Accordion> </div> + <div className="pagination-wrapper"> + <PagePagination + pages={this.state.noOfPages} + activePage={this.state.activePage} + changePage={this.changePage} + /> + </div> </div> ) } diff --git a/client/src/components/PagePagination.js b/client/src/components/PagePagination.js new file mode 100644 index 0000000..4f84b3d --- /dev/null +++ b/client/src/components/PagePagination.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { Pagination } from 'react-bootstrap'; + +const PagePagination = ({ pages, activePage, changePage }) => { + return( + <Pagination> + <Pagination.First /> + <Pagination.Prev /> + {[...Array(pages).keys()].map(page => { + if(page + 1 === 1) { + return( + <Pagination.Item + active={page + 1 === activePage} + > + {page + 1} + </Pagination.Item> + ) + } else if(page + 1 === pages) { + return( + <Pagination.Item + active={page + 1 === activePage} + > + {page + 1} + </Pagination.Item> + ) + } else if(page + 1 < activePage + 3 && page + 1 > activePage - 3) { + return( + <Pagination.Item + active={page + 1 === activePage} + onClick={() => changePage(page + 1)} + > + {page + 1} + </Pagination.Item> + ) + } else if(page + 1 === activePage + 3) { + return( + <Pagination.Ellipsis /> + ) + } else if(page + 1 === activePage - 3) { + return( + <Pagination.Ellipsis /> + ) + } + })} + <Pagination.Next /> + <Pagination.Last /> + </Pagination> + ) +} + +export default PagePagination; diff --git a/server/routes/movies.js b/server/routes/movies.js index 1432cd9..1aa5188 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -36,9 +36,7 @@ router.post('/GetMovieTitles', function(req, res) { if(req.query.filterRatings === undefined) req.query.filterRatings = []; - var query; - - let numberOfHits = Number(req.query.from) - Number(req.query.to); + let numberOfHits = Number(req.query.to) - Number(req.query.from); let sortBy = sorting(req.query.sortOrder, req.query.sortType); @@ -48,22 +46,45 @@ router.post('/GetMovieTitles', function(req, res) { req.query.filterRatings ); - //filters = JSON.parse(filters) - query = Movies.find({ - //filters, + var query = { $and: [ {$or: filters[0]}, {$or: filters[1]}, {$or: filters[2]} ], "Title": { $regex: req.query.searchString, $options: "i" } - }, { "Title": 1 }).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); + }; + + // We want the count query to finish before getting the dataset from + // MongoDB. Using a promise to get async functionality. + var hitsPromise = () => { + return new Promise(function(resolve, reject) { + var hitsQuery = Movies.countDocuments(query).sort(sortBy); + hitsQuery.exec(function(err, data) { + err ? reject(err) : resolve(data); + }); + }); + } - query.exec(function(err, data) { - if (err) return res.json({ success: false, error: err }); + var hitsResult = async () => { + var result = await(hitsPromise()); - return res.json({ success: true, data: data }); - }); + // The find query that actually returns the dataset from database + var findQuery = Movies.find(query).sort(sortBy).skip(Number(req.query.from)).limit(Number(numberOfHits)); + findQuery.exec(function(err, data) { + if (err) return res.json({ success: false, error: err }); + + // Calculating the number of pages needed for pagination + let noOfPages = Math.ceil(result / Number(numberOfHits)); + + if(isNaN(noOfPages)) + noOfPages = 1; + + return res.json({ success: true, data: data, pages: noOfPages }); + }); + } + + hitsResult(); }); /* GET movie details -- GitLab From c6d7391c46497644ea0cc1d3ef2bf6ded80d93e1 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Wed, 23 Oct 2019 15:05:49 +0200 Subject: [PATCH 40/61] Pagination is now functional. Added to grid layout of App. Ref. #14, #31 --- client/src/App.css | 7 +++++- client/src/App.js | 21 +++++++++++++---- client/src/components/PagePagination.js | 31 ++++++++++++++++++++----- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 0d334bd..a4abcd1 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -9,7 +9,8 @@ 'Search Search' 'Filter Sort' 'FilterOptions FilterOptions' - 'Content Content'; + 'Content Content' + 'Pagination Pagination'; grid-template-columns: 1fr 1fr; grid-template-rows: 0.1fr 0.1fr 0.1fr 0.1fr 0.01fr 1.5fr; grid-gap: 1%; @@ -99,3 +100,7 @@ opacity: 1; filter: alpha(opacity=1); /* For IE8 and earlier */ } + +.pagination-wrapper { + grid-area: Pagination; +} diff --git a/client/src/App.js b/client/src/App.js index 060d469..8f01957 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -22,10 +22,10 @@ class App extends Component { movies: [], error: "", noOfPages: 0, - activePage: 20 + activePage: 1 } - this.sort = false; + this.pageChanged = false; this.searchString = ""; this.from = 0; @@ -33,16 +33,21 @@ class App extends Component { this.getMovies = this.getMovies.bind(this); this.searchChange = this.searchChange.bind(this); + this.changePage = this.changePage.bind(this); } searchChange(searchInput) { this.searchString = searchInput; - this.getMovies(); } getMovies = () => { - //this.props.currentMovie(searchInput); + if(!this.pageChanged) { + this.from = 0; + this.to = 10; + this.setState({ activePage: 1 }); + } + let filterLanguagesArr = Array.from(this.props.filter_languages); let filterRatingsArr = Array.from(this.props.filter_ratings); let filterGenresArr = Array.from(this.props.filter_genres); @@ -64,11 +69,19 @@ class App extends Component { } else { this.setState({ error: res.data.error }); } + + this.pageChanged = false; }); } changePage = (newPage) => { this.setState({ activePage: newPage }); + + this.from = newPage * 10 - 10; + this.to = newPage * 10; + this.pageChanged = true; + + this.getMovies(); } render() { diff --git a/client/src/components/PagePagination.js b/client/src/components/PagePagination.js index 4f84b3d..fd43ad2 100644 --- a/client/src/components/PagePagination.js +++ b/client/src/components/PagePagination.js @@ -4,13 +4,21 @@ import { Pagination } from 'react-bootstrap'; const PagePagination = ({ pages, activePage, changePage }) => { return( <Pagination> - <Pagination.First /> - <Pagination.Prev /> + <Pagination.First + onClick={() => changePage(1)} + disabled={activePage === 1} + /> + <Pagination.Prev + onClick={() => changePage(activePage - 1)} + disabled={activePage === 1} + /> {[...Array(pages).keys()].map(page => { if(page + 1 === 1) { return( <Pagination.Item active={page + 1 === activePage} + onClick={() => changePage(page + 1)} + key={page} > {page + 1} </Pagination.Item> @@ -19,6 +27,8 @@ const PagePagination = ({ pages, activePage, changePage }) => { return( <Pagination.Item active={page + 1 === activePage} + onClick={() => changePage(page + 1)} + key={page} > {page + 1} </Pagination.Item> @@ -28,22 +38,31 @@ const PagePagination = ({ pages, activePage, changePage }) => { <Pagination.Item active={page + 1 === activePage} onClick={() => changePage(page + 1)} + key={page} > {page + 1} </Pagination.Item> ) } else if(page + 1 === activePage + 3) { return( - <Pagination.Ellipsis /> + <Pagination.Ellipsis key={page} /> ) } else if(page + 1 === activePage - 3) { return( - <Pagination.Ellipsis /> + <Pagination.Ellipsis key={page} /> ) + } else { + return(<></>) } })} - <Pagination.Next /> - <Pagination.Last /> + <Pagination.Next + onClick={() => changePage(activePage + 1)} + disabled={activePage === pages} + /> + <Pagination.Last + onClick={() => changePage(pages)} + disabled={activePage === pages} + /> </Pagination> ) } -- GitLab From 9998dc45514cc322eaca80b3a206b0965dc4b30d Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 09:16:45 +0200 Subject: [PATCH 41/61] Added README with info about the movie_details_scraper --- movie_details_scraper/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 movie_details_scraper/README.md diff --git a/movie_details_scraper/README.md b/movie_details_scraper/README.md new file mode 100644 index 0000000..1f52d6e --- /dev/null +++ b/movie_details_scraper/README.md @@ -0,0 +1,8 @@ +## Movie Details Scraper +This small `Python` script automatically creates a `.json` file containing information on [IMDBs Top 250](https://www.imdb.com/chart/top?ref_=nv_mv_250) movie list using the [Open Movie Database API](https://omdbapi.com/). The list of movie titles were gathered by copying all the titles from [IMDB](https://www.imdb.com) and some clever use of `regex` in Atom. + +The `.json` file was then inserted into our `MongoDB` collection using +```bash +mongoimport --db project3 --collection movies --file movie_details.json +``` +directly on our VM. -- GitLab From cc294b65bddd2a5e48e0e8be08dda197660001c6 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 10:27:56 +0200 Subject: [PATCH 42/61] Code cleanup and comments --- client/src/App.css | 3 -- client/src/App.js | 47 ++++++++++++++++++++--- client/src/components/Filter.css | 3 -- client/src/components/Filter.js | 42 ++++++++++---------- client/src/components/InterestOverTime.js | 28 +++++++------- client/src/components/MovieDetail.css | 2 - client/src/components/MovieDetail.js | 1 - client/src/components/MovieDetailBody.js | 9 +++-- client/src/components/PagePagination.js | 4 ++ client/src/components/SortButton.js | 46 ++++++++++++++++------ client/src/index.js | 12 +++--- server/routes/movies.js | 25 +++++++----- 12 files changed, 144 insertions(+), 78 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index a4abcd1..4911261 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,4 +1,3 @@ - .main-container { display: grid; text-align: center; @@ -29,8 +28,6 @@ font-family: 'Righteous', cursive; width: 90vw; height: 100%; - /* width: auto; - height: 90%; */ font-size: 5vh; } diff --git a/client/src/App.js b/client/src/App.js index 8f01957..e260b09 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -14,6 +14,18 @@ import './App.css'; const logo = require('./images/logo.svg'); +/* + * Main component. Only component that is a React class and not a functional + * component. Started with classes, and as functionality grew we did not + * have time to convert this into a functional one. + * + * Most functionality is based on calls to the back end as we wanted + * to have most of the data processing on the server. This to reduce load + * on the client. + * + * We initially wanted to have error handling with messages to the user + * when something went wrong, but we did not have time to develop this. + */ class App extends Component { constructor(props) { super(props); @@ -36,18 +48,28 @@ class App extends Component { this.changePage = this.changePage.bind(this); } + // Changes the current search string and updates the result based on + // it. searchChange(searchInput) { this.searchString = searchInput; this.getMovies(); } + // The meat of the application. We send the relevant data to + // the API to get an array of all movie titles based on the input + // from the user. We did not want to load all the movie details here + // as this reduced performance. getMovies = () => { + // If the pagination is not the caller of this function, reset + // pagination to default. if(!this.pageChanged) { this.from = 0; this.to = 10; this.setState({ activePage: 1 }); } - + + // As the redux state is an object, it has to be converted to an + // array such that the API can handle the data. let filterLanguagesArr = Array.from(this.props.filter_languages); let filterRatingsArr = Array.from(this.props.filter_ratings); let filterGenresArr = Array.from(this.props.filter_genres); @@ -74,6 +96,8 @@ class App extends Component { }); } + // Pagination logic. When the user changes page, load the relevant + // movie list. changePage = (newPage) => { this.setState({ activePage: newPage }); @@ -94,15 +118,28 @@ class App extends Component { <p> Online Movie Gathering </p> </div> <div className="search-wrapper"> - <Form.Control className="search-bar" autoFocus size="lg" type="text" placeholder="Search..." onChange={change => this.searchChange(change.target.value)} /> + <Form.Control + className="search-bar" + autoFocus size="lg" + type="text" + placeholder="Search..." + onChange={change => this.searchChange(change.target.value)} + /> </div> <div className="filter-wrapper"> - <Button className= "filter-button" variant="outline-secondary" size="lg" onClick={() => this.props.filterState(this.props.filter_state)} block >Filter</Button> + <Button + className= "filter-button" + variant="outline-secondary" + size="lg" + onClick={() => this.props.filterState(this.props.filter_state)} block + >Filter</Button> </div> <div className="sort-wrapper"> - <SortButton getMovies = {this.getMovies} /> {/*<Button variant="outline-secondary" size="lg" block>Sort</Button>*/} + <SortButton getMovies = {this.getMovies} /> </div> - <div className={this.props.filter_state ? "filter-options-active" : "filter-options"}> + <div + className={this.props.filter_state ? "filter-options-active" : "filter-options"} + > <Filter getMovies = {this.getMovies}/> </div> <div className="main-content"> diff --git a/client/src/components/Filter.css b/client/src/components/Filter.css index deb93fc..f15a6ad 100644 --- a/client/src/components/Filter.css +++ b/client/src/components/Filter.css @@ -1,6 +1,3 @@ - - - .col-class { overflow-y: scroll; height: 20vh; diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js index 2ce6517..9b54acd 100644 --- a/client/src/components/Filter.js +++ b/client/src/components/Filter.js @@ -1,22 +1,24 @@ -import React, { useState, useEffect } from 'react'; -import { Table, Form, Row, Col, Button} from 'react-bootstrap'; +import React from 'react'; +import { Form, Row, Col } from 'react-bootstrap'; import { connect } from 'react-redux'; -import store from '../redux/store' + import { filterRated, filterGenre, filterLanguage } from '../redux/actions'; import { FILTER_TYPE } from '../constants' - +/* + * Component to render and handle functionality for the filters + */ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies}) => { - -// TODO: If we have the time, consider imporoving the checkboxes with an select/ -// deselect all button. + // TODO: If we have the time, consider imporoving the checkboxes with an select/ + // deselect all button. // const [PGClear, setPGClear] = useState(false); // <Button onClick={ () => setPGClear(!PGClear)}>Unselect/select all</Button> -// TODO: make constants instead of fuckings hard coded searchInput -// <Form.Check type={'checkbox'} onClick={ () =>filterRated(FILTER.PG[0])} id={"PG1"} label={"G"}/> - + // TODO: make constants instead of fuckings hard coded searchInput + // <Form.Check type={'checkbox'} onClick={ () =>filterRated(FILTER.PG[0])} id={"PG1"} label={"G"}/> + // Function to update the search result based on + // selected filters const combinedFunction = filter => { switch (filter[0]) { case FILTER_TYPE.PG: @@ -38,9 +40,9 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies} return ( <Form as={Row}> - <Form.Group as={Col}> + <Form.Group as={Col}> <Form.Label column sm="15"> - <b>PG-Rating</b> + <b>PG-Rating</b> </Form.Label> <Col sm="15"> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG,"G"])} id={"PG1"} label={"G"}/> @@ -54,9 +56,9 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies} </Col> </Form.Group> <Form.Group as={Col}> - <Form.Label column sm="15"> - <b>Genre</b> - </Form.Label> + <Form.Label column sm="15"> + <b>Genre</b> + </Form.Label> <Col className="col-class" sm="15"> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Action"])} id={"G1"} label={"Action"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.GENRE, "Animation"])} id={"G2"} label={"Animation"}/> @@ -81,9 +83,9 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies} </Col> </Form.Group> <Form.Group as={Col}> - <Form.Label column sm="15"> - <b>Language</b> - </Form.Label> + <Form.Label column sm="15"> + <b>Language</b> + </Form.Label> <Col className="col-class" sm="15"> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Arabic"])} id={"L1"} label={"Arabic"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "American Sign Language"])} id={"L2"} label={"American Sign Language"}/> @@ -113,8 +115,8 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies} </Col> </Form.Group> <Form.Group as={Col}> - <Form.Label column sm="15"> - </Form.Label> + <Form.Label column sm="15"> + </Form.Label> <Col className="col-class" sm="15"> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Old English"])} id={"L26"} label={"Old English"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Persian"])} id={"L27"} label={"Persian"}/> diff --git a/client/src/components/InterestOverTime.js b/client/src/components/InterestOverTime.js index ff6bea6..c74122c 100644 --- a/client/src/components/InterestOverTime.js +++ b/client/src/components/InterestOverTime.js @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; -import store from '../redux/store'; import { currentMovie } from '../redux/actions'; import { connect } from 'react-redux'; @@ -11,6 +10,9 @@ import { Chart } from 'react-google-charts'; * Component to get and show the Google search trend over the last week. * Param: title - movie title to search for * return: the component HTML as an Area chart. + * + * An error check is added so the chart gets the related data + * before it renders. This makes the site behave more natural. */ const InterestOverTime = ({ title }) => { const [chartData, setChartData] = useState(['Date', 'Intereset'], ['Temp', 10]); @@ -31,18 +33,18 @@ const InterestOverTime = ({ title }) => { return( <> - {!error ? - <Chart - width={300} - height={300} - chartType="AreaChart" - loader={<div>Loading chart</div>} - data={chartData} - options={{ - title: 'Interest over time on Google' - }} - /> - : <b>No data found on Google Trends</b> } + {!error ? + <Chart + width={300} + height={300} + chartType="AreaChart" + loader={<div>Loading chart</div>} + data={chartData} + options={{ + title: 'Interest over time on Google' + }} + /> + : <b>No data found on Google Trends</b> } </> ) } diff --git a/client/src/components/MovieDetail.css b/client/src/components/MovieDetail.css index eb04b36..518df62 100644 --- a/client/src/components/MovieDetail.css +++ b/client/src/components/MovieDetail.css @@ -2,7 +2,6 @@ display: inline; float: left; font-size: 100%; - /* font-size: 2.5vmax; */ } .list-group-item-grade { @@ -47,7 +46,6 @@ img { grid-area: 1 / 2 / 2 / 3; } - .movie-details-body-text-header h1 { display: inline; font-size: 16px; diff --git a/client/src/components/MovieDetail.js b/client/src/components/MovieDetail.js index 3963bda..c8612d6 100644 --- a/client/src/components/MovieDetail.js +++ b/client/src/components/MovieDetail.js @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { Accordion, Card } from 'react-bootstrap'; -import store from '../redux/store'; import { currentMovie } from '../redux/actions'; import { connect } from 'react-redux'; diff --git a/client/src/components/MovieDetailBody.js b/client/src/components/MovieDetailBody.js index 690d14d..824b80b 100644 --- a/client/src/components/MovieDetailBody.js +++ b/client/src/components/MovieDetailBody.js @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { Card } from 'react-bootstrap'; -import store from '../redux/store'; import { currentMovie } from '../redux/actions'; import { connect } from 'react-redux'; @@ -45,17 +44,19 @@ const MovieDetailBody = ({ title }) => { }); } - // Hook to update the selected user rating + // Hook to get the rating the current user has given the selected + // movie. useEffect(() => { let userRate = localStorage.getItem(title + "-Rating"); if (userRate !== null) { setRating(userRate); } - }, [rating]); + }, [rating, title]); + // When the current movie title changes in store, get the relevant + // movie detail from backend useEffect(() => { - console.log("Calling GetMovieDetail") axios.get('/api/GetMovieDetail', { params: { searchString: title diff --git a/client/src/components/PagePagination.js b/client/src/components/PagePagination.js index fd43ad2..e146b5f 100644 --- a/client/src/components/PagePagination.js +++ b/client/src/components/PagePagination.js @@ -1,6 +1,10 @@ import React from 'react'; import { Pagination } from 'react-bootstrap'; +/* + * Component to render the pagination. All functionality resides + * in App.js. + */ const PagePagination = ({ pages, activePage, changePage }) => { return( <Pagination> diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js index 9251654..24c8c69 100644 --- a/client/src/components/SortButton.js +++ b/client/src/components/SortButton.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { Dropdown, ButtonToolbar, Button } from 'react-bootstrap'; @@ -7,9 +7,14 @@ import './SortButton.css'; import { selectSort, selectOrder } from "../redux/actions"; import { SORT, ORDER } from "../constants"; +/* + * Component to render and handle the sort functionality. + */ const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder, getMovies }) => { - + // Updates the search result as sort is selected. setTimeout is implemented + // to simulate async functionality. Did not have time to develop a better + // solution const combinedFunction = input => { switch (input[0]) { case SORT.SORT: @@ -33,28 +38,45 @@ const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder, getMovie return( <div> <Dropdown > - <Dropdown.Toggle className="sort-button" variant="outline-secondary" id="dropdown-basic"> + <Dropdown.Toggle + className="sort-button" + variant="outline-secondary" + id="dropdown-basic" + > {activeSort} </Dropdown.Toggle> <Dropdown.Menu className="sort-button-drop"> - <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.TITLE])}}>{SORT.TITLE}</Dropdown.Item> - <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.YEAR])}}>{SORT.YEAR}</Dropdown.Item> - <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.BOX_OFFICE])}}>{SORT.BOX_OFFICE}</Dropdown.Item> - <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.IMDB_RATING])}}>{SORT.IMDB_RATING}</Dropdown.Item> - <Dropdown.Item onClick={() => {combinedFunction([SORT.SORT, SORT.USER_RATING])}}>{SORT.USER_RATING}</Dropdown.Item> + <Dropdown.Item + onClick={() => {combinedFunction([SORT.SORT, SORT.TITLE])}} + >{SORT.TITLE}</Dropdown.Item> + <Dropdown.Item + onClick={() => {combinedFunction([SORT.SORT, SORT.YEAR])}} + >{SORT.YEAR}</Dropdown.Item> + <Dropdown.Item + onClick={() => {combinedFunction([SORT.SORT, SORT.BOX_OFFICE])}} + >{SORT.BOX_OFFICE}</Dropdown.Item> + <Dropdown.Item + onClick={() => {combinedFunction([SORT.SORT, SORT.IMDB_RATING])}} + >{SORT.IMDB_RATING}</Dropdown.Item> + <Dropdown.Item + onClick={() => {combinedFunction([SORT.SORT, SORT.USER_RATING])}} + >{SORT.USER_RATING}</Dropdown.Item> </Dropdown.Menu> </Dropdown> <ButtonToolbar> - <Button className="sort-order-button" variant="outline-secondary" onClick={() => {combinedFunction([ORDER.ORDER, activeOrder])}}> - {activeOrder == "DESC" ? '↑' : '↓'} + <Button + className="sort-order-button" + variant="outline-secondary" + onClick={() => {combinedFunction([ORDER.ORDER, activeOrder])}} + > + {activeOrder === "DESC" ? '↑' : '↓'} </Button> </ButtonToolbar> </div> -) + ) } const mapStateToProps = state => { - console.log('state.sort', state.sort, 'state.order', state.sort); return { activeSort: state.sort, activeOrder: state.order }; }; diff --git a/client/src/index.js b/client/src/index.js index 93fc5d5..d279d2b 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -6,12 +6,12 @@ import * as serviceWorker from './serviceWorker'; import { Provider } from 'react-redux'; import store from './redux/store'; - ReactDOM.render( - <Provider store={store}> - <App /> - </Provider>, - document.getElementById('root') - ); +ReactDOM.render( + <Provider store={store}> + <App /> + </Provider>, + document.getElementById('root') +); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/server/routes/movies.js b/server/routes/movies.js index 1aa5188..3e63a6c 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -11,18 +11,16 @@ const Movies = require('../Schemas/Movies'); * searchString: searches for titles containing this string, * from: start index of search, * to: end index of search, - * sortType: the sort type. Get the current type from state - * sortOrder: the sort order. Get the current order from state - * filter: boolean if filter should be active - * filters: string of all filters structured: - * [ - * { FILTER_TYPE_1: VALUE_1 }, - * { FILTER_TYPE_2: VALUE_2 } - * ] + * sortType: the sort type. Get the current type from state, + * sortOrder: the sort order. Get the current order from state, + * filterLanguages: Array with all language filters, + * filterRatings: Array with all rated filters, + * filterGenres: Array with all genre filters * } * Returns: { * success: Boolean, - * data: Array of results + * data: Array of results, + * pages: no of pages with hits * } * * Gets all movie titles containing the searchString in title. @@ -176,6 +174,15 @@ function sorting(sortOrder, sortType) { /* * GET movie headers * + * Params: { + * Title: title of movie + * } + * Returns: { + * success: Boolean, + * data: the title and user rating of the title + * } + * + * This data is used int the result list */ router.get('/GetMovieHeaders', function(req, res) { var query = Movies.findOne({ "Title": req.query.Title }, { "Title": 1, "UserRating": 1 }); -- GitLab From 66a0c1bbaa1cf05eca173e0a41301c8a81f5b45e Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 24 Oct 2019 11:32:21 +0200 Subject: [PATCH 43/61] Fix wrongful state update in currentReducer. Ref. #16 --- .gitignore | 3 +++ client/src/redux/reducers/current.js | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73e8888..9a6c50f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ .env.production.local /client/src/List.txt /client/src/redux/reducers/frontpage.js +/client/src/components/Welcome.css +/client/src/components/Welcome.js + npm-debug.log* diff --git a/client/src/redux/reducers/current.js b/client/src/redux/reducers/current.js index 5371daa..baaf065 100644 --- a/client/src/redux/reducers/current.js +++ b/client/src/redux/reducers/current.js @@ -5,9 +5,10 @@ const initialState = { } function currentReducer(state = initialState, action) { + console.log("action:\t", action); if (action.type === CURRENT_MOVIE) { return{ - ...state, current_title: action.title + current_title: action.title }; } else { return state; -- GitLab From 49c3f9d568bf773178bb27ff0ce85d32d82133f5 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 12:05:53 +0200 Subject: [PATCH 44/61] Added welcome page and result count. Ref. #32 #24 --- client/src/App.css | 12 +++++++++++- client/src/App.js | 32 ++++++++++++++++++++++++-------- server/routes/movies.js | 2 +- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 4911261..8dd1a8f 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -8,10 +8,11 @@ 'Search Search' 'Filter Sort' 'FilterOptions FilterOptions' + 'Result Result' 'Content Content' 'Pagination Pagination'; grid-template-columns: 1fr 1fr; - grid-template-rows: 0.1fr 0.1fr 0.1fr 0.1fr 0.01fr 1.5fr; + grid-template-rows: 0.1fr 0.1fr 0.1fr 0.1fr 0.01fr 0.01fr 1.5fr 0.01fr; grid-gap: 1%; margin: 2% 5% 0 5%; } @@ -63,6 +64,15 @@ padding-bottom: 5%; } +.welcome-text { + font-size: 120%; +} + +.result-amount { + grid-area: Result; + padding-left: 90%; +} + .list-group-item { background-color: white; padding-left: 5%; diff --git a/client/src/App.js b/client/src/App.js index e260b09..e1f622a 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -87,7 +87,11 @@ class App extends Component { } }).then((res) => { if (res.data.success) { - this.setState({ movies: res.data.data, noOfPages: res.data.pages }); + this.setState({ + movies: res.data.data, + noOfPages: res.data.pages, + resultCount: res.data.hits + }); } else { this.setState({ error: res.data.error }); } @@ -143,13 +147,25 @@ class App extends Component { <Filter getMovies = {this.getMovies}/> </div> <div className="main-content"> - <Accordion> - {this.state.movies.map((movie, index) => { - return( - <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} /> - ) - })} - </Accordion> + { + this.state.movies.length === 0 ? + <div className="welcome-text"> + No results found. Search by typing a movie title in the searchbar. + </div> + : + <> + <div className="result-amount"> + Results: {this.state.resultCount} + </div> + <Accordion> + {this.state.movies.map((movie, index) => { + return( + <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} /> + ) + })} + </Accordion> + </> + } </div> <div className="pagination-wrapper"> <PagePagination diff --git a/server/routes/movies.js b/server/routes/movies.js index 3e63a6c..fb596d9 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -78,7 +78,7 @@ router.post('/GetMovieTitles', function(req, res) { if(isNaN(noOfPages)) noOfPages = 1; - return res.json({ success: true, data: data, pages: noOfPages }); + return res.json({ success: true, data: data, pages: noOfPages, hits: result }); }); } -- GitLab From 9686536a4903778ebaeb73801230f8fd7f13fb68 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 24 Oct 2019 12:21:23 +0200 Subject: [PATCH 45/61] Fix bug with handling of sort state in both backend and frontend. Ref. #16 --- client/src/components/SortButton.js | 2 +- client/src/redux/reducers/order.js | 2 +- server/routes/movies.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/SortButton.js b/client/src/components/SortButton.js index 24c8c69..c4eef5d 100644 --- a/client/src/components/SortButton.js +++ b/client/src/components/SortButton.js @@ -69,7 +69,7 @@ const SortButton = ({ activeSort, selectSort, activeOrder, selectOrder, getMovie variant="outline-secondary" onClick={() => {combinedFunction([ORDER.ORDER, activeOrder])}} > - {activeOrder === "DESC" ? '↑' : '↓'} + {activeOrder === "ASC" ? '↑' : '↓'} </Button> </ButtonToolbar> </div> diff --git a/client/src/redux/reducers/order.js b/client/src/redux/reducers/order.js index 4efb994..1f09cb7 100644 --- a/client/src/redux/reducers/order.js +++ b/client/src/redux/reducers/order.js @@ -1,7 +1,7 @@ import { SELECT_ORDER } from "../actions"; import { ORDER } from "../../constants"; -const initialState = ORDER.DESC; +const initialState = ORDER.ASC; const order = (state = initialState, action) => { switch (action.type) { diff --git a/server/routes/movies.js b/server/routes/movies.js index 3e63a6c..d5304da 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -142,10 +142,10 @@ function sorting(sortOrder, sortType) { switch(sortOrder) { case 'ASC': - sortOrder = -1; + sortOrder = 1; break; case 'DESC': - sortOrder = 1; + sortOrder = -1; break; default: sortOrder = 1; -- GitLab From 2785afd9ff8393948458623d83b5d20cd872a8b7 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 24 Oct 2019 13:07:52 +0200 Subject: [PATCH 46/61] Make filter window scrollable. Remove unneccesary prints in console. Ref. #27, #31 --- client/src/components/Filter.css | 4 ++-- client/src/components/Filter.js | 10 +++------- client/src/redux/actions.js | 2 +- client/src/redux/reducers/current.js | 1 - client/src/redux/reducers/filter.js | 2 +- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/client/src/components/Filter.css b/client/src/components/Filter.css index f15a6ad..e07ecd0 100644 --- a/client/src/components/Filter.css +++ b/client/src/components/Filter.css @@ -1,4 +1,4 @@ .col-class { - overflow-y: scroll; - height: 20vh; + overflow-y: auto; + height: 25vh; } diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js index 9b54acd..4ef10cf 100644 --- a/client/src/components/Filter.js +++ b/client/src/components/Filter.js @@ -5,6 +5,8 @@ import { connect } from 'react-redux'; import { filterRated, filterGenre, filterLanguage } from '../redux/actions'; import { FILTER_TYPE } from '../constants' +import './Filter.css' + /* * Component to render and handle functionality for the filters */ @@ -44,7 +46,7 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies} <Form.Label column sm="15"> <b>PG-Rating</b> </Form.Label> - <Col sm="15"> + <Col className="col-class" sm="15"> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG,"G"])} id={"PG1"} label={"G"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "PG"])} id={"PG2"} label={"PG"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "PG-13"])} id={"PG3"} label={"PG-13"}/> @@ -112,12 +114,6 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies} <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Nepali"])} id={"L23"} label={"Nepali"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Norwegian"])} id={"L24"} label={"Norwegian"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "North American Indian"])} id={"L25"} label={"North American Indian"}/> - </Col> - </Form.Group> - <Form.Group as={Col}> - <Form.Label column sm="15"> - </Form.Label> - <Col className="col-class" sm="15"> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Old English"])} id={"L26"} label={"Old English"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Persian"])} id={"L27"} label={"Persian"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.LANGUAGE, "Polish"])} id={"L28"} label={"Polish"}/> diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js index 9ac059c..5be76cb 100644 --- a/client/src/redux/actions.js +++ b/client/src/redux/actions.js @@ -12,7 +12,7 @@ export const selectSort = sortType => ({ }); export const selectOrder = orderType => { - if (orderType == ORDER.ASC){ + if (orderType === ORDER.ASC){ return ({ type: SELECT_ORDER, payload: { diff --git a/client/src/redux/reducers/current.js b/client/src/redux/reducers/current.js index baaf065..17025b2 100644 --- a/client/src/redux/reducers/current.js +++ b/client/src/redux/reducers/current.js @@ -5,7 +5,6 @@ const initialState = { } function currentReducer(state = initialState, action) { - console.log("action:\t", action); if (action.type === CURRENT_MOVIE) { return{ current_title: action.title diff --git a/client/src/redux/reducers/filter.js b/client/src/redux/reducers/filter.js index d10a83e..2b62eb0 100644 --- a/client/src/redux/reducers/filter.js +++ b/client/src/redux/reducers/filter.js @@ -1,4 +1,4 @@ -import { FILTER_IMDB, FILTER_RATED, FILTER_GENRE, FILTER_LANGUAGE, FILTER_STATE} from '../actions'; +import { FILTER_RATED, FILTER_GENRE, FILTER_LANGUAGE, FILTER_STATE} from '../actions'; // Set initialState for the filter -- GitLab From 0a11b06776a2fb97fa4a185fb1262cf6d397ea1c Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 24 Oct 2019 13:28:16 +0200 Subject: [PATCH 47/61] Remove unused PG-filter --- client/src/components/Filter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/Filter.js b/client/src/components/Filter.js index 4ef10cf..0c0b197 100644 --- a/client/src/components/Filter.js +++ b/client/src/components/Filter.js @@ -51,7 +51,6 @@ const Filter = ({PGActive, filterRated, filterLanguage, filterGenre, getMovies} <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "PG"])} id={"PG2"} label={"PG"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "PG-13"])} id={"PG3"} label={"PG-13"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG,"R"])} id={"PG4"} label={"R"}/> - <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "NC-17"])} id={"PG5"} label={"NC-17"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "Not Rated"])} id={"PG6"} label={"Not Rated"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "Passed"])} id={"PG7"} label={"Passed"}/> <Form.Check type={'checkbox'} onClick={ () =>combinedFunction([FILTER_TYPE.PG, "Approved"])} id={"PG8"} label={"Approved"}/> -- GitLab From c23d52d293a8de5b1a8e47e5852f0f4ac37fb2ea Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 14:45:39 +0200 Subject: [PATCH 48/61] Added CORS rules to the server --- server/server.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/server.js b/server/server.js index 9b7babd..5122991 100644 --- a/server/server.js +++ b/server/server.js @@ -21,5 +21,13 @@ db.once('open', () => console.log('Connected to database')); db.on('error', console.error.bind(console, 'MongoDB connection error: ')); app.use('/api', movieRouter); +app.use(function(req, res, next) { + res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); + res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + res.setHeader('Access-Control-Allow-Credentials', false); + + next(); +}) app.listen(API_PORT, () => console.log('Listening on port ', API_PORT)); -- GitLab From c0a0c908e0afa87a95ea6cab8f6df9a89e9259d0 Mon Sep 17 00:00:00 2001 From: asszewcz <asszewcz@stud.ntnu.no> Date: Thu, 24 Oct 2019 15:00:29 +0200 Subject: [PATCH 49/61] Add unit test and snapshot test to project. Ref #34. --- client/src/App.test.js | 10 ++- client/src/components/Filter.test.js | 11 +++ client/src/components/MovieDetail.test.js | 11 +++ client/src/components/SortButton.test.js | 11 +++ client/src/redux/actions.test.js | 64 ++++++++++++++++ client/src/redux/reducers/reducers.test.js | 88 ++++++++++++++++++++++ 6 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 client/src/components/Filter.test.js create mode 100644 client/src/components/MovieDetail.test.js create mode 100644 client/src/components/SortButton.test.js create mode 100644 client/src/redux/actions.test.js create mode 100644 client/src/redux/reducers/reducers.test.js diff --git a/client/src/App.test.js b/client/src/App.test.js index a754b20..7743a98 100644 --- a/client/src/App.test.js +++ b/client/src/App.test.js @@ -1,9 +1,17 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +import { Provider } from 'react-redux'; +import store from './redux/store'; +import renderer from 'react-test-renderer'; it('renders without crashing', () => { const div = document.createElement('div'); - ReactDOM.render(<App />, div); + ReactDOM.render(<Provider store={store}> <App /></Provider>, div); ReactDOM.unmountComponentAtNode(div); }); + +it('App renders correctly', () => { + const tree = renderer.create(<Provider store={store} ><App /></Provider>).toJSON(); + expect(tree).toMatchSnapshot(); +}) diff --git a/client/src/components/Filter.test.js b/client/src/components/Filter.test.js new file mode 100644 index 0000000..0ef6aef --- /dev/null +++ b/client/src/components/Filter.test.js @@ -0,0 +1,11 @@ +import React from 'react'; +import Filter from './Filter'; +import renderer from 'react-test-renderer'; +import { Provider } from 'react-redux'; +import store from '../redux/store'; + + +it('Filter renders correctly', () => { + const tree = renderer.create(<Provider store={store} ><Filter /></Provider>).toJSON(); + expect(tree).toMatchSnapshot(); +}) diff --git a/client/src/components/MovieDetail.test.js b/client/src/components/MovieDetail.test.js new file mode 100644 index 0000000..e16abb6 --- /dev/null +++ b/client/src/components/MovieDetail.test.js @@ -0,0 +1,11 @@ +import React from 'react'; +import MovieDetail from './MovieDetail'; +import renderer from 'react-test-renderer'; +import { Provider } from 'react-redux'; +import store from '../redux/store'; + + +it('Movie detail renders correctly', () => { + const tree = renderer.create(<Provider store={store} ><MovieDetail /></Provider>).toJSON(); + expect(tree).toMatchSnapshot(); +}) diff --git a/client/src/components/SortButton.test.js b/client/src/components/SortButton.test.js new file mode 100644 index 0000000..a8fe3d8 --- /dev/null +++ b/client/src/components/SortButton.test.js @@ -0,0 +1,11 @@ +import React from 'react'; +import SortButton from './SortButton'; +import renderer from 'react-test-renderer'; +import { Provider } from 'react-redux'; +import store from '../redux/store'; + + +it('Sort button renders correctly', () => { + const tree = renderer.create(<Provider store={store} ><SortButton /></Provider>).toJSON(); + expect(tree).toMatchSnapshot(); +}) diff --git a/client/src/redux/actions.test.js b/client/src/redux/actions.test.js new file mode 100644 index 0000000..f36b64f --- /dev/null +++ b/client/src/redux/actions.test.js @@ -0,0 +1,64 @@ +import * as actions from './actions'; +import { ORDER, SORT } from '../constants'; + +describe('acions', () => { + it('Should create an action to switch order', () => { + const expectedAction = { + type: actions.SELECT_ORDER, + payload: { + orderType: ORDER.DESC + } + } + expect(actions.selectOrder(ORDER.ASC)).toEqual(expectedAction); + }) + + it('Should create an action to change sort type', () => { + const expectedAction = { + type: actions.SELECT_SORT, + payload: { + sortType: SORT.TITLE + } + } + expect(actions.selectSort(SORT.TITLE)).toEqual(expectedAction); + }) + + it('Should create an action to change the filter state (toggle it)', () => { + const expectedAction = { + type: actions.FILTER_STATE, + bool: true + } + expect(actions.filterState(false)).toEqual(expectedAction); + }) + + it('Should create an action to change the filter rating', () => { + const expectedAction = { + type: actions.FILTER_RATED, + rating: 'R' + } + expect(actions.filterRated('R')).toEqual(expectedAction); + }) + + it('Should create an action to change the filter state (toggle it)', () => { + const expectedAction = { + type: actions.FILTER_GENRE, + genre: 'Crime' + } + expect(actions.filterGenre('Crime')).toEqual(expectedAction); + }) + + it('Should create an action to change a ', () => { + const expectedAction = { + type: actions.FILTER_LANGUAGE, + language: 'Norwegian' + } + expect(actions.filterLanguage('Norwegian')).toEqual(expectedAction); + }) + + it('Should create an action to set a current movie', () => { + const expectedAction = { + type: actions.CURRENT_MOVIE, + title: 'lorem ipsum' + } + expect(actions.currentMovie('lorem ipsum')).toEqual(expectedAction); + }) +}) diff --git a/client/src/redux/reducers/reducers.test.js b/client/src/redux/reducers/reducers.test.js new file mode 100644 index 0000000..1fdff51 --- /dev/null +++ b/client/src/redux/reducers/reducers.test.js @@ -0,0 +1,88 @@ +import order from './order'; +import sort from './sort'; +import currentReducer from './current'; +import filter from './filter'; +import { ORDER, SORT } from '../../constants'; +import * as actions from '../actions'; + +describe('order reducer', () => { + it('should return the initial state', () => { + expect(order(undefined, {})).toEqual(ORDER.ASC) + }) + + it('should handle an order change', () => { + expect(order(ORDER.DESC, { + type: actions.SELECT_ORDER, + payload: { + orderType: ORDER.DESC + } + })).toEqual(ORDER.DESC) + }) +}) + + + +describe('sort reducer', () => { + it('should return the initial state', () => { + expect(sort(undefined, {})).toEqual(SORT.TITLE) + }) + it('should handle a sort change', () => { + expect(sort(SORT.TITLE, { + type: actions.SELECT_SORT, + payload: { sortType: SORT.YEAR } + })).toEqual(SORT.YEAR) + }) +}) + + + + +describe('filter reducer', () => { + it('should return the initial state', () => { + expect(filter(undefined, {})).toEqual({ + filter_state : false, + filter_rated : [], + filter_language: [], + filter_genre:[] + }) + }) + it('should handle change in filters', () => { + expect(filter(undefined, { + type: actions.FILTER_RATED, + rating: 'PG' + })).toEqual({ + filter_state : false, + filter_rated : ['PG'], + filter_language: [], + filter_genre:[] + }) + expect(filter({ + filter_state : false, + filter_rated : ['PG'], + filter_language: [], + filter_genre:[] + }, { + type: actions.FILTER_RATED, + rating: 'R' + })).toEqual({ + filter_state : false, + filter_rated : ['PG', 'R'], + filter_language: [], + filter_genre:[] + }) + }) +}) + + +describe('current movie reducer', () => { + it('should return the initial state', () => { + expect(currentReducer(undefined, {})).toEqual({ + current_title : "" + }) + }) + it('should handle an input', () => { + expect(currentReducer({ current_title: "EARLIER" }, { title: "LATER", type: actions.CURRENT_MOVIE } )).toEqual({ + current_title: "LATER" + }) + }) +}) -- GitLab From f48a54384b28803600fb8c5f0efa8f7f16582f30 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 15:42:40 +0200 Subject: [PATCH 50/61] Try to set up deployment --- server/routes/movies.js | 14 +++++++------- server/server.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/routes/movies.js b/server/routes/movies.js index a2c928e..b0cdf35 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -26,7 +26,7 @@ const Movies = require('../Schemas/Movies'); * Gets all movie titles containing the searchString in title. * Only returns the number of hits wanted */ -router.post('/GetMovieTitles', function(req, res) { +router.post('/GetMovieTitles', function(req, res, next) { if(req.query.filterLanguages === undefined) req.query.filterLanguages = []; if(req.query.filterGenres === undefined) @@ -78,7 +78,7 @@ router.post('/GetMovieTitles', function(req, res) { if(isNaN(noOfPages)) noOfPages = 1; - return res.json({ success: true, data: data, pages: noOfPages, hits: result }); + return res.json({ success: true, data: data, pages: noOfPages }); }); } @@ -142,10 +142,10 @@ function sorting(sortOrder, sortType) { switch(sortOrder) { case 'ASC': - sortOrder = 1; + sortOrder = -1; break; case 'DESC': - sortOrder = -1; + sortOrder = 1; break; default: sortOrder = 1; @@ -184,7 +184,7 @@ function sorting(sortOrder, sortType) { * * This data is used int the result list */ -router.get('/GetMovieHeaders', function(req, res) { +router.get('/GetMovieHeaders', function(req, res, next) { var query = Movies.findOne({ "Title": req.query.Title }, { "Title": 1, "UserRating": 1 }); query.exec(function(err, data) { @@ -210,7 +210,7 @@ router.get('/GetMovieHeaders', function(req, res) { * * Update the collection in DB with new ratings and average */ -router.post('/UpdateUserRating', function(req, res) { +router.post('/UpdateUserRating', function(req, res, next) { Movies.find({ "Title" : req.query.Title }, function(err, data) { if (err) return res.json({ success: false, error: err }); @@ -254,7 +254,7 @@ router.post('/UpdateUserRating', function(req, res) { * Uses the Google Trends API to get the interest over time on the * selected movie title. Used to get the graph view on each movie. */ -router.get('/MovieTrend', function(req, res) { +router.get('/MovieTrend', function(req, res, next) { // Get the date 7 days ago. var days = 7; var date = new Date(); diff --git a/server/server.js b/server/server.js index 5122991..a5659f0 100644 --- a/server/server.js +++ b/server/server.js @@ -23,9 +23,9 @@ db.on('error', console.error.bind(console, 'MongoDB connection error: ')); app.use('/api', movieRouter); app.use(function(req, res, next) { res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); + res.setHeader('Access-Control-Allow-Origin', 'http://it2810-29.idi.ntnu.no:80'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); - res.setHeader('Access-Control-Allow-Credentials', false); next(); }) -- GitLab From c4f93d0dfbfd12bfb9514b19ddb53a14f3376bc6 Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Thu, 24 Oct 2019 15:57:02 +0200 Subject: [PATCH 51/61] Install cypress, and set up basic tests. Ref. #35 --- client/cypress.json | 3 + client/cypress/fixtures/example.json | 5 + client/cypress/integration/home_page_spec.js | 59 ++ client/cypress/plugins/index.js | 17 + client/cypress/support/commands.js | 25 + client/cypress/support/index.js | 20 + client/package-lock.json | 854 +++++++++++++++++++ client/package.json | 1 + 8 files changed, 984 insertions(+) create mode 100644 client/cypress.json create mode 100644 client/cypress/fixtures/example.json create mode 100644 client/cypress/integration/home_page_spec.js create mode 100644 client/cypress/plugins/index.js create mode 100644 client/cypress/support/commands.js create mode 100644 client/cypress/support/index.js diff --git a/client/cypress.json b/client/cypress.json new file mode 100644 index 0000000..a03016d --- /dev/null +++ b/client/cypress.json @@ -0,0 +1,3 @@ +{ +"baseUrl": "http://localhost:3000" +} diff --git a/client/cypress/fixtures/example.json b/client/cypress/fixtures/example.json new file mode 100644 index 0000000..da18d93 --- /dev/null +++ b/client/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/client/cypress/integration/home_page_spec.js b/client/cypress/integration/home_page_spec.js new file mode 100644 index 0000000..eace4ba --- /dev/null +++ b/client/cypress/integration/home_page_spec.js @@ -0,0 +1,59 @@ +describe('The Home Page', function() { + beforeEach(function () { + // reset and seed the database prior to every test + cy.exec('npm start') + }) + it('successfully loads', function() { + cy.visit('/') // change URL to match your dev URL + }) +}) + + +describe('On Load', function() { + it('welcome screen showing', function() { + expect(cy.get('.welcome-text')).to.exist + }) + it('search bar focused', function() { + assert(cy.focused()) + }) +}) + + +describe('Search bar functionality', function() { + it('write "s" in search bar', function() { + cy.focused().type("s") + }) + it('clear search bar', function() { + cy.focused().type("{backspace}") + }) + it('Result #', function() { + cy.get('.result-amount').contains("Results: 247") + }) + // TODO: get +}) + +describe('Result on search', function() { + it('write "Shawshank" in search bar', function() { + cy.focused().type("Shawshank") + }) + it('Result #', function() { + cy.get('.result-amount').contains("Results: 1") + }) + it('"Shawshank Redemption" found', function() { + cy.get('.list-group-item-title').contains("Shawshank Redemption") + }) +}) + +// TODO: if starratingclick() => state update in localstorage +// Expanding Moviedetails on onClick +// Sort functionality - though prop-state +// search that gives 2/3 results, check that order reverses? order - arrow direction +// +// Filter functionality - though prop-state +// + +// .to.be("Result: 247") + + +// ).is.equal(cy.get('[type="text"]')) +// cy.get('[type="checkbox"]').check() diff --git a/client/cypress/plugins/index.js b/client/cypress/plugins/index.js new file mode 100644 index 0000000..fd170fb --- /dev/null +++ b/client/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js new file mode 100644 index 0000000..ca4d256 --- /dev/null +++ b/client/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/client/cypress/support/index.js b/client/cypress/support/index.js new file mode 100644 index 0000000..d68db96 --- /dev/null +++ b/client/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/client/package-lock.json b/client/package-lock.json index b49672e..868e36a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -925,6 +925,104 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-9.0.1.tgz", "integrity": "sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA==" }, + "@cypress/listr-verbose-renderer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", + "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=", + "requires": { + "chalk": "^1.1.3", + "cli-cursor": "^1.0.2", + "date-fns": "^1.27.2", + "figures": "^1.7.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "requires": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, "@hapi/address": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.2.tgz", @@ -1364,6 +1462,11 @@ "csstype": "^2.2.0" } }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==" + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -1731,6 +1834,11 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "arch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", + "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2576,6 +2684,11 @@ "isarray": "^1.0.0" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -2639,6 +2752,14 @@ "unset-value": "^1.0.0" } }, + "cachedir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-1.3.0.tgz", + "integrity": "sha512-O1ji32oyON9laVPJL1IZ5bmwd2cB46VfpxkDequezH+15FDzzVddEyrGEeX4WusDSqKxdyFdDQDEG1yo1GoWkg==", + "requires": { + "os-homedir": "^1.0.1" + } + }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", @@ -2728,6 +2849,11 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=" + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -3330,6 +3456,58 @@ "restore-cursor": "^2.0.0" } }, + "cli-spinners": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz", + "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=" + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "requires": { + "slice-ansi": "0.0.4", + "string-width": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -3991,6 +4169,115 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, + "cypress": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-3.5.0.tgz", + "integrity": "sha512-I1iSReD2C8CTP6s4BvQky4gEqHBnKLmhBIqFyCUZdj6BQ6ZDxGnmIbQPM5g79E2iP60KTIbTK99ZPSDVtsNUUg==", + "requires": { + "@cypress/listr-verbose-renderer": "0.4.1", + "@cypress/xvfb": "1.2.4", + "@types/sizzle": "2.3.2", + "arch": "2.1.1", + "bluebird": "3.5.0", + "cachedir": "1.3.0", + "chalk": "2.4.2", + "check-more-types": "2.24.0", + "commander": "2.15.1", + "common-tags": "1.8.0", + "debug": "3.2.6", + "execa": "0.10.0", + "executable": "4.1.1", + "extract-zip": "1.6.7", + "fs-extra": "5.0.0", + "getos": "3.1.1", + "is-ci": "1.2.1", + "is-installed-globally": "0.1.0", + "lazy-ass": "1.6.0", + "listr": "0.12.0", + "lodash": "4.17.15", + "log-symbols": "2.2.0", + "minimist": "1.2.0", + "moment": "2.24.0", + "ramda": "0.24.1", + "request": "2.88.0", + "request-progress": "3.0.0", + "supports-color": "5.5.0", + "tmp": "0.1.0", + "untildify": "3.0.3", + "url": "0.11.0", + "yauzl": "2.10.0" + }, + "dependencies": { + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "requires": { + "ci-info": "^1.5.0" + } + }, + "tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "requires": { + "rimraf": "^2.6.3" + } + } + } + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -4035,6 +4322,11 @@ } } }, + "date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==" + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -4425,6 +4717,11 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.273.tgz", "integrity": "sha512-0kUppiHQvHEENHh+nTtvTt4eXMwcPyWmMaj73GPrSEm3ldKhmmHuOH6IjrmuW6YmyS/fpXcLvMQLNVpqRhpNWw==" }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=" + }, "elliptic": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", @@ -5068,11 +5365,31 @@ "strip-eof": "^1.0.0" } }, + "executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "requires": { + "pify": "^2.2.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -5291,6 +5608,40 @@ } } }, + "extract-zip": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "requires": { + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", + "yauzl": "2.4.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "requires": { + "fd-slicer": "~1.0.1" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -5361,6 +5712,14 @@ "bser": "^2.0.0" } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "requires": { + "pend": "~1.2.0" + } + }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -5725,6 +6084,24 @@ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, + "getos": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.1.tgz", + "integrity": "sha512-oUP1rnEhAr97rkitiszGP9EgDVYnmchgFzfqRzSkgtfv7ai6tEi7Ko8GgjNXts7VLWEqrTWyhsOKLe5C5b/Zkg==", + "requires": { + "async": "2.6.1" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + } + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -5759,6 +6136,14 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "requires": { + "ini": "^1.3.4" + } + }, "global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -6276,6 +6661,14 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, "indexes-of": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", @@ -6469,6 +6862,14 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -6487,6 +6888,15 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -7858,6 +8268,11 @@ "webpack-sources": "^1.1.0" } }, + "lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=" + }, "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", @@ -7890,6 +8305,254 @@ "type-check": "~0.3.2" } }, + "listr": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.12.0.tgz", + "integrity": "sha1-a84sD1YD+klYDqF81qAMwOX6RRo=", + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "figures": "^1.7.0", + "indent-string": "^2.1.0", + "is-promise": "^2.1.0", + "is-stream": "^1.1.0", + "listr-silent-renderer": "^1.1.1", + "listr-update-renderer": "^0.2.0", + "listr-verbose-renderer": "^0.4.0", + "log-symbols": "^1.0.2", + "log-update": "^1.0.2", + "ora": "^0.2.3", + "p-map": "^1.1.1", + "rxjs": "^5.0.0-beta.11", + "stream-to-observable": "^0.1.0", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "requires": { + "chalk": "^1.0.0" + } + }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "requires": { + "symbol-observable": "1.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + } + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=" + }, + "listr-update-renderer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz", + "integrity": "sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk=", + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", + "log-symbols": "^1.0.2", + "log-update": "^1.0.2", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "requires": { + "chalk": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "listr-verbose-renderer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", + "integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=", + "requires": { + "chalk": "^1.1.3", + "cli-cursor": "^1.0.2", + "date-fns": "^1.27.2", + "figures": "^1.7.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -7996,6 +8659,11 @@ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8028,6 +8696,52 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } + }, + "log-update": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", + "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=", + "requires": { + "ansi-escapes": "^1.0.0", + "cli-cursor": "^1.0.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + } + } + }, "loglevel": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.4.tgz", @@ -8374,6 +9088,11 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -8890,6 +9609,76 @@ "wordwrap": "~1.0.0" } }, + "ora": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz", + "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=", + "requires": { + "chalk": "^1.1.1", + "cli-cursor": "^1.0.2", + "cli-spinners": "^0.1.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", @@ -8903,6 +9692,11 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, "os-locale": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", @@ -9131,6 +9925,11 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -10317,6 +11116,11 @@ "performance-now": "^2.1.0" } }, + "ramda": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz", + "integrity": "sha1-w7d1UZfzW43DUCIoJixMkd22uFc=" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10876,6 +11680,14 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -10919,6 +11731,14 @@ } } }, + "request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", + "requires": { + "throttleit": "^1.0.0" + } + }, "request-promise-core": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", @@ -11904,6 +12724,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, + "stream-to-observable": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.1.0.tgz", + "integrity": "sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4=" + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -12203,6 +13028,11 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=" }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -12548,6 +13378,11 @@ } } }, + "untildify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", + "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==" + }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -13405,6 +14240,25 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + }, + "dependencies": { + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + } + } } } } diff --git a/client/package.json b/client/package.json index b09a007..3e1df70 100644 --- a/client/package.json +++ b/client/package.json @@ -5,6 +5,7 @@ "dependencies": { "axios": "^0.19.0", "bootstrap": "^4.3.1", + "cypress": "^3.5.0", "google-trends-api": "^4.9.0", "react": "^16.10.1", "react-bootstrap": "^1.0.0-beta.14", -- GitLab From 4deaf82a679d014f5e1ceb565bb2af8652e8f3a3 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 16:20:24 +0200 Subject: [PATCH 52/61] Try to get deploment working --- client/package.json | 2 +- server/server.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index b09a007..db84eaa 100644 --- a/client/package.json +++ b/client/package.json @@ -35,5 +35,5 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:3001" + "proxy": "http://it2810-29.idi.ntnu.no:3001" } diff --git a/server/server.js b/server/server.js index a5659f0..76dc613 100644 --- a/server/server.js +++ b/server/server.js @@ -23,6 +23,7 @@ db.on('error', console.error.bind(console, 'MongoDB connection error: ')); app.use('/api', movieRouter); app.use(function(req, res, next) { res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); + res.setHeader('Access-Control-Allow-Origin', 'http://it2810-29.idi.ntnu.no:3000'); res.setHeader('Access-Control-Allow-Origin', 'http://it2810-29.idi.ntnu.no:80'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); -- GitLab From 24c7c573834f024245f9c7583505807a9c8301d5 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 16:30:27 +0200 Subject: [PATCH 53/61] Trying different port --- client/src/App.js | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/client/src/App.js b/client/src/App.js index e1f622a..8129f95 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -74,7 +74,7 @@ class App extends Component { let filterRatingsArr = Array.from(this.props.filter_ratings); let filterGenresArr = Array.from(this.props.filter_genres); - axios.post('/api/GetMovieTitles', null, { + axios.post(':3001/api/GetMovieTitles', null, { params: { searchString: this.searchString, from: this.from, @@ -87,11 +87,7 @@ class App extends Component { } }).then((res) => { if (res.data.success) { - this.setState({ - movies: res.data.data, - noOfPages: res.data.pages, - resultCount: res.data.hits - }); + this.setState({ movies: res.data.data, noOfPages: res.data.pages }); } else { this.setState({ error: res.data.error }); } @@ -147,25 +143,13 @@ class App extends Component { <Filter getMovies = {this.getMovies}/> </div> <div className="main-content"> - { - this.state.movies.length === 0 ? - <div className="welcome-text"> - No results found. Search by typing a movie title in the searchbar. - </div> - : - <> - <div className="result-amount"> - Results: {this.state.resultCount} - </div> - <Accordion> - {this.state.movies.map((movie, index) => { - return( - <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} /> - ) - })} - </Accordion> - </> - } + <Accordion> + {this.state.movies.map((movie, index) => { + return( + <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} /> + ) + })} + </Accordion> </div> <div className="pagination-wrapper"> <PagePagination -- GitLab From 36c62f10c267ae2503656832fc0d0b91dd91bcb6 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 17:05:34 +0200 Subject: [PATCH 54/61] Final deployment fixes. Build now works --- client/package.json | 3 ++- client/src/App.js | 2 +- server/package-lock.json | 14 ++++++++++++++ server/package.json | 1 + server/server.js | 11 ++--------- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/client/package.json b/client/package.json index db84eaa..23e9c76 100644 --- a/client/package.json +++ b/client/package.json @@ -35,5 +35,6 @@ "last 1 safari version" ] }, - "proxy": "http://it2810-29.idi.ntnu.no:3001" + "proxy": "http://it2810-29.idi.ntnu.no:3001", + "homepage": "http://it2810-29.idi.ntnu.no/prosjekt3" } diff --git a/client/src/App.js b/client/src/App.js index 8129f95..e260b09 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -74,7 +74,7 @@ class App extends Component { let filterRatingsArr = Array.from(this.props.filter_ratings); let filterGenresArr = Array.from(this.props.filter_genres); - axios.post(':3001/api/GetMovieTitles', null, { + axios.post('/api/GetMovieTitles', null, { params: { searchString: this.searchString, from: this.from, diff --git a/server/package-lock.json b/server/package-lock.json index 6632d14..ec6b072 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -188,6 +188,15 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "css": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", @@ -544,6 +553,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/server/package.json b/server/package.json index aafcbc6..dbd93da 100644 --- a/server/package.json +++ b/server/package.json @@ -7,6 +7,7 @@ }, "dependencies": { "cookie-parser": "~1.4.3", + "cors": "^2.8.5", "debug": "~2.6.9", "express": "~4.16.0", "google-trends-api": "^4.9.0", diff --git a/server/server.js b/server/server.js index 76dc613..14f65bc 100644 --- a/server/server.js +++ b/server/server.js @@ -1,5 +1,6 @@ const express = require('express'); const mongoose = require('mongoose'); +const cors = require('cors'); var movieRouter = require('./routes/movies'); @@ -20,15 +21,7 @@ db.once('open', () => console.log('Connected to database')); db.on('error', console.error.bind(console, 'MongoDB connection error: ')); +app.use(cors()); app.use('/api', movieRouter); -app.use(function(req, res, next) { - res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); - res.setHeader('Access-Control-Allow-Origin', 'http://it2810-29.idi.ntnu.no:3000'); - res.setHeader('Access-Control-Allow-Origin', 'http://it2810-29.idi.ntnu.no:80'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); - res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); - - next(); -}) app.listen(API_PORT, () => console.log('Listening on port ', API_PORT)); -- GitLab From 9783f09ac665d63ff1473ac7104bbd05727ef7b6 Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 17:13:15 +0200 Subject: [PATCH 55/61] Fixed axios baseURL --- client/src/App.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/App.js b/client/src/App.js index e260b09..8652534 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -30,6 +30,8 @@ class App extends Component { constructor(props) { super(props); + axios.defaults.baseURL = 'http://it2810-29.idi.ntnu.no:3001'; + this.state = { movies: [], error: "", -- GitLab From 6130fbd1053448bee2d7735222a8a1a620a9da9c Mon Sep 17 00:00:00 2001 From: Rolf Erik Sesseng Aas <reaas@stud.ntnu.no> Date: Thu, 24 Oct 2019 19:18:19 +0200 Subject: [PATCH 56/61] Update README.md --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 923e2f4..dcff12d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,53 @@ +# Documentation +### REST API +The group decided early to develop a REST API using `express` and `mongoose` with a MongoDB database. This because a group member already had expreience using this, and an API with several, distinct, end points, is more perferrable than a `GraphQL` backend in our opinion. The code is naturally more readable and the functionality is easier to understand for someone with less experience in backend programming. + +The backend is made such that most of the data handling and computation is done on the server instead of on the client. This to make the site more snappy and faster. +The content is dynamically loaded to the client as the user navigates on the site. The search result is updated live when the user types in the search box, or when a filter or sort is applied. Only 10 results are loaded for each change, and the 10 results are updated when navigating to a different page. The downside of this is that the number of calls to the database is huge. One optimization could have been to store the initial result set in the back end and return parts of it according to the inputs from frontend. This could easily have been utilized with sort and filter, and only have a database call when changing the search input. + +The group tried to minimalize the amount of data sent from server to client by dividing the backend into several different end points that handles a sall query each. + +### CSS and react-bootstrap +Since the project allowed us to use third party packages, we felt `react-bootstrap` was a must. This allowed us to focus on functionality instead of component creation. This package contains numerous components that are already designed and functionality is easy to implement. Among others we used +```javascript +Button +Dropdown +Accordion +Pagination +``` +from `react-bootstrap`. The group felt these components was a natural choice based on our own experience to enhance UX. + +As for our CSS we enjoyed making the site responisve even though this was not an requirement. It should work properly on both desktop, laptops and mobile devices. The site is not optimized for tablets, but should be usable nontheless. We used CSSs GridLayout since this was utilized in project 2, and the group already had experience with it. + +For the user rating stars we used [Material Icons](https://material.io/resources/icons/?style=baseline). These icons are very beautiful and easy to use. + +### General design and layout +We heavily relied on [Komplett.no](https://www.komplett.no)s filter and sort functionality as inspiration. We all felt this was an intuitive way of handling this. No filters selected behaves exactly like if all filters are selected. The filter functionality is inclusive or. When two filters are selected, the user will get the results of either one of them. Since the user can filter on official age ratings, it's intuitive to get movies that are either R rated or G rated when selecting these filters. No movie is rated both R and G. We decided to have this functionality on all three filter methods as consistency is important for good UX-design. The filter component also includes scrollable elements to minimize the size of it. +Due to time constraints, the rendering of each checkbox is hard coded. If time had allowed it, we would have printed these out using some sort of iteration to make the code more clean and understandable. + +We limited the number of results per page to 10 as this did not make the site too crowded. This way we could also show off our pagination more frequently. We chose to use `Accordion` from `react-bootstrap` to show the search results. This makes the result list clear and consistant, and easy to use. When the result is clicked the `Accordion` opens and details about the selected movie is shown. The poster is taken from Amazons webservice via. a URL we got from the source of the data ([discussed below](#external-sources)). The Google Trends Chart is discussed [here](#advanced-view). + +Dropdown was chosen for the sort functionality as the user should only be able to select one. Radio-buttons would have made the site too crowded. The sort order is a single button which alternates between two states, ascending and descending. + +We chose a pagination system instead of dynamic scrolling as this reduces the load on the client side. If the user scrolls far enough down, the data on the client will grow and the site will become slower and slower. A pagination system is preferable since the amount of data on the client is restricted by the server. It also looks better according to us. The system itself is purly `react-bootstrap` with added functionality. Same as with the filter component, the actual render if the pagination could have been handled better. This was not improved as time ran out. + +### Advanced view +One of the requirments was to have some sort of advanced view using third party npm packages. We decided to use the `google-trends-api` to get the [Google Trends](https://trends.google.com) data for the selected movie title. This API call returns a `JSON`-object containing date and amount of searches that day. After some restructuring of the data we passed it to the `Chart` component from the `react-google-charts` package. As both of these packages are developed by Google, they are easy to understand and easy to use. + +We also included the number of results of the given query above the result list, just to show the user exactly how many movies where mached. + +### Deployment +At first we had some problems with deployment as the client tried to send API request to the wrong port. This was solved by setting `axios.defaults.baseURL` in `App.js` to the REST API URL. We then allowed CORS headers to each call, and everything went well. +The API is run on a detached screen using `screen` on Ubuntu. This way you can logout and the server will keep running as long as the VM is. + +### External sources +Since we decided to have a movie database, the natural choice of inspiration was [IMDb](https://www-imdb.com), however the API provided is pay to use. We scoured the web and stumbled upon [OMDb](https://omdbapi.com/) which is a free to use movie API with restructions on the number of calls per hour. The task required us to have our own database, so we wrote a webscraper that gathered the output from [OMDb](https://omdbapi.com/) on 250 movie titles, and stored it in a `.json` file. This we imported to our own MongoDB collection. + + +--- +--- +--- + # Project3 ## Install ```bash -- GitLab From 073be0badda143ac2d4ade7410ca005b4863660c Mon Sep 17 00:00:00 2001 From: Sigurd Augdal <sigurdra@stud.ntnu.no> Date: Fri, 25 Oct 2019 15:09:24 +0200 Subject: [PATCH 57/61] Add more tests in cypress. Ref. #35 --- client/cypress/integration/home_page_spec.js | 113 +++++++++++++++++-- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/client/cypress/integration/home_page_spec.js b/client/cypress/integration/home_page_spec.js index eace4ba..1369b78 100644 --- a/client/cypress/integration/home_page_spec.js +++ b/client/cypress/integration/home_page_spec.js @@ -1,3 +1,5 @@ +import { SORT } from '../../src/constants' + describe('The Home Page', function() { beforeEach(function () { // reset and seed the database prior to every test @@ -11,15 +13,15 @@ describe('The Home Page', function() { describe('On Load', function() { it('welcome screen showing', function() { - expect(cy.get('.welcome-text')).to.exist + cy.get('.welcome-text').should('exist') }) it('search bar focused', function() { - assert(cy.focused()) + cy.focused().should('match', '.search-bar') }) }) -describe('Search bar functionality', function() { +describe('Basic search bar functionality', function() { it('write "s" in search bar', function() { cy.focused().type("s") }) @@ -42,18 +44,107 @@ describe('Result on search', function() { it('"Shawshank Redemption" found', function() { cy.get('.list-group-item-title').contains("Shawshank Redemption") }) + it('clear search bar', function() { + cy.focused().clear() + }) +}) + +describe('Filter functionality 1', function() { + it('Toggle filter button', function() { + cy.get('.filter-button').click() + }) + it('toggle filter checkboxes for G rating, Animation and Spanish ', function() { + cy.get('[id="PG1"]').check() + cy.get('[id="G2"]').check() + cy.get('[id="L37"]').check() + }) + it('"Toy Story 3" listed', function() { + cy.contains('Toy Story 3').should('exist') + }) + it('"Shawshank Redemption" not listed', function() { + cy.contains('Shawshank Redemption').should('not.exist') + }) + it('Reset window', function() { + cy.get('[id="PG1"]').uncheck() + cy.get('[id="G2"]').uncheck() + cy.get('[id="L37"]').uncheck() + cy.get('.search-bar').focus() + cy.focused().clear() + }) +}) + + +describe('Filter functionality 2', function() { + it('toggle filter checkboxes for G rating, Animation and Spanish ', function() { + cy.get('[id="PG1"]').check() + cy.get('[id="G2"]').check() + cy.get('[id="L37"]').check() + }) + it('"Toy Story 3" listed', function() { + cy.contains('Toy Story 3').should('exist') + }) + it('"Shawshank Redemption" not listed', function() { + cy.contains('Shawshank Redemption').should('not.exist') + }) + it('Reset', function() { + cy.get('[id="PG1"]').uncheck() + cy.get('[id="G2"]').uncheck() + cy.get('[id="L37"]').uncheck() + cy.get('.search-bar').focus() + cy.focused().clear() + }) +}) + + + + +describe('Change in order', function() { + it('changes search', function() { + cy.focused().type('s') + cy.contains("12 Years a Slave").should('to.exist') + }) + + it('press order button', function() { + cy.contains("Witness for the Prosecution").should('not.exist') + cy.get('.sort-order-button').click() + }) + + it('has changed order', function() { + cy.get('.list-group-item-title') + cy.contains("Witness for the Prosecution").should('exist') + }) + + it('Reset', function() { + cy.get('.search-bar').focus() + cy.focused().clear() + cy.get('.sort-order-button').click() + }) +}) + +describe('Change in sort', function() { + it('receives input', function() { + cy.get('.search-bar').focus() + cy.focused().type('a') + }) + + it('changes sort', function() { + cy.get('.sort-button').click() + cy.contains(SORT.YEAR).click() + cy.get('.sort-button').contains(SORT.YEAR) + }) + + it('sorts the content', function() { + cy.contains('The General').should('exist') + cy.contains('12 Years a Slave').should('not.exist') + }) }) + + + + // TODO: if starratingclick() => state update in localstorage // Expanding Moviedetails on onClick // Sort functionality - though prop-state // search that gives 2/3 results, check that order reverses? order - arrow direction // -// Filter functionality - though prop-state -// - -// .to.be("Result: 247") - - -// ).is.equal(cy.get('[type="text"]')) -// cy.get('[type="checkbox"]').check() -- GitLab From 2ee2f43a7102737794254f0a0ce7ccaa3f7c67b6 Mon Sep 17 00:00:00 2001 From: Alexander Stensland Iversen Szewczyk <asszewcz@stud.ntnu.no> Date: Fri, 25 Oct 2019 16:50:36 +0200 Subject: [PATCH 58/61] Update README.md test --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index dcff12d..7ac4496 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,18 @@ The API is run on a detached screen using `screen` on Ubuntu. This way you can l ### External sources Since we decided to have a movie database, the natural choice of inspiration was [IMDb](https://www-imdb.com), however the API provided is pay to use. We scoured the web and stumbled upon [OMDb](https://omdbapi.com/) which is a free to use movie API with restructions on the number of calls per hour. The task required us to have our own database, so we wrote a webscraper that gathered the output from [OMDb](https://omdbapi.com/) on 250 movie titles, and stored it in a `.json` file. This we imported to our own MongoDB collection. +### Testing +In this project we have used `jest` for unit and snapshot-testing, and `cypress` for the end-to-end testing (E2E). + +##### Unit testing +The unit testing in this project primarily focus on our implementation of `redux`. This is due to most of the functionality being handled in our `reducers` and `actions`. As an example we could look at the tests written in `actions.test.js`. An action in redux usually takes in a parameter, handles it in some way, and returns the desired json-object to be handled by a reducer. Testing an action is therefore fairly simple, as we only need to feed it a logical argument, and see check that the action returns the wanted json-object. + +##### Snapshot testing +Snapshot testing is done on the main component `App`, and also on any sub-component. The tests can be found alongside each of their component files. + +##### End-to-End testing +E2E is done using the `cypress` testing tools in order to achieve a systematic test of the functionality and flow of the webpage. The tests focus on the main things that the webpage offer; searching, filtering, ordering and sorting movies. Each result page only houses 10 results, so we could search something very general, like the letter 's', and sort by year to check if a very old movie pops up, if the order button is pressed, and the movie is still there, it would tell us that ordering might not be correct since it should be on the later pages. E2E testing code is located in `client/cypress/integration/home_page_spec.js`. + --- --- @@ -147,3 +159,14 @@ Response: String, UserRatings: Array, UserRating: Number ``` + +### Running tests +In order to run the `unit` and `snapshot` tests use +```javascript +npm test +``` + +For `end-to-end` testing use +```javascript +npx cypress open +``` -- GitLab From a5b1b8a225a3399c461bcc5b0027db98ab1a108f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20R=C3=B8stad=20Augdal?= <sigurdra@stud.ntnu.no> Date: Fri, 25 Oct 2019 18:18:27 +0200 Subject: [PATCH 59/61] Update README.md Added redux documentation --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7ac4496..228bd1f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ Snapshot testing is done on the main component `App`, and also on any sub-compon ##### End-to-End testing E2E is done using the `cypress` testing tools in order to achieve a systematic test of the functionality and flow of the webpage. The tests focus on the main things that the webpage offer; searching, filtering, ordering and sorting movies. Each result page only houses 10 results, so we could search something very general, like the letter 's', and sort by year to check if a very old movie pops up, if the order button is pressed, and the movie is still there, it would tell us that ordering might not be correct since it should be on the later pages. E2E testing code is located in `client/cypress/integration/home_page_spec.js`. +### Redux +We had no experience in using state management tools, so we did some reasearch on both `Redux` and `Mobx` before deciding to use the former as a data store in our project. Since both filter and sort functionality was required for the web site we implemented the state for both these components in our store, as well as the activation state of the filter and the string used to search. We found it to be sufficient to store these states in redux, as the remaining states are not needed outside of the App component, and we wanted to reduce the number of states stored in redux. + +The redux implementation follows the standard setup, with action creators prompting the store for state changes in the store, and reducers for each state that handles the change. We did make good use of the "mapStateToProps" for example to feed the state directly into the RestAPI calls. --- --- -- GitLab From 9f3bbe4329321e78a77d8aaf344985c660960976 Mon Sep 17 00:00:00 2001 From: Rolf Erik Sesseng Aas <reaas@stud.ntnu.no> Date: Fri, 25 Oct 2019 18:45:25 +0200 Subject: [PATCH 60/61] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 228bd1f..89261f7 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Dropdown was chosen for the sort functionality as the user should only be able t We chose a pagination system instead of dynamic scrolling as this reduces the load on the client side. If the user scrolls far enough down, the data on the client will grow and the site will become slower and slower. A pagination system is preferable since the amount of data on the client is restricted by the server. It also looks better according to us. The system itself is purly `react-bootstrap` with added functionality. Same as with the filter component, the actual render if the pagination could have been handled better. This was not improved as time ran out. ### Advanced view -One of the requirments was to have some sort of advanced view using third party npm packages. We decided to use the `google-trends-api` to get the [Google Trends](https://trends.google.com) data for the selected movie title. This API call returns a `JSON`-object containing date and amount of searches that day. After some restructuring of the data we passed it to the `Chart` component from the `react-google-charts` package. As both of these packages are developed by Google, they are easy to understand and easy to use. +As we understood the requirment "Advanced view" both from the project description and the Piazza forum, we had to show some sort of data using some third part packages. We decided to use the `google-trends-api` to get the [Google Trends](https://trends.google.com) data for the selected movie title. This API call returns a `JSON`-object containing date and amount of searches that day. After some restructuring of the data we passed it to the `Chart` component from the `react-google-charts` package. As both of these packages are developed by Google, they are easy to understand and easy to use. We also included the number of results of the given query above the result list, just to show the user exactly how many movies where mached. -- GitLab From 3fafecbaccc9d3be15852ca3ac13b9581bd49c7c Mon Sep 17 00:00:00 2001 From: reaas <reaas@stud.ntnu.no> Date: Fri, 25 Oct 2019 19:00:01 +0200 Subject: [PATCH 61/61] Fixed last issues --- client/src/App.js | 32 ++++++++++++++++++++++++-------- server/routes/movies.js | 6 +++--- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/client/src/App.js b/client/src/App.js index 8652534..3f7fe9c 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -89,7 +89,11 @@ class App extends Component { } }).then((res) => { if (res.data.success) { - this.setState({ movies: res.data.data, noOfPages: res.data.pages }); + this.setState({ + movies: res.data.data, + noOfPages: res.data.pages, + resultCount: res.data.hits + }); } else { this.setState({ error: res.data.error }); } @@ -145,13 +149,25 @@ class App extends Component { <Filter getMovies = {this.getMovies}/> </div> <div className="main-content"> - <Accordion> - {this.state.movies.map((movie, index) => { - return( - <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} /> - ) - })} - </Accordion> + { + this.state.movies.length === 0 ? + <div className="welcome-text"> + No results found. Search by typing a movie title in the searchbar. + </div> + : + <> + <div className="result-amount"> + Results: {this.state.resultCount} + </div> + <Accordion> + {this.state.movies.map((movie, index) => { + return( + <MovieDetail key={"movie-detail-" + movie.Title} title={movie.Title} /> + ) + })} + </Accordion> + </> + } </div> <div className="pagination-wrapper"> <PagePagination diff --git a/server/routes/movies.js b/server/routes/movies.js index b0cdf35..cdefa17 100644 --- a/server/routes/movies.js +++ b/server/routes/movies.js @@ -78,7 +78,7 @@ router.post('/GetMovieTitles', function(req, res, next) { if(isNaN(noOfPages)) noOfPages = 1; - return res.json({ success: true, data: data, pages: noOfPages }); + return res.json({ success: true, data: data, pages: noOfPages, hits: result }); }); } @@ -142,10 +142,10 @@ function sorting(sortOrder, sortType) { switch(sortOrder) { case 'ASC': - sortOrder = -1; + sortOrder = 1; break; case 'DESC': - sortOrder = 1; + sortOrder = -1; break; default: sortOrder = 1; -- GitLab