diff --git a/_test_/Articles.spec.js b/_test_/Articles.spec.js index 0dc9485..a5f31ae 100644 --- a/_test_/Articles.spec.js +++ b/_test_/Articles.spec.js @@ -5,6 +5,7 @@ import Adapter from 'enzyme-adapter-react-16'; import { GetAllArticles } from '../src/views/Articles'; + Enzyme.configure({ adapter: new Adapter() }); describe('Articles', () => { diff --git a/_test_/Dropdown.spec.js b/_test_/Dropdown.spec.js new file mode 100644 index 0000000..e8ef03a --- /dev/null +++ b/_test_/Dropdown.spec.js @@ -0,0 +1,17 @@ +import React from 'react'; +import expect from 'expect'; +import { shallow } from 'enzyme'; + +import DropLeft from '../src/components/SingleArticle/RatingsBtn/index'; + + +describe('App', () => { + let app; + beforeEach(() => { + app = shallow(); + }); + + it('renders successfully', () => { + expect(app).toBeDefined(); + }); +}); diff --git a/_test_/RatingsModal.spec.js b/_test_/RatingsModal.spec.js new file mode 100644 index 0000000..346befd --- /dev/null +++ b/_test_/RatingsModal.spec.js @@ -0,0 +1,41 @@ +import React from 'react'; +import expect from 'expect'; +import { shallow } from 'enzyme'; + +import RatingsModal from '../src/components/SingleArticle/RatingsModal/index'; + + +describe('App', () => { + let app; + beforeEach(() => { + app = shallow(); + }); + + it('renders successfully', () => { + expect(app).toBeDefined(); + console.log(app.debug()) + }); + + it('tags have classes', () => { + expect(app.find('_class').length).toBe(9); + }); + + it('renders a span tag', () => { + expect(app.find('span').length).toBe(1); + }); + + it('renders a div', () => { + expect(app.find('div').length).toBe(1); + }); + + it('renders a h2 tag', () => { + expect(app.find('h2').length).toBe(1); + }); + + it('renders StarRatingComponent', () => { + expect(app.find('StarRatingComponent').length).toBe(1); + }); +}); + + + diff --git a/_test_/SingleArticle.spec.js b/_test_/SingleArticle.spec.js index 61e8274..c80e49e 100644 --- a/_test_/SingleArticle.spec.js +++ b/_test_/SingleArticle.spec.js @@ -7,22 +7,29 @@ import MainArticle from '../src/components/SingleArticle/MainArticle'; import Tags from '../src/components/SingleArticle/Tags'; import Recommended from '../src/components/SingleArticle/Recommended'; import CommentsBtn from '../src/components/SingleArticle/CommentsBtn'; +import RatingsModal from '../src/components/SingleArticle/RatingsModal'; describe('Single Article', () => { let component; let prevProps; - + let nextValue; + const props = { prevProps: { match: { params: { slug: 'hello' } } }, match: { params: { slug: '' } }, + article: { userId: 1 }, viewArticle: jest.fn(), fetchArticles: jest.fn(), + articleRating: jest.fn(), loading: false, componentDidUpdate: jest.fn(prevProps), + onStarClick: jest.fn(nextValue), + handleRatingsSubmit: jest.fn(), }; beforeEach(() => { + window.localStorage.setItem('jwtToken', 'token'); component = shallow(); }); @@ -50,6 +57,10 @@ describe('Single Article', () => { expect(component.find(CommentsBtn).length).toBe(1); }); + it('renders a RatingsModal component', () => { + expect(component.find(RatingsModal).length).toBe(1); + }); + it('renders a Loader component', () => { component.setProps({ loading: true }); expect(component.find(Loader).length).toBe(1); @@ -60,4 +71,16 @@ describe('Single Article', () => { const render = jest.spyOn(component.instance(), 'render'); expect(render).toHaveBeenCalledTimes(0); }); + + it('Click on ratings modal', () => { + component.instance().onStarClick(nextValue); + const starClick = jest.spyOn(component.instance(), 'onStarClick'); + expect(starClick).toHaveBeenCalledTimes(0); + }); + + it('submits ratings', () => { + component.instance().handleRatingsSubmit(); + const starSubmit = jest.spyOn(component.instance(), 'handleRatingsSubmit'); + expect(starSubmit).toHaveBeenCalledTimes(0); + }); }); diff --git a/_test_/article.action.spec.js b/_test_/article.action.spec.js new file mode 100644 index 0000000..960dcb9 --- /dev/null +++ b/_test_/article.action.spec.js @@ -0,0 +1,67 @@ +import expect from 'expect'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import moxios from 'moxios'; +import * as types from '../src/actions/types'; +import * as actions from '../src/actions/article.action'; +import axios from '../src/config/axiosInstance'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +const initialState = {}; +const store = mockStore(initialState); + + +describe('rate Article Tests', () => { + afterEach(() => { + moxios.install(axios); + store.clearActions(); + }); + + afterEach(() => moxios.uninstall(axios)); + + it('Returns success if rate article was successful', (done) => { + const dataValue = { + value: 3, + }; + jest.spyOn(axios, 'post').mockResolvedValue({ data: { message: '', data: {} } }); + + const expectedActions = [ + { + type: types.RATE_ARTICLE_START, + }, + { + type: types.RATE_ARTICLE_SUCCESS, + }, + ]; + store.dispatch(actions.articleRating(dataValue)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }); + + }); + + it('Return failure if rate article was unsuccessful', (done) => { + + jest.spyOn(axios, 'post').mockRejectedValue({ response: { data: { message: '' } } }); + + + const expectedActions = [ + { + type: types.RATE_ARTICLE_START, + }, + { + type: types.RATE_ARTICLE_FAILURE, + }, + ]; + + store.dispatch(actions.articleRating(3)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }); + + }); +}); diff --git a/_test_/singleArticleReducer.spec.js b/_test_/singleArticleReducer.spec.js index c91b9bd..25fc264 100644 --- a/_test_/singleArticleReducer.spec.js +++ b/_test_/singleArticleReducer.spec.js @@ -8,6 +8,8 @@ describe('Login reducer', () => { initialState = { article: {}, loading: false, + alert: '', + rating: '', }; }); it('should return the initial state', () => { diff --git a/package-lock.json b/package-lock.json index b539dab..fd4543c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1283,6 +1283,44 @@ "@types/yargs": "^13.0.0" } }, + "@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": { + "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==" + }, + "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.12", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.12.tgz", + "integrity": "sha512-nltMFo5JkYcnntf0Cs3Kq7jskrKeGcftAKOqbEEa74sxlx0bfO3RjBly2aiRb7hnsYJCB8/99l+acQcl2lnq1w==" + }, "@tinymce/tinymce-react": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-3.3.1.tgz", @@ -2818,6 +2856,20 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "bootstrap-4-react": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/bootstrap-4-react/-/bootstrap-4-react-0.0.59.tgz", + "integrity": "sha512-j3a618tWHl/ajYQi6zn0LdVfksRH+gyQ4RwIanTPin6xkHkgYfk+47ZlHFSwyiO9zVFn4xznoGjTwWA0AIlJkA==", + "requires": { + "bootstrap-4-required": "0.0.1", + "fsts": "0.0.44" + } + }, + "bootstrap-4-required": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/bootstrap-4-required/-/bootstrap-4-required-0.0.1.tgz", + "integrity": "sha512-4S6Trn9pRVSR756GRYr3hy2cZL3Vc0tw0/H9E+mbNeOR+4tn6CeRgcLx0YqZmL2XlabtEV73+XAesmwkgTDKvQ==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3646,6 +3698,36 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz", + "integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "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" + } + } + } + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -6436,6 +6518,11 @@ "rimraf": "2" } }, + "fsts": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/fsts/-/fsts-0.0.44.tgz", + "integrity": "sha512-0U4qvbzOE+3s2711DdszIyaAnZ3M0dbFAhnkez/ITy31MwzDI2lepGSkVeFOyx6jqWvwaSZr01RP4hdM4I8wxQ==" + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7725,6 +7812,17 @@ "requires": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" + }, + "dependencies": { + "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" + } + } } }, "isstream": { @@ -8787,6 +8885,11 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" }, + "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", @@ -8942,6 +9045,11 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -8952,6 +9060,11 @@ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" }, + "lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -8973,6 +9086,11 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "lodash.tonumber": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash.tonumber/-/lodash.tonumber-4.0.3.tgz", + "integrity": "sha1-C5azGzVnJ5Prf1pj7nkfG56QJdk=" + }, "lodash.unescape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", @@ -9439,15 +9557,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.7.5", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", @@ -10306,6 +10415,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "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.23", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.23.tgz", @@ -10571,6 +10685,25 @@ "reflect.ownkeys": "^0.2.0" } }, + "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", @@ -10772,6 +10905,46 @@ "resolved": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz", "integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=" }, + "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" + }, + "dependencies": { + "react-transition-group": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.2.2.tgz", + "integrity": "sha512-uP0tjqewtvjb7kGZFpZYPoD/NlVZmIgts9eTt1w35pAaEApPxQGv94lD3VkqyXf2aMqrSGwhs6EV/DLaoKbLSw==", + "requires": { + "@babel/runtime": "^7.4.5", + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + } + } + }, + "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-dom": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz", @@ -10850,6 +11023,32 @@ "prop-types": "^15.5.8" } }, + "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-paginate": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-6.3.0.tgz", @@ -10858,6 +11057,19 @@ "prop-types": "^15.6.1" } }, + "react-popper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.3.tgz", + "integrity": "sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "<=0.2.2", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + } + }, "react-redux": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz", @@ -10935,6 +11147,15 @@ "prop-types": "^15.5.8" } }, + "react-star-rating-component": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-star-rating-component/-/react-star-rating-component-1.4.1.tgz", + "integrity": "sha512-i0YEvQzToS0s0GDkxn01Jy4EeLpVEyh023NXJTJ+/1+xkvhpACyD4d1YeBhYWZab53ppUnUxs5gmp75gJr3khA==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.1" + } + }, "react-test-renderer": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.9.0.tgz", @@ -10989,6 +11210,45 @@ "loose-envify": "^1.3.1", "prop-types": "^15.5.6", "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" + } + } + } + }, + "reactstrap": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.0.1.tgz", + "integrity": "sha512-GvUWEL+a2+3npK1OxTXcNBMHXX4x6uc1KQRzK7yAOl+8sAHTRWqjunvMUfny3oDh8yKVzgqpqQlWWvs1B2HR9A==", + "requires": { + "@babel/runtime": "^7.2.0", + "classnames": "^2.2.3", + "lodash.isfunction": "^3.0.9", + "lodash.isobject": "^3.0.2", + "lodash.tonumber": "^4.0.3", + "prop-types": "^15.5.8", + "react-lifecycles-compat": "^3.0.4", + "react-popper": "^1.3.3", + "react-transition-group": "^2.3.1" + }, + "dependencies": { + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + } } }, "read-pkg": { @@ -13042,6 +13302,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", @@ -13082,6 +13347,15 @@ } } }, + "uncontrollable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.0.0.tgz", + "integrity": "sha512-HFhKHDACiAsTYoV3el/LP4PqcLzqyWrNRHE6nMdr0h8f7qbvTPXIN2S4q+tdfc64PHEXaSFBs/fKVB2+UwSYOA==", + "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", @@ -13385,9 +13659,9 @@ } }, "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "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" } diff --git a/package.json b/package.json index a65f7b9..722f7b4 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "ansi-colors": "^4.1.1", "axios": "^0.19.0", "axios-mock-adapter": "^1.17.0", + "bootstrap-4-react": "0.0.59", "check-prop-types": "^1.1.2", "classnames": "^2.2.6", "css-loader": "^3.1.0", @@ -55,6 +56,7 @@ "react": "^16.8.6", "react-addons-css-transition-group": "^15.6.2", "react-addons-test-utils": "^15.6.2", + "react-bootstrap": "^1.0.0-beta.10", "react-dom": "^16.8.6", "react-draft-wysiwyg": "^1.13.2", "react-images-upload": "^1.2.7", @@ -67,8 +69,10 @@ "react-redux-toastr": "^7.5.1", "react-router-dom": "^5.0.1", "react-spring": "^8.0.27", + "react-star-rating-component": "^1.4.1", "react-toastify": "^5.3.2", "react-tooltip": "^3.10.0", + "reactstrap": "^8.0.1", "recompose": "^0.30.0", "redux": "^4.0.4", "redux-devtools-extension": "^2.13.8", diff --git a/src/actions/article.action.js b/src/actions/article.action.js new file mode 100644 index 0000000..6cd869d --- /dev/null +++ b/src/actions/article.action.js @@ -0,0 +1,39 @@ +import { toast } from 'react-toastify'; +import { + RATE_ARTICLE_START, + RATE_ARTICLE_SUCCESS, + RATE_ARTICLE_FAILURE, +} from './types'; +import axios from '../config/axiosInstance'; + +export const rateArticleStart = () => ({ + type: RATE_ARTICLE_START, +}); + +export const rateArticleSuccess = rating => ({ + type: RATE_ARTICLE_SUCCESS, + rating, +}); + +export const rateArticleFailure = () => ({ + type: RATE_ARTICLE_FAILURE, +}); + +export const articleRating = (slug, value) => (dispatch) => { + const dataValue = { + value, + }; + dispatch(rateArticleStart()); + return axios + .post(`/articles/rate/${slug}`, dataValue) + .then((res) => { + const { data, message } = res.data; + dispatch(rateArticleSuccess(data[0])); + toast.success(message); + }) + .catch((error) => { + const { message } = error.response.data; + dispatch(rateArticleFailure()); + toast.error(message); + }); +}; diff --git a/src/actions/types.js b/src/actions/types.js index 920eecf..d508928 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -22,3 +22,6 @@ export const LOGIN_LOADING = 'LOGGIN_LOADING'; export const SUCCESS = 'SUCCESS'; export const SIGNUP_LOADING = 'SIGNUP_LOADING'; export const VIEW_SINGLE_ARTICLE = 'VIEW_SINGLE_ARTICLE'; +export const RATE_ARTICLE_START = 'RATE_ARTICLE_START'; +export const RATE_ARTICLE_SUCCESS = 'RATE_ARTICLE_SUCCESS'; +export const RATE_ARTICLE_FAILURE = 'RATE_ARTICLE_FAILURE'; diff --git a/src/components/App.js b/src/components/App.js index 33f8125..6851479 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -31,6 +31,7 @@ if (token) { loggedInUser = store.dispatch(setCurrentUser(decoded)); } + const App = () => ( ({ + dropdownOpen: !prevState.dropdownOpen, + })); + } + + render() { + return ( +
+ + + + + + Report This Article + + Rate this Article + + Bookmark this Article + + +
+ ); + } +} + +export default DropLeft; diff --git a/src/components/SingleArticle/RatingsModal/index.jsx b/src/components/SingleArticle/RatingsModal/index.jsx new file mode 100644 index 0000000..5ed2973 --- /dev/null +++ b/src/components/SingleArticle/RatingsModal/index.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { + Modal, Button, +} from 'bootstrap-4-react'; +import StarRatingComponent from 'react-star-rating-component'; +import PropTypes from 'prop-types'; + +/** + * This component is called RatingsModal component, it renders the ratings modal and also holds + * the button to close or save ratings. + */ +const RatingsModal = (props) => { + const { + title, rating, starClick, handleRatingsSubmit, + } = props; + return ( + + + + {title} + + + + + +
+

+Your current rating for this article is: + {' '} + {rating} + {' '} +stars +

+ +
+
+ + + + +
+
+ ); +}; + +RatingsModal.defaultProps = { + title: '', + starClick: () => '', + handleRatingsSubmit: () => '', + rating: 0, +}; + +RatingsModal.propTypes = { + title: PropTypes.string, + starClick: PropTypes.func, + handleRatingsSubmit: PropTypes.func, + rating: PropTypes.number, +}; + +export default RatingsModal; diff --git a/src/components/SingleArticle/index.jsx b/src/components/SingleArticle/index.jsx index ccbe595..60a9a28 100644 --- a/src/components/SingleArticle/index.jsx +++ b/src/components/SingleArticle/index.jsx @@ -1,6 +1,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { Modal } from 'bootstrap-4-react'; +import jwtDecode from 'jwt-decode'; import MainArticle from './MainArticle'; import articleActions from '../../actions/ArticleActions'; import Tags from './Tags'; @@ -8,11 +10,16 @@ import Recommended from './Recommended'; import CommentsBtn from './CommentsBtn'; import Loader from '../Loader'; import './index.scss'; +import DropLeft from './RatingsBtn/index'; +import RatingsModal from './RatingsModal/index'; +import { articleRating } from '../../actions/article.action'; const { viewArticle, fetchArticles } = articleActions; export class SingleArticle extends Component { - state = {} + state = { + value: 0, + } componentDidMount() { const { slug } = this.props.match.params; @@ -27,19 +34,53 @@ export class SingleArticle extends Component { } } - render() { - if (this.props.article === {} || this.props.loading) { - return ; - } - return ( -
- - - - -
- ); + onStarClick = (nextValue) => { + this.setState({ value: nextValue }); } + + +handleRatingsSubmit = () => { + const { match } = this.props; + const { params: { slug } } = match; + const { value } = this.state; + const { articleRating: articleRate } = this.props; + articleRate(slug, value); +} + +checkUser = () => { + const token = localStorage.jwtToken; + const decoded = jwtDecode(token); + const { id } = decoded; + const { userId } = this.props.article; + if (id !== userId) { + return true; + } +} + +render() { + const token = localStorage.jwtToken; + if (this.props.article === {} || this.props.loading) { + return ; + } + const { value } = this.state; + return ( +
+ + + { token && this.checkUser() && } + + + + + +
+ ); +} } export const mapStateToProps = state => ({ @@ -56,10 +97,12 @@ SingleArticle.propTypes = { }).isRequired, viewArticle: PropTypes.func.isRequired, fetchArticles: PropTypes.func.isRequired, + articleRating: PropTypes.func.isRequired, article: PropTypes.shape({}).isRequired, recommendedArticles: PropTypes.shape({}).isRequired, loading: PropTypes.bool.isRequired, history: PropTypes.shape({}).isRequired, }; -export default connect(mapStateToProps, { viewArticle, fetchArticles })(SingleArticle); +export default connect(mapStateToProps, + { viewArticle, fetchArticles, articleRating })(SingleArticle); diff --git a/src/components/SingleArticle/index.scss b/src/components/SingleArticle/index.scss index e53846b..ddbb693 100644 --- a/src/components/SingleArticle/index.scss +++ b/src/components/SingleArticle/index.scss @@ -1,3 +1,16 @@ .article-container { padding-bottom: 100px; } + +.remove-border { + border-color: transparent !important; +} +.remove-border:focus { + box-shadow: none !important; +} + +@media only screen and (max-width: 870px) { + .fa-4x { + font-size: 2rem !important; + } +} \ No newline at end of file diff --git a/src/config/axiosInstance.js b/src/config/axiosInstance.js index dd5092b..e538b71 100644 --- a/src/config/axiosInstance.js +++ b/src/config/axiosInstance.js @@ -1,7 +1,14 @@ import axios from 'axios'; +const token = localStorage.getItem('jwtToken'); + + const instance = axios.create({ baseURL: 'https://ah-nyati-backend-staging.herokuapp.com/api/v1', + headers: { + 'Content-Type': 'application/json', + token, + }, }); export default instance; diff --git a/src/reducers/index.js b/src/reducers/index.js index 0f2c889..f2deca3 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -9,7 +9,6 @@ import articles from './articlesReducer'; import user from './getUserReducer'; import success from './success'; import singleArticleReducer from './singleArticleReducer'; - import createArticleReducer from './article/article'; import fetchCategoryReducer from './category/category'; diff --git a/src/reducers/singleArticleReducer.js b/src/reducers/singleArticleReducer.js index 663daec..c0eb90c 100644 --- a/src/reducers/singleArticleReducer.js +++ b/src/reducers/singleArticleReducer.js @@ -1,6 +1,17 @@ -import { VIEW_SINGLE_ARTICLE, SET_LOADING } from '../actions/types'; +import { + VIEW_SINGLE_ARTICLE, + SET_LOADING, + RATE_ARTICLE_START, + RATE_ARTICLE_SUCCESS, + RATE_ARTICLE_FAILURE, +} from '../actions/types'; -const initialState = { article: {}, loading: false }; +const initialState = { + article: {}, + loading: false, + rating: '', + alert: '', +}; export default function (state = initialState, action) { switch (action.type) { @@ -15,6 +26,23 @@ export default function (state = initialState, action) { loading: false, article: action.payload, }; + case RATE_ARTICLE_START: + return { + ...state, + loading: true, + }; + case RATE_ARTICLE_SUCCESS: + return { + ...state, + loading: false, + rating: action.rating, + alert: action.message, + }; + case RATE_ARTICLE_FAILURE: + return { + ...state, + loading: false, + }; default: return state; }