├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── README.md
├── dist
├── fonts
│ ├── Montserrat-Regular.ttf
│ └── Muli-Regular.ttf
├── index.html
└── style.css
├── package.json
├── src
├── components
│ ├── components
│ │ ├── Breadcrumbs.js
│ │ ├── LargeTextarea.js
│ │ ├── SmallButton.js
│ │ └── SmallInput.js
│ └── views
│ │ ├── App.js
│ │ ├── Entries
│ │ ├── Entries.js
│ │ └── SingleEntry.js
│ │ └── Post
│ │ ├── Post.js
│ │ └── PostContainer.js
├── index.jsx
└── state
│ ├── entities
│ ├── posts
│ │ ├── actions.js
│ │ ├── reducers.js
│ │ └── sagas.js
│ ├── reducers.js
│ └── tags
│ │ ├── actions.js
│ │ └── reducers.js
│ └── view
│ ├── actions.js
│ └── reducers.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react"
5 | ],
6 | "plugins": [
7 | "transform-class-properties",
8 | "transform-object-rest-spread",
9 | "syntax-object-rest-spread",
10 | "transform-runtime"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/bundle.js
3 | npm-debug.log
4 | .idea/
5 | *-compiled.js
6 | *-compiled.js.map
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "plugins": [
5 | "react"
6 | ],
7 | "parserOptions": {
8 | "ecmaFeatures": {
9 | "experimentalObjectRestSpread": true
10 | }
11 | },
12 | "rules": {
13 | "indent": ["error", 2, {
14 | "VariableDeclarator": { "var": 2, "let": 2, "const": 3 }
15 | }]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/bundle.js
3 | npm-debug.log
4 | .idea/
5 | *-compiled.js
6 | *-compiled.js.map
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## react-redux-blog-seed
2 |
3 | This is a small seed for a React/Redux project. It is also an experiment on how to build a React project with a better structure. A blog was used for the example to keep it simple.
4 |
5 | ## Installation
6 | To get going you just need to:
7 |
8 | npm install
9 | npm start
10 |
11 | App will run from `http://localhost:8080`
12 |
13 | ## Structure explanation
14 | The project is divided into two main folders: `components` and `state`.
15 |
16 | When an app starts to scale is usually hard to keep the actions and reducers of the app component or view related because some components start to interact with actions and reducers from other components. Because of this, the state of the app was kept in a separate folder and the files inside of it try to reproduce the structure of the actual app `state`. This way things are easy to find and to work with.
17 |
18 | .state
19 | ├── Entities
20 | │ ├── posts
21 | │ ├── tags
22 | ├── View
23 |
24 | Something similar happens to components when an application starts to grow. If we create a component for every single view we have we suddenly start having lots of repeated code everywhere. Having a separate component for every view is ok but to let an application scale well we need to keep every UI element of our app in one place. This way, the moment we need to add a view to our app all we need to do is to stick together some of those UI components into a view. An example from the `Post` view:
25 |
26 | ```javascript
27 | {/* from src/components/views/Post.jsx */}
28 |
29 |
30 |
Add a new Post
31 |
32 |
38 |
39 |
45 |
46 |
50 |
51 | ```
52 |
53 | Because of this, the `components` folder is divided into to main folders: `components` and `views`. `components` is where we should keep all our UI elements (and by UI elements I really mean that: button, form, text-area, etc...) and `views` is where we should keep that: the views of our app.
54 |
55 |
56 | ## WIP
57 | - [x] Add propTypes
58 | - [x] Add proper routing
59 | - [ ] Add normalizer to normalize the posts coming from the requests
60 | - [ ] Add testing
61 | - [ ] Keep every sass file related to a UI component
62 |
63 |
64 | ## License
65 |
66 | Released under The MIT License.
--------------------------------------------------------------------------------
/dist/fonts/Montserrat-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keyserfaty/react-redux-blog-seed/a90231a7b0c250def986ec6330968a0217098627/dist/fonts/Montserrat-Regular.ttf
--------------------------------------------------------------------------------
/dist/fonts/Muli-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keyserfaty/react-redux-blog-seed/a90231a7b0c250def986ec6330968a0217098627/dist/fonts/Muli-Regular.ttf
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
React Redux Seed
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dist/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Montserrat-Regular';
3 | src: url('./fonts/Montserrat-Regular.ttf') format('opentype');
4 | }
5 |
6 | @font-face {
7 | font-family: 'Muli-Regular';
8 | src: url('./fonts/Muli-Regular.ttf') format('opentype');
9 | }
10 |
11 | body {
12 | background-color: #F3F3F4;
13 | margin: 15px;
14 | }
15 |
16 | h1, h2, h3 {
17 | margin: 0;
18 | }
19 |
20 | h3 {
21 | font: 28px/32px 'Montserrat-Regular', Serif;
22 | margin-bottom: 15px;
23 | }
24 |
25 | p {
26 | font: 16px/24px 'Montserrat-Regular', Serif;
27 | }
28 |
29 | .box {
30 | background: #fff;
31 | border-radius: 3px;
32 | padding: 18px;
33 | margin-bottom: 15px;
34 | border: 1px solid #ebeff6;
35 | box-shadow: 0 1px 1px rgba(0,0,0,.05);
36 | }
37 |
38 | .breadcrumbs a {
39 | font: 14px 'Muli-Regular', Serif;
40 | color: #D95459;
41 | text-decoration: none;
42 | padding-right: 15px;
43 | }
44 |
45 | .small-button {
46 | font: 14px 'Muli-Regular', Serif;
47 | padding: 10px 15px;
48 | border-radius: 3px;
49 | border: none;
50 | color: #fff;
51 | background-color: #D95459;
52 | margin-top: 15px;
53 | cursor: pointer;
54 | }
55 |
56 | .small-button:hover {
57 | background-color: #D9545f;
58 | cursor: pointer;
59 | }
60 |
61 | .small-button:focus {
62 | outline: none;
63 | }
64 |
65 | .small-input {
66 | border: 1px solid #e0e0e0;
67 | border-radius: 3px;
68 | color: #616161;
69 | font: 14px 'Muli-Regular', Serif;
70 | margin-top: 15px;
71 | padding: 5px 8px;
72 | height: 35px;
73 | width: 100%;
74 | outline: none;
75 | box-sizing:border-box
76 | }
77 |
78 | .large-textarea {
79 | border: 1px solid #e0e0e0;
80 | border-radius: 3px;
81 | color: #616161;
82 | font: 14px 'Muli-Regular', Serif;
83 | margin-top: 15px;
84 | padding: 5px 8px;
85 | width: 100%;
86 | outline: none;
87 | box-sizing:border-box
88 | }
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-seed",
3 | "version": "1.0.0",
4 | "description": "A simple app using react and redux",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server",
8 | "lint": "eslint --ext .js,.jsx src/ test/",
9 | "pretest": "npm run lint"
10 | },
11 | "author": "@keyserfaty",
12 | "license": "MIT",
13 | "dependencies": {
14 | "autoprefixer": "^6.3.6",
15 | "history": "^2.1.1",
16 | "lodash": "^4.12.0",
17 | "react": "0.14.7",
18 | "react-dom": "0.14.7",
19 | "react-ga": "^1.4.1",
20 | "react-redux": "^4.4.5",
21 | "react-router": "^2.0.0",
22 | "redux": "^3.5.2",
23 | "redux-saga": "^0.11.1",
24 | "redux-thunk": "^2.1.0"
25 | },
26 | "devDependencies": {
27 | "babel-core": "6.5.1",
28 | "babel-eslint": "^6.0.4",
29 | "babel-loader": "6.2.2",
30 | "babel-plugin-syntax-object-rest-spread": "^6.5.0",
31 | "babel-plugin-transform-class-properties": "^6.11.5",
32 | "babel-plugin-transform-object-rest-spread": "^6.6.5",
33 | "babel-plugin-transform-runtime": "^6.15.0",
34 | "babel-preset-es2015": "^6.6.0",
35 | "babel-preset-react": "6.5.0",
36 | "css-loader": "0.23.1",
37 | "eslint": "^2.4.0",
38 | "eslint-config-airbnb": "^6.1.0",
39 | "eslint-plugin-react": "^4.2.3",
40 | "jsdom": "8.0.4",
41 | "react-addons-pure-render-mixin": "0.14.7",
42 | "react-addons-test-utils": "0.14.7",
43 | "react-hot-loader": "^1.3.0",
44 | "redux-logger": "^2.6.1",
45 | "webpack": "1.12.14",
46 | "webpack-dev-server": "1.14.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/components/Breadcrumbs.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | import { Link } from 'react-router';
4 | import { map } from 'lodash';
5 |
6 | const BreadCrumbs = ({ links }) => (
7 |
8 | { map(links, (link, i) => (
9 |
10 | {link.name}
11 |
12 | ))}
13 |
14 | );
15 |
16 | BreadCrumbs.propTypes = {
17 | links: PropTypes.array.isRequired,
18 | };
19 |
20 | export default BreadCrumbs;
21 |
--------------------------------------------------------------------------------
/src/components/components/LargeTextarea.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | const LargeTextarea = ({ name, value, placeholder, onChange }) => (
4 |
13 | );
14 |
15 | LargeTextarea.propTypes = {
16 | name: PropTypes.string.isRequired,
17 | value: PropTypes.string.isRequired,
18 | placeholder: PropTypes.string.isRequired,
19 | onChange: PropTypes.func.isRequired,
20 | };
21 |
22 | export default LargeTextarea;
23 |
--------------------------------------------------------------------------------
/src/components/components/SmallButton.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | const SmallButton = ({ value, onClick }) => (
4 |
{value}
9 | );
10 |
11 | SmallButton.propTypes = {
12 | value: PropTypes.string.isRequired,
13 | onClick: PropTypes.func.isRequired,
14 | };
15 |
16 | export default SmallButton;
17 |
--------------------------------------------------------------------------------
/src/components/components/SmallInput.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | const SmallInput = ({ name, value, placeholder, onChange }) => (
4 |
13 | );
14 |
15 | SmallInput.propTypes = {
16 | name: PropTypes.string.isRequired,
17 | value: PropTypes.string.isRequired,
18 | placeholder: PropTypes.string.isRequired,
19 | onChange: PropTypes.func.isRequired,
20 | };
21 |
22 | export default SmallInput;
23 |
--------------------------------------------------------------------------------
/src/components/views/App.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import BreadCrumbs from '../components/BreadCrumbs';
3 |
4 | export default class App extends Component {
5 | static propTypes = {
6 | children: PropTypes.object.isRequired,
7 | };
8 |
9 | static breadCrumbsLinks = [
10 | {
11 | name: 'All Posts',
12 | href: '/entries',
13 | }, {
14 | name: 'Add New Post',
15 | href: '/post',
16 | },
17 | ];
18 |
19 | render() {
20 | const { breadCrumbsLinks } = App;
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | { this.props.children }
29 |
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/views/Entries/Entries.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { map } from 'lodash';
4 |
5 | import SingleEntry from './SingleEntry';
6 |
7 | let Entries = ({ entries }) => (
8 |
9 | { map(entries, entry =>
10 |
11 | )}
12 |
13 | );
14 |
15 | Entries.propTypes = {
16 | entries: PropTypes.array.isRequired,
17 | };
18 |
19 | const mapStateToProps = (state) => ({
20 | entries: state.entities.posts.entries,
21 | });
22 |
23 | Entries = connect(mapStateToProps)(Entries);
24 |
25 | export default Entries;
26 |
--------------------------------------------------------------------------------
/src/components/views/Entries/SingleEntry.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | const SingleEntry = ({ title, content }) => (
4 |
5 |
{title}
6 |
{content}
7 |
8 | );
9 |
10 | SingleEntry.propTypes = {
11 | title: PropTypes.string.isRequired,
12 | content: PropTypes.string.isRequired,
13 | };
14 |
15 | export default SingleEntry;
16 |
--------------------------------------------------------------------------------
/src/components/views/Post/Post.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import SmallInput from '../../components/SmallInput';
3 | import LargeTextarea from '../../components/LargeTextarea';
4 | import SmallButton from '../../components/SmallButton';
5 |
6 | const Post = ({ onPublishClick, onChange, title, content }) => (
7 |
8 |
Add a new Post
9 |
10 |
16 |
17 |
23 |
24 |
28 |
29 |
30 | );
31 |
32 | Post.propTypes = {
33 | onPublishClick: PropTypes.func.isRequired,
34 | onChange: PropTypes.func.isRequired,
35 | title: PropTypes.string.isRequired,
36 | content: PropTypes.string.isRequired,
37 | };
38 |
39 | export default Post;
40 |
--------------------------------------------------------------------------------
/src/components/views/Post/PostContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import Post from './Post';
5 | import { postEntry } from '../../../state/entities/posts/actions';
6 |
7 | class PostContainerComponent extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | title: '',
12 | content: '',
13 | };
14 |
15 | this.onPublishClick = this.onPublishClick.bind(this);
16 | this.onChange = this.onChange.bind(this);
17 | }
18 |
19 | onPublishClick() {
20 | const { onPublishClick } = this.props;
21 | onPublishClick(this.state);
22 | }
23 |
24 | onChange(e) {
25 | this.setState({
26 | [e.target.name]: e.target.value,
27 | });
28 | }
29 |
30 | render() {
31 | return (
32 |
33 | );
34 | }
35 | }
36 |
37 | const mapStateToProps = state => ({});
38 | const mapDispatchToProps = (dispatch, ownProps) => ({
39 | onPublishClick: state => dispatch(postEntry(state))
40 | });
41 |
42 | export const PostContainer = connect(mapStateToProps, mapDispatchToProps)(PostContainerComponent);
43 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, Route, useRouterHistory, IndexRoute } from 'react-router';
4 |
5 | import { applyMiddleware, createStore, combineReducers } from 'redux';
6 | import { Provider } from 'react-redux';
7 | import createSagaMiddleware from 'redux-saga';
8 | import { fork } from 'redux-saga/effects';
9 |
10 | import createLogger from 'redux-logger';
11 |
12 | import { createHashHistory } from 'history';
13 |
14 | import App from './components/views/App';
15 | import { PostContainer } from './components/views/Post/PostContainer';
16 | import Entries from './components/views/Entries/Entries';
17 |
18 | import { entities } from './state/entities/reducers';
19 |
20 | import postsSagas from './state/entities/posts/sagas';
21 |
22 | const appHistory = useRouterHistory(createHashHistory)({ queryKey: false });
23 | const loggerMiddleware = createLogger();
24 | const sagaMiddleware = createSagaMiddleware();
25 |
26 | function startSagas (...sagas) {
27 | return function * rootSaga() {
28 | yield sagas.map(saga => fork(saga));
29 | };
30 | }
31 |
32 | const store = createStore(combineReducers({
33 | entities,
34 | }), applyMiddleware(
35 | sagaMiddleware,
36 | loggerMiddleware
37 | ));
38 |
39 | sagaMiddleware.run(startSagas(
40 | ...postsSagas,
41 | ));
42 |
43 | render(
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
,
56 | document.getElementById('root')
57 | );
58 |
59 |
--------------------------------------------------------------------------------
/src/state/entities/posts/actions.js:
--------------------------------------------------------------------------------
1 | export const ENTRIES_REQUEST = 'ENTRIES_REQUEST';
2 | export const ENTRIES_REQUEST_SUCCESS = 'ENTRIES_REQUEST_SUCCESS';
3 | export const ENTRIES_REQUEST_FAILURE = 'ENTRIES_REQUEST_FAILURE';
4 |
5 | export function entriesRequest() {
6 | return {
7 | type: ENTRIES_REQUEST,
8 | payload: {
9 | requestingEntries: true,
10 | requestingEntriesSuccess: false,
11 | },
12 | };
13 | }
14 |
15 | export function entriesRequestSuccess(entries) {
16 | return {
17 | type: ENTRIES_REQUEST_SUCCESS,
18 | payload: {
19 | requestingEntries: false,
20 | requestingEntriesSuccess: true,
21 | entries,
22 | },
23 | };
24 | }
25 |
26 | export function entriesRequestFailure(error) {
27 | return {
28 | type: ENTRIES_REQUEST_FAILURE,
29 | payload: {
30 | requestingEntries: false,
31 | requestingEntriesSuccess: false,
32 | error: new Error(error),
33 | },
34 | };
35 | }
36 |
37 | export const POST_ENTRY = 'POST_ENTRY';
38 | export const POST_ENTRY_SUCCESS = 'POST_ENTRY_SUCCESS';
39 | export const POST_ENTRY_FAILURE = 'POST_ENTRY_FAILURE';
40 |
41 | export function postEntry() {
42 | return {
43 | type: POST_ENTRY,
44 | payload: {
45 | postingEntry: true,
46 | postingEntrySuccess: false,
47 | },
48 | };
49 | }
50 |
51 | export function postEntrySuccess(entry) {
52 | return {
53 | type: POST_ENTRY_SUCCESS,
54 | payload: {
55 | postingEntry: false,
56 | postingEntrySuccess: true,
57 | entry
58 | },
59 | };
60 | }
61 |
62 | export function postEntryFailure(error) {
63 | return {
64 | type: POST_ENTRY_FAILURE,
65 | payload: {
66 | postingEntry: false,
67 | postingEntrySuccess: false,
68 | error: new Error(error),
69 | },
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/src/state/entities/posts/reducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ENTRIES_REQUEST,
3 | ENTRIES_REQUEST_SUCCESS,
4 | ENTRIES_REQUEST_FAILURE,
5 | POST_ENTRY,
6 | POST_ENTRY_SUCCESS,
7 | POST_ENTRY_FAILURE,
8 | } from './actions';
9 |
10 | const initialState = {
11 | entries: [],
12 | requestingEntries: '',
13 | requestingEntriesSuccess: '',
14 | postingEntry: '',
15 | postingEntrySuccess: '',
16 | };
17 |
18 | export const posts = (state = initialState, action) => {
19 | switch (action.type) {
20 | case ENTRIES_REQUEST:
21 | case ENTRIES_REQUEST_FAILURE:
22 | return {
23 | ...state,
24 | requestingEntries: action.payload.requestingEntries,
25 | requestingEntriesSuccess: action.payload.requestingEntriesSuccess,
26 | };
27 |
28 | case ENTRIES_REQUEST_SUCCESS:
29 | return {
30 | ...state,
31 | requestingEntries: action.payload.requestingEntries,
32 | requestingEntriesSuccess: action.payload.requestingEntriesSuccess,
33 | entries: [
34 | ...state.entries,
35 | action.payload.entry,
36 | ],
37 | };
38 |
39 | case POST_ENTRY:
40 | case POST_ENTRY_FAILURE:
41 | return {
42 | ...state,
43 | postingEntry: action.payload.postingEntry,
44 | postingEntrySuccess: action.payload.postingEntrySuccess,
45 | };
46 |
47 | case POST_ENTRY_SUCCESS:
48 | return {
49 | ...state,
50 | postingEntry: action.payload.postingEntry,
51 | postingEntrySuccess: action.payload.postingEntrySuccess,
52 | entries: [
53 | ...state.entries,
54 | action.payload.entry,
55 | ],
56 | };
57 |
58 | default:
59 | return state;
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/src/state/entities/posts/sagas.js:
--------------------------------------------------------------------------------
1 | import { takeEvery, delay } from 'redux-saga';
2 | import { put, call } from 'redux-saga/effects';
3 |
4 | import * as actions from './actions';
5 |
6 | const entriesFetch = () =>
7 | fetch('', {
8 | method: 'GET',
9 | credentials: 'include',
10 | })
11 | .then(response => response.json())
12 | .then(response => response)
13 | .catch(error => error);
14 |
15 | /**
16 | * Performs the async call to fetch entries and handles
17 | * the response or error
18 | */
19 | export function* fetchEntriesWorker() {
20 | const { response, error } = yield call(entriesFetch);
21 |
22 | if (response) yield put(actions.entriesRequestSuccess({ response }));
23 | if (error) yield put(actions.entriesRequestFailure({ error }));
24 | }
25 |
26 | export function* fetchPostEntryWorker(action) {
27 | yield put(actions.postEntrySuccess(action.payload.entry));
28 | }
29 |
30 | /**
31 | * Launch the action
32 | */
33 | function* watchFetchEntries() {
34 | yield* takeEvery(actions.entriesRequest, fetchEntriesWorker);
35 | }
36 |
37 | function* watchPostEntry() {
38 | yield* takeEvery(actions.postEntry, fetchPostEntryWorker);
39 | }
40 |
41 | export default [
42 | watchFetchEntries,
43 | watchPostEntry
44 | ];
--------------------------------------------------------------------------------
/src/state/entities/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { posts } from './posts/reducers';
3 | import { tags } from './tags/reducers';
4 |
5 | export const entities = combineReducers({
6 | posts,
7 | tags,
8 | });
9 |
--------------------------------------------------------------------------------
/src/state/entities/tags/actions.js:
--------------------------------------------------------------------------------
1 | export const TAGS_REQUEST = 'TAGS_REQUEST';
2 | export const TAGS_REQUEST_SUCCESS = 'TAGS_REQUEST_SUCCESS';
3 | export const TAGS_REQUEST_FAILURE = 'TAGS_REQUEST_FAILURE';
4 |
5 | function tagsRequest() {
6 | return {
7 | type: TAGS_REQUEST,
8 | payload: {
9 | requestingTags: true,
10 | requestingTagsSuccess: false,
11 | },
12 | };
13 | }
14 |
15 | function tagsRequestSuccess(tags) {
16 | return {
17 | type: TAGS_REQUEST_SUCCESS,
18 | payload: {
19 | requestingTags: false,
20 | requestingTagsSuccess: true,
21 | tags,
22 | },
23 | };
24 | }
25 |
26 | function tagsRequestFailure(error) {
27 | return {
28 | type: TAGS_REQUEST_FAILURE,
29 | payload: {
30 | requestingTags: false,
31 | requestingTagsSuccess: false,
32 | error: new Error(error),
33 | },
34 | };
35 | }
36 |
37 | export function fetchTags() {
38 | return function tagsFetch(dispatch) {
39 | dispatch(tagsRequest());
40 |
41 | return fetch('', {
42 | method: 'GET',
43 | credentials: 'include',
44 | })
45 | .then(response => response.json())
46 | .then(response => {
47 | if (response.status === 'true') {
48 | return dispatch(tagsRequestSuccess(response));
49 | }
50 |
51 | return dispatch(tagsRequestFailure('Fetching tags failed'));
52 | });
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/src/state/entities/tags/reducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | TAGS_REQUEST,
3 | TAGS_REQUEST_SUCCESS,
4 | TAGS_REQUEST_FAILURE,
5 | } from './actions';
6 |
7 | const initialState = {
8 | tags: [],
9 | requestingTags: '',
10 | requestingTagsSuccess: '',
11 | };
12 |
13 | export const tags = (state = initialState, action) => {
14 | switch (action.type) {
15 | case TAGS_REQUEST:
16 | case TAGS_REQUEST_FAILURE:
17 | return {
18 | ...state,
19 | requestingTags: action.payload.requestingTags,
20 | requestingTagsSuccess: action.payload.requestingTagsSuccess,
21 | };
22 |
23 | case TAGS_REQUEST_SUCCESS:
24 | return {
25 | ...state,
26 | requestingTags: action.payload.requestingTags,
27 | requestingTagsSuccess: action.payload.requestingTagsSuccess,
28 | tags: [
29 | ...state.tags,
30 | action.payload.tags,
31 | ],
32 | };
33 |
34 | default:
35 | return state;
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/src/state/view/actions.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keyserfaty/react-redux-blog-seed/a90231a7b0c250def986ec6330968a0217098627/src/state/view/actions.js
--------------------------------------------------------------------------------
/src/state/view/reducers.js:
--------------------------------------------------------------------------------
1 | // export const view = (state = {}, action) => {};
2 |
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable */
2 |
3 | var webpack = require('webpack');
4 | var autoprefixer = require('autoprefixer');
5 |
6 | module.exports = {
7 | entry: [
8 | // 'webpack-dev-server/client?http://localhost:8080',
9 | // 'webpack/hot/only-dev-server',
10 | './src/index.jsx'
11 | ],
12 | module: {
13 | loaders: [{
14 | test: /\.js$|.jsx$/,
15 | exclude: /node_modules/,
16 | loader: 'react-hot!babel'
17 | }, {
18 | test: /\.css$/,
19 | loader: 'style!css!postcss'
20 | }, {
21 | test: /\.scss$/,
22 | loaders: ["style", "css", "sass"]
23 | }]
24 | },
25 | resolve: {
26 | extensions: ['', '.js', '.jsx']
27 | },
28 | output: {
29 | path: __dirname + '/dist',
30 | publicPath: '/',
31 | filename: 'bundle.js'
32 | },
33 | devServer: {
34 | contentBase: './dist',
35 | hot: true
36 | },
37 | plugins: [
38 | new webpack.HotModuleReplacementPlugin(),
39 | // new webpack.optimize.OccurenceOrderPlugin(true),
40 | // new webpack.optimize.DedupePlugin(),
41 | // new webpack.optimize.UglifyJsPlugin({
42 | // output: {
43 | // comments: false
44 | // },
45 | // compress: {
46 | // warnings: false,
47 | // screw_ie8: true
48 | // }
49 | // })
50 | ],
51 | postcss: function() {
52 | return [autoprefixer];
53 | }
54 | };
55 |
56 | /*eslint-enable */
57 |
--------------------------------------------------------------------------------