Skip to content

Commit

Permalink
feat(search/filter): Users can search and/or filter Articles
Browse files Browse the repository at this point in the history
-Search by title, author, tag
-Filter by title, author, tag
-Search/Filter by any two, or all three above

[Starts #161348769]
  • Loading branch information
Bruce Allan Makaaru authored and Bruce Allan Makaaru committed Dec 19, 2018
1 parent 337bbac commit 9f51e76
Show file tree
Hide file tree
Showing 31 changed files with 2,126 additions and 122 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"dotenv-webpack": "^1.5.7",
"expect": "^23.6.0",
"file-loader": "^2.0.0",
"history": "^4.7.2",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"identity-obj-proxy": "^3.0.0",
Expand Down
52 changes: 49 additions & 3 deletions src/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,50 @@
exports[`Provider and App renders <Provider/> correctly 1`] = `<App />`;

exports[`Provider and App renders <Provider/> correctly 2`] = `
<BrowserRouter>
<withRouter(Header) />
<Router
history={
Object {
"action": "POP",
"block": [Function],
"createHref": [Function],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"length": 1,
"listen": [Function],
"location": Object {
"hash": "",
"pathname": "/",
"search": "",
"state": undefined,
},
"push": [Function],
"replace": [Function],
}
}
>
<Connect(Header)
history={
Object {
"action": "POP",
"block": [Function],
"createHref": [Function],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"length": 1,
"listen": [Function],
"location": Object {
"hash": "",
"pathname": "/",
"search": "",
"state": undefined,
},
"push": [Function],
"replace": [Function],
}
}
/>
<Switch>
<Route
component={[Function]}
Expand Down Expand Up @@ -35,7 +77,11 @@ exports[`Provider and App renders <Provider/> correctly 2`] = `
component={[Function]}
path="/profiles/edit"
/>
<Route
component={[Function]}
path="/search"
/>
</Switch>
<Footer />
</BrowserRouter>
</Router>
`;
6 changes: 5 additions & 1 deletion src/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ const ACTION_TYPE = {
GET_PROFILE_ERROR: 'GET_PROFILE_ERROR',
EDIT_PROFILE: 'EDIT_PROFILE',
UPLOAD_IMAGE: 'UPLOAD_IMAGE',

GET_RATING_SUCCESS: 'GET_RATING_SUCCESS',
POST_RATING_SUCCESS: 'POST_RATING_SUCCESS',
POST_RATING_FAILED: 'POST_RATING_FAILED',
POST_RATING_DATA: 'POST_RATING_DATA',
SAVE_SEARCH_QUERY: 'SAVE_SEARCH_QUERY',
CLEAR_SEARCH_QUERY: 'CLEAR_SEARCH_QUERY',
SAVE_SEARCH_RESULTS: 'SAVE_SEARCH_RESULTS',
SEARCH_FAILED: 'SEARCH_FAILED',
UPDATE_FILTER_FIELD: 'UPDATE_FILTER_FIELD',
};

export default ACTION_TYPE;
2 changes: 1 addition & 1 deletion src/actions/ratingActions/ratingActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('Login Actions tests', () => {
moxios.stubRequest(`${APP_URL}/articles/7/rating`, statusData(200, { message: 'ok' }));
store.dispatch(postRating(7)).then(() => {
expect(store.getActions()).toEqual(expect.objectContaining(
actionTypesData(ACTION_TYPE.POST_RATING_SUCCESS, { message: 'ok' }),
[actionTypesData(ACTION_TYPE.POST_RATING_SUCCESS, { message: 'ok' })],
));
});
});
Expand Down
25 changes: 25 additions & 0 deletions src/actions/searchActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import axios from 'axios';
import ACTION_TYPE from './actionTypes';

const saveSearchQueryAction = payload => ({
type: ACTION_TYPE.SAVE_SEARCH_QUERY,
payload,
});

const saveSearchResultsAction = payload => ({
type: ACTION_TYPE.SAVE_SEARCH_RESULTS,
payload,
});

export const updateFilterFieldAction = payload => ({
type: ACTION_TYPE.UPDATE_FILTER_FIELD,
payload,
});

export const fetchSearchArticlesThunk = url => dispatch => axios.get(url)
.then((response) => {
dispatch(saveSearchResultsAction(response.data.results));
})
.catch(() => {});

export default saveSearchQueryAction;
39 changes: 39 additions & 0 deletions src/actions/tests/searchActions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import moxios from 'moxios';
import configureMockStore from 'redux-mock-store';
import reduxThunk from 'redux-thunk';
import { fetchSearchArticlesThunk } from '../searchActions';
import APP_URL from '../../utils/constants';
import ACTION_TYPE from '../actionTypes';
import { sampleListOfArticles } from '../../commons/initialStates';

describe('Search Actions', () => {
let store;
let url;

beforeEach(() => {
moxios.install();
const mockStore = configureMockStore([reduxThunk]);
store = mockStore({});
url = `${APP_URL}/articles?title=dragon`;
});

afterEach(() => {
moxios.uninstall();
});

test('search articles thunk', () => {
moxios.stubRequest(url, {
status: 200,
responseText: sampleListOfArticles,
});
const expectedActions = [{
type: ACTION_TYPE.SAVE_SEARCH_RESULTS,
payload: sampleListOfArticles,
}];
store.dispatch((fetchSearchArticlesThunk(url)))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});
});
3 changes: 3 additions & 0 deletions src/commons/history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createBrowserHistory } from 'history';

export default createBrowserHistory();
13 changes: 13 additions & 0 deletions src/commons/initialStates.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ const initialState = {
},
},
ratingReducer: {},
searchReducer: {
searchArticles: [],
searchQuery: '',
title: '',
tag: '',
author: '',
},
};

export const sampleArticle = {
Expand Down Expand Up @@ -85,4 +92,10 @@ export const initialStateWithSample2 = {
article: sampleArticle2,
};

export const sampleListOfArticles = [
sampleArticle, { ...sampleArticle, id: 2 }, { ...sampleArticle, id: 3 },
{ ...sampleArticle, id: 4 }, { ...sampleArticle, id: 5 }, { ...sampleArticle, id: 6 },
{ ...sampleArticle, id: 7 }, { ...sampleArticle, id: 8 }, { ...sampleArticle, id: 9 },
];

export default initialState;
19 changes: 12 additions & 7 deletions src/components/App/index.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import {
Router, Route, Switch,
} from 'react-router-dom';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import history from '../../commons/history';
import Home from '../Home';
import Login from '../Login/index';
import Header from '../Header';
import LoginPage from '../../containers/LoginPage';
import Header from '../../containers/Header';
import { Footer } from '../Footer';
import SignUpPageConnected from '../../containers/SignUpPage';
import Articles from '../../containers/Articles';
import ArticlePageConnected from '../../containers/ArticlePage';
import ProfileConnected from '../../containers/profiles/profiles';
import EditProfilePageConnected from '../../containers/profiles/editProfile';
import SearchPage from '../../containers/SearchPage';

library.add(faSearch);
const App = () => (
<BrowserRouter>
<Router history={history}>
<React.Fragment>
<Header />
<Header history={history} />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/login" component={LoginPage} />
<Route path="/signup" component={SignUpPageConnected} />
<Route path="/articles" component={Articles} />
<Route path="/article/:articleId" component={ArticlePageConnected} />
<Route path="/profile" component={ProfileConnected} />
<Route path="/profiles/edit" component={EditProfilePageConnected} />
<Route path="/search" component={SearchPage} />
</Switch>
<Footer />
</React.Fragment>
</BrowserRouter>
</Router>
);

export default App;
49 changes: 49 additions & 0 deletions src/components/Filter/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Filter /> component should render correctly 1`] = `
<Filter
onChange={[MockFunction]}
onClick={[MockFunction]}
searchQuery="test"
>
<div
className="container filter-box pt-3 text-center"
>
<h6>
Search or Filter by:
</h6>
<input
className="p-1 m-1"
id="title"
name="title"
onChange={[MockFunction]}
placeholder="Title"
type="text"
/>
<input
className="p-1 m-1"
id="tag"
name="tag"
onChange={[MockFunction]}
placeholder="Tag"
type="text"
/>
<input
className="p-1 m-1"
id="author"
name="author"
onChange={[MockFunction]}
placeholder="Author"
type="text"
/>
<button
className="btn btn-primary btn-sm"
id="btFilter"
onClick={[MockFunction]}
type="button"
>
Filter
</button>
</div>
</Filter>
`;
40 changes: 40 additions & 0 deletions src/components/Filter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';

const Filter = ({ searchQuery, onChange, onClick }) => {
const renderInputElement = (id, placeholder) => (
<input
type="text"
name={id}
id={id}
placeholder={placeholder}
className="p-1 m-1"
onChange={onChange}
/>
);

return (
<div className="container filter-box pt-3 text-center">
<h6>Search or Filter by:</h6>
{renderInputElement('title', 'Title', searchQuery)}
{renderInputElement('tag', 'Tag')}
{renderInputElement('author', 'Author')}
<button
type="button"
id="btFilter"
onClick={onClick}
className="btn btn-primary btn-sm"
>
Filter
</button>
</div>
);
};

Filter.propTypes = {
searchQuery: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
};

export default Filter;
12 changes: 12 additions & 0 deletions src/components/Filter/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { mount } from 'enzyme';
import Filter from '.';

describe('<Filter /> component', () => {
it('should render correctly', () => {
const wrapper = mount(
<Filter searchQuery="test" onChange={jest.fn()} onClick={jest.fn()} />,
);
expect(wrapper).toMatchSnapshot();
});
});
17 changes: 0 additions & 17 deletions src/components/Header/header.test.js

This file was deleted.

Loading

0 comments on commit 9f51e76

Please sign in to comment.