Skip to content

Commit

Permalink
Merge b8c98fe into ab04d1a
Browse files Browse the repository at this point in the history
  • Loading branch information
actlikewill committed Jan 24, 2019
2 parents ab04d1a + b8c98fe commit 73dd695
Show file tree
Hide file tree
Showing 16 changed files with 477 additions and 5 deletions.
5 changes: 4 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import 'semantic-ui-css/semantic.min.css';
import { Route } from 'react-router';
import { Switch, BrowserRouter } from 'react-router-dom';

import RegistrationPage from './components/Authentication/Registration/RegistrationPage';
import SearchResultsPage from './components/Search/SearchResultsPage';
import Error from './components/ErrorHandlers/MissingPageError';
import ArticleContainer from './components/Articles/ArticlesContainer';
import RegistrationPage from './components/Authentication/Registration/RegistrationPage';
import LoginContainer from './components/Authentication/Login/LoginContainer';


Expand All @@ -19,6 +20,8 @@ class App extends Component {
<Route path="/articles" component={ArticleContainer} exact />
<Route path="/register" component={RegistrationPage} exact />
<Route exact path="/login" component={LoginContainer} />
<Route path="/search" component={SearchResultsPage} exact />

<Route component={Error} />
</Switch>
</BrowserRouter>
Expand Down
65 changes: 65 additions & 0 deletions src/actions/__tests__/searchActions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import moxios from 'moxios';
import * as searchActions from '../searchActions';
import * as types from '../actionTypes';

const middleware = [thunk];
const mockStore = configureMockStore(middleware);

describe('Search Actions', () => {
beforeEach(() => {
moxios.install();
});
beforeEach(() => {
moxios.install();
});

it('should call the SEARCH_SUCCESS action on success', () => {
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
// TODO : fix this response message
response: { results: 'keywords present' },
});
});

const searchKeyWords = 'some search keywords';

const results = { results: 'keywords present' };

const expectedActions = [
{ type: types.SEARCH_SUBMIT, searchKeyWords },
{ type: types.SEARCH_SUCCESS, results },
];
const store = mockStore({});
return store.dispatch(searchActions.searchAction(searchKeyWords)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it('should call the SEARCH_FAIL action on failure', () => {
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 400,
// TODO : fix this response message
response: { error: 'no results found' },
});
});

const searchKeyWords = 'some search keywords';

const response = { error: 'no results found' };

const expectedActions = [
{ type: types.SEARCH_SUBMIT, searchKeyWords },
{ type: types.SEARCH_FAIL, response },
];
const store = mockStore({});
return store.dispatch(searchActions.searchAction(searchKeyWords)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
3 changes: 3 additions & 0 deletions src/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export const FETCH_ARTICLES_FAILS = 'FETCH_ARTICLES';
export const LOGIN_ACTION = 'LOGIN_ACTION';
export const LOGIN_SUCCESSFUL = 'LOGIN_SUCCESSFUL';
export const LOGIN_REJECTED = 'LOGIN_REJECTED';
export const SEARCH_SUBMIT = 'SEARCH_SUBMIT';
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
export const SEARCH_FAIL = 'SEARCH_FAIL';
33 changes: 33 additions & 0 deletions src/actions/searchActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Axios from 'axios';
import * as types from './actionTypes';

export const searchSubmit = searchKeyWords => ({
type: types.SEARCH_SUBMIT,
searchKeyWords,
});

export const searchSuccess = results => ({
type: types.SEARCH_SUCCESS,
results,
});

export const searchFail = response => ({
type: types.SEARCH_FAIL,
response,
});
/**
* This is the main action to dispatch the call to the API
* @function searchAction
* @param {string} searchKeyWords
* @return {Promise}
*/
export const searchAction = searchKeyWords => (dispatch) => {
dispatch(searchSubmit(searchKeyWords));
return Axios.get(`https://ah-technocrats.herokuapp.com/api/article/?search=${searchKeyWords}`)
.then((res) => {
dispatch(searchSuccess(res.data));
})
.catch((error) => {
dispatch(searchFail(error.response.data));
});
};
6 changes: 4 additions & 2 deletions src/components/Articles/ArticlesContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export class ArticleContainer extends React.Component {
}

render() {
const { articles } = this.props;
const { articles, history } = this.props;
return (
<React.Fragment>
<Header />
<Header history={history} />
<Sidebar.Pushable as={Segment} attached="bottom">
<SideBarMenu />
<Sidebar.Pusher id="pusher" className="pusher-height">
Expand All @@ -45,11 +45,13 @@ export class ArticleContainer extends React.Component {
ArticleContainer.propTypes = {
fetchArticles: PropTypes.func,
articles: PropTypes.arrayOf(PropTypes.object),
history: PropTypes.shape({}),
};

ArticleContainer.defaultProps = {
fetchArticles: () => {},
articles: [],
history: null,
};

const mapStateToProps = state => ({
Expand Down
17 changes: 15 additions & 2 deletions src/components/Header/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import './header.scss';
import {
Menu, Image, Sticky,
} from 'semantic-ui-react';
import ahLogoWide from './ah-logo-wide.svg';

import SearchBox from '../Search/SearchBox';

export const toggleSideBar = () => {
const sideBar = document.getElementById('sidebar');
Expand All @@ -15,15 +16,27 @@ export const toggleSideBar = () => {
};


const Header = () => (
const Header = ({ history }) => (
<Sticky offset={0}>
<Menu borderless attached="top">
<Menu.Item name="Menu" icon="sidebar" id="menuBtn" onClick={toggleSideBar} link position="left" />
<div className="ui centered">
<Image src={ahLogoWide} className="logo" />
</div>
<div className="searchBox">
<SearchBox history={history} />
</div>
</Menu>
</Sticky>
);

Header.propTypes = {
history: PropTypes.shape({}),
};

Header.defaultProps = {
history: null,
};


export default Header;
4 changes: 4 additions & 0 deletions src/components/Header/header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ $menu_color: rgba(0,0,0,.87);

}

.searchBox {
padding: 20px
}


@media screen and (max-width: 552px) {
.logo {
Expand Down
19 changes: 19 additions & 0 deletions src/components/Search/Search.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.searchResultsPage {
min-height: (100vh)-25;
}

.searchResultsTitle {
padding: 20px;
}

.noSearchResults {
margin: 0 auto;
}

.searchResults {
margin-left: 150px !important;
}

.searchResult {
margin: 30px !important;
}
61 changes: 61 additions & 0 deletions src/components/Search/SearchBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Input } from 'semantic-ui-react';
import { connect } from 'react-redux';
import { searchAction } from '../../actions/searchActions';

export class SearchBar extends Component {
constructor(props) {
super(props);
this.state = {
searchKeyWords: '',
};
this.handleChange = this.handleChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}

onSubmit(e) {
e.preventDefault();
const { search, history } = this.props;
const { searchKeyWords } = this.state;
search(searchKeyWords);
history.push('/search');
}

handleChange(event) {
this.setState({ [event.target.name]: event.target.value });
}

render() {
return (
<div>
<form onSubmit={this.onSubmit} data-test="searchBoxComponent">
<div className="ui icon centered input">
<Input onChange={this.handleChange} name="searchKeyWords" placeholder="Search" />
<i className="search icon" />
</div>
</form>
</div>
);
}
}

SearchBar.propTypes = {
search: PropTypes.func,
history: PropTypes.shape({}),
};

SearchBar.defaultProps = {
search: null,
history: null,
};

function mapDispatchToProps(dispatch) {
return {
search: keyword => (
dispatch(searchAction(keyword))
),
};
}

export default connect(null, mapDispatchToProps)(SearchBar);
50 changes: 50 additions & 0 deletions src/components/Search/SearchResultsComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';

const SearchResultsComponent = ({ results, noResults }) => (
<div>
<div className="ui searchResults items">
{ results
? results.map(result => (
<div key={result.article_slug} data-test="searchResult" className="ui searchResult item">
<div className=" content">
<div className="header">
<Link to={`/articles/${result.article_slug}`}>{result.title}</Link>
</div>
<div className="meta">
Written By:
{' '}
<b>{result.author.username}</b>
</div>
</div>
</div>
))
: null
}
{ noResults
? (
<div className="ui item">
<div data-test="noResults" className="content">
<div className="header">No results Found.</div>
</div>
</div>
)
: null
}
</div>
</div>
);

SearchResultsComponent.propTypes = {
results: PropTypes.arrayOf(PropTypes.shape({})),
noResults: PropTypes.bool,
};

SearchResultsComponent.defaultProps = {
results: null,
noResults: null,
};


export default SearchResultsComponent;
Loading

0 comments on commit 73dd695

Please sign in to comment.