├── .gitignore
├── demos
└── scoped-redux
│ ├── src
│ ├── router
│ │ ├── index.js
│ │ └── Router.js
│ ├── shared
│ │ ├── components
│ │ │ ├── List
│ │ │ │ ├── index.js
│ │ │ │ └── List.js
│ │ │ ├── Repo
│ │ │ │ ├── index.js
│ │ │ │ └── Repo.js
│ │ │ ├── Profile
│ │ │ │ ├── index.js
│ │ │ │ └── Profile.js
│ │ │ └── DevTools
│ │ │ │ └── index.js
│ │ ├── constants.js
│ │ └── api
│ │ │ └── index.js
│ ├── features
│ │ ├── repo
│ │ │ ├── selectors.js
│ │ │ ├── index.js
│ │ │ ├── renderers
│ │ │ │ ├── Empty.js
│ │ │ │ └── Loading.js
│ │ │ ├── actionTypes.js
│ │ │ ├── actionCreators.js
│ │ │ ├── reducer.js
│ │ │ └── containers
│ │ │ │ └── Repo.js
│ │ ├── search
│ │ │ ├── index.js
│ │ │ └── renderers
│ │ │ │ └── Search.js
│ │ ├── error
│ │ │ ├── selectors.js
│ │ │ ├── actionTypes.js
│ │ │ ├── index.js
│ │ │ ├── actionCreators.js
│ │ │ ├── reducer.js
│ │ │ ├── containers
│ │ │ │ └── Error.js
│ │ │ └── renderers
│ │ │ │ └── Error.js
│ │ ├── profile
│ │ │ ├── selectors.js
│ │ │ ├── index.js
│ │ │ ├── renderers
│ │ │ │ ├── Title.js
│ │ │ │ ├── Empty.js
│ │ │ │ └── Loading.js
│ │ │ ├── actionTypes.js
│ │ │ ├── actionCreators.js
│ │ │ ├── reducer.js
│ │ │ └── containers
│ │ │ │ └── Profile.js
│ │ ├── stargazers
│ │ │ ├── selectors.js
│ │ │ ├── index.js
│ │ │ ├── renderers
│ │ │ │ └── Title.js
│ │ │ ├── actionTypes.js
│ │ │ ├── actionCreators.js
│ │ │ ├── reducer.js
│ │ │ └── containers
│ │ │ │ └── Stargazers.js
│ │ └── starredRepos
│ │ │ ├── selectors.js
│ │ │ ├── index.js
│ │ │ ├── renderers
│ │ │ └── Title.js
│ │ │ ├── actionTypes.js
│ │ │ ├── actionCreators.js
│ │ │ ├── reducer.js
│ │ │ └── containers
│ │ │ └── StarredRepos.js
│ ├── pages
│ │ ├── repo
│ │ │ ├── index.js
│ │ │ ├── route.js
│ │ │ └── containers
│ │ │ │ └── RepoPage.js
│ │ ├── root
│ │ │ ├── index.js
│ │ │ ├── route.js
│ │ │ └── containers
│ │ │ │ └── RootPage.js
│ │ └── user
│ │ │ ├── index.js
│ │ │ ├── route.js
│ │ │ └── containers
│ │ │ └── UserPage.js
│ ├── index.js
│ └── store
│ │ └── index.js
│ ├── .gitattributes
│ ├── readme.md
│ ├── .gitignore
│ ├── .prettierrc
│ ├── public
│ └── index.html
│ └── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/router/index.js:
--------------------------------------------------------------------------------
1 | export { default as Router } from './Router'
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/components/List/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './List'
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/components/Repo/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './Repo'
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/components/Profile/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './Profile'
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectRepo = state => state.features.repo
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/search/index.js:
--------------------------------------------------------------------------------
1 | export { default as Search } from './renderers/Search'
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/error/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectError = state => state.features.error
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectProfile = state => state.features.profile
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/constants.js:
--------------------------------------------------------------------------------
1 | export const LOADING_STATES = ['initial', 'loading', 'loaded', 'error']
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/stargazers/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectStargazers = state => state.features.stargazers
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/starredRepos/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectStarredRepos = state => state.features.starredRepos
2 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/error/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const DISMISS = 'feature/error/DISMISS'
2 | export const HANDLE = 'feature/error/HANDLE'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/index.js:
--------------------------------------------------------------------------------
1 | export { default as Repo } from './containers/Repo'
2 | export { default as reducer } from './reducer'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/repo/index.js:
--------------------------------------------------------------------------------
1 | export { default as RepoPage } from './containers/RepoPage'
2 | export { default as route } from './route'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/root/index.js:
--------------------------------------------------------------------------------
1 | export { default as RootPage } from './containers/RootPage'
2 | export { default as route } from './route'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/user/index.js:
--------------------------------------------------------------------------------
1 | export { default as UserPage } from './containers/UserPage'
2 | export { default as route } from './route'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/index.js:
--------------------------------------------------------------------------------
1 | export { default as Profile } from './containers/Profile'
2 | export { default as reducer } from './reducer'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/renderers/Title.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Title = () =>
User
4 |
5 | export default Title
6 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/stargazers/index.js:
--------------------------------------------------------------------------------
1 | export { default as Stargazers } from './containers/Stargazers'
2 | export { default as reducer } from './reducer'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/stargazers/renderers/Title.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Title = () => Stargazers
4 |
5 | export default Title
6 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/starredRepos/index.js:
--------------------------------------------------------------------------------
1 | export { default as StarredRepos } from './containers/StarredRepos'
2 | export { default as reducer } from './reducer'
3 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/starredRepos/renderers/Title.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Title = () => Starred repositories
4 |
5 | export default Title
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # feature-driven-architecture
2 | Defining boundaries for a large application.
3 |
4 | I have restarted this effort under a separte org https://github.com/feature-driven-architecture/spec
5 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/error/index.js:
--------------------------------------------------------------------------------
1 | export { default as Error } from './containers/Error'
2 | export { default as reducer } from './reducer'
3 | export { handle, dismiss } from './actionCreators'
4 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/renderers/Empty.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Empty = () => (
4 |
5 | Repo empty state.
6 |
7 | )
8 |
9 | export default Empty
10 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/renderers/Empty.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Empty = () => (
4 |
5 | Profile empty state.
6 |
7 | )
8 |
9 | export default Empty
10 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const LOAD = 'features/repo/LOAD'
2 | export const HANDLE_RESPONSE = 'features/repo/HANDLE_RESPONSE'
3 | export const HANDLE_ERROR = 'features/repo/HANDLE_ERROR'
4 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/root/route.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RootPage from './containers/RootPage'
3 | import { Route } from 'react-router-dom'
4 |
5 | export default
6 |
--------------------------------------------------------------------------------
/demos/scoped-redux/readme.md:
--------------------------------------------------------------------------------
1 | ## Scoped Redux
2 |
3 | You can clone and run `npm i && npm start` or open it directly in [codesandbox](https://codesandbox.io/s/github/kof/feature-driven-architecture/tree/master/demos/scoped-redux).
4 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const LOAD = 'features/profile/LOAD'
2 | export const HANDLE_RESPONSE = 'features/profile/HANDLE_RESPONSE'
3 | export const HANDLE_ERROR = 'features/profile/HANDLE_ERROR'
4 |
--------------------------------------------------------------------------------
/demos/scoped-redux/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # production
7 | build
8 |
9 | # misc
10 | .DS_Store
11 | npm-debug.log
12 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/user/route.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import UserPage from './containers/UserPage'
3 | import { Route } from 'react-router-dom'
4 |
5 | export default
6 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/repo/route.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RepoPage from './containers/RepoPage'
3 | import { Route } from 'react-router-dom'
4 |
5 | export default
6 |
--------------------------------------------------------------------------------
/demos/scoped-redux/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "jsxBracketSameLine": false,
4 | "printWidth": 80,
5 | "semi": false,
6 | "singleQuote": true,
7 | "tabWidth": 2,
8 | "trailingComma": "es5",
9 | "useTabs": false
10 | }
11 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/error/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { DISMISS, HANDLE } from './actionTypes'
2 |
3 | export const dismiss = () => dispatch => {
4 | dispatch({ type: DISMISS })
5 | }
6 |
7 | export const handle = error => dispatch => {
8 | dispatch({ type: HANDLE, payload: error })
9 | }
10 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/stargazers/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const LOAD = 'features/stargazers/LOAD'
2 | export const LOAD_NEXT = 'features/stargazers/LOAD_NEXT'
3 | export const HANDLE_RESPONSE = 'features/stargazers/HANDLE_RESPONSE'
4 | export const HANDLE_NEXT_RESPONSE = 'features/stargazers/HANDLE_NEXT_RESPONSE'
5 | export const HANDLE_ERROR = 'features/stargazers/HANDLE_ERROR'
6 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/starredRepos/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const LOAD = 'features/starredRepos/LOAD'
2 | export const LOAD_NEXT = 'features/starredRepos/LOAD_NEXT'
3 | export const HANDLE_RESPONSE = 'features/starredRepos/HANDLE_RESPONSE'
4 | export const HANDLE_NEXT_RESPONSE = 'features/starredRepos/HANDLE_NEXT_RESPONSE'
5 | export const HANDLE_ERROR = 'features/starredRepos/HANDLE_ERROR'
6 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/renderers/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const propTypes = {
5 | login: PropTypes.string.isRequired,
6 | }
7 |
8 | const Loading = ({ login }) => (
9 |
10 | {`Loading ${login}'s profile...`}
11 |
12 | )
13 |
14 | Loading.propTypes = propTypes
15 |
16 | export default Loading
17 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/renderers/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const propTypes = {
5 | fullName: PropTypes.string.isRequired,
6 | }
7 |
8 | const Loading = ({ fullName }) => (
9 |
10 | {`Loading ${fullName} details...`}
11 |
12 | )
13 |
14 | Loading.propTypes = propTypes
15 |
16 | export default Loading
17 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/error/reducer.js:
--------------------------------------------------------------------------------
1 | import { DISMISS, HANDLE } from './actionTypes'
2 |
3 | const defaultState = {}
4 |
5 | const states = {
6 | [DISMISS]: () => defaultState,
7 | [HANDLE]: (state, payload) => ({
8 | message: payload.message,
9 | }),
10 | }
11 |
12 | export default (state = defaultState, { type, payload } = {}) =>
13 | states[type] ? states[type](state, payload) : state
14 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/components/DevTools/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createDevTools } from 'redux-devtools'
3 | import LogMonitor from 'redux-devtools-log-monitor'
4 | import DockMonitor from 'redux-devtools-dock-monitor'
5 |
6 | export default createDevTools(
7 |
8 |
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/router/Router.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import { BrowserRouter } from 'react-router-dom'
3 | import { route as rootRoute } from '../pages/root'
4 | import { route as repoRoute } from '../pages/repo'
5 | import { route as userRoute } from '../pages/user'
6 |
7 | const Router = () => (
8 |
9 |
10 | {rootRoute}
11 | {userRoute}
12 | {repoRoute}
13 |
14 |
15 | )
16 |
17 | export default Router
18 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import { render } from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import { Router } from './router'
5 | import { configure as configureStore } from './store'
6 | import DevTools from './shared/components/DevTools'
7 |
8 | const store = configureStore()
9 |
10 | render(
11 |
12 |
13 |
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | )
19 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/actionCreators.js:
--------------------------------------------------------------------------------
1 | import * as api from '../../shared/api'
2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes'
3 |
4 | export const load = ({ login }) => dispatch => {
5 | dispatch({ type: LOAD, payload: login })
6 |
7 | return api
8 | .call(`users/${login}`)
9 | .then(({ result }) => {
10 | dispatch({ type: HANDLE_RESPONSE, payload: result })
11 | })
12 | .catch(error => {
13 | dispatch({ type: HANDLE_ERROR, payload: error })
14 | throw error
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/actionCreators.js:
--------------------------------------------------------------------------------
1 | import * as api from '../../shared/api'
2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes'
3 |
4 | export const load = ({ fullName }) => dispatch => {
5 | dispatch({ type: LOAD, payload: fullName })
6 |
7 | return api
8 | .call(`repos/${fullName}`)
9 | .then(({ result }) => {
10 | dispatch({ type: HANDLE_RESPONSE, payload: result })
11 | })
12 | .catch(error => {
13 | dispatch({ type: HANDLE_ERROR, payload: error })
14 | throw error
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/error/containers/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import ErrorRenderer from '../renderers/Error'
5 | import { dismiss } from '../actionCreators'
6 | import { selectError } from '../selectors'
7 |
8 | const propTypes = {
9 | message: PropTypes.string,
10 | }
11 |
12 | const Error = props => (props.message ? : null)
13 |
14 | Error.propTypes = propTypes
15 |
16 | export default connect(
17 | selectError,
18 | {
19 | onDismiss: dismiss,
20 | }
21 | )(Error)
22 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/error/renderers/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const propTypes = {
5 | message: PropTypes.string.isRequired,
6 | onDismiss: PropTypes.func.isRequired,
7 | }
8 |
9 | const Error = ({ message, onDismiss }) => (
10 |
11 | {message}{' '}
12 |
20 |
21 | )
22 |
23 | Error.propTypes = propTypes
24 |
25 | export default Error
26 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/components/Profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 |
5 | const Profile = ({ login, avatarUrl, name }) => (
6 |
7 |
8 |

9 |
10 | {login} {name && ({name})}
11 |
12 |
13 |
14 | )
15 |
16 | Profile.propTypes = {
17 | login: PropTypes.string.isRequired,
18 | avatarUrl: PropTypes.string.isRequired,
19 | name: PropTypes.string,
20 | }
21 |
22 | export default Profile
23 |
--------------------------------------------------------------------------------
/demos/scoped-redux/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React App
7 |
8 |
9 |
10 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/reducer.js:
--------------------------------------------------------------------------------
1 | import pick from 'lodash/pick'
2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes'
3 |
4 | const defaultState = {
5 | user: null,
6 | status: 'initial',
7 | }
8 |
9 | const states = {
10 | [LOAD]: state => ({
11 | ...defaultState,
12 | status: 'loading',
13 | }),
14 | [HANDLE_RESPONSE]: (state, user) => ({
15 | ...state,
16 | user: pick(user, 'login', 'avatarUrl', 'name'),
17 | status: 'loaded',
18 | }),
19 | [HANDLE_ERROR]: state => ({
20 | ...defaultState,
21 | status: 'error',
22 | }),
23 | }
24 |
25 | export default (state = defaultState, { type, payload } = {}) =>
26 | states[type] ? states[type](state, payload) : state
27 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/reducer.js:
--------------------------------------------------------------------------------
1 | import pick from 'lodash/pick'
2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes'
3 |
4 | const defaultState = {
5 | repo: null,
6 | owner: null,
7 | status: 'initial',
8 | }
9 |
10 | const states = {
11 | [LOAD]: state => ({
12 | ...defaultState,
13 | status: 'loading',
14 | }),
15 | [HANDLE_RESPONSE]: (state, repo) => ({
16 | ...state,
17 | repo: pick(repo, 'name', 'description'),
18 | owner: {
19 | login: repo.owner.login,
20 | },
21 | status: 'loaded',
22 | }),
23 | [HANDLE_ERROR]: state => ({
24 | ...defaultState,
25 | status: 'error',
26 | }),
27 | }
28 |
29 | export default (state = defaultState, { type, payload } = {}) =>
30 | states[type] ? states[type](state, payload) : state
31 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/components/Repo/Repo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 |
5 | const Repo = ({ repo, owner }) => {
6 | const { login } = owner
7 | const { name, description } = repo
8 |
9 | return (
10 |
11 |
12 | {name}
13 | {' by '}
14 | {login}
15 |
16 | {description &&
{description}
}
17 |
18 | )
19 | }
20 |
21 | Repo.propTypes = {
22 | repo: PropTypes.shape({
23 | name: PropTypes.string.isRequired,
24 | description: PropTypes.string,
25 | }).isRequired,
26 | owner: PropTypes.shape({
27 | login: PropTypes.string.isRequired,
28 | }).isRequired,
29 | }
30 |
31 | export default Repo
32 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/user/containers/UserPage.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { Profile } from '../../../features/profile'
5 | import { StarredRepos } from '../../../features/starredRepos'
6 | import { handle as handleError } from '../../../features/error'
7 |
8 | const propTypes = {
9 | login: PropTypes.string.isRequired,
10 | }
11 |
12 | const UserPage = ({ login, onError }) => (
13 |
14 |
15 |
16 |
17 | )
18 |
19 | UserPage.propTypes = propTypes
20 |
21 | export default connect(
22 | (state, props) => ({
23 | login: props.match.params.login,
24 | }),
25 | {
26 | onError: handleError,
27 | }
28 | )(UserPage)
29 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/repo/containers/RepoPage.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { Repo } from '../../../features/repo'
5 | import { Stargazers } from '../../../features/stargazers'
6 | import { handle as handleError } from '../../../features/error'
7 |
8 | const propTypes = {
9 | fullName: PropTypes.string.isRequired,
10 | }
11 |
12 | const RepoPage = ({ fullName, onError }) => (
13 |
14 |
15 |
16 |
17 | )
18 |
19 | RepoPage.propTypes = propTypes
20 |
21 | export default connect(
22 | (state, props) => ({
23 | fullName: `${props.match.params.login}/${props.match.params.repo}`,
24 | }),
25 | {
26 | onError: handleError,
27 | }
28 | )(RepoPage)
29 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/starredRepos/actionCreators.js:
--------------------------------------------------------------------------------
1 | import * as api from '../../shared/api'
2 | import {
3 | LOAD,
4 | LOAD_NEXT,
5 | HANDLE_RESPONSE,
6 | HANDLE_NEXT_RESPONSE,
7 | HANDLE_ERROR,
8 | } from './actionTypes'
9 |
10 | const handleError = dispatch => error => {
11 | dispatch({ type: HANDLE_ERROR, payload: error })
12 | throw error
13 | }
14 |
15 | export const load = ({ login }) => dispatch => {
16 | dispatch({ type: LOAD, payload: login })
17 | return api
18 | .call(`users/${login}/starred`)
19 | .then(payload => {
20 | dispatch({ type: HANDLE_RESPONSE, payload })
21 | })
22 | .catch(handleError(dispatch))
23 | }
24 |
25 | export const loadNext = ({ url }) => dispatch => {
26 | dispatch({ type: LOAD_NEXT })
27 | return api
28 | .call(url)
29 | .then(payload => {
30 | dispatch({ type: HANDLE_NEXT_RESPONSE, payload })
31 | })
32 | .catch(handleError(dispatch))
33 | }
34 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/stargazers/actionCreators.js:
--------------------------------------------------------------------------------
1 | import * as api from '../../shared/api'
2 | import {
3 | LOAD,
4 | LOAD_NEXT,
5 | HANDLE_RESPONSE,
6 | HANDLE_NEXT_RESPONSE,
7 | HANDLE_ERROR,
8 | } from './actionTypes'
9 |
10 | const handleError = dispatch => error => {
11 | dispatch({ type: HANDLE_ERROR, payload: error })
12 | throw error
13 | }
14 |
15 | export const load = ({ fullName }) => dispatch => {
16 | dispatch({ type: LOAD, payload: fullName })
17 | return api
18 | .call(`repos/${fullName}/stargazers`)
19 | .then(payload => {
20 | dispatch({ type: HANDLE_RESPONSE, payload })
21 | })
22 | .catch(handleError(dispatch))
23 | }
24 |
25 | export const loadNext = ({ url }) => dispatch => {
26 | dispatch({ type: LOAD_NEXT })
27 | return api
28 | .call(url)
29 | .then(payload => {
30 | dispatch({ type: HANDLE_NEXT_RESPONSE, payload })
31 | })
32 | .catch(handleError(dispatch))
33 | }
34 |
--------------------------------------------------------------------------------
/demos/scoped-redux/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scoped-redux",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "react-scripts": "^2.0.4",
7 | "redux-devtools": "^3.4.1",
8 | "redux-devtools-dock-monitor": "^1.1.3",
9 | "redux-devtools-log-monitor": "^1.4.0",
10 | "redux-logger": "^3.0.6"
11 | },
12 | "dependencies": {
13 | "camelcase-keys": "^4.2.0",
14 | "lodash": "^4.17.5",
15 | "prop-types": "^15.6.1",
16 | "react": "^16.3.1",
17 | "react-dom": "^16.3.1",
18 | "react-redux": "^5.0.7",
19 | "react-router-dom": "^4.1.2",
20 | "redux": "^3.5.2",
21 | "redux-thunk": "^2.1.0"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "eject": "react-scripts eject",
27 | "test": "react-scripts test"
28 | },
29 | "browserslist": [
30 | ">0.2%",
31 | "not dead",
32 | "not ie <= 11",
33 | "not op_mini all"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { createLogger } from 'redux-logger'
4 | import DevTools from '../shared/components/DevTools'
5 | import { reducer as errorReducer } from '../features/error'
6 | import { reducer as profileReducer } from '../features/profile'
7 | import { reducer as repoReducer } from '../features/repo'
8 | import { reducer as starredReposReducer } from '../features/starredRepos'
9 | import { reducer as stargazersReducer } from '../features/stargazers'
10 |
11 | const reducer = combineReducers({
12 | features: combineReducers({
13 | error: errorReducer,
14 | profile: profileReducer,
15 | repo: repoReducer,
16 | starredRepos: starredReposReducer,
17 | stargazers: stargazersReducer,
18 | }),
19 | pages: {},
20 | })
21 |
22 | export const configure = preloadedState => {
23 | const store = createStore(
24 | reducer,
25 | preloadedState,
26 | compose(
27 | applyMiddleware(thunk, createLogger()),
28 | DevTools.instrument()
29 | )
30 | )
31 |
32 | return store
33 | }
34 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/stargazers/reducer.js:
--------------------------------------------------------------------------------
1 | import pick from 'lodash/pick'
2 | import {
3 | LOAD,
4 | HANDLE_RESPONSE,
5 | HANDLE_NEXT_RESPONSE,
6 | HANDLE_ERROR,
7 | LOAD_NEXT,
8 | } from './actionTypes'
9 |
10 | const defaultState = {
11 | users: [],
12 | status: 'initial',
13 | }
14 |
15 | const mapResult = data => pick(data, 'login', 'avatarUrl')
16 |
17 | const states = {
18 | [LOAD]: state => ({
19 | ...defaultState,
20 | status: 'loading',
21 | }),
22 | [LOAD_NEXT]: state => ({
23 | ...state,
24 | status: 'loading',
25 | }),
26 | [HANDLE_RESPONSE]: (state, { result, ...rest }) => ({
27 | ...state,
28 | ...rest,
29 | users: result.map(mapResult),
30 | status: 'loaded',
31 | }),
32 | [HANDLE_NEXT_RESPONSE]: (state, { result, ...rest }) => ({
33 | ...state,
34 | ...rest,
35 | users: [...state.users, ...result.map(mapResult)],
36 | status: 'loaded',
37 | }),
38 | [HANDLE_ERROR]: state => ({
39 | ...defaultState,
40 | status: 'error',
41 | }),
42 | }
43 |
44 | export default (state = defaultState, { type, payload } = {}) =>
45 | states[type] ? states[type](state, payload) : state
46 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/pages/root/containers/RootPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { Search } from '../../../features/search'
5 | import { Error, dismiss as dismissError } from '../../../features/error'
6 |
7 | const propTypes = {
8 | search: PropTypes.string,
9 | onChangeSearch: PropTypes.func.isRequired,
10 | onSearch: PropTypes.func.isRequired,
11 | }
12 |
13 | class RootPageContainer extends Component {
14 | static propTypes = propTypes
15 |
16 | componentDidUpdate(prevProps) {
17 | if (prevProps.search !== this.props.search) {
18 | this.props.onChangeSearch()
19 | }
20 | }
21 |
22 | render() {
23 | const { search, onSearch } = this.props
24 | return (
25 |
26 |
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | export default connect(
34 | (state, { location, history }) => ({
35 | search: location.pathname.substr(1),
36 | onSearch: ({ value }) => {
37 | history.push(`/${value}`)
38 | },
39 | }),
40 | {
41 | onChangeSearch: dismissError,
42 | }
43 | )(RootPageContainer)
44 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/starredRepos/reducer.js:
--------------------------------------------------------------------------------
1 | import pick from 'lodash/pick'
2 | import {
3 | LOAD,
4 | HANDLE_RESPONSE,
5 | HANDLE_NEXT_RESPONSE,
6 | HANDLE_ERROR,
7 | LOAD_NEXT,
8 | } from './actionTypes'
9 |
10 | const defaultState = {
11 | starred: [],
12 | status: 'initial',
13 | }
14 |
15 | const mapResult = data => ({
16 | repo: pick(data, 'name', 'fullName', 'description'),
17 | owner: {
18 | login: data.owner.login,
19 | },
20 | })
21 |
22 | const states = {
23 | [LOAD]: state => ({
24 | ...defaultState,
25 | status: 'loading',
26 | }),
27 | [LOAD_NEXT]: state => ({
28 | ...state,
29 | status: 'loading',
30 | }),
31 | [HANDLE_RESPONSE]: (state, { result, ...rest }) => ({
32 | ...state,
33 | ...rest,
34 | starred: result.map(mapResult),
35 | status: 'loaded',
36 | }),
37 | [HANDLE_NEXT_RESPONSE]: (state, { result, ...rest }) => ({
38 | ...state,
39 | ...rest,
40 | starred: [...state.starred, ...result.map(mapResult)],
41 | status: 'loaded',
42 | }),
43 | [HANDLE_ERROR]: state => ({
44 | ...defaultState,
45 | status: 'error',
46 | }),
47 | }
48 |
49 | export default (state = defaultState, { type, payload } = {}) =>
50 | states[type] ? states[type](state, payload) : state
51 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/api/index.js:
--------------------------------------------------------------------------------
1 | import camelizeKeys from 'camelcase-keys'
2 |
3 | const API_ROOT = 'https://api.github.com/'
4 |
5 | // Extracts the next page URL from Github API response.
6 | const getLink = (rel, response) => {
7 | const link = response.headers.get('link')
8 | if (!link) {
9 | return null
10 | }
11 |
12 | const nextLink = link.split(',').find(s => s.indexOf(`rel="${rel}"`) > -1)
13 | if (!nextLink) {
14 | return null
15 | }
16 |
17 | return nextLink
18 | .trim()
19 | .split(';')[0]
20 | .slice(1, -1)
21 | }
22 |
23 | // Fetches an API response and normalizes the result JSON according to schema.
24 | // This makes every API response have the same shape, regardless of how nested it was.
25 | export const call = endpoint => {
26 | const url = endpoint.indexOf(API_ROOT) !== 0 ? API_ROOT + endpoint : endpoint
27 |
28 | return fetch(url).then(response =>
29 | response.json().then(json => {
30 | if (!response.ok) {
31 | return Promise.reject(json)
32 | }
33 |
34 | const result = camelizeKeys(json, { deep: true })
35 | const nextPageUrl = getLink('next', response)
36 | const lastPageUrl = getLink('last', response)
37 | return { result, nextPageUrl, lastPageUrl }
38 | })
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/repo/containers/Repo.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { LOADING_STATES } from '../../../shared/constants'
5 | import Repo from '../../../shared/components/Repo'
6 | import * as actions from '../actionCreators'
7 | import { selectRepo } from '../selectors'
8 | import Loading from '../renderers/Loading'
9 | import Empty from '../renderers/Empty'
10 |
11 | const propTypes = {
12 | fullName: PropTypes.string.isRequired,
13 | status: PropTypes.oneOf(LOADING_STATES).isRequired,
14 | onLoad: PropTypes.func.isRequired,
15 | repo: PropTypes.shape(),
16 | owner: PropTypes.shape(),
17 | }
18 |
19 | class RepoContainer extends Component {
20 | static propTypes = propTypes
21 |
22 | componentDidMount() {
23 | this.load()
24 | }
25 |
26 | componentDidUpdate(prevProps) {
27 | this.load(prevProps.fullName)
28 | }
29 |
30 | load(prevFullName) {
31 | const { fullName, onLoad, onError, status } = this.props
32 | if (fullName && fullName !== prevFullName && status !== 'loading') {
33 | onLoad({ fullName }).catch(onError)
34 | }
35 | }
36 |
37 | render() {
38 | const { status, fullName, repo, owner } = this.props
39 |
40 | if (status === 'loading') {
41 | return
42 | }
43 |
44 | if (status === 'loaded') {
45 | return
46 | }
47 |
48 | return
49 | }
50 | }
51 |
52 | export default connect(
53 | selectRepo,
54 | {
55 | onLoad: actions.load,
56 | }
57 | )(RepoContainer)
58 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/profile/containers/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { LOADING_STATES } from '../../../shared/constants'
5 | import Profile from '../../../shared/components/Profile'
6 | import * as actions from '../actionCreators'
7 | import { selectProfile } from '../selectors'
8 | import Loading from '../renderers/Loading'
9 | import Empty from '../renderers/Empty'
10 | import Title from '../renderers/Title'
11 |
12 | const propTypes = {
13 | login: PropTypes.string.isRequired,
14 | status: PropTypes.oneOf(LOADING_STATES).isRequired,
15 | onLoad: PropTypes.func.isRequired,
16 | user: PropTypes.shape(),
17 | }
18 |
19 | class ProfileContainer extends Component {
20 | static propTypes = propTypes
21 |
22 | componentDidMount() {
23 | this.load()
24 | }
25 |
26 | componentDidUpdate(prevProps) {
27 | this.load(prevProps.login)
28 | }
29 |
30 | load(prevLogin) {
31 | const { login, onLoad, onError, status } = this.props
32 | if (login && login !== prevLogin && status !== 'loading') {
33 | onLoad({ login }).catch(onError)
34 | }
35 | }
36 |
37 | render() {
38 | const { status, login, user } = this.props
39 |
40 | return (
41 |
42 |
43 | {status === 'loading' && }
44 | {status === 'loaded' && }
45 | {status === 'initial' && }
46 |
47 | )
48 | }
49 | }
50 |
51 | export default connect(
52 | selectProfile,
53 | {
54 | onLoad: actions.load,
55 | }
56 | )(ProfileContainer)
57 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/shared/components/List/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { LOADING_STATES } from '../../constants'
4 |
5 | const LoadMoreButton = ({ status, onClick }) => (
6 |
13 | )
14 |
15 | const Loading = ({ label }) => (
16 |
17 | {label}
18 |
19 | )
20 |
21 | const Empty = () => (
22 |
23 | Nothing here!
24 |
25 | )
26 |
27 | const List = props => {
28 | const {
29 | status,
30 | nextPageUrl,
31 | lastPageUrl,
32 | items,
33 | renderItem,
34 | loadingLabel,
35 | onLoadNext,
36 | } = props
37 |
38 | const isEmpty = items.length === 0
39 | const isLastPage = !nextPageUrl
40 | const isSinglePage = nextPageUrl === lastPageUrl
41 |
42 | if (isEmpty && status === 'loading') {
43 | return
44 | }
45 |
46 | if (isEmpty && isLastPage) {
47 | return
48 | }
49 |
50 | return (
51 |
52 | {items.map(renderItem)}
53 | {!isSinglePage &&
54 | !isLastPage && (
55 | {
58 | onLoadNext({ url: nextPageUrl })
59 | }}
60 | />
61 | )}
62 |
63 | )
64 | }
65 |
66 | List.propTypes = {
67 | loadingLabel: PropTypes.string.isRequired,
68 | renderItem: PropTypes.func.isRequired,
69 | items: PropTypes.array.isRequired,
70 | status: PropTypes.oneOf(LOADING_STATES).isRequired,
71 | onLoadNext: PropTypes.func.isRequired,
72 | nextPageUrl: PropTypes.string,
73 | }
74 |
75 | List.defaultProps = {
76 | status: 'loading',
77 | loadingLabel: 'Loading...',
78 | items: [],
79 | }
80 |
81 | export default List
82 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/search/renderers/Search.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const propTypes = {
5 | value: PropTypes.string.isRequired,
6 | onSearch: PropTypes.func.isRequired,
7 | }
8 |
9 | const Input = ({ value, onSearch, onChange }) => (
10 | {
14 | if (keyCode === 13) onSearch()
15 | }}
16 | onChange={e => {
17 | onChange({ value: e.target.value })
18 | }}
19 | />
20 | )
21 |
22 | const Label = () => (
23 |
26 | )
27 |
28 | const RepoHint = () => (
29 |
30 | Code on{' '}
31 |
36 | Github
37 |
38 | .
39 |
40 | )
41 |
42 | const DevToolsHint = () => (
43 | Move the DevTools with Ctrl+W or hide them with Ctrl+H.
44 | )
45 |
46 | export default class Search extends Component {
47 | static propTypes = propTypes
48 |
49 | constructor(props) {
50 | super(props)
51 | this.state = { value: props.value }
52 | }
53 |
54 | componentWillReceiveProps(nextProps) {
55 | if (nextProps.value !== this.state.value) {
56 | this.setState({ value: nextProps.value })
57 | }
58 | }
59 |
60 | onChange = ({ value }) => {
61 | this.setState({ value })
62 | }
63 |
64 | onSearch = () => {
65 | this.props.onSearch({ value: this.state.value })
66 | }
67 |
68 | render() {
69 | return (
70 |
71 |
72 |
77 |
78 |
79 |
80 |
81 | )
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/stargazers/containers/Stargazers.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { LOADING_STATES } from '../../../shared/constants'
5 | import List from '../../../shared/components/List'
6 | import Profile from '../../../shared/components/Profile'
7 | import Title from '../renderers/Title'
8 | import * as actions from '../actionCreators'
9 | import { selectStargazers } from '../selectors'
10 |
11 | const propTypes = {
12 | fullName: PropTypes.string.isRequired,
13 | status: PropTypes.oneOf(LOADING_STATES).isRequired,
14 | onLoad: PropTypes.func.isRequired,
15 | onLoadNext: PropTypes.func.isRequired,
16 | nextPageUrl: PropTypes.string,
17 | lastPageUrl: PropTypes.string,
18 | users: PropTypes.array.isRequired,
19 | }
20 |
21 | class StargazersContainer extends Component {
22 | static propTypes = propTypes
23 |
24 | componentDidMount() {
25 | this.load()
26 | }
27 |
28 | componentDidUpdate(prevProps) {
29 | this.load(prevProps.fullName)
30 | }
31 |
32 | load(prevfullName) {
33 | const { fullName, onLoad, onError, status } = this.props
34 |
35 | if (fullName && fullName !== prevfullName && status !== 'loading') {
36 | onLoad({ fullName }).catch(onError)
37 | }
38 | }
39 |
40 | render() {
41 | const {
42 | fullName,
43 | users,
44 | onLoadNext,
45 | nextPageUrl,
46 | lastPageUrl,
47 | status,
48 | } = this.props
49 |
50 | return (
51 |
52 |
53 | }
55 | items={users}
56 | onLoadNext={onLoadNext}
57 | loadingLabel={`Loading ${fullName}'s stargazers...`}
58 | nextPageUrl={nextPageUrl}
59 | lastPageUrl={lastPageUrl}
60 | status={status}
61 | />
62 |
63 | )
64 | }
65 | }
66 |
67 | export default connect(
68 | selectStargazers,
69 | {
70 | onLoad: actions.load,
71 | onLoadNext: actions.loadNext,
72 | }
73 | )(StargazersContainer)
74 |
--------------------------------------------------------------------------------
/demos/scoped-redux/src/features/starredRepos/containers/StarredRepos.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { LOADING_STATES } from '../../../shared/constants'
5 | import List from '../../../shared/components/List'
6 | import Repo from '../../../shared/components/Repo'
7 | import Title from '../renderers/Title'
8 | import * as actions from '../actionCreators'
9 | import { selectStarredRepos } from '../selectors'
10 |
11 | const propTypes = {
12 | login: PropTypes.string.isRequired,
13 | status: PropTypes.oneOf(LOADING_STATES).isRequired,
14 | onLoad: PropTypes.func.isRequired,
15 | onLoadNext: PropTypes.func.isRequired,
16 | nextPageUrl: PropTypes.string,
17 | lastPageUrl: PropTypes.string,
18 | starred: PropTypes.array.isRequired,
19 | }
20 |
21 | class StarredReposContainer extends Component {
22 | static propTypes = propTypes
23 |
24 | componentDidMount() {
25 | this.load()
26 | }
27 |
28 | componentDidUpdate(prevProps) {
29 | this.load(prevProps.login)
30 | }
31 |
32 | load = prevLogin => {
33 | const { login, onLoad, onError, status } = this.props
34 |
35 | if (login && login !== prevLogin && status !== 'loading') {
36 | onLoad({ login }).catch(onError)
37 | }
38 | }
39 |
40 | loadNext = options => {
41 | const { onLoadNext, onError } = this.props
42 | onLoadNext(options).catch(onError)
43 | }
44 |
45 | render() {
46 | const {
47 | login,
48 | starred,
49 | onLoadNext,
50 | nextPageUrl,
51 | lastPageUrl,
52 | status,
53 | } = this.props
54 |
55 | return (
56 |
57 |
58 | }
60 | items={starred}
61 | onLoadNext={this.loadNext}
62 | loadingLabel={`Loading ${login}'s starred...`}
63 | nextPageUrl={nextPageUrl}
64 | lastPageUrl={lastPageUrl}
65 | status={status}
66 | />
67 |
68 | )
69 | }
70 | }
71 |
72 | export default connect(
73 | selectStarredRepos,
74 | {
75 | onLoad: actions.load,
76 | onLoadNext: actions.loadNext,
77 | }
78 | )(StarredReposContainer)
79 |
--------------------------------------------------------------------------------