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 &&
+
+ }
+ { user.location &&
+
+
Location
+ {user.location}
+
+ }
+ { 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)