From 5c5b84c32b1e4388b5005b72a801201c6d1035be Mon Sep 17 00:00:00 2001 From: Chad Martinson Date: Wed, 1 Jun 2016 15:08:56 -0400 Subject: [PATCH] ready for dem0 --- actions/index.js | 85 ++++++ components/List/index.js | 44 +++ components/Search/index.js | 58 ++++ components/User/index.js | 20 ++ package.json | 13 +- reducers/index.js | 31 ++ routes.js | 20 +- store/index.js | 27 ++ stub.js | 603 +++++++++++++++++++++++++++++++++++++ utils/index.js | 157 ++++++++++ views/App/index.js | 74 +++++ views/FishView/index.js | 11 - views/RootView/index.js | 17 +- views/UserDetails/index.js | 153 ++++++++++ views/UsersView/index.js | 65 ++++ 15 files changed, 1353 insertions(+), 25 deletions(-) create mode 100644 actions/index.js create mode 100644 components/List/index.js create mode 100644 components/Search/index.js create mode 100644 components/User/index.js create mode 100644 reducers/index.js create mode 100644 store/index.js create mode 100644 stub.js create mode 100644 utils/index.js create mode 100644 views/App/index.js delete mode 100644 views/FishView/index.js create mode 100644 views/UserDetails/index.js create mode 100644 views/UsersView/index.js diff --git a/actions/index.js b/actions/index.js new file mode 100644 index 0000000..fd20874 --- /dev/null +++ b/actions/index.js @@ -0,0 +1,85 @@ +import { CALL_API, Schemas } from '../utils'; + +/** + * [SEARCH_REQUEST description] + * @type {String} + */ +export const SEARCH_REQUEST = 'SEARCH_REQUEST'; +export const SEARCH_SUCCESS = 'SEARCH_SUCCESS'; +export const SEARCH_FAILURE = 'SEARCH_FAILURE'; + +function searchUsers(query) { + return { + [CALL_API]: { + types: [ SEARCH_REQUEST, SEARCH_SUCCESS, SEARCH_FAILURE ], + endpoint: `search/users?q=${query}`, + schema: Schemas.RESULTS_LIST + } + } +} + +export function loadSearchResults(query, requiredFields = []) { + return (dispatch) => dispatch(searchUsers(query)); +} + +export const USER_REQUEST = 'USER_REQUEST'; +export const USER_SUCCESS = 'USER_SUCCESS'; +export const USER_FAILURE = 'USER_FAILURE'; + +function getUserData(login) { + return { + [CALL_API]: { + types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ], + endpoint: `users/${login}`, + schema: Schemas.USER_DETAILS + } + } +} + +export function loadUserDetails(login, requiredFields = []) { + return (dispatch) => dispatch(getUserData(login)); +} + +export const FOLLOWERS_REQUEST = 'FOLLOWERS_REQUEST'; +export const FOLLOWERS_SUCCESS = 'FOLLOWERS_SUCCESS'; +export const FOLLOWERS_FAILURE = 'FOLLOWERS_FAILURE'; + +function getFollowers(login) { + return { + [CALL_API]: { + types: [ FOLLOWERS_REQUEST, FOLLOWERS_SUCCESS, FOLLOWERS_FAILURE ], + endpoint: `users/${login}/followers`, + schema: Schemas.FOLLOWERS_LIST + } + } +} + +export function loadFollowers(login, requiredFields = []) { + return (dispatch) => dispatch(getFollowers(login)); +} + +export const REPOS_REQUEST = 'REPOS_REQUEST'; +export const REPOS_SUCCESS = 'REPOS_SUCCESS'; +export const REPOS_FAILURE = 'REPOS_FAILURE'; + +function getRepos(login) { + return { + [CALL_API]: { + types: [ REPOS_REQUEST, REPOS_SUCCESS, REPOS_FAILURE ], + endpoint: `users/${login}/repos`, + schema: Schemas.REPOS_LIST + } + } +} + +export function loadRepos(login, requiredFields = []) { + return (dispatch) => dispatch(getRepos(login)); +} + +export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE'; + +export function resetErrorMessage() { + return { + type: RESET_ERROR_MESSAGE + } +} diff --git a/components/List/index.js b/components/List/index.js new file mode 100644 index 0000000..5f579aa --- /dev/null +++ b/components/List/index.js @@ -0,0 +1,44 @@ +import React, { Component, PropTypes } from 'react' +import { Link } from 'react-router' +import User from '../User' +import values from 'lodash/values' +import isEmpty from 'lodash/isEmpty' + +export default class List extends Component { + constructor(props) { + super(props) + this.renderUser = this.renderUser.bind(this) + // this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this) + } + + renderUser(user) { +console.log('user: ', user); + return ( +
+ +
+ ) + } + // renderLoadMore() { + // const { isFetching, onLoadMoreClick } = this.props + // return ( + // + // ) + // } + + render() { + const { items } = this.props + const usersArray = values(items) + + return ( +
+ {usersArray.map(this.renderUser)} +
+ ) + } +} diff --git a/components/Search/index.js b/components/Search/index.js new file mode 100644 index 0000000..cee287b --- /dev/null +++ b/components/Search/index.js @@ -0,0 +1,58 @@ +import React, { Component, PropTypes } from 'react'; + +export default class Search extends Component { + + static propTypes = { + value: PropTypes.string, + onChange: PropTypes.func.isRequired + } + + constructor(props) { + super(props); + this.handleKeyUp = this.handleKeyUp.bind(this); + this.handleClick = this.handleClick.bind(this); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.value !== this.props.value) { + this.setInputValue(nextProps.value); + } + } + + getInputValue() { + return this.refs.input.value + } + + setInputValue(val) { + // Generally mutating DOM is a bad idea in React components, + // but doing this for a single uncontrolled field is less fuss + // than making it controlled and maintaining a state for it. + this.refs.input.value = val + } + + handleKeyUp(e) { + if (e.keyCode === 13) { + this.handleClick() + } + } + + handleClick() { + this.props.onChange(this.getInputValue()) + } + + render() { + return ( +
+

Search GitHub for Users:

+ + + +
+ ) + } +} diff --git a/components/User/index.js b/components/User/index.js new file mode 100644 index 0000000..6b66fd7 --- /dev/null +++ b/components/User/index.js @@ -0,0 +1,20 @@ +import React, { Component, PropTypes } from 'react' +import { Link } from 'react-router' + +export default class User extends Component { + + + render() { + const { user: { login, avatarUrl } } = this.props + return ( +
+ + +

+ {login} +

+ +
+ ) + } +} diff --git a/package.json b/package.json index 7782bfe..1aa5dca 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,20 @@ }, "homepage": "https://github.com/GasStationTV/react-coding-exercise#readme", "dependencies": { + "async": "^2.0.0-rc.5", + "async-es": "^2.0.0-rc.5", "bootstrap": "^4.0.0-alpha.2", + "humps": "^1.1.0", + "isomorphic-fetch": "^2.2.1", + "lodash": "^4.13.1", + "normalizr": "^2.1.0", "react": "0.14.7", - "react-dom": "^0.14.7" + "react-dom": "^0.14.7", + "react-redux": "^4.4.5", + "react-router-redux": "^4.0.4", + "redux": "^3.5.2", + "redux-logger": "^2.6.1", + "redux-thunk": "^2.1.0" }, "devDependencies": { "babel-eslint": "^6.0.2", diff --git a/reducers/index.js b/reducers/index.js new file mode 100644 index 0000000..6f8e8b6 --- /dev/null +++ b/reducers/index.js @@ -0,0 +1,31 @@ +import * as ActionTypes from '../actions'; +import merge from 'lodash/merge'; +import { routerReducer as routing } from 'react-router-redux'; +import { combineReducers } from 'redux'; + +function entities(state = { users: {}, user: {}, followers: {}, repos: {} }, action) { + if (action.response && action.response.entities) { + return merge({}, state, action.response.entities); + } + return state; +} + +function errorMessage(state = null, action) { + const { type, error } = action; + + if (type === ActionTypes.RESET_ERROR_MESSAGE) { + return null; + } else if (error) { + return action.error; + } + + return state; +} + +const mainReducer = combineReducers({ + entities, + errorMessage, + routing +}); + +export default mainReducer; diff --git a/routes.js b/routes.js index 3ea20c9..3ca22f3 100644 --- a/routes.js +++ b/routes.js @@ -1,13 +1,19 @@ -import React from 'react'; -import { Route } from 'react-router'; -import HomeView from './views/RootView'; -import FishView from './views/FishView'; +import React from 'react'; +import { Route, IndexRoute } from 'react-router'; +import { Provider } from 'react-redux'; +import HomeView from './views/RootView'; +import App from './views/App'; +import UsersView from './views/UsersView'; +import UserDetails from './views/UserDetails'; import 'bootstrap/dist/css/bootstrap.min.css'; const routes = ( - - - + + + + + + ); export default routes; diff --git a/store/index.js b/store/index.js new file mode 100644 index 0000000..869ba66 --- /dev/null +++ b/store/index.js @@ -0,0 +1,27 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import createLogger from 'redux-logger'; +import api from '../utils'; +import mainReducer from '../reducers'; + +export default function configureStore(preloadedState) { + + const store = createStore( + mainReducer, + preloadedState, + compose( + applyMiddleware(thunk, api, createLogger()), + window.devToolsExtension ? window.devToolsExtension() : f => f + ) + ) + + if (module.hot) { + // Enable Webpack hot module replacement for reducers + module.hot.accept('../reducers', () => { + const nextRootReducer = require('../reducers/index') + store.replaceReducer(nextRootReducer) + }) + } + + return store +} diff --git a/stub.js b/stub.js new file mode 100644 index 0000000..d6d20ad --- /dev/null +++ b/stub.js @@ -0,0 +1,603 @@ +const stub = [ + { + "login": "dan", + "id": 219, + "avatar_url": "https://avatars.githubusercontent.com/u/219?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dan", + "html_url": "https://github.com/dan", + "followers_url": "https://api.github.com/users/dan/followers", + "following_url": "https://api.github.com/users/dan/following{/other_user}", + "gists_url": "https://api.github.com/users/dan/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dan/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dan/subscriptions", + "organizations_url": "https://api.github.com/users/dan/orgs", + "repos_url": "https://api.github.com/users/dan/repos", + "events_url": "https://api.github.com/users/dan/events{/privacy}", + "received_events_url": "https://api.github.com/users/dan/received_events", + "type": "User", + "site_admin": false, + "score": 111.750275 + }, + { + "login": "dhrrgn", + "id": 149921, + "avatar_url": "https://avatars.githubusercontent.com/u/149921?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dhrrgn", + "html_url": "https://github.com/dhrrgn", + "followers_url": "https://api.github.com/users/dhrrgn/followers", + "following_url": "https://api.github.com/users/dhrrgn/following{/other_user}", + "gists_url": "https://api.github.com/users/dhrrgn/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dhrrgn/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dhrrgn/subscriptions", + "organizations_url": "https://api.github.com/users/dhrrgn/orgs", + "repos_url": "https://api.github.com/users/dhrrgn/repos", + "events_url": "https://api.github.com/users/dhrrgn/events{/privacy}", + "received_events_url": "https://api.github.com/users/dhrrgn/received_events", + "type": "User", + "site_admin": false, + "score": 28.468105 + }, + { + "login": "danlucraft", + "id": 6118, + "avatar_url": "https://avatars.githubusercontent.com/u/6118?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danlucraft", + "html_url": "https://github.com/danlucraft", + "followers_url": "https://api.github.com/users/danlucraft/followers", + "following_url": "https://api.github.com/users/danlucraft/following{/other_user}", + "gists_url": "https://api.github.com/users/danlucraft/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danlucraft/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danlucraft/subscriptions", + "organizations_url": "https://api.github.com/users/danlucraft/orgs", + "repos_url": "https://api.github.com/users/danlucraft/repos", + "events_url": "https://api.github.com/users/danlucraft/events{/privacy}", + "received_events_url": "https://api.github.com/users/danlucraft/received_events", + "type": "User", + "site_admin": false, + "score": 28.468105 + }, + { + "login": "DanielTomlinson", + "id": 1330683, + "avatar_url": "https://avatars.githubusercontent.com/u/1330683?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/DanielTomlinson", + "html_url": "https://github.com/DanielTomlinson", + "followers_url": "https://api.github.com/users/DanielTomlinson/followers", + "following_url": "https://api.github.com/users/DanielTomlinson/following{/other_user}", + "gists_url": "https://api.github.com/users/DanielTomlinson/gists{/gist_id}", + "starred_url": "https://api.github.com/users/DanielTomlinson/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/DanielTomlinson/subscriptions", + "organizations_url": "https://api.github.com/users/DanielTomlinson/orgs", + "repos_url": "https://api.github.com/users/DanielTomlinson/repos", + "events_url": "https://api.github.com/users/DanielTomlinson/events{/privacy}", + "received_events_url": "https://api.github.com/users/DanielTomlinson/received_events", + "type": "User", + "site_admin": false, + "score": 28.468105 + }, + { + "login": "danzeeeman", + "id": 719564, + "avatar_url": "https://avatars.githubusercontent.com/u/719564?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danzeeeman", + "html_url": "https://github.com/danzeeeman", + "followers_url": "https://api.github.com/users/danzeeeman/followers", + "following_url": "https://api.github.com/users/danzeeeman/following{/other_user}", + "gists_url": "https://api.github.com/users/danzeeeman/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danzeeeman/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danzeeeman/subscriptions", + "organizations_url": "https://api.github.com/users/danzeeeman/orgs", + "repos_url": "https://api.github.com/users/danzeeeman/repos", + "events_url": "https://api.github.com/users/danzeeeman/events{/privacy}", + "received_events_url": "https://api.github.com/users/danzeeeman/received_events", + "type": "User", + "site_admin": false, + "score": 28.468105 + }, + { + "login": "gaearon", + "id": 810438, + "avatar_url": "https://avatars.githubusercontent.com/u/810438?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/gaearon", + "html_url": "https://github.com/gaearon", + "followers_url": "https://api.github.com/users/gaearon/followers", + "following_url": "https://api.github.com/users/gaearon/following{/other_user}", + "gists_url": "https://api.github.com/users/gaearon/gists{/gist_id}", + "starred_url": "https://api.github.com/users/gaearon/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/gaearon/subscriptions", + "organizations_url": "https://api.github.com/users/gaearon/orgs", + "repos_url": "https://api.github.com/users/gaearon/repos", + "events_url": "https://api.github.com/users/gaearon/events{/privacy}", + "received_events_url": "https://api.github.com/users/gaearon/received_events", + "type": "User", + "site_admin": false, + "score": 28.468105 + }, + { + "login": "danielgtaylor", + "id": 106826, + "avatar_url": "https://avatars.githubusercontent.com/u/106826?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danielgtaylor", + "html_url": "https://github.com/danielgtaylor", + "followers_url": "https://api.github.com/users/danielgtaylor/followers", + "following_url": "https://api.github.com/users/danielgtaylor/following{/other_user}", + "gists_url": "https://api.github.com/users/danielgtaylor/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danielgtaylor/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danielgtaylor/subscriptions", + "organizations_url": "https://api.github.com/users/danielgtaylor/orgs", + "repos_url": "https://api.github.com/users/danielgtaylor/repos", + "events_url": "https://api.github.com/users/danielgtaylor/events{/privacy}", + "received_events_url": "https://api.github.com/users/danielgtaylor/received_events", + "type": "User", + "site_admin": false, + "score": 26.167 + }, + { + "login": "dmotz", + "id": 302080, + "avatar_url": "https://avatars.githubusercontent.com/u/302080?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dmotz", + "html_url": "https://github.com/dmotz", + "followers_url": "https://api.github.com/users/dmotz/followers", + "following_url": "https://api.github.com/users/dmotz/following{/other_user}", + "gists_url": "https://api.github.com/users/dmotz/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dmotz/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dmotz/subscriptions", + "organizations_url": "https://api.github.com/users/dmotz/orgs", + "repos_url": "https://api.github.com/users/dmotz/repos", + "events_url": "https://api.github.com/users/dmotz/events{/privacy}", + "received_events_url": "https://api.github.com/users/dmotz/received_events", + "type": "User", + "site_admin": false, + "score": 26.167 + }, + { + "login": "dmgarland", + "id": 133712, + "avatar_url": "https://avatars.githubusercontent.com/u/133712?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dmgarland", + "html_url": "https://github.com/dmgarland", + "followers_url": "https://api.github.com/users/dmgarland/followers", + "following_url": "https://api.github.com/users/dmgarland/following{/other_user}", + "gists_url": "https://api.github.com/users/dmgarland/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dmgarland/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dmgarland/subscriptions", + "organizations_url": "https://api.github.com/users/dmgarland/orgs", + "repos_url": "https://api.github.com/users/dmgarland/repos", + "events_url": "https://api.github.com/users/dmgarland/events{/privacy}", + "received_events_url": "https://api.github.com/users/dmgarland/received_events", + "type": "User", + "site_admin": false, + "score": 24.790255 + }, + { + "login": "danro", + "id": 393520, + "avatar_url": "https://avatars.githubusercontent.com/u/393520?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danro", + "html_url": "https://github.com/danro", + "followers_url": "https://api.github.com/users/danro/followers", + "following_url": "https://api.github.com/users/danro/following{/other_user}", + "gists_url": "https://api.github.com/users/danro/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danro/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danro/subscriptions", + "organizations_url": "https://api.github.com/users/danro/orgs", + "repos_url": "https://api.github.com/users/danro/repos", + "events_url": "https://api.github.com/users/danro/events{/privacy}", + "received_events_url": "https://api.github.com/users/danro/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "dbs", + "id": 317113, + "avatar_url": "https://avatars.githubusercontent.com/u/317113?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dbs", + "html_url": "https://github.com/dbs", + "followers_url": "https://api.github.com/users/dbs/followers", + "following_url": "https://api.github.com/users/dbs/following{/other_user}", + "gists_url": "https://api.github.com/users/dbs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dbs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dbs/subscriptions", + "organizations_url": "https://api.github.com/users/dbs/orgs", + "repos_url": "https://api.github.com/users/dbs/repos", + "events_url": "https://api.github.com/users/dbs/events{/privacy}", + "received_events_url": "https://api.github.com/users/dbs/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "DanielVartanov", + "id": 21388, + "avatar_url": "https://avatars.githubusercontent.com/u/21388?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/DanielVartanov", + "html_url": "https://github.com/DanielVartanov", + "followers_url": "https://api.github.com/users/DanielVartanov/followers", + "following_url": "https://api.github.com/users/DanielVartanov/following{/other_user}", + "gists_url": "https://api.github.com/users/DanielVartanov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/DanielVartanov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/DanielVartanov/subscriptions", + "organizations_url": "https://api.github.com/users/DanielVartanov/orgs", + "repos_url": "https://api.github.com/users/DanielVartanov/repos", + "events_url": "https://api.github.com/users/DanielVartanov/events{/privacy}", + "received_events_url": "https://api.github.com/users/DanielVartanov/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "dangrossman", + "id": 407706, + "avatar_url": "https://avatars.githubusercontent.com/u/407706?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dangrossman", + "html_url": "https://github.com/dangrossman", + "followers_url": "https://api.github.com/users/dangrossman/followers", + "following_url": "https://api.github.com/users/dangrossman/following{/other_user}", + "gists_url": "https://api.github.com/users/dangrossman/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dangrossman/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dangrossman/subscriptions", + "organizations_url": "https://api.github.com/users/dangrossman/orgs", + "repos_url": "https://api.github.com/users/dangrossman/repos", + "events_url": "https://api.github.com/users/dangrossman/events{/privacy}", + "received_events_url": "https://api.github.com/users/dangrossman/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "dpup", + "id": 206453, + "avatar_url": "https://avatars.githubusercontent.com/u/206453?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dpup", + "html_url": "https://github.com/dpup", + "followers_url": "https://api.github.com/users/dpup/followers", + "following_url": "https://api.github.com/users/dpup/following{/other_user}", + "gists_url": "https://api.github.com/users/dpup/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dpup/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dpup/subscriptions", + "organizations_url": "https://api.github.com/users/dpup/orgs", + "repos_url": "https://api.github.com/users/dpup/repos", + "events_url": "https://api.github.com/users/dpup/events{/privacy}", + "received_events_url": "https://api.github.com/users/dpup/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "damelang", + "id": 20817, + "avatar_url": "https://avatars.githubusercontent.com/u/20817?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/damelang", + "html_url": "https://github.com/damelang", + "followers_url": "https://api.github.com/users/damelang/followers", + "following_url": "https://api.github.com/users/damelang/following{/other_user}", + "gists_url": "https://api.github.com/users/damelang/gists{/gist_id}", + "starred_url": "https://api.github.com/users/damelang/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/damelang/subscriptions", + "organizations_url": "https://api.github.com/users/damelang/orgs", + "repos_url": "https://api.github.com/users/damelang/repos", + "events_url": "https://api.github.com/users/damelang/events{/privacy}", + "received_events_url": "https://api.github.com/users/damelang/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "dazld", + "id": 201036, + "avatar_url": "https://avatars.githubusercontent.com/u/201036?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dazld", + "html_url": "https://github.com/dazld", + "followers_url": "https://api.github.com/users/dazld/followers", + "following_url": "https://api.github.com/users/dazld/following{/other_user}", + "gists_url": "https://api.github.com/users/dazld/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dazld/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dazld/subscriptions", + "organizations_url": "https://api.github.com/users/dazld/orgs", + "repos_url": "https://api.github.com/users/dazld/repos", + "events_url": "https://api.github.com/users/dazld/events{/privacy}", + "received_events_url": "https://api.github.com/users/dazld/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "danopia", + "id": 40628, + "avatar_url": "https://avatars.githubusercontent.com/u/40628?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danopia", + "html_url": "https://github.com/danopia", + "followers_url": "https://api.github.com/users/danopia/followers", + "following_url": "https://api.github.com/users/danopia/following{/other_user}", + "gists_url": "https://api.github.com/users/danopia/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danopia/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danopia/subscriptions", + "organizations_url": "https://api.github.com/users/danopia/orgs", + "repos_url": "https://api.github.com/users/danopia/repos", + "events_url": "https://api.github.com/users/danopia/events{/privacy}", + "received_events_url": "https://api.github.com/users/danopia/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "dnewcome", + "id": 160722, + "avatar_url": "https://avatars.githubusercontent.com/u/160722?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dnewcome", + "html_url": "https://github.com/dnewcome", + "followers_url": "https://api.github.com/users/dnewcome/followers", + "following_url": "https://api.github.com/users/dnewcome/following{/other_user}", + "gists_url": "https://api.github.com/users/dnewcome/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dnewcome/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dnewcome/subscriptions", + "organizations_url": "https://api.github.com/users/dnewcome/orgs", + "repos_url": "https://api.github.com/users/dnewcome/repos", + "events_url": "https://api.github.com/users/dnewcome/events{/privacy}", + "received_events_url": "https://api.github.com/users/dnewcome/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "danpalmer", + "id": 202400, + "avatar_url": "https://avatars.githubusercontent.com/u/202400?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danpalmer", + "html_url": "https://github.com/danpalmer", + "followers_url": "https://api.github.com/users/danpalmer/followers", + "following_url": "https://api.github.com/users/danpalmer/following{/other_user}", + "gists_url": "https://api.github.com/users/danpalmer/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danpalmer/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danpalmer/subscriptions", + "organizations_url": "https://api.github.com/users/danpalmer/orgs", + "repos_url": "https://api.github.com/users/danpalmer/repos", + "events_url": "https://api.github.com/users/danpalmer/events{/privacy}", + "received_events_url": "https://api.github.com/users/danpalmer/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "danmayer", + "id": 24925, + "avatar_url": "https://avatars.githubusercontent.com/u/24925?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danmayer", + "html_url": "https://github.com/danmayer", + "followers_url": "https://api.github.com/users/danmayer/followers", + "following_url": "https://api.github.com/users/danmayer/following{/other_user}", + "gists_url": "https://api.github.com/users/danmayer/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danmayer/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danmayer/subscriptions", + "organizations_url": "https://api.github.com/users/danmayer/orgs", + "repos_url": "https://api.github.com/users/danmayer/repos", + "events_url": "https://api.github.com/users/danmayer/events{/privacy}", + "received_events_url": "https://api.github.com/users/danmayer/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "schlosser", + "id": 2433509, + "avatar_url": "https://avatars.githubusercontent.com/u/2433509?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/schlosser", + "html_url": "https://github.com/schlosser", + "followers_url": "https://api.github.com/users/schlosser/followers", + "following_url": "https://api.github.com/users/schlosser/following{/other_user}", + "gists_url": "https://api.github.com/users/schlosser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/schlosser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/schlosser/subscriptions", + "organizations_url": "https://api.github.com/users/schlosser/orgs", + "repos_url": "https://api.github.com/users/schlosser/repos", + "events_url": "https://api.github.com/users/schlosser/events{/privacy}", + "received_events_url": "https://api.github.com/users/schlosser/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "danjenkins", + "id": 243117, + "avatar_url": "https://avatars.githubusercontent.com/u/243117?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danjenkins", + "html_url": "https://github.com/danjenkins", + "followers_url": "https://api.github.com/users/danjenkins/followers", + "following_url": "https://api.github.com/users/danjenkins/following{/other_user}", + "gists_url": "https://api.github.com/users/danjenkins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danjenkins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danjenkins/subscriptions", + "organizations_url": "https://api.github.com/users/danjenkins/orgs", + "repos_url": "https://api.github.com/users/danjenkins/repos", + "events_url": "https://api.github.com/users/danjenkins/events{/privacy}", + "received_events_url": "https://api.github.com/users/danjenkins/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "daneden", + "id": 439365, + "avatar_url": "https://avatars.githubusercontent.com/u/439365?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/daneden", + "html_url": "https://github.com/daneden", + "followers_url": "https://api.github.com/users/daneden/followers", + "following_url": "https://api.github.com/users/daneden/following{/other_user}", + "gists_url": "https://api.github.com/users/daneden/gists{/gist_id}", + "starred_url": "https://api.github.com/users/daneden/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/daneden/subscriptions", + "organizations_url": "https://api.github.com/users/daneden/orgs", + "repos_url": "https://api.github.com/users/daneden/repos", + "events_url": "https://api.github.com/users/daneden/events{/privacy}", + "received_events_url": "https://api.github.com/users/daneden/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "flyswatter", + "id": 542863, + "avatar_url": "https://avatars.githubusercontent.com/u/542863?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/flyswatter", + "html_url": "https://github.com/flyswatter", + "followers_url": "https://api.github.com/users/flyswatter/followers", + "following_url": "https://api.github.com/users/flyswatter/following{/other_user}", + "gists_url": "https://api.github.com/users/flyswatter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flyswatter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flyswatter/subscriptions", + "organizations_url": "https://api.github.com/users/flyswatter/orgs", + "repos_url": "https://api.github.com/users/flyswatter/repos", + "events_url": "https://api.github.com/users/flyswatter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flyswatter/received_events", + "type": "User", + "site_admin": false, + "score": 24.401234 + }, + { + "login": "danielsdeleo", + "id": 37162, + "avatar_url": "https://avatars.githubusercontent.com/u/37162?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danielsdeleo", + "html_url": "https://github.com/danielsdeleo", + "followers_url": "https://api.github.com/users/danielsdeleo/followers", + "following_url": "https://api.github.com/users/danielsdeleo/following{/other_user}", + "gists_url": "https://api.github.com/users/danielsdeleo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danielsdeleo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danielsdeleo/subscriptions", + "organizations_url": "https://api.github.com/users/danielsdeleo/orgs", + "repos_url": "https://api.github.com/users/danielsdeleo/repos", + "events_url": "https://api.github.com/users/danielsdeleo/events{/privacy}", + "received_events_url": "https://api.github.com/users/danielsdeleo/received_events", + "type": "User", + "site_admin": false, + "score": 22.428858 + }, + { + "login": "danprince", + "id": 1266011, + "avatar_url": "https://avatars.githubusercontent.com/u/1266011?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danprince", + "html_url": "https://github.com/danprince", + "followers_url": "https://api.github.com/users/danprince/followers", + "following_url": "https://api.github.com/users/danprince/following{/other_user}", + "gists_url": "https://api.github.com/users/danprince/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danprince/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danprince/subscriptions", + "organizations_url": "https://api.github.com/users/danprince/orgs", + "repos_url": "https://api.github.com/users/danprince/repos", + "events_url": "https://api.github.com/users/danprince/events{/privacy}", + "received_events_url": "https://api.github.com/users/danprince/received_events", + "type": "User", + "site_admin": false, + "score": 22.428858 + }, + { + "login": "dphiffer", + "id": 38114, + "avatar_url": "https://avatars.githubusercontent.com/u/38114?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dphiffer", + "html_url": "https://github.com/dphiffer", + "followers_url": "https://api.github.com/users/dphiffer/followers", + "following_url": "https://api.github.com/users/dphiffer/following{/other_user}", + "gists_url": "https://api.github.com/users/dphiffer/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dphiffer/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dphiffer/subscriptions", + "organizations_url": "https://api.github.com/users/dphiffer/orgs", + "repos_url": "https://api.github.com/users/dphiffer/repos", + "events_url": "https://api.github.com/users/dphiffer/events{/privacy}", + "received_events_url": "https://api.github.com/users/dphiffer/received_events", + "type": "User", + "site_admin": false, + "score": 22.428858 + }, + { + "login": "dangrover", + "id": 96156, + "avatar_url": "https://avatars.githubusercontent.com/u/96156?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/dangrover", + "html_url": "https://github.com/dangrover", + "followers_url": "https://api.github.com/users/dangrover/followers", + "following_url": "https://api.github.com/users/dangrover/following{/other_user}", + "gists_url": "https://api.github.com/users/dangrover/gists{/gist_id}", + "starred_url": "https://api.github.com/users/dangrover/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/dangrover/subscriptions", + "organizations_url": "https://api.github.com/users/dangrover/orgs", + "repos_url": "https://api.github.com/users/dangrover/repos", + "events_url": "https://api.github.com/users/dangrover/events{/privacy}", + "received_events_url": "https://api.github.com/users/dangrover/received_events", + "type": "User", + "site_admin": false, + "score": 22.428858 + }, + { + "login": "danhively", + "id": 971699, + "avatar_url": "https://avatars.githubusercontent.com/u/971699?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danhively", + "html_url": "https://github.com/danhively", + "followers_url": "https://api.github.com/users/danhively/followers", + "following_url": "https://api.github.com/users/danhively/following{/other_user}", + "gists_url": "https://api.github.com/users/danhively/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danhively/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danhively/subscriptions", + "organizations_url": "https://api.github.com/users/danhively/orgs", + "repos_url": "https://api.github.com/users/danhively/repos", + "events_url": "https://api.github.com/users/danhively/events{/privacy}", + "received_events_url": "https://api.github.com/users/danhively/received_events", + "type": "User", + "site_admin": false, + "score": 22.428858 + }, + { + "login": "danclien", + "id": 764962, + "avatar_url": "https://avatars.githubusercontent.com/u/764962?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/danclien", + "html_url": "https://github.com/danclien", + "followers_url": "https://api.github.com/users/danclien/followers", + "following_url": "https://api.github.com/users/danclien/following{/other_user}", + "gists_url": "https://api.github.com/users/danclien/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danclien/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danclien/subscriptions", + "organizations_url": "https://api.github.com/users/danclien/orgs", + "repos_url": "https://api.github.com/users/danclien/repos", + "events_url": "https://api.github.com/users/danclien/events{/privacy}", + "received_events_url": "https://api.github.com/users/danclien/received_events", + "type": "User", + "site_admin": false, + "score": 20.33436 + } + ] +export default stub diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..e8b101a --- /dev/null +++ b/utils/index.js @@ -0,0 +1,157 @@ +import 'isomorphic-fetch'; +import { camelizeKeys } from 'humps'; +import { Schema, arrayOf, normalize } from 'normalizr'; + +/** + * [getNextPageUrl description] + * @param {[obj]} res [response header from apiCall()] + * @return {[strin]} [url for next set of data] + */ +function getNextPageUrl(res) { + const link = res.headers.get('link'); + if (!link) { + return null; + } + const nextLink = link.split(',').find(string => string.indexOf('rel="next"') > -1); + + if (!nextLink) { + return null; + } + + return nextLink.split(';')[0].slice(1, -1); + +} + +/** + * [getLastPageUrl description] + * @param {[Object]} res [response header from apiCall()] + * @return {[string]} [url for last set of data] + */ +function getLastPageUrl(res) { + const link = res.headers.get('link'); + + if (!link) { + return null; + } + const lastLink = link.split(',').find(string => string.indexOf('rel="last"') > -1); + + if (!lastLink) { + return null; + } + + return lastLink.split(';')[0].slice(1, -1); +} + +/** + * [BASE_URL description] + * @type {String} + */ +const BASE_URL = 'https://api.github.com/'; + +/** + * [callApi description] + * @param {[string]} endpoint [concat to BASE_URL to complete the url] + * @param {[type]} schema [flattens the json for the store] + * @return {[Object]} [finalized data to be reduced] + */ +function callApi(endpoint, schema) { + const URL = (endpoint.indexOf(BASE_URL) === -1 ? BASE_URL + endpoint : endpoint); + + return fetch(URL) + .then(res => + res.json().then(json => ({ json, res })) + ).then(({ json, res }) => { + if (!res.ok) { + return Promise.reject(json); + } + + const results = json.items ? json.items : json; + const camelizedJson = camelizeKeys(results); + const nextPageUrl = getNextPageUrl(res); + const lastPageUrl = getLastPageUrl(res); + + return Object.assign({}, + normalize(camelizedJson, schema), + { nextPageUrl }, + { lastPageUrl } + ); + }); +} + +/** + * [Schema configs for normalizing res.josn] + */ +const searchSchema = new Schema('users', { idAttribute: user => user.login }); +const userSchema = new Schema('user', { idAttribute: user => user.login }); +const followerSchema = new Schema('followers', {idAttribute: follower => follower.login}); +const repoSchema = new Schema('repos', {idAttribute: repo => repo.name}); +/** + * [Schemas list of schemas] + * @type {Object} + */ +export const Schemas = { + RESULTS_LIST: arrayOf(searchSchema), + USER_DETAILS: userSchema, + FOLLOWERS_LIST: arrayOf(followerSchema), + REPOS_LIST: arrayOf(repoSchema) +}; + +/** + * [Symbol ActionType for middleware function] + * @param {[type]} 'Call API' [description] + */ +export const CALL_API = Symbol('Call API'); + +/** + * [store middleware for dispatching actions asynchronously] + * @type {[type]} + */ +export default store => next => action => { + const callAPI = action[CALL_API]; + if (typeof callAPI === 'undefined') { + return next(action); + } + + let { endpoint } = callAPI; + const { schema, types } = callAPI; + + if (typeof endpoint === 'function') { + endpoint = endpoint(store.getState()); + } + + if (typeof endpoint !== 'string') { + throw new Error('Expected endpoint to be a string'); + } + + if (!schema) { + throw new Error('Needs one of the the available Schemas.'); + } + + if (!Array.isArray(types) || types.length !== 3) { + throw new Error('Needs an array of three action types.'); + } + + if (!types.every(type => typeof type === 'string')) { + throw new Error('Action types need to be strings.'); + } + + function actionCreator(data) { + const finalAction = Object.assign({}, action, data); + delete finalAction[CALL_API]; + return finalAction; + } + + const [ requestType, successType, failureType ] = types; + next(actionCreator({ type: requestType })); + + return callApi(endpoint, schema).then( + response => next(actionCreator({ + response, + type: successType + })), + error => next(actionCreator({ + type: failureType, + error: error.message || 'Failure in callAPI.' + })) + ) +} diff --git a/views/App/index.js b/views/App/index.js new file mode 100644 index 0000000..db64d2e --- /dev/null +++ b/views/App/index.js @@ -0,0 +1,74 @@ +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import { browserHistory } from 'react-router' +import Search from '../../components/Search' +import { resetErrorMessage } from '../../actions' + +class App extends Component { + constructor(props) { + super(props) + this.handleChange = this.handleChange.bind(this) + this.handleDismissClick = this.handleDismissClick.bind(this) + } + + handleDismissClick(e) { + this.props.resetErrorMessage() + e.preventDefault() + } + + handleChange(nextValue) { + + browserHistory.push(`search/${nextValue}`) + } + + renderErrorMessage() { + const { errorMessage } = this.props + if (!errorMessage) { + return null + } + + return ( +

+ {errorMessage} + {' '} + ( + Dismiss + ) +

+ ) + } + + render() { + const { children, inputValue } = this.props + return ( +
+ +
+ {this.renderErrorMessage()} + {children} +
+ ) + } +} + +App.propTypes = { + // Injected by React Redux + errorMessage: PropTypes.string, + resetErrorMessage: PropTypes.func.isRequired, + inputValue: PropTypes.string, + // Injected by React Router + children: PropTypes.node +} + +function mapStateToProps(state,) { + return { + errorMessage: state.errorMessage, + inputValue: state.inputValue, + } +} + +export default connect(mapStateToProps, { + resetErrorMessage +})(App) diff --git a/views/FishView/index.js b/views/FishView/index.js deleted file mode 100644 index 44fcbde..0000000 --- a/views/FishView/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -export default class FishView extends React.Component { - render () { - return ( -
-

No Fishes Here

-
- ); - } -} diff --git a/views/RootView/index.js b/views/RootView/index.js index 554ebd9..0ddfbc2 100644 --- a/views/RootView/index.js +++ b/views/RootView/index.js @@ -1,15 +1,20 @@ -import React from 'react'; +import React, { Component, PropTypes } from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../store'; -export default class RootView extends React.Component { +const store = configureStore(); + +export default class RootView extends Component { static propTypes = { - children: React.PropTypes.any + children: PropTypes.any, } render () { return ( -
-

Welcome To The Exercise

- {this.props.children} +
+ + {this.props.children} +
); } diff --git a/views/UserDetails/index.js b/views/UserDetails/index.js new file mode 100644 index 0000000..0027bab --- /dev/null +++ b/views/UserDetails/index.js @@ -0,0 +1,153 @@ +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import { loadUserDetails, resetErrorMessage, loadFollowers, loadRepos } from '../../actions' +import zip from 'lodash/zip' +import isEmpty from 'lodash/isEmpty' +import values from 'lodash/values' + +function loadData(props) { + const { params: { login } } = props + props.loadUserDetails(login, [ 'name' ]) + props.loadFollowers(login, ['name']) +} + +export default class UserDetails extends Component { + + static propTypes = { + user: PropTypes.object, + loadUserDetails: PropTypes.func, + loadRepos: PropTypes.func + } + + constructor(props) { + super(props) + this.renderRepo = this.renderRepo.bind(this) + this.renderFollower = this.renderFollower.bind(this) + // this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this) + } + + componentWillMount() { + loadData(this.props) + } + + componentWillReceiveProps(nextProps) { + const { params: { login } } = this.props + if (nextProps.followers !== this.props.followers) { + } + } + + renderFollower(follower) { + return
  • {follower.login}
  • + } + + renderRepo(repo) { + return
  • {repo.name}
  • + } + + render() { + const {params: { login } } = this.props; + const { repos, followers } = this.props; + const user = this.props.user[login]; + if (!user) { + return

    Loading...

    + } + if (isEmpty(repos)) { + console.log(console.log("get repos: ")); + this.props.loadRepos(login, ['name']); + } + if (isEmpty(followers)) { + console.log(console.log("get repos: ")); + this.props.loadFollowers(login, ['name']); + } + const followersArray = values(followers) + const reposArray = values(repos) + return ( +
    +
    +

    {user.name}

    +
    + +
    +
    +
    +
    Github Username
    + {user.login} + { user.company && +
    +
    Company
    + {user.company} +
    + } + { user.blog && +
    +
    Blog
    + {user.blog} +
    + } + { user.location && +
    +
    Location
    + {user.location} +
    + } + { user.email && +
    +
    Email
    + + {user.email} + +
    + } + { user.bio && +
    +
    Bio
    + {user.bio} +
    + } + { user.created_at && +
    +
    Account Creation
    + {user.created_at} +
    + } +
    +
    + { followersArray && +
    +

    Followers

    +
      + {followersArray.map(this.renderFollower)} +
    +
    + } + { reposArray && +
    +

    Repos

    +
      + {reposArray.map(this.renderRepo)} +
    +
    + } +
    +
    + + ) + } +} + +function mapStateToProps(state,) { + const { entities: { user, followers, repos }, errorMessage } = state + return { + errorMessage, + user, + followers, + repos + } +} + +export default connect(mapStateToProps, { + resetErrorMessage, + loadUserDetails, + loadFollowers, + loadRepos +})(UserDetails) diff --git a/views/UsersView/index.js b/views/UsersView/index.js new file mode 100644 index 0000000..3cd9d47 --- /dev/null +++ b/views/UsersView/index.js @@ -0,0 +1,65 @@ +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import List from '../../components/List' +import { loadSearchResults } from '../../actions' +import zip from 'lodash/zip' + +function loadData(props) { + const { query } = props + props.loadSearchResults(query, [ 'name' ]) +} + +class UsersList extends Component { + + static propTypes = { + query: PropTypes.string, + users: PropTypes.array, + loadSearchResults: PropTypes.func + } + + constructor(props) { + super(props) + + } + + componentWillMount() { + loadData(this.props) + } + + componentWillReceiveProps(nextProps) { + if (nextProps.query !== this.props.query) { + loadData(nextProps) + } + } + + + render() { + const { users } = this.props + if (!users) { + return

    Your search returned no results. Please search for another user.

    + } + + return ( +
    + +
    + ) + } +} + +function mapStateToProps(state, ownProps) { + + const query = ownProps.params.query.toLowerCase() + const { entities: { users } } = state + + return { + query, + users + } +} + +export default connect(mapStateToProps, { + loadSearchResults +})(UsersList)