From 4d4023435462be0ac2ba36818feb0563411db62d Mon Sep 17 00:00:00 2001 From: Devon Wijesinghe Date: Fri, 16 Aug 2019 09:18:28 +0530 Subject: [PATCH 1/3] Adds search redux flow --- .../src/pages/Dashboard/Dashboard.jsx | 2 + .../src/pages/Search/Search.jsx | 39 +++++++++++++++++++ fact-bounty-client/src/pages/Search/index.js | 2 + .../src/pages/Search/style.sass | 14 +++++++ .../src/redux/actions/actionTypes.js | 1 + .../src/redux/actions/postActions.js | 18 ++++++++- .../src/redux/reducers/postReducers.js | 12 +++++- .../src/services/PostsService.js | 12 +++++- 8 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 fact-bounty-client/src/pages/Search/Search.jsx create mode 100644 fact-bounty-client/src/pages/Search/index.js create mode 100644 fact-bounty-client/src/pages/Search/style.sass diff --git a/fact-bounty-client/src/pages/Dashboard/Dashboard.jsx b/fact-bounty-client/src/pages/Dashboard/Dashboard.jsx index d9b05f88..d64f3b86 100644 --- a/fact-bounty-client/src/pages/Dashboard/Dashboard.jsx +++ b/fact-bounty-client/src/pages/Dashboard/Dashboard.jsx @@ -8,6 +8,7 @@ import Posts from '../Posts' import PostDetailView from '../PostDetailView' import TwitterGraph from '../TwitterGraph' import DashboardSideNav from '../../components/DashboardSideNav' +import Search from '../Search' class Dashboard extends Component { render() { @@ -42,6 +43,7 @@ class Dashboard extends Component { path={`${match.url}/twitter`} component={TwitterGraph} /> + diff --git a/fact-bounty-client/src/pages/Search/Search.jsx b/fact-bounty-client/src/pages/Search/Search.jsx new file mode 100644 index 00000000..9217db2d --- /dev/null +++ b/fact-bounty-client/src/pages/Search/Search.jsx @@ -0,0 +1,39 @@ +import React, { Component, Fragment } from 'react' +import { connect } from 'react-redux' +import { getPostsFromKeyword } from '../../redux/actions/postActions' +import './style.sass' + +class Search extends Component { + + componentDidMount() { + this.props.getPostsFromKeyword('attack') + } + + render() { + return ( + +
+
+

Search

+
+
+
+
+ ) + } +} + +Search.propTypes = {} + +const mapStatetoProps = state => ({ + searchedPosts: state.posts.searchedPosts +}) + +const mapDispatchToProps = dispatch => ({ + getPostsFromKeyword: keyword => dispatch(getPostsFromKeyword(keyword)), +}) + +export default connect( + mapStatetoProps, + mapDispatchToProps +)(Search) diff --git a/fact-bounty-client/src/pages/Search/index.js b/fact-bounty-client/src/pages/Search/index.js new file mode 100644 index 00000000..0c70b7a2 --- /dev/null +++ b/fact-bounty-client/src/pages/Search/index.js @@ -0,0 +1,2 @@ +import Search from './Search' +export default Search diff --git a/fact-bounty-client/src/pages/Search/style.sass b/fact-bounty-client/src/pages/Search/style.sass new file mode 100644 index 00000000..c14710ce --- /dev/null +++ b/fact-bounty-client/src/pages/Search/style.sass @@ -0,0 +1,14 @@ +.search-wrapper + .header + padding: 20px 0px 10px 0px + display: flex + justify-content: space-between + align-items: center + input + width: 300px; + box-shadow: 1px 1px 15px rgba(0, 0, 0, .1) + border: none + border-radius: 3px + padding: 10px + .hr + margin-bottom: 40px diff --git a/fact-bounty-client/src/redux/actions/actionTypes.js b/fact-bounty-client/src/redux/actions/actionTypes.js index 456d3820..3e60688e 100644 --- a/fact-bounty-client/src/redux/actions/actionTypes.js +++ b/fact-bounty-client/src/redux/actions/actionTypes.js @@ -12,6 +12,7 @@ export const LOADING_USER_VOTES = 'LOADING_USER_VOTES' export const SET_USER_VOTES = 'SET_USER_VOTES' export const UPDATE_USER_VOTES = 'UPDATE_USER_VOTES' export const UPDATE_POST_VOTES = 'UPDATE_POST_VOTES' +export const SET_POSTS_ON_SEARCH = 'SET_POSTS_ON_SEARCH' export const START_CONTACT_FORM_SUBMIT = 'START_CONTACT_FORM_SUBMIT' export const CONTACT_FORM_SUBMIT_SUCCESS = 'CONTACT_FORM_SUBMIT_SUCCESS' diff --git a/fact-bounty-client/src/redux/actions/postActions.js b/fact-bounty-client/src/redux/actions/postActions.js index f1d76a1a..406fac78 100644 --- a/fact-bounty-client/src/redux/actions/postActions.js +++ b/fact-bounty-client/src/redux/actions/postActions.js @@ -7,7 +7,8 @@ import { LOADING_USER_VOTES, SET_USER_VOTES, UPDATE_USER_VOTES, - UPDATE_POST_VOTES + UPDATE_POST_VOTES, + SET_POSTS_ON_SEARCH } from './actionTypes' import PostsService from '../../services/PostsService' @@ -78,3 +79,18 @@ export const loadUserVotes = () => dispatch => { console.error('Server response invalid:', err) }) } + +export const getPostsFromKeyword = keyword => dispatch => { + dispatch({ type: LOADING_USER_VOTES }) + PostsService.getPostsFromKeyword(keyword) + .then(res => { + console.log(res) + dispatch({ + type: SET_POSTS_ON_SEARCH, + payload: res.data + }) + }) + .catch(err => { + console.error('Server response invalid:', err) + }) +} diff --git a/fact-bounty-client/src/redux/reducers/postReducers.js b/fact-bounty-client/src/redux/reducers/postReducers.js index 6f6a7fce..b824fde3 100644 --- a/fact-bounty-client/src/redux/reducers/postReducers.js +++ b/fact-bounty-client/src/redux/reducers/postReducers.js @@ -7,7 +7,8 @@ import { LOADING_USER_VOTES, SET_USER_VOTES, UPDATE_USER_VOTES, - UPDATE_POST_VOTES + UPDATE_POST_VOTES, + SET_POSTS_ON_SEARCH } from '../actions/actionTypes' const initialState = { @@ -16,7 +17,8 @@ const initialState = { page: 1, userVotes: [], loadingPosts: false, - loadingUserVotes: false + loadingUserVotes: false, + searchedPosts: null } export default function(state = initialState, action) { @@ -112,6 +114,12 @@ export default function(state = initialState, action) { items: updatedItems } } + case SET_POSTS_ON_SEARCH: { + return { + ...state, + searchedPosts: action.payload + } + } default: { return state } diff --git a/fact-bounty-client/src/services/PostsService.js b/fact-bounty-client/src/services/PostsService.js index 399ced35..b3d30c81 100644 --- a/fact-bounty-client/src/services/PostsService.js +++ b/fact-bounty-client/src/services/PostsService.js @@ -39,9 +39,19 @@ const loadUserVotes = () => { return ApiBuilder.API.post(`/api/stories/load-user-votes`, {}) } +/** + * + * GET : getPostsFromKeyword + * + */ +const getPostsFromKeyword = keyword => { + return ApiBuilder.API.get(`/api/stories/search/${keyword}`) +} + export default { fetchPosts, fetchPostById, changeVoteCount, - loadUserVotes + loadUserVotes, + getPostsFromKeyword } From 6b45b0f86ef0f829fe05db8d620717efce38a328 Mon Sep 17 00:00:00 2001 From: Devon Wijesinghe Date: Fri, 16 Aug 2019 10:19:44 +0530 Subject: [PATCH 2/3] Wires up the earch form --- .../src/pages/Search/Search.jsx | 103 +++++++++++++++++- .../src/pages/Search/style.sass | 14 +++ .../src/redux/actions/postActions.js | 4 +- .../src/redux/reducers/postReducers.js | 68 ++++++------ 4 files changed, 151 insertions(+), 38 deletions(-) diff --git a/fact-bounty-client/src/pages/Search/Search.jsx b/fact-bounty-client/src/pages/Search/Search.jsx index 9217db2d..149002e1 100644 --- a/fact-bounty-client/src/pages/Search/Search.jsx +++ b/fact-bounty-client/src/pages/Search/Search.jsx @@ -1,36 +1,131 @@ import React, { Component, Fragment } from 'react' import { connect } from 'react-redux' -import { getPostsFromKeyword } from '../../redux/actions/postActions' +import PropTypes from 'prop-types' +import { + getPostsFromKeyword, + loadUserVotes +} from '../../redux/actions/postActions' +import PostItem from '../../components/PostItem' +import { Button } from '@material-ui/core' import './style.sass' class Search extends Component { + constructor(props) { + super(props) + this.state = { + keyword: '', + currentKeyword: '' + } + } componentDidMount() { - this.props.getPostsFromKeyword('attack') + this.props.loadUserVotes() + } + + onSearchPressed = e => { + e.preventDefault() + const { keyword } = this.state + this.props.getPostsFromKeyword(keyword) + this.setState({ currentKeyword: keyword }) + } + + renderPostList = () => { + const { currentKeyword } = this.state + const { posts, userVotes } = this.props + const validPosts = posts ? posts : [] + + if (validPosts.length === 0) { + return ( +
+ {currentKeyword === '' ? ( +

Type a keyword and press search!

+ ) : ( +

+ No result found for: {currentKeyword} +

+ )} +
+ ) + } + return ( +
+

+ Showing results for: {currentKeyword} +

+ {validPosts.map((post, index) => { + console.log(userVotes) + const userVote = userVotes.filter(uv => uv.story_id === post._id) + return ( + + ) + })} +
+ ) } render() { + const { keyword } = this.state return (

Search

+
+
+ { + this.setState({ keyword: e.target.value }) + }} + /> + +
+

+ {this.renderPostList()}
) } } -Search.propTypes = {} +Search.propTypes = { + posts: PropTypes.array, + loadingPosts: PropTypes.bool, + userVotes: PropTypes.array, + loadingUserVotes: PropTypes.bool, + user: PropTypes.object, + getPostsFromKeyword: PropTypes.func, + loadUserVotes: PropTypes.func +} const mapStatetoProps = state => ({ - searchedPosts: state.posts.searchedPosts + posts: state.posts.searchedPosts, + userVotes: state.posts.userVotes, + user: state.auth.user }) const mapDispatchToProps = dispatch => ({ getPostsFromKeyword: keyword => dispatch(getPostsFromKeyword(keyword)), + loadUserVotes: () => dispatch(loadUserVotes()) }) export default connect( diff --git a/fact-bounty-client/src/pages/Search/style.sass b/fact-bounty-client/src/pages/Search/style.sass index c14710ce..950c0b02 100644 --- a/fact-bounty-client/src/pages/Search/style.sass +++ b/fact-bounty-client/src/pages/Search/style.sass @@ -12,3 +12,17 @@ padding: 10px .hr margin-bottom: 40px + .search-form-container + text-align: center + form + display: flex + flex-direction: row + input + z-index: 1 + box-shadow: 1px 1px 15px rgba(0, 0, 0, .1) + border: none + border-radius: 3px + padding: 12px + resize: none + width: 500px + margin-right: 15px \ No newline at end of file diff --git a/fact-bounty-client/src/redux/actions/postActions.js b/fact-bounty-client/src/redux/actions/postActions.js index 406fac78..3d7887e9 100644 --- a/fact-bounty-client/src/redux/actions/postActions.js +++ b/fact-bounty-client/src/redux/actions/postActions.js @@ -84,10 +84,10 @@ export const getPostsFromKeyword = keyword => dispatch => { dispatch({ type: LOADING_USER_VOTES }) PostsService.getPostsFromKeyword(keyword) .then(res => { - console.log(res) + console.log('getPostsFromKeyword', res) dispatch({ type: SET_POSTS_ON_SEARCH, - payload: res.data + payload: res.data.stories ? res.data.stories : null }) }) .catch(err => { diff --git a/fact-bounty-client/src/redux/reducers/postReducers.js b/fact-bounty-client/src/redux/reducers/postReducers.js index b824fde3..1d526373 100644 --- a/fact-bounty-client/src/redux/reducers/postReducers.js +++ b/fact-bounty-client/src/redux/reducers/postReducers.js @@ -13,12 +13,12 @@ import { const initialState = { items: [], // the posts + searchedPosts: [], // the posts in search currentPost: null, // the current post in the post detail view page: 1, userVotes: [], loadingPosts: false, - loadingUserVotes: false, - searchedPosts: null + loadingUserVotes: false } export default function(state = initialState, action) { @@ -80,38 +80,10 @@ export default function(state = initialState, action) { } } case UPDATE_POST_VOTES: { - const { story_id, value, userVote } = action.payload - const updatedItems = state.items.map(item => { - if (item._id === story_id) { - let approved_count_diffValue = 0 - let mixedvote_count_diffValue = 0 - let fake_count_diffValue = 0 - - if (value === 1) { - approved_count_diffValue = userVote === 1 ? 0 : 1 - mixedvote_count_diffValue = userVote === 0 ? -1 : 0 - fake_count_diffValue = userVote === -1 ? -1 : 0 - } else if (value === 0) { - approved_count_diffValue = userVote === 1 ? -1 : 0 - mixedvote_count_diffValue = userVote === 0 ? 0 : 1 - fake_count_diffValue = userVote === -1 ? -1 : 0 - } else if (value === -1) { - approved_count_diffValue = userVote === 1 ? -1 : 0 - mixedvote_count_diffValue = userVote === 0 ? -1 : 0 - fake_count_diffValue = userVote === -1 ? 0 : 1 - } - return { - ...item, - approved_count: item.approved_count + approved_count_diffValue, - mixedvote_count: item.mixedvote_count + mixedvote_count_diffValue, - fake_count: item.fake_count + fake_count_diffValue - } - } - return item - }) return { ...state, - items: updatedItems + items: getUpdatedItemsAfterVote(state.items, action), + searchedPosts: getUpdatedItemsAfterVote(state.searchedPosts, action) } } case SET_POSTS_ON_SEARCH: { @@ -125,3 +97,35 @@ export default function(state = initialState, action) { } } } + +const getUpdatedItemsAfterVote = (items, action) => { + const { story_id, value, userVote } = action.payload + return items.map(item => { + if (item._id === story_id) { + let approved_count_diffValue = 0 + let mixedvote_count_diffValue = 0 + let fake_count_diffValue = 0 + + if (value === 1) { + approved_count_diffValue = userVote === 1 ? 0 : 1 + mixedvote_count_diffValue = userVote === 0 ? -1 : 0 + fake_count_diffValue = userVote === -1 ? -1 : 0 + } else if (value === 0) { + approved_count_diffValue = userVote === 1 ? -1 : 0 + mixedvote_count_diffValue = userVote === 0 ? 0 : 1 + fake_count_diffValue = userVote === -1 ? -1 : 0 + } else if (value === -1) { + approved_count_diffValue = userVote === 1 ? -1 : 0 + mixedvote_count_diffValue = userVote === 0 ? -1 : 0 + fake_count_diffValue = userVote === -1 ? 0 : 1 + } + return { + ...item, + approved_count: item.approved_count + approved_count_diffValue, + mixedvote_count: item.mixedvote_count + mixedvote_count_diffValue, + fake_count: item.fake_count + fake_count_diffValue + } + } + return item + }) +} From 5659b0a9183939f8a846d0571b6a0ac5814f6271 Mon Sep 17 00:00:00 2001 From: Devon Wijesinghe Date: Fri, 16 Aug 2019 10:32:17 +0530 Subject: [PATCH 3/3] Adds loading and validation text --- fact-bounty-client/src/pages/Search/Search.jsx | 10 ++++------ .../src/redux/actions/actionTypes.js | 1 + .../src/redux/actions/postActions.js | 6 +++--- .../src/redux/reducers/postReducers.js | 15 ++++++++++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/fact-bounty-client/src/pages/Search/Search.jsx b/fact-bounty-client/src/pages/Search/Search.jsx index 149002e1..a127874e 100644 --- a/fact-bounty-client/src/pages/Search/Search.jsx +++ b/fact-bounty-client/src/pages/Search/Search.jsx @@ -69,6 +69,7 @@ class Search extends Component { render() { const { keyword } = this.state + const { loadingPosts } = this.props return (
@@ -92,15 +93,14 @@ class Search extends Component { color="primary" style={{ width: 120 }} type="submit" - loading={false} - disabled={false} + disabled={loadingPosts} > SEARCH

- {this.renderPostList()} + {loadingPosts ?

Loading...

: this.renderPostList()}
) @@ -111,8 +111,6 @@ Search.propTypes = { posts: PropTypes.array, loadingPosts: PropTypes.bool, userVotes: PropTypes.array, - loadingUserVotes: PropTypes.bool, - user: PropTypes.object, getPostsFromKeyword: PropTypes.func, loadUserVotes: PropTypes.func } @@ -120,7 +118,7 @@ Search.propTypes = { const mapStatetoProps = state => ({ posts: state.posts.searchedPosts, userVotes: state.posts.userVotes, - user: state.auth.user + loadingPosts: state.posts.searchedPostsLoading }) const mapDispatchToProps = dispatch => ({ diff --git a/fact-bounty-client/src/redux/actions/actionTypes.js b/fact-bounty-client/src/redux/actions/actionTypes.js index 3e60688e..4e578750 100644 --- a/fact-bounty-client/src/redux/actions/actionTypes.js +++ b/fact-bounty-client/src/redux/actions/actionTypes.js @@ -12,6 +12,7 @@ export const LOADING_USER_VOTES = 'LOADING_USER_VOTES' export const SET_USER_VOTES = 'SET_USER_VOTES' export const UPDATE_USER_VOTES = 'UPDATE_USER_VOTES' export const UPDATE_POST_VOTES = 'UPDATE_POST_VOTES' +export const START_POSTS_SEARCH = 'START_POSTS_SEARCH' export const SET_POSTS_ON_SEARCH = 'SET_POSTS_ON_SEARCH' export const START_CONTACT_FORM_SUBMIT = 'START_CONTACT_FORM_SUBMIT' diff --git a/fact-bounty-client/src/redux/actions/postActions.js b/fact-bounty-client/src/redux/actions/postActions.js index 3d7887e9..93708d0d 100644 --- a/fact-bounty-client/src/redux/actions/postActions.js +++ b/fact-bounty-client/src/redux/actions/postActions.js @@ -8,7 +8,8 @@ import { SET_USER_VOTES, UPDATE_USER_VOTES, UPDATE_POST_VOTES, - SET_POSTS_ON_SEARCH + SET_POSTS_ON_SEARCH, + START_POSTS_SEARCH } from './actionTypes' import PostsService from '../../services/PostsService' @@ -81,10 +82,9 @@ export const loadUserVotes = () => dispatch => { } export const getPostsFromKeyword = keyword => dispatch => { - dispatch({ type: LOADING_USER_VOTES }) + dispatch({ type: START_POSTS_SEARCH }) PostsService.getPostsFromKeyword(keyword) .then(res => { - console.log('getPostsFromKeyword', res) dispatch({ type: SET_POSTS_ON_SEARCH, payload: res.data.stories ? res.data.stories : null diff --git a/fact-bounty-client/src/redux/reducers/postReducers.js b/fact-bounty-client/src/redux/reducers/postReducers.js index 1d526373..672c33dd 100644 --- a/fact-bounty-client/src/redux/reducers/postReducers.js +++ b/fact-bounty-client/src/redux/reducers/postReducers.js @@ -8,7 +8,8 @@ import { SET_USER_VOTES, UPDATE_USER_VOTES, UPDATE_POST_VOTES, - SET_POSTS_ON_SEARCH + SET_POSTS_ON_SEARCH, + START_POSTS_SEARCH } from '../actions/actionTypes' const initialState = { @@ -18,7 +19,8 @@ const initialState = { page: 1, userVotes: [], loadingPosts: false, - loadingUserVotes: false + loadingUserVotes: false, + searchedPostsLoading: false } export default function(state = initialState, action) { @@ -86,10 +88,17 @@ export default function(state = initialState, action) { searchedPosts: getUpdatedItemsAfterVote(state.searchedPosts, action) } } + case START_POSTS_SEARCH: { + return { + ...state, + searchedPostsLoading: true + } + } case SET_POSTS_ON_SEARCH: { return { ...state, - searchedPosts: action.payload + searchedPosts: action.payload, + searchedPostsLoading: false } } default: {