From 4ad207ebea892c7a371aa6196e9cb0a2dae63407 Mon Sep 17 00:00:00 2001 From: nadralia Date: Thu, 20 Jun 2019 08:53:13 +0300 Subject: [PATCH] feat(follow and unfollow): users should be able to follow each other Add action creators for follow Add reducers for follow [Maintains #165273540] --- package.json | 3 +- src/assets/MainStyle.scss | 2 +- src/components/Apps/App.js | 3 + src/components/ArticlePreview/index.js | 4 +- src/components/NavBar/NavBarStyle.scss | 1 + src/components/NavBar/index.js | 4 +- src/components/Profile/Index.js | 51 +++++ src/components/Profile/Profile.scss | 62 ++++++ src/components/Profile/Profile.test.js | 25 +++ src/components/Profile/Setting.scss | 67 +++++++ src/components/Profile/Settings.js | 154 +++++++++++++++ src/components/Profile/Settings.test.js | 47 +++++ src/components/Profile/Sidebar.js | 182 ++++++++++++++++++ src/components/Profile/Sidebar.scss | 105 ++++++++++ .../__snapshots__/Profile.test.js.snap | 156 +++++++++++++++ .../__snapshots__/Settings.test.js.snap | 64 ++++++ src/constants/constantStrings.js | 2 + src/firebase/config.js | 17 ++ src/pages/Landing/ArticleColumn.js | 4 +- src/pages/Landing/index.js | 4 +- src/pages/Profile/Profile.scss | 24 +++ src/pages/Profile/Profile.test.js | 32 +++ .../__snapshots__/Profile.test.js.snap | 60 ++++++ src/pages/Profile/index.js | 88 +++++++++ src/routers/PrivateRoute.js | 27 +++ src/routers/PrivateRoute.test.js | 24 +++ .../__snapshots__/PrivateRoute.test.js.snap | 7 + src/store/actions/__mocks__/profile.js | 39 ++++ src/store/actions/followActions/index.js | 100 ++++++++++ src/store/actions/followTypes.js | 6 + src/store/actions/profileActions/index.js | 78 ++++++++ .../profileActions/profileActions.test.js | 162 ++++++++++++++++ src/store/actions/types.js | 4 + src/store/reducers/followReducer/index.js | 57 ++++++ src/store/reducers/profileReducer/index.js | 48 +++++ .../profileReducer/profileReducer.test.js | 50 +++++ src/store/rootReducer.js | 4 +- src/utils/mainAxios.js | 11 ++ webpack.config.js | 1 + 39 files changed, 1769 insertions(+), 10 deletions(-) create mode 100644 src/components/Profile/Index.js create mode 100644 src/components/Profile/Profile.scss create mode 100644 src/components/Profile/Profile.test.js create mode 100644 src/components/Profile/Setting.scss create mode 100644 src/components/Profile/Settings.js create mode 100644 src/components/Profile/Settings.test.js create mode 100644 src/components/Profile/Sidebar.js create mode 100644 src/components/Profile/Sidebar.scss create mode 100644 src/components/Profile/__snapshots__/Profile.test.js.snap create mode 100644 src/components/Profile/__snapshots__/Settings.test.js.snap create mode 100644 src/firebase/config.js create mode 100644 src/pages/Profile/Profile.scss create mode 100644 src/pages/Profile/Profile.test.js create mode 100644 src/pages/Profile/__snapshots__/Profile.test.js.snap create mode 100644 src/pages/Profile/index.js create mode 100644 src/routers/PrivateRoute.js create mode 100644 src/routers/PrivateRoute.test.js create mode 100644 src/routers/__snapshots__/PrivateRoute.test.js.snap create mode 100644 src/store/actions/__mocks__/profile.js create mode 100644 src/store/actions/followActions/index.js create mode 100644 src/store/actions/followTypes.js create mode 100644 src/store/actions/profileActions/index.js create mode 100644 src/store/actions/profileActions/profileActions.test.js create mode 100644 src/store/reducers/followReducer/index.js create mode 100644 src/store/reducers/profileReducer/index.js create mode 100644 src/store/reducers/profileReducer/profileReducer.test.js create mode 100644 src/utils/mainAxios.js diff --git a/package.json b/package.json index b538784..9725036 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "enzyme-adapter-react-16": "^1.13.2", "express": "^4.17.1", "file-loader": "^3.0.1", + "firebase": "^6.2.0", "jwt-decode": "^2.2.0", "prop-types": "^15.7.2", "react": "^16.8.6", @@ -80,8 +81,8 @@ "redux-devtools-extension": "^2.13.8", "redux-thunk": "^2.3.0", "sass-rem": "^2.0.1", + "semantic-ui-react": "^0.87.2", "serve": "^11.0.1", - "toastify": "^1.0.12", "url-loader": "^1.1.2" }, "devDependencies": { diff --git a/src/assets/MainStyle.scss b/src/assets/MainStyle.scss index f88f770..03491bd 100644 --- a/src/assets/MainStyle.scss +++ b/src/assets/MainStyle.scss @@ -1,5 +1,4 @@ @import url("https://fonts.googleapis.com/css?family=Lobster&display=swap"); -@import "sass-rem"; $primary-font-family: "Open Sans", sans-serif; $secondary-font-family: "Lobster", sans-serif; @@ -8,6 +7,7 @@ $primary: #2D6686; $primary-ultra-light: #D9E3F0; $secondary: #E91E63; $white: #FFFFFF; +$black: #021019; $shadow: #D3D6DB; * { diff --git a/src/components/Apps/App.js b/src/components/Apps/App.js index c24ea38..61ad19b 100644 --- a/src/components/Apps/App.js +++ b/src/components/Apps/App.js @@ -2,7 +2,9 @@ import React, { Component } from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import LandingPage from 'pages/Landing'; import PasswordResetPage from 'pages/PasswordReset'; +import ProfilePage from 'pages/Profile'; import PageNotFound from 'pages/Error'; +import PrivateRoute from 'routers/PrivateRoute'; import 'assets/MainStyle.scss'; @@ -13,6 +15,7 @@ class App extends Component { } /> } /> + diff --git a/src/components/ArticlePreview/index.js b/src/components/ArticlePreview/index.js index b9788eb..3b5e858 100644 --- a/src/components/ArticlePreview/index.js +++ b/src/components/ArticlePreview/index.js @@ -80,10 +80,10 @@ ArticlePreview.propTypes = { views: PropTypes.number.isRequired, reads: PropTypes.number.isRequired, }), - likeCount: PropTypes.arrayOf(PropTypes.shape({ + likeCount: PropTypes.shape({ likes: PropTypes.number.isRequired, dislikes: PropTypes.number.isRequired, - })), + }), }), }; diff --git a/src/components/NavBar/NavBarStyle.scss b/src/components/NavBar/NavBarStyle.scss index 3acfdc9..f9ade95 100644 --- a/src/components/NavBar/NavBarStyle.scss +++ b/src/components/NavBar/NavBarStyle.scss @@ -1,4 +1,5 @@ @import 'assets/MainStyle.scss'; +@import 'sass-rem'; .navbar { padding-top: 0.5rem; diff --git a/src/components/NavBar/index.js b/src/components/NavBar/index.js index 799a671..de9a49a 100644 --- a/src/components/NavBar/index.js +++ b/src/components/NavBar/index.js @@ -85,7 +85,9 @@ export class Navbar extends Component { className="navbar__navigation__auth__button--link" style={{ cursor: 'pointer' }} > - { user.username } + + { user.username } + diff --git a/src/components/Profile/Index.js b/src/components/Profile/Index.js new file mode 100644 index 0000000..cd84716 --- /dev/null +++ b/src/components/Profile/Index.js @@ -0,0 +1,51 @@ +// import react library +import React from 'react'; +// import third pary libraries +import PropTypes from 'prop-types'; +import { Tab } from 'semantic-ui-react'; + +// import settings components +import Settings from 'components/Profile/Settings'; + +// import profile style +import './Profile.scss'; + +export const ProfileMain = ({ match: { params: { profileUser } }, authenticatedUser }) => { + const panes = [ + { menuItem: 'Articles ', render: () => }, + ]; + if (authenticatedUser === profileUser) { + panes.push({ menuItem: '| Settings', render: () => }); + } + + return ( +
+
+ +
+
+ ); +}; + +/** +* Assigning props types +*/ +ProfileMain.propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + profileUser: PropTypes.string, + }) + }), + authenticatedUser: PropTypes.string, +}; + +ProfileMain.defaultProps = { + match: PropTypes.shape({ + params: PropTypes.shape({ + profileUser: 'nadralia', + }) + }), + authenticatedUser: 'lou', +}; + +export default ProfileMain; diff --git a/src/components/Profile/Profile.scss b/src/components/Profile/Profile.scss new file mode 100644 index 0000000..dbb4ce3 --- /dev/null +++ b/src/components/Profile/Profile.scss @@ -0,0 +1,62 @@ +@import 'assets/MainStyle.scss'; + +.main { + width: 70%; + padding-left: 2%; + font-family: 'Open Sans', sans-serif; +} + +.profile-tab-menu { + &__menu { + border-bottom: .125rem solid $secondary; + display: flex; + margin-bottom: 0; + width: 100%; + + &__item { + color: $black; + font-family: 'Open Sans', sans-serif; + font-size: 1.5rem; + line-height: 1.8125rem; + padding: 2.075rem; + } + + &__active.item { + font-weight: 400 !important; + } + } + + &__segment { + border: 0; + border-radius: 0; + box-shadow: none; + margin: 0; + padding: 0; + } +} + +.settings { + margin-top: 1.875rem; +} + +.edit-profile { + background: $white; + border: .0625rem solid $secondary; + border-radius: .25rem; + box-sizing: border-box; + padding: 4.6875rem 1.3125rem 1.5rem; +} + +@media only screen and (max-width: 37.5rem) { + .main { + width: 100%; + } +} + +menuItem { + padding: 4.6875rem 1.3125rem 1.5rem; +} + +.tab a { + cursor: pointer; +} diff --git a/src/components/Profile/Profile.test.js b/src/components/Profile/Profile.test.js new file mode 100644 index 0000000..defbe5b --- /dev/null +++ b/src/components/Profile/Profile.test.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import ProfileMain from 'components/Profile/Index'; + +describe('Setting Component', () => { + const props = { + match: + { + params: { + profileUser: 'nadralia', + } + }, + authenticatedUser: 'nadralia', + }; + + it('should match the snapshot', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should render correctly', () => { + const wrapper = mount(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/components/Profile/Setting.scss b/src/components/Profile/Setting.scss new file mode 100644 index 0000000..6cbebbd --- /dev/null +++ b/src/components/Profile/Setting.scss @@ -0,0 +1,67 @@ +.main { + display: 'flex'; +} + +.edit-btn { + width: 40%; + padding: 0.5rem 2rem; + margin: 10px auto; + border: 1px solid #F34579; + background-color: #F34579; + border-radius: 25px; + display: block; + font-size: 1.2rem; + cursor: pointer; + + &:hover { + outline: transparent; + background-color: #2D6686; + color: #FFFFFF; + } + + &:focus { + outline: transparent; + } +} + +.settings__edit-profile textarea { + margin: 0; + width: 100%; + -webkit-appearance: none; + padding: .78571429rem 1rem; + background: #FFF; + border: 1px solid rgba(34,36,38,.15); + outline: 0; + color: rgba(0,0,0,.87); + border-radius: .28571429rem; + -webkit-box-shadow: 0 0 0 0 transparent inset; + box-shadow: 0 0 0 0 transparent inset; + -webkit-transition: color .1s ease,border-color .1s ease; + transition: color .1s ease,border-color .1s ease; + font-size: 1rem; + line-height: 1.2857; + resize: vertical; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.settings__edit-profile input[type=text] { + font-family: 'Open Sans', sans-serif; + margin: 0; + outline: 0; + width: 100%; + -webkit-appearance: none; + line-height: 1.21428571rem; + padding: .67857143rem 1rem; + font-size: 1rem; + background: #FFF; + border: 1px solid rgba(34,36,38,.15); + color: rgba(0,0,0,.87); + border-radius: .28571429rem; + -webkit-box-shadow: 0 0 0 0 transparent inset; + box-shadow: 0 0 0 0 transparent inset; + -webkit-transition: color .1s ease,border-color .1s ease; + transition: color .1s ease,border-color .1s ease; + margin-bottom: 0.978rem; +} diff --git a/src/components/Profile/Settings.js b/src/components/Profile/Settings.js new file mode 100644 index 0000000..b935d7f --- /dev/null +++ b/src/components/Profile/Settings.js @@ -0,0 +1,154 @@ +// react library +import React, { Component } from 'react'; + +// third party libraries +import { Form } from 'semantic-ui-react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +// import action creator for update profile +import { + updateProfileRequest, +} from 'store/actions/profileActions'; + +// import Button component; +import Button from '../Button'; + +// import style +import './Setting.scss'; + +export class Settings extends Component { + constructor(props) { + super(props); + const { userData: { firstname, lastname, bio } } = this.props; + this.state = { + firstname, + lastname, + bio, + }; + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + /** + * This is a function which handles onclick events for the submit + */ + handleSubmit = (event) => { + event.preventDefault(); + const userData = { profile: this.state }; + const { username } = this.props; + const { updateProfile } = this.props; + updateProfile(userData, username); + }; + + /** + * This is a function for onChange events + */ + handleChange(event) { + this.setState({ [event.target.name]: event.target.value }); + } + + render() { + const { + firstname, lastname, bio + } = this.state; + return ( +
+
+
+ + + + + +
+ +
+ +
+
+ ); + } +} + +/** +* Assigning props types +*/ +Settings.propTypes = { + updateProfile: PropTypes.func.isRequired, + userData: PropTypes.shape({ + firstname: PropTypes.string, + lastname: PropTypes.string, + bio: PropTypes.string, + }), + username: PropTypes.string.isRequired, +}; + +/** +* Assigning default props +*/ +Settings.defaultProps = { + userData: { + firstname: '', + lastname: '', + bio: '', + }, +}; + +/** +* This is a function which maps the props to state +* we pick profile info from state and username from state +*/ +export const mapStateToProps = state => ({ + userData: state.profile, + username: state.loginReducer.user.username, +}); + +/** +* This is a function shich dispatches updateProfileRequest function to props +*/ +export const mapDispatchToProps = dispatch => ({ + updateProfile: (userData, username) => { + dispatch(updateProfileRequest(userData, username)); + } +}); + +/** +* Connect the mapStateToProps and mapDispatchToProps to state +*/ +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Settings); diff --git a/src/components/Profile/Settings.test.js b/src/components/Profile/Settings.test.js new file mode 100644 index 0000000..f04c43d --- /dev/null +++ b/src/components/Profile/Settings.test.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { + Settings, + mapDispatchToProps, +} from 'components/Profile/Settings'; + +describe('Setting Component', () => { + const mockedFunction = jest.fn(); + const component = shallow(); + it('should match the snapshot', () => { + expect(component).toMatchSnapshot(); + }); + + it('submit user details', () => { + component.setProps({ + userData: { firstname: 'john', lastname: 'doe', bio: 'this is biodata' } + }); + + const event = { + userData: { profile: { firstname: 'john', lastname: 'doe', bio: 'this is biodata' } }, + preventDefault: jest.fn() + }; + + const instance = component.instance(); + instance.handleSubmit(event); + expect(instance.props.updateProfile).toBeCalled(); + }); + it('should handle the onchange event', () => { + const event = { + target: { + name: 'firstname', + value: 'nadralia', + }, + }; + + component.instance().handleChange(event); + expect(component.instance().state.firstname).toBe('nadralia'); + }); + + it('should map dispatch to props', () => { + const mockedDispatch = jest.fn(); + + mapDispatchToProps(mockedDispatch).updateProfile(); + expect(mockedDispatch).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Profile/Sidebar.js b/src/components/Profile/Sidebar.js new file mode 100644 index 0000000..3b1677a --- /dev/null +++ b/src/components/Profile/Sidebar.js @@ -0,0 +1,182 @@ +// react library +import React, { Component } from 'react'; + +// third party library +import PropTypes from 'prop-types'; +import { Form } from 'semantic-ui-react'; +import { connect } from 'react-redux'; + +// import action creator for update profile +import { + updateProfileRequest, +} from 'store/actions/profileActions'; + +// import default image +import { + DEFAULT_IMAGE +} from '../../constants/constantStrings'; + +// import firebase +import firebase from '../../firebase/config'; + +// styles +import './Sidebar.scss'; + +class Sidebar extends Component { + onChange = (event) => { + /* + Because we named the inputs to match their + corresponding values in state, it's + super easy to update the state + */ + this.setState({ [event.target.name]: event.target.value }); + } + + uploadImage(files) { + /** + * This is a function uses firebase config setting to upload + * image to our firebase storage + * you can find the config in the firebase folder + */ + const fileload = firebase + .storage() + .ref(`images/${files[0].name}`) + .put(files[0]); + + fileload.then(() => { + firebase + .storage() + .ref(`images/${files[0].name}`) + .getDownloadURL() + .then((url) => { + const image = { + image: url, + }; + const { authenticatedUsername } = this.props; + const data = { profile: image }; + const { uploadProfileImage } = this.props; + uploadProfileImage(data, authenticatedUsername); + }); + }); + } + + render() { + const { + authenticatedUsername, profile: { profile }, + match: { params: { profileUser } } + } = this.props; + const { + firstname, lastname, username, email, image, bio, + } = profile; + return ( +
+
+ {image.length === 0 && ( + profile + )} + + {image.length > 0 && ( + profile + )} + + { profileUser === authenticatedUsername && ( +
+ this.uploadImage(event.target.files)} + /> + + ) } +
+
{username}
+
+ {firstname} + {' '} + {lastname} +
+
{email}
+
{bio}
+
+
+
+ 0 +
+
Followings
+
+
+
+ 0 +
+
Followers
+
+
+
+
+ ); + } +} + +/** +* Assigning props types +*/ +Sidebar.propTypes = { + uploadProfileImage: PropTypes.func.isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + profileUser: PropTypes.string.isRequired, + }) + }).isRequired, + profile: PropTypes.shape({ + firstname: PropTypes.string, + lastname: PropTypes.string, + username: PropTypes.string, + email: PropTypes.string, + bio: PropTypes.string, + }), + authenticatedUsername: PropTypes.string.isRequired, +}; + +/** +* Assigning default props +*/ +Sidebar.defaultProps = { + profile: { + firstname: '', + lastname: '', + username: '', + email: '', + bio: '', + }, +}; + +/** +* This is a function which maps the props to state +* we pick profile info from state and username from state +*/ +export const mapStateToProps = state => ({ + userData: state.profile, + authenticatedUsername: state.loginReducer.user.username, +}); + +/** +* This is a function which dispatches updateProfileRequest function to props +*/ +export const mapDispatchToProps = dispatch => ({ + uploadProfileImage: (data, authenticatedUsername) => { + dispatch(updateProfileRequest(data, authenticatedUsername)); + } +}); + +/** +* Connect the mapStateToProps and mapDispatchToProps to state +*/ +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Sidebar); diff --git a/src/components/Profile/Sidebar.scss b/src/components/Profile/Sidebar.scss new file mode 100644 index 0000000..2f06549 --- /dev/null +++ b/src/components/Profile/Sidebar.scss @@ -0,0 +1,105 @@ +@import '../../assets/MainStyle.scss'; + +.sidebar { + width: 30%; + + .user-image-div { + display: flex; + width: 15rem; + height: 15rem; + justify-content: center; + margin-left: 4rem; + border-radius: 50%; + } + + .username { + font-family: 'Open Sans', sans-serif; + font-size: 1.25rem; + font-style: normal; + font-weight: normal; + line-height: 1.08rem; + margin-bottom: 1.5rem; + } + + .fullname { + font-family: 'Open Sans', sans-serif; + font-size: 1.25rem; + font-style: normal; + font-weight: normal; + line-height: 1.08rem; + margin-bottom: 1.5rem; + } + + .email { + font-family: 'Open Sans', sans-serif; + font-size: .875rem; + line-height: 1.0625rem; + margin-bottom: .9375rem; + } + + .bio { + font-style: normal; + font-weight: normal; + font-family: 'Open Sans', sans-serif; + font-size: .875rem; + line-height: 1.0625rem; + margin-bottom: .935rem; + } + + .sidebar-hr { + margin-top: 1.875rem; + width: 100%; + } +} + +.follow-div { + color: $black; + display: flex; + font-family: 'Open Sans', sans-serif; + font-size: .875rem; + font-style: normal; + font-weight: normal; + justify-content: space-around; + line-height: 1.0625rem; + margin-bottom: .9375rem; + + &__count { + font-size: .9375rem; + margin-bottom: 0; + } +} + +.center-align { + text-align: center; +} + +.buttonDiv { + padding-left: '15%'; + padding-right: '15%'; + margin-top: '35px'; +} + +@media only screen and (max-width: 37.5rem) { + .sidebar { + width: 100%; + + &__sidebar-hr { + display: none; + } + } +} + +.custom-file-upload { + border: 1px solid #CCC; + display: inline-block; + padding: 6px 12px; + cursor: pointer; + margin-left: 2rem; + + *:before, + *:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } +} diff --git a/src/components/Profile/__snapshots__/Profile.test.js.snap b/src/components/Profile/__snapshots__/Profile.test.js.snap new file mode 100644 index 0000000..723886b --- /dev/null +++ b/src/components/Profile/__snapshots__/Profile.test.js.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Setting Component should match the snapshot 1`] = ` +
+
+ +
+
+`; + +exports[`Setting Component should render correctly 1`] = ` + +
+
+ +
+ + + + + +
+ + +
+ +
+
+ +`; diff --git a/src/components/Profile/__snapshots__/Settings.test.js.snap b/src/components/Profile/__snapshots__/Settings.test.js.snap new file mode 100644 index 0000000..2add422 --- /dev/null +++ b/src/components/Profile/__snapshots__/Settings.test.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Setting Component should match the snapshot 1`] = ` +
+
+
+ + + + + +
+
+ +
+
+`; diff --git a/src/constants/constantStrings.js b/src/constants/constantStrings.js index e24bd41..d401a24 100644 --- a/src/constants/constantStrings.js +++ b/src/constants/constantStrings.js @@ -9,3 +9,5 @@ export const PASSWORD_RESET = 'Reset Password'; export const EMAIL_CONFIRMATION_HEADER = 'Confirm Email'; export const EMAIL_CONFIRMATION_SUCCESS_MESSAGE = 'We have sent you an email. Please check your email to proceed with your password reset.'; export const EMAIL_CONFIRMATION_ERROR_MESSAGE = 'There is no active user associated with this e-mail address or the password can not be changed'; + +export const DEFAULT_IMAGE = 'https://firebasestorage.googleapis.com/v0/b/ah-frontend-dojo.appspot.com/o/images%2Fplaceholder-face-big.png?alt=media&token=8309d1b3-058b-4b6f-a8f0-b076cf7a72ee'; diff --git a/src/firebase/config.js b/src/firebase/config.js new file mode 100644 index 0000000..23e2efa --- /dev/null +++ b/src/firebase/config.js @@ -0,0 +1,17 @@ +import firebase from 'firebase'; + +import 'firebase/storage'; + +const config = { + apiKey: 'AIzaSyDGctfjXbF7MLE5SrndkJzENzoeDWdZeuQ', + authDomain: 'ah-frontend-dojo.firebaseapp.com', + databaseURL: 'https://ah-frontend-dojo.firebaseio.com', + projectId: 'ah-frontend-dojo', + storageBucket: 'ah-frontend-dojo.appspot.com', + messagingSenderId: '396083612843', + appId: '1:396083612843:web:b140ca33e2038761', +}; + +firebase.initializeApp(config); + +export default firebase; diff --git a/src/pages/Landing/ArticleColumn.js b/src/pages/Landing/ArticleColumn.js index 667ff17..2de8ccc 100644 --- a/src/pages/Landing/ArticleColumn.js +++ b/src/pages/Landing/ArticleColumn.js @@ -43,10 +43,10 @@ ArticleColumn.propTypes = { views: PropTypes.number.isRequired, reads: PropTypes.number.isRequired, }), - likeCount: PropTypes.arrayOf(PropTypes.shape({ + likeCount: PropTypes.shape({ likes: PropTypes.number.isRequired, dislikes: PropTypes.number.isRequired, - })), + }), })), }; diff --git a/src/pages/Landing/index.js b/src/pages/Landing/index.js index 51d60f4..e083b33 100644 --- a/src/pages/Landing/index.js +++ b/src/pages/Landing/index.js @@ -96,10 +96,10 @@ LandingPage.propTypes = { views: PropTypes.number.isRequired, reads: PropTypes.number.isRequired, }), - likeCount: PropTypes.arrayOf(PropTypes.shape({ + likeCount: PropTypes.shape({ likes: PropTypes.number.isRequired, dislikes: PropTypes.number.isRequired, - })), + }), })), }; diff --git a/src/pages/Profile/Profile.scss b/src/pages/Profile/Profile.scss new file mode 100644 index 0000000..e2a1839 --- /dev/null +++ b/src/pages/Profile/Profile.scss @@ -0,0 +1,24 @@ +.profile-container { + display: flex; + margin-top: 2.125rem; + padding: 0% 6%; +} + +.d-flex { + display: flex; +} + +.justify-content-between { + justify-content: space-between; +} + +.ui.items > .item .extra > * { + margin: 0; +} + +@media only screen and (max-width: 37.5rem) { + .profile-container { + display: block; + padding: 0% 6%; + } +} diff --git a/src/pages/Profile/Profile.test.js b/src/pages/Profile/Profile.test.js new file mode 100644 index 0000000..b25b451 --- /dev/null +++ b/src/pages/Profile/Profile.test.js @@ -0,0 +1,32 @@ +// react imports +import React from 'react'; + +// third party libraries +import { shallow } from 'enzyme'; + +// import profile +import { + Profile, + mapDispatchToProps, +} from 'pages/Profile'; + +const profile = { + firstname: 'nadralia', + lastname: 'lonerk', + bio: 'this is my biodata', + image: 'http://biodatainformation.jpg', +}; + +describe('Profile', () => { + it('should render', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should map dispatch to props', () => { + const mockedDispatch = jest.fn(); + + mapDispatchToProps(mockedDispatch).fetchProfile(); + expect(mockedDispatch).toHaveBeenCalled(); + }); +}); diff --git a/src/pages/Profile/__snapshots__/Profile.test.js.snap b/src/pages/Profile/__snapshots__/Profile.test.js.snap new file mode 100644 index 0000000..bd94dce --- /dev/null +++ b/src/pages/Profile/__snapshots__/Profile.test.js.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Profile should render 1`] = ` +
+ + } + closeOnClick={true} + draggable={true} + draggablePercent={80} + hideProgressBar={false} + newestOnTop={false} + pauseOnFocusLoss={true} + pauseOnHover={true} + position="top-right" + progressClassName={null} + progressStyle={null} + role="alert" + rtl={false} + style={null} + toastClassName={null} + transition={[Function]} + /> + +
+ <[object Object] + fetchProfile={[Function]} + profile={ + Object { + "bio": "this is my biodata", + "firstname": "nadralia", + "image": "http://biodatainformation.jpg", + "lastname": "lonerk", + } + } + /> + +
+
+`; diff --git a/src/pages/Profile/index.js b/src/pages/Profile/index.js new file mode 100644 index 0000000..646d452 --- /dev/null +++ b/src/pages/Profile/index.js @@ -0,0 +1,88 @@ +// react libraries +import React, { Component } from 'react'; + +// third-party libraries +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { ToastContainer } from 'react-toastify'; + +// components +import Navbar from 'components/NavBar/index'; +import ProfileSidebar from 'components/Profile/Sidebar'; +import ProfileMain from 'components/Profile/Index'; + +// actions creator +import { fetchProfileRequest } from 'store/actions/profileActions'; + +// styles +import './Profile.scss'; +import 'react-toastify/dist/ReactToastify.css'; + + +export class Profile extends Component { + componentDidMount() { + const { match } = this.props; + if (match) { + const { params: { profileUser } } = match; + const { fetchProfile } = this.props; + fetchProfile(profileUser); + } + } + + render() { + return ( +
+ + +
+ + +
+
+ ); + } +} + +Profile.propTypes = { + profile: PropTypes.shape({ + firstname: PropTypes.string, + lastname: PropTypes.string, + username: PropTypes.string, + email: PropTypes.string, + bio: PropTypes.string, + }), + match: PropTypes.shape({ + params: PropTypes.shape({ + profileUser: PropTypes.string.isRequired, + }) + }).isRequired, + fetchProfile: PropTypes.func, + authenticatedUser: PropTypes.string.isRequired, +}; + +Profile.defaultProps = { + profile: { + firstname: '', + lastname: '', + username: '', + email: '', + bio: '', + }, + fetchProfile: () => { }, +}; + +export const mapStateToProps = state => ({ + profile: state.profile, + authenticatedUser: state.loginReducer.user.username +}); + +export const mapDispatchToProps = dispatch => ({ + fetchProfile: (username) => { + dispatch(fetchProfileRequest(username)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Profile); diff --git a/src/routers/PrivateRoute.js b/src/routers/PrivateRoute.js new file mode 100644 index 0000000..2d08d2d --- /dev/null +++ b/src/routers/PrivateRoute.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Route, Redirect } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +// eslint-disable-next-line react/prop-types +export const PrivateRoute = ({ component: Component, isAuthenticated, ...rest }) => ( + (isAuthenticated ? ( + + ) : ( + + )) + } + /> +); + +/** +* This is a function which maps the props to state +* we pick username from state +*/ +const mapStateToProps = state => ({ + isAuthenticated: !!state.loginReducer.user.username +}); + +export default connect(mapStateToProps)(PrivateRoute); diff --git a/src/routers/PrivateRoute.test.js b/src/routers/PrivateRoute.test.js new file mode 100644 index 0000000..fb3df88 --- /dev/null +++ b/src/routers/PrivateRoute.test.js @@ -0,0 +1,24 @@ +// react imports +import React from 'react'; + +// third party libraries +import { shallow, mount } from 'enzyme'; + +// import PrivateRoute +import { PrivateRoute } from './PrivateRoute'; + +describe('PrivateRoute Component', () => { + const props = { + component: { + ComingComponent: 'Settings', + isAuthenticated: 'nadralia', + rest: { + setting: '', + } + } + }; + it('should match the snapshot', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/routers/__snapshots__/PrivateRoute.test.js.snap b/src/routers/__snapshots__/PrivateRoute.test.js.snap new file mode 100644 index 0000000..99cc23c --- /dev/null +++ b/src/routers/__snapshots__/PrivateRoute.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PrivateRoute Component should match the snapshot 1`] = ` + +`; diff --git a/src/store/actions/__mocks__/profile.js b/src/store/actions/__mocks__/profile.js new file mode 100644 index 0000000..6c2c198 --- /dev/null +++ b/src/store/actions/__mocks__/profile.js @@ -0,0 +1,39 @@ +const profileData = { + get: { + success: { + profile: { + firstname: 'nadralia', + lastname: 'fieldmarshal', + username: 'fieldmarshal', + email: 'dfieldmarshal@ah.com', + bio: '', + image: 'http://fieldmarshal.jpg', + } + }, + failure: { + profile: { + detail: 'Not found.' + }, + }, + }, + update: { + success: { + profile: { + firstname: 'nadralia', + lastname: 'fieldmarshal', + username: 'fieldmarshal', + email: 'dfieldmarshal@ah.com', + bio: '', + image: 'http://fieldmarshal.jpg', + } + }, + failure: { + profile: { + detail: 'Not found.' + }, + }, + }, + +}; + +export default profileData; diff --git a/src/store/actions/followActions/index.js b/src/store/actions/followActions/index.js new file mode 100644 index 0000000..7c675dd --- /dev/null +++ b/src/store/actions/followActions/index.js @@ -0,0 +1,100 @@ +// third party libraries +import { toast } from 'react-toastify'; + +// import axios instance +import axios from 'utils/mainAxios'; + +// user constants +import { + FOLLOW_AUTHOR_ERROR, + FOLLOW_AUTHOR_SUCCESS, + GET_FOLLOWERS_ERROR, + GET_FOLLOWERS_SUCCESS, + GET_FOLLOWING_ERROR, + GET_FOLLOWING_SUCCESS, +} from 'store/actions/followTypes'; + +/** +* follow author action function on success +* @param {string} response +*/ +export const followAuthorSuccess = response => ({ + type: FOLLOW_AUTHOR_SUCCESS, + response, +}); + +/** + * follow author action function on error + * @param {string} error + */ +export const followAuthorError = error => ({ + type: FOLLOW_AUTHOR_ERROR, + error, +}); + +/** +* follow author action function on success +* @param {string} response +*/ +export const getFollowersSuccess = response => ({ + type: GET_FOLLOWERS_SUCCESS, + response, +}); + +/** + * get followers action function on error + * @param {string} error + */ +export const getFollowersError = error => ({ + type: GET_FOLLOWERS_ERROR, + error, +}); + +/** +* get followers action function on success +* @param {string} response +*/ +export const getFollowingSuccess = response => ({ + type: GET_FOLLOWING_SUCCESS, + response, +}); + +/** + * follow author action function on error + * @param {string} error + */ +export const getFollowingError = error => ({ + type: GET_FOLLOWING_ERROR, + error, +}); + +/** +* action creator function for following authors +* username as a parameter and dispatch as a function +* @param {string} username +*/ +export const fellowAuthorActionCreator = username => dispatch => axios + .post(`authors/${username}/follow/`) + .then(response => dispatch(followAuthorSuccess(response.data.profile))) + .catch(error => dispatch(followAuthorError(error))); + + +/** +* action creator function for getting author's followers +* username as a parameter and dispatch as a function +* @param {string} username +*/ +export const getFollowersActionCreator = () => dispatch => axios + .get('authors/followers/') + .then(response => dispatch(getFollowersSuccess(response.data.profile))) + .catch(error => dispatch(getFollowersError(error))); + +/** +* action creator function for getting author's followers +* username as a parameter and dispatch as a function +* @param {string} username +*/ +export const getFollowingActionCreator = () => dispatch => axios + .get('authors/followers/') + .then(response => dispatch(getFollowingSuccess(response.data.profile))) + .catch(error => dispatch(getFollowingError(error))); diff --git a/src/store/actions/followTypes.js b/src/store/actions/followTypes.js new file mode 100644 index 0000000..913ab30 --- /dev/null +++ b/src/store/actions/followTypes.js @@ -0,0 +1,6 @@ +export const FOLLOW_AUTHOR_ERROR = 'FOLLOW_AUTHOR_ERROR'; +export const FOLLOW_AUTHOR_SUCCESS = 'FOLLOW_AUTHOR_SUCCESS'; +export const GET_FOLLOWERS_ERROR = 'GET_FOLLOWERS_ERROR'; +export const GET_FOLLOWERS_SUCCESS = 'GET_FOLLOWERS_SUCCESS'; +export const GET_FOLLOWING_ERROR = 'GET_FOLLOWING_ERROR'; +export const GET_FOLLOWING_SUCCESS = 'GET_FOLLOWING_SUCCESS'; diff --git a/src/store/actions/profileActions/index.js b/src/store/actions/profileActions/index.js new file mode 100644 index 0000000..a8eeb11 --- /dev/null +++ b/src/store/actions/profileActions/index.js @@ -0,0 +1,78 @@ +// third party libraries +import { toast } from 'react-toastify'; + +// import axios instance +import axios from 'utils/mainAxios'; + +// action types +import { + GET_PROFILE_ERROR, + GET_PROFILE_SUCCESS, + UPDATE_PROFILE_ERROR, + UPDATE_PROFILE_SUCCESS, +} from 'store/actions/types'; + +/** +* Fetch user profile action function on success +* @param {object} profile +*/ +export const fetchUserProfile = profile => ({ + type: GET_PROFILE_SUCCESS, + profile, +}); + +/** +* Fetch user profile action function on failure +* @param {string} error +*/ +export const fetchUserProfileError = error => ({ + type: GET_PROFILE_ERROR, + error, +}); + +/** +* update user profile action function on success +* @param {object} profile +*/ +export const updateUserProfile = profile => ({ + type: UPDATE_PROFILE_SUCCESS, + profile, +}); + +/** +* Fetch user profile action function on failure +* @param {string} error +*/ +export const updateUserProfileError = error => ({ + type: UPDATE_PROFILE_ERROR, + error, +}); + + +/** +* action creator function for fetch profile request that takes +* username as a parameter and dispatch as a function +* @param {string} username +*/ +export const fetchProfileRequest = username => dispatch => axios + .get(`/profiles/${username}`) + .then(response => dispatch(fetchUserProfile(response.data.profile))) + .catch(error => dispatch(fetchUserProfileError(error))); + +/** +* action creator for update profile request that takes in profile object and username +* as parameters and dispatch as a function +* @param {string} username +* @param {object} profile user info +* using the put method of http we upload user info +*/ +export const updateProfileRequest = (profile, username) => dispatch => axios + .put(`/profiles/${username}`, profile) + .then((response) => { + if (response.status === 200) { + toast.success('Profile successfully updated'); + return dispatch(updateUserProfile(response.data.profile)); + } + return dispatch(updateUserProfileError(response.data.message)); + }) + .catch(error => error.response); diff --git a/src/store/actions/profileActions/profileActions.test.js b/src/store/actions/profileActions/profileActions.test.js new file mode 100644 index 0000000..24ce5c3 --- /dev/null +++ b/src/store/actions/profileActions/profileActions.test.js @@ -0,0 +1,162 @@ +// import third party libraries +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import axios from 'utils/mainAxios'; +import moxios from 'moxios'; +import profileData from 'store/actions/__mocks__/profile'; + +// import action types +import { + GET_PROFILE_ERROR, + GET_PROFILE_SUCCESS, + UPDATE_PROFILE_SUCCESS, +} from '../types'; + +// import actions +import { + fetchUserProfile, + fetchUserProfileError, + fetchProfileRequest, + updateProfileRequest, +} from './index'; + +// initial state object +const initialState = { + profile: { + firstname: '', + lastname: '', + bio: '', + image: '', + email: '', + username: '', + }, + error: {}, + isLoading: true, +}; + + +const middlewares = [thunk]; + +const mockStore = configureMockStore(middlewares); + +const profile = { + firstname: 'fieldmarshal', + lastname: 'nadralia', + bio: 'This is my biodata information', + image: 'http://fieldmarshal.jpg', +}; + + +describe('actions', () => { + beforeEach(() => { + moxios.install(axios); + }); + afterEach(() => { + moxios.uninstall(axios); + }); + + it('should create an action to fetch user profile info', () => { + const expectedAction = { + type: GET_PROFILE_SUCCESS, + profile, + }; + expect(fetchUserProfile(profile)).toEqual(expectedAction); + }); + + it('should create an error action if it fails to fetch user profile', () => { + const error = ''; + const expectedAction = { + type: GET_PROFILE_ERROR, + error, + }; + expect(fetchUserProfileError(error)).toEqual(expectedAction); + }); + + it('should fail with an unknown id', () => { + const error = { + message: 'fetch user profile unsuccessful', + status: 'error', + }; + const username = 'fieldmarshal007'; + axios.get = jest.fn().mockReturnValue(Promise.reject(error)); + + const expectedActions = [ + { + type: 'GET_PROFILE_ERROR', + error, + }, + ]; + const store = mockStore(initialState); + return store.dispatch(fetchProfileRequest(username)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should fetch a user profile successfully', () => { + const dataProfile = { + type: GET_PROFILE_SUCCESS, + profile: { + firstname: 'nadralia', + lastname: 'fieldmarshal', + username: 'fieldmarshal', + email: 'dfieldmarshal@ah.com', + bio: '', + image: 'http://fieldmarshal.jpg', + }, + }; + axios.get = jest.fn().mockReturnValue(Promise.resolve({ data: dataProfile })); + + const username = 'fieldmarshal'; + const expectedActions = [{ + type: GET_PROFILE_SUCCESS, + profile: { + firstname: 'nadralia', + lastname: 'fieldmarshal', + username: 'fieldmarshal', + email: 'dfieldmarshal@ah.com', + bio: '', + image: 'http://fieldmarshal.jpg', + }, + }]; + const store = mockStore(initialState); + return store.dispatch(fetchProfileRequest(username)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should update user profile successfully', () => { + const store = mockStore({}); + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: profileData.update.success, + }); + }); + const dataProfile = { + profile: { + firstname: 'nadralia', + lastname: 'fieldmarshal', + username: 'fieldmarshal', + email: 'dfieldmarshal@ah.com', + bio: '', + image: 'http://fieldmarshal.jpg', + }, + }; + const expectedActions = [{ + type: UPDATE_PROFILE_SUCCESS, + profile: { + firstname: 'nadralia', + lastname: 'fieldmarshal', + username: 'fieldmarshal', + email: 'dfieldmarshal@ah.com', + bio: '', + image: 'http://fieldmarshal.jpg', + }, + }]; + return store.dispatch(updateProfileRequest(dataProfile, 'nadralia')) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/store/actions/types.js b/src/store/actions/types.js index 7cb35fb..9b577f8 100644 --- a/src/store/actions/types.js +++ b/src/store/actions/types.js @@ -2,3 +2,7 @@ export const GET_ARTICLES = 'GET_ARTICLES'; export const GET_ARTICLES_START = 'GET_ARTICLES_START'; export const GET_ARTICLES_ERROR = 'GET_ARTICLES_ERROR'; +export const GET_PROFILE_ERROR = 'GET_PROFILE_ERROR'; +export const GET_PROFILE_SUCCESS = 'GET_PROFILE_SUCCESS'; +export const UPDATE_PROFILE_ERROR = 'UPDATE_PROFILE_ERROR'; +export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS'; diff --git a/src/store/reducers/followReducer/index.js b/src/store/reducers/followReducer/index.js new file mode 100644 index 0000000..78f6757 --- /dev/null +++ b/src/store/reducers/followReducer/index.js @@ -0,0 +1,57 @@ +// import action types +import { + FOLLOW_AUTHOR_ERROR, + FOLLOW_AUTHOR_SUCCESS, + GET_FOLLOWERS_ERROR, + GET_FOLLOWERS_SUCCESS, + GET_FOLLOWING_ERROR, + GET_FOLLOWING_SUCCESS, +} from 'store/actions/followTypes'; + +// initial state object +export const initialState = { + followers: 0, + following: 0, + data: [ + { + author_name: '', + author_stats: { + followers: '', + following: 0 + } + } + ], + error: {}, +}; + +/** + * This is a switch function for our follow reducer. + * @param {Object} - initialState* + * @return {object} - action + * @example + * + */ + +const followReducer = (state = initialState, action) => { + switch (action.type) { + case FOLLOW_AUTHOR_SUCCESS: + return Object.assign({}, state, { + follow: action.follow, + }); + case FOLLOW_AUTHOR_ERROR: + case GET_FOLLOWERS_SUCCESS: + return Object.assign({}, state, { + follow: action.follow, + }); + case GET_FOLLOWERS_ERROR: + case GET_FOLLOWING_SUCCESS: + return Object.assign({}, state, { + follow: action.follow, + }); + case GET_FOLLOWING_ERROR: + default: + return state; + } +}; + +export default followReducer; diff --git a/src/store/reducers/profileReducer/index.js b/src/store/reducers/profileReducer/index.js new file mode 100644 index 0000000..0410601 --- /dev/null +++ b/src/store/reducers/profileReducer/index.js @@ -0,0 +1,48 @@ +// import action types +import { + GET_PROFILE_ERROR, + GET_PROFILE_SUCCESS, + UPDATE_PROFILE_ERROR, + UPDATE_PROFILE_SUCCESS, +} from 'store/actions/types'; + +// initial state object +export const initialState = { + profile: { + firstname: '', + lastname: '', + bio: '', + image: '', + email: '', + username: '', + }, + error: {}, + isLoading: true, +}; + +/** + * This is a switch function for our profile reducer. + * @param {Object} - initialState* + * @return {object} - action + * @example + * + */ + +const profileReducer = (state = initialState, action) => { + switch (action.type) { + case GET_PROFILE_SUCCESS: + return Object.assign({}, state, { + profile: action.profile, + }); + case GET_PROFILE_ERROR: + case UPDATE_PROFILE_SUCCESS: + return Object.assign({}, state, { + profile: action.profile, + }); + case UPDATE_PROFILE_ERROR: + default: + return state; + } +}; + +export default profileReducer; diff --git a/src/store/reducers/profileReducer/profileReducer.test.js b/src/store/reducers/profileReducer/profileReducer.test.js new file mode 100644 index 0000000..08d624c --- /dev/null +++ b/src/store/reducers/profileReducer/profileReducer.test.js @@ -0,0 +1,50 @@ +import { + fetchUserProfile, + fetchUserProfileError, +} from 'store/actions/profileActions'; +import profileReducer from './index'; + + +const initialState = { + firstname: 'nadra', + lastname: 'john', + bio: 'this is my bio', + image: 'http://johndoeimagelink.jpg', + email: 'nadrajohn@gmail.com', + username: 'nadralia', + isLoading: false, +}; + +const profile = { + firstname: 'john', + lastname: 'doe', + bio: 'I am the original 007', + image: 'http://johndoeimagelink.jpg', + email: 'johndoe@gmail.com', + username: 'johndoe', + error: {}, + isLoading: false, +}; + +describe('profile reducers', () => { + it('should return the default state ', () => { + const state = profileReducer(initialState, { + type: 'unknown', + }); + expect(state).toEqual(initialState); + }); + + it('should return the profile', () => { + const action = fetchUserProfile(profile); + const state = profileReducer(initialState, action); + expect(state.profile).toEqual(action.profile); + expect(state.isLoading).toEqual(false); + }); + + it('should return an error if any on getting a profile', () => { + const action = fetchUserProfileError(); + const state = profileReducer(initialState, action); + expect(state.error).toEqual(action.error); + expect(state.isLoading).toEqual(false); + }); +}); diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js index 6f8567e..61fb407 100644 --- a/src/store/rootReducer.js +++ b/src/store/rootReducer.js @@ -6,6 +6,7 @@ import confirmEmailReducer from 'store/reducers/confirmEmailReducer'; import passwordResetReducer from 'store/reducers/passwordResetReducer'; import articles from './reducers/articles'; +import profile from './reducers/profileReducer'; const rootReducers = combineReducers({ @@ -13,7 +14,8 @@ const rootReducers = combineReducers({ loginReducer, articles, confirmEmailReducer, - passwordResetReducer + passwordResetReducer, + profile, }); export default rootReducers; diff --git a/src/utils/mainAxios.js b/src/utils/mainAxios.js new file mode 100644 index 0000000..319651c --- /dev/null +++ b/src/utils/mainAxios.js @@ -0,0 +1,11 @@ +import axios from 'axios'; +import { isAuthenticated } from 'utils'; + +const instance = axios.create({ + baseURL: 'https://ah-backend-dojo-dev.herokuapp.com/api', + headers: { + Authorization: `Bearer ${isAuthenticated().token}`, + }, +}); + +export default instance; diff --git a/webpack.config.js b/webpack.config.js index 199223f..2ed0192 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,6 +16,7 @@ module.exports = { store: path.resolve(__dirname, 'src/store/'), utils: path.resolve(__dirname, 'src/utils/'), constants: path.resolve(__dirname, 'src/constants/'), + routers: path.resolve(__dirname, 'src/routers/'), }, }, module: {