<file_sep>export const createAnimal = (name, color) => ({
  type: 'ANIMALS:CREATE',
  cat: {
    name,
    color,
  },
});

export const getCats = () => ({
  type: 'CATS:FETCH_ALL',
});

export const createCat = name => ({
  type: 'CAT:CREATE',
  name,
});

export const deleteCat = id => ({
  type: 'CAT:DELETE',
  id,
});

export const updateCat = (id, name) => ({
  type: 'CAT:UPDATE',
  id,
  name,
});
<file_sep># React Boilerplate using Webpack

I'm creating this as a part of me 'deep-dive' into Webpack.

I've always used CRA for my personal React projects even though I learned how to use Webpack early on. This repo is just a reference to my learning process so I have a base to look at for future projects. It's not exactly a boilerplate in the normal sense.

It definitely won't be on the level of CRA but I did feel the need to get more immersed with Webpack. It's pretty awesome and will definitely come in handy with non React projects.

# Dev/Workspace Setup

This project was created with eslint and prettier formatting on save hooks. If you would like to have the same eslint setup then add `.eslint.rc` to your project root directory and use these workspace settings in vscode.

```javascript
// .eslint.rc
{
  "extends": "fbjs"
}
```

```javascript
// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "javascript.format.enable": false,
  "prettier.eslintIntegration": true
}
```

You can find the list of packages I used [here](https://www.npmjs.com/package/eslint-config-fbjs).

```bash
yarn add --dev \
  eslint-config-fbjs \
  eslint-plugin-babel \
  eslint-plugin-flowtype \
  eslint-plugin-jsx-a11y \
  eslint-plugin-react \
  eslint-plugin-relay \
  eslint \
  babel-eslint
```
<file_sep>const defaultState = {
  cats: [],
};

export default (state = defaultState, action) => {
  switch (action.type) {
    case 'ANIMALS:CREATE':
      return {
        ...state,
        cats: [...state.cats, action.cat],
      };
    case 'ANIMALS:FETCH_ALL:SUCCEED':
      return {
        ...state,
        cats: action.cats,
      };
    case 'CAT:CREATE:SUCCEED':
      return {
        ...state,
        cats: [...state.cats, action.cat],
      };
    case 'CAT:CREATE:FAIL':
      return {
        ...state,
        error: action.error,
      };
    case 'CAT:DELETE:SUCCEED':
      return {
        ...state,
        cats: state.cats.filter(cat => cat.id !== action.cat),
      };
    case 'CAT:UPDATE:SUCCEED':
      return {
        ...state,
        cats: state.cats.map(cat => {
          if (cat.id === action.cat.id) {
            cat.name = action.cat.name;
            return cat;
          } else {
            return cat;
          }
        }),
      };
    default:
      return state;
  }
};
<file_sep>import React from 'react'
import { Link } from 'react-router-dom'
export default () => {
  return (
    <div>
      <h3>Sorry! That page couldn't be found!</h3>
      <p>Click <Link to="/">here</Link> to return home</p>
    </div>
  )
}
<file_sep>import React from 'react'
import styled from 'styled-components'
import { Wrapper, TitlePanel, Title, SubTitle, Body } from '../welcome/Welcome'

// LOCAL STYLES
const Code = styled.span`
  background: #e8e8e8;
  padding: 2px 4px;
  color: darkred;
`


// COMPONENT
export default () => {
  return (
    <Wrapper>
      <TitlePanel>
        <Title>Code Snippets</Title>
        <SubTitle>A collection of useful code</SubTitle>
      </TitlePanel>

      <Body>This will contain snippets like npm install commands or config lines so I can reference small deatils</Body>
      <Body>An example can be setting up <Code>webpack.config</Code> for sass</Body>
    </Wrapper>
  )
}
<file_sep>import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../header/Header';
import Welcome from '../welcome/Welcome';
import Snippets from '../snippets/Snippets';
import Cat from '../testReducer/AnimalContainer';
import NotFound from '../404/NotFound';

export default () => {
  return (
    <BrowserRouter>
      <div>
        <Header />
        <Switch>
          <Route exact path="/add-cat" component={Cat} />
          <Route exact path="/snippets" component={Snippets} />
          <Route exact path="/" component={Welcome} />
          <Route component={NotFound} />
        </Switch>
      </div>
    </BrowserRouter>
  );
};
<file_sep>import React, { Component } from 'react'
import styled from 'styled-components'

// STYLES
export const Wrapper = styled.div`
  margin: 10px;
  padding: 15px;
`
export const TitlePanel = styled.div`
  display: inline-block;
  border-bottom: 2px solid darkgreen;
  width: 50vw;
  margin-bottom: 15px;
`
export const Title = styled.h1`
  font-weight: 400;
  margin-bottom: 15px;
`
export const SubTitle = styled.h3`
  margin-bottom: 15px;
`
export const Body = styled.p`
  margin-bottom: 15px;
`
export const List = styled.ul`
  list-style: none;
  margin-left: 15px;
  margin-bottom: 25px;
`
export const ListItem = styled.li`
  &:before {
    content: '+ ';
    font-weight: 900;
    color: darkgreen;
    margin-right: 5px;
  }
  margin-bottom: 5px;
`

// COMPONENT
class App extends Component {
  render() {
    return (
      <Wrapper>
        <TitlePanel>
          <Title>React Boilerplate</Title>
          <SubTitle>Using Webpack</SubTitle>
        </TitlePanel>
        <Body>Including:</Body>    
        <List>
          <ListItem>Sass loaders</ListItem>
          <ListItem>Dev Server</ListItem>
          <ListItem>Clean Webpack Plugin</ListItem>
          <ListItem>HTML Webpack Plugin</ListItem>
          <ListItem>Styled Components</ListItem>
          <ListItem>Babel Class Transform Properties</ListItem>
        </List>
        <Body>To Do:</Body>
        <List>
          <ListItem>Redux</ListItem>
          <ListItem>React-Router</ListItem>
        </List>
      </Wrapper>
    )
  }
}

export default App<file_sep>import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';

// STYLES
const Wrapper = styled.nav`
  display: flex;
  margin: 10px;
  padding: 15px;
`;
const Button = styled(Link)`
  text-decoration: none;
  background: #383838;
  color: #f8f8f8;
  padding: 5px 10px;
  border-radius: 5px;

  &:hover {
    cursor: pointer;
    color: paleturquoise;
  }

  margin-right: 5px;
`;

export default () => {
  return (
    <Wrapper>
      <Button to="/">Welcome</Button>
      <Button to="/snippets">Snippets</Button>
      <Button to="/add-cat">Add Cat</Button>
    </Wrapper>
  );
};
<file_sep>import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
  getCats,
  createCat,
  deleteCat,
  updateCat,
} from '../../actions/animals';
import Cat from './Cat';
import ModalForm from '../reusable/ModalForm';

export class AnimalContainer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      newName: '',
      id: '',
      cats: [],
      modalIsOpen: false,
    };
  }
  // Cat
  handleSubmit = () => {
    const { name } = this.state;

    if (name !== '') {
      this.props.createCat(name);
      this.setState(() => ({ name: '' }));
    }
  };
  handleDelete = id => {
    this.props.deleteCat(id);
  };
  handleOnChange = e => {
    const value = e.target.value;
    const name = e.target.name;
    this.setState({ [name]: value });
  };
  handleUpdate = () => {
    const { newName, id } = this.state;
    this.props.updateCat(id, newName);
    this.setState({ newName: '', name: '', id: '', modalIsOpen: false });
  };
  // Modal
  openModal = (name, id) => {
    this.setState({ modalIsOpen: true, name, id });
  };
  closeModal = () => {
    this.setState({ modalIsOpen: false, name: '', id: '' });
  };
  afterMOdalOpen = () => {
    console.log('modal');
  };
  // lifecyle
  componentDidMount() {
    this.props.getCats();
  }
  componentWillReceiveProps(nextProps) {
    const cats = nextProps.cats || [];
    if (cats.length !== 0) {
      this.setState(() => ({ cats }));
    }
  }
  render() {
    return (
      <div>
        <Cat
          handleSubmit={this.handleSubmit}
          handleDelete={this.handleDelete}
          handleOnChange={this.handleOnChange}
          name={this.state.name}
          cats={this.state.cats}
          openModal={this.openModal}
        />
        <ModalForm
          isOpen={this.state.modalIsOpen}
          reqClose={this.closeModal}
          title="Edit Cat"
          subtitle="Please complete the following fields"
          afterOpen={this.afterMOdalOpen}
          catName={this.state.name}
          handleOnChange={this.handleOnChange}
          newName={this.state.newName}
          handleUpdate={this.handleUpdate}
        />
      </div>
    );
  }
}

const mapStateToProps = state => ({
  cats: state.animals.cats,
});

const mapDispatchToProps = dispatch => ({
  getCats: () => dispatch(getCats()),
  createCat: name => dispatch(createCat(name)),
  deleteCat: id => dispatch(deleteCat(id)),
  updateCat: (id, name) => dispatch(updateCat(id, name)),
});

export default connect(mapStateToProps, mapDispatchToProps)(AnimalContainer);
<file_sep>import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import animals from './reducers/animals';
import saga from './sagas/rootSaga';

// create saga middleware
const sagaMiddleware = createSagaMiddleware();

// dev tools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

export default () => {
  const store = createStore(
    combineReducers({
      animals,
    }),
    composeEnhancers(applyMiddleware(sagaMiddleware))
  );
  // run saga listener
  sagaMiddleware.run(saga);
  return store;
};

/*
  to add multiple middleware ledgibly
  use array
  
  const middleware = [sagaMiddleware, thunk]

  composeEnhancers(applyMiddleware(...middleware))
*/
<file_sep>import React from 'react';
import styled from 'styled-components';
import { Wrapper, TitlePanel, Title, SubTitle, Body } from '../welcome/Welcome';

const FieldWrapper = styled.div`
  margin-bottom: 15px;
`;
const ActionWrapper = styled.div`
  margin-bottom: 35px;
`;
const Input = styled.input`
  display: inline-block;
  border: none;
  border-bottom: 2px solid darkgreen;
  background: #e8e8e8;
  color: #383838;
  padding: 5px 10px;
`;

const ListButtons = styled.div`
  display: flex;
  justify-content: flex-end;
`;

const SubmitButton = styled.button`
  display: inline-block;
  padding: 5px 10px;
  border: none;
  border-radius: 5px;
  background: #383838;
  color: #f8f8f8;

  &:hover {
    cursor: pointer;
  }
`;

const DeleteButton = SubmitButton.extend`
  background: #b71c1c;
  margin-right: 5px;
`;

const EditButton = SubmitButton.extend`
  background: #ffc107;
  color: #383838;
  margin-right: 5px;
`;

const List = styled.ul`
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  flex-direction: column;
`;
const ListItem = styled.li`
  margin: 10px;
  padding: 25px;
  background: #e8e8e8;
  display: inline-block;
`;
const ListDetails = styled.div`
  margin-bottom: 15px;
`;

export default ({
  handleOnChange,
  handleDelete,
  handleSubmit,
  name,
  cats,
  openModal,
}) => {
  return (
    <Wrapper>
      <TitlePanel>
        <Title>Test Animals Reducer</Title>
        <SubTitle>Add Cat</SubTitle>
      </TitlePanel>
      <Body>Complete the following fields to add a cat to redux state</Body>
      <FieldWrapper>
        <Input
          type="text"
          placeholder="Cat Name"
          name="name"
          value={name}
          onChange={handleOnChange}
        />
      </FieldWrapper>
      <ActionWrapper>
        <SubmitButton onClick={handleSubmit}>Create Cat</SubmitButton>
      </ActionWrapper>

      <TitlePanel>
        <Title>All Cats</Title>
        <SubTitle>A list of all cats</SubTitle>
      </TitlePanel>

      <List>
        {cats &&
          cats.map(cat => (
            <ListItem key={cat.id}>
              <ListDetails>
                <SubTitle>{cat.name}</SubTitle>
                <p>Created: {cat.created_at}</p>
              </ListDetails>
              <ListButtons>
                <DeleteButton onClick={() => handleDelete(cat.id)}>
                  Delete
                </DeleteButton>
                <EditButton onClick={() => openModal(cat.name, cat.id)}>
                  Edit
                </EditButton>
              </ListButtons>
            </ListItem>
          ))}
      </List>
    </Wrapper>
  );
};
<file_sep>import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import './index.sass';

// attach <App /> to anchor point in HTML file
const wrapper = document.getElementById('container');
wrapper && ReactDOM.render(<App />, wrapper);
<file_sep>import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import axios from 'axios';

const API_GET = 'http://localhost:3001/cats';
const API_POST = 'http://localhost:3001/cats';
const API_DELETE = 'http://localhost:3001/cats';
const API_PATCH = 'http://localhost:3001/cats';

// getall saga : fired on CATS:FETCH_ALL
function* fetchCats(action) {
  try {
    // api call
    const cats = yield call(axios.get, API_GET);
    // if succeed
    yield put({ type: 'ANIMALS:FETCH_ALL:SUCCEED', cats: cats.data });
  } catch (e) {
    // if fail
    yield put({ type: 'ANIMALS:FETCH_ALL:FAIL' });
  }
}

// create cat saga : fired on CATS:CREATE
function* createCat(action) {
  try {
    // api call
    const cat = yield call(axios.post, API_POST, { name: action.name });
    // if succeed
    yield put({ type: 'CAT:CREATE:SUCCEED', cat: cat.data });
  } catch (e) {
    yield put({ type: 'CAT:CREATE:FAIL', error: e });
  }
}

function* deleteCat(action) {
  try {
    // api call
    const cat = yield call(axios.delete, `${API_DELETE}/${action.id}`);
    // if success
    yield put({ type: 'CAT:DELETE:SUCCEED', cat: cat.data });
  } catch (e) {
    yield put({ type: 'CAT:DELETE:FAIL', error: e });
  }
}

function* updateCat(action) {
  try {
    // api call
    const cat = yield call(axios.patch, `${API_PATCH}/${action.id}`, {
      name: action.name,
    });
    // if success
    yield put({ type: 'CAT:UPDATE:SUCCEED', cat: cat.data });
  } catch (e) {
    yield put({ type: 'CAT:UPDATE:FAIL', error: e });
  }
}

/*
  Starts fetchCats on each dispatched 'CATS:FETCH_ALL' action
  Allows concurrent fetches of user
*/

export function* startFetchCats() {
  yield takeEvery('CATS:FETCH_ALL', fetchCats);
}

export function* startCreateCat() {
  yield takeEvery('CAT:CREATE', createCat);
}

export function* startDeleteCat() {
  yield takeLatest('CAT:DELETE', deleteCat);
}

export function* startUpdateCat() {
  yield takeLatest('CAT:UPDATE', updateCat);
}

/*
  Alternatively you can use takeLatest
  doesn't allow concurrent fetchs, if CATS:FETCH_ALL gets dispatched
  whiles a getch is already pending then the pending fetch is cancelled
  and only the lastest will be ran
*/

export function* mySagaLatest() {
  yield takeLatest('CATS:FETCH_ALL', fetchCats);
}
