Skip to content

Redux workshop solution 2 #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -9,12 +9,17 @@
"bootstrap": "^4.0.0",
"classnames": "^2.2.5",
"express": "^4.16.2",
"lodash": "4.17.10",
"mongoose": "^4.13.6",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-scripts": "1.0.17",
"reactstrap": "^5.0.0-beta.3",
"redux": "^4.0.0",
"redux-pack": "0.1.5",
"redux-thunk": "^2.2.0",
"uuid": "^3.1.0"
},
"devDependencies": {
34 changes: 34 additions & 0 deletions src/client/components/AddToCart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { Button } from "reactstrap";
import * as actionCreators from "../redux/actions";

class AddToCart extends React.Component {
render() {
const {
actions: { addItemsToCart },
cartItems: { isLoading },
product
} = this.props;
return (
<Button
className="mt-4"
color="primary"
onClick={() => addItemsToCart(product)}
>
{isLoading ? "Adding..." : "Add to cart"}
</Button>
);
}
}

const mapStateToProps = state => ({
cartItems: state.cartItems
});

const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(actionCreators, dispatch)
});

export default connect(mapStateToProps, mapDispatchToProps)(AddToCart);
27 changes: 27 additions & 0 deletions src/client/components/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import Home from "./Home";
import Products from "./Products";
import Product from "./Product";
import Cart from "./Cart";
import Header from "./Header";
import { Container } from "reactstrap";

export default class App extends React.Component {
render() {
return (
<div>
<Header location={this.props.location} />
<Container className="mt-5">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/products" component={Products} />
<Route exact path="/products/:id(\d+)" component={Product} />
<Route exact path="/products/:brand(\w+)" component={Products} />
<Route path="/cart" component={Cart} />
</Switch>
</Container>
</div>
);
}
}
39 changes: 39 additions & 0 deletions src/client/components/Cart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import { ListGroup, ListGroupItem, Alert } from "reactstrap";
import DeleteCartItem from "./DeleteCartItem";
import { connect } from "react-redux";

function CartItem({ cartItem }) {
return (
<ListGroupItem>
<div className="row">
<div className="col-auto mr-auto">{cartItem.product.name}</div>
<div className="col-auto">
<DeleteCartItem id={cartItem.id} />
</div>
</div>
</ListGroupItem>
);
}

export class Cart extends React.Component {
render() {
const { cartItems } = this.props;
if (!cartItems.ids.length) {
return <Alert color="primary">Cart is empty</Alert>;
}
return (
<ListGroup>
{Object.values(cartItems.byId).map(cartItem => (
<CartItem key={cartItem.id} cartItem={cartItem} />
))}
</ListGroup>
);
}
}

const mapStateToProps = state => ({
cartItems: state.cartItems
});

export default connect(mapStateToProps)(Cart);
32 changes: 32 additions & 0 deletions src/client/components/CartBadge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actions from "../redux/actions";
import { Badge } from "reactstrap";

class CartBadge extends React.Component {
componentDidMount() {
const { actions } = this.props;
actions.getCartItems();
}

render() {
const { cartItems } = this.props;
if (!cartItems.ids.length) {
return null;
}
return cartItems.ids.length ? (
<Badge color="dark">{cartItems.ids.length}</Badge>
) : null;
}
}

const mapStateToProps = state => ({
cartItems: state.cartItems
});

const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(actions, dispatch)
});

export default connect(mapStateToProps, mapDispatchToProps)(CartBadge);
7 changes: 7 additions & 0 deletions src/client/components/DeleteCartItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

export default class DeleteCartItem extends React.Component {
render() {
return <img src="/img/trash-o.svg" alt="delete" />;
}
}
60 changes: 60 additions & 0 deletions src/client/components/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from "react";
import { Navbar, NavbarBrand, Nav, NavItem } from "reactstrap";
import { Link } from "react-router-dom";
import classnames from "classnames";
import CartBadge from "./CartBadge";

export default class Header extends React.Component {
render() {
const { location: { pathname } } = this.props;
return (
<div>
<Navbar color="light" light expand="md">
<NavbarBrand href="/">Apollo Store</NavbarBrand>
<Nav className="ml-auto" navbar>
<NavItem>
<Link
className={classnames("nav-link", {
active: pathname === "/products"
})}
to="/products"
>
Products
</Link>
</NavItem>
<NavItem>
<Link
className={classnames("nav-link", {
active: pathname === "/products/samsung"
})}
to="/products/samsung"
>
Samsung
</Link>
</NavItem>
<NavItem>
<Link
className={classnames("nav-link", {
active: pathname === "/products/apple"
})}
to="/products/apple"
>
Apple
</Link>
</NavItem>
<NavItem>
<Link
className={classnames("nav-link", {
active: pathname === "/cart"
})}
to="/cart"
>
Cart <CartBadge />
</Link>
</NavItem>
</Nav>
</Navbar>
</div>
);
}
}
7 changes: 7 additions & 0 deletions src/client/components/Home.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

export default class Home extends React.Component {
render() {
return <div>Welcome to apollo store</div>;
}
}
5 changes: 5 additions & 0 deletions src/client/components/Price.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from "react";

export default function Price({ value }) {
return <span>&#8377; {value}</span>;
}
51 changes: 51 additions & 0 deletions src/client/components/Product.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { Media } from "reactstrap";
import * as actionCreators from "../redux/actions";
import AddToCart from "./AddToCart";
import Price from "./Price";

class Product extends React.Component {
componentDidMount() {
const { product, actions, match } = this.props;
if (!product) {
const productId = parseInt(match.params.id, 10);
actions.getProducts(productId);
}
}

render() {
const { products, match } = this.props;
const productId = parseInt(match.params.id, 10);
const product = products.ids.length && products.byId[productId];
if (!product) {
return null;
}
return (
<Media>
<Media left>
<img src={product.url} alt="product" />
</Media>
<Media body>
<Media heading>{product.name}</Media>
<div>{product.description}</div>
<div>
Price: <Price value={product.price} />
</div>
<AddToCart product={product} />
</Media>
</Media>
);
}
}

const mapStateToProps = (state, props) => ({
products: state.products
});

const mapDisptachToProps = dispatch => ({
actions: bindActionCreators(actionCreators, dispatch)
});

export default connect(mapStateToProps, mapDisptachToProps)(Product);
7 changes: 7 additions & 0 deletions src/client/components/ProductSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

export default class ProductSelect extends React.Component {
render() {
return <input type="checkbox" className="mr-4" />;
}
}
71 changes: 71 additions & 0 deletions src/client/components/Products.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { ListGroup, ListGroupItem } from "reactstrap";
import * as productActionCreators from "../redux/actions";
import Price from "./Price";
import ProductSelect from "./ProductSelect";

function Product({ product }) {
return (
<ListGroupItem>
<div className="row">
<div className="col-auto mr-auto">
<ProductSelect product={product} />
<Link to={`/products/${product.id}`}>{product.name}</Link>
</div>
<div className="col-auto">
<Price value={product.price} />
</div>
</div>
</ListGroupItem>
);
}

class Products extends React.Component {
componentDidMount() {
const { productActions, match } = this.props;
const brand = match.params.brand;
productActions.getProducts(brand);
}

componentWillReceiveProps(nextProps) {
const { match, productActions } = this.props;
if (nextProps.match.params.brand !== match.params.brand) {
productActions.getProducts(nextProps.match.params.brand);
}
}

render() {
const { products, match } = this.props;
// const brand = match.params.brand;
// const filteredProducts =
// brand ?
// Object.values(products.byId).filter(product => {
// console.log(product.brand);
// return product.brand.toLowerCase() === brand.toLowerCase();
// })
// : Object.values(products.byId);
return (
<div>
Products
<ListGroup>
{Object.values(products.byId).map(product => (
<Product key={product.id} product={product} />
))}
</ListGroup>
</div>
);
}
}

const mapStateToProps = state => ({
products: state.products
});

const mapDispatchToProps = dispatch => ({
productActions: bindActionCreators(productActionCreators, dispatch)
});

export default connect(mapStateToProps, mapDispatchToProps)(Products);
12 changes: 12 additions & 0 deletions src/client/redux/actionTypes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const GET_PRODUCTS = "GET_PRODUCTS";
export const GET_CART_ITEMS = "GET_CART_ITEMS";
export const ADD_ITEMS_TO_CART = "ADD_ITEMS_TO_CART";
export const GET_PRODUCTS_REQUEST = "GET_PRODUCTS_REQUEST";
export const GET_PRODUCTS_SUCCESS = "GET_PRODUCTS_SUCCESS";
export const GET_PRODUCTS_FAILURE = "GET_PRODUCTS_SUCCESS";
export const GET_CART_ITEMS_REQUEST = "GET_CART_ITEMS_REQUEST";
export const GET_CART_ITEMS_SUCCESS = "GET_CART_ITEMS_SUCCESS";
export const GET_CART_ITEMS_FAILURE = "GET_CART_ITEMS_FAILURE";
export const ADD_ITEMS_TO_CART_REQUEST = "ADD_ITEMS_TO_CART_REQUEST";
export const ADD_ITEMS_TO_CART_SUCCESS = "ADD_ITEMS_TO_CART_SUCCESS";
export const ADD_ITEMS_TO_CART_FAILURE = "ADD_ITEMS_TO_CART_FAILURE";
61 changes: 61 additions & 0 deletions src/client/redux/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as actionTypes from "../actionTypes";
import { transformProductsApi } from "../transformers/transformProductsApi";
import { transformGetCartItemsApi } from "../transformers/transformGetCartItemsApi";

export const getProducts = productId => {
return dispatch => {
const apiUrl = productId ? `/api/products/${productId}` : "/api/products";
return dispatch({
type: actionTypes.GET_PRODUCTS,
promise: fetch(apiUrl).then(async response => {
const responseData = await response.json();
if (response.ok) {
return transformProductsApi(responseData);
} else {
Promise.reject("Something went wrong");
}
})
});
};
};

export const getCartItems = () => {
return dispatch => {
return dispatch({
type: actionTypes.GET_CART_ITEMS,
promise: fetch("/api/cart-items").then(async response => {
const responseData = await response.json();
if (response.ok) {
return transformGetCartItemsApi(responseData);
} else {
Promise.reject("Something went wrong");
}
})
});
};
};

export const addItemsToCart = product => {
const data = {
productId: product.id
};
return dispatch => {
return dispatch({
type: actionTypes.ADD_ITEMS_TO_CART,
promise: fetch("/api/cart-items", {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json"
}
}).then(async response => {
const responseData = await response.json();
if (response.ok) {
return responseData;
} else {
return Promise.reject("Something went wrong");
}
})
});
};
};
53 changes: 53 additions & 0 deletions src/client/redux/reducers/cartItems.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as actionTypes from "../actionTypes";
import { handle } from "redux-pack";

const initialState = {
byId: {},
ids: [],
isLoading: false,
isError: false,
errorMsg: ""
};

export default function cartItemsReducer(state = initialState, action) {
switch (action.type) {
case actionTypes.GET_CART_ITEMS:
return handle(state, action, {
start: s => ({ ...s, isLoading: true }),
success: s => ({
...s,
...action.payload,
isLoading: false
}),
failure: s => ({
...s,
isLoading: false,
isError: true,
errorMsg: action.payload
})
});

case actionTypes.ADD_ITEMS_TO_CART:
return handle(state, action, {
start: s => ({ ...s, isLoading: true }),
success: s => ({
...s,
byId: {
...state.byId,
[action.payload.id]: action.payload
},
ids: [...state.ids, action.payload.id],
isLoading: false
}),
failure: s => ({
...s,
isLoading: false,
isError: true,
errorMsg: action.payload
})
});

default:
return state;
}
}
8 changes: 8 additions & 0 deletions src/client/redux/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { combineReducers } from "redux";
import products from "./products";
import cartItems from "./cartItems";

export default combineReducers({
products,
cartItems
});
40 changes: 40 additions & 0 deletions src/client/redux/reducers/products.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as actionTypes from "../actionTypes";
import { handle } from "redux-pack";
import merge from "lodash/merge";

const initialState = {
byId: {},
ids: [],
isLoading: false,
isError: false,
errorMsg: ""
};

export default function productsReducer(state = initialState, action) {
switch (action.type) {
case actionTypes.GET_PRODUCTS:
return handle(state, action, {
start: s => ({
...s,
byId: {},
ids: [],
isLoading: true
}),
success: s => ({
...s,
byId: merge({}, s.byId, action.payload.byId),
ids: [...s.ids, action.payload.ids],
isLoading: false
}),
failure: s => ({
...s,
isLoading: false,
isError: true,
errorMsg: action.payload
})
});

default:
return state;
}
}
10 changes: 10 additions & 0 deletions src/client/redux/transformers/transformGetCartItemsApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const transformGetCartItemsApi = data => ({
byId: data.reduce(
(obj, cartItem) => ({
...obj,
[cartItem.id]: cartItem
}),
{}
),
ids: data.map(cartItem => cartItem.id)
});
10 changes: 10 additions & 0 deletions src/client/redux/transformers/transformProductsApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const transformProductsApi = data => ({
byId: data.reduce(
(obj, product) => ({
...obj,
[product.id]: product
}),
{}
),
ids: data.map(product => product.id)
});
14 changes: 14 additions & 0 deletions src/client/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { middleware as reduxPack } from "redux-pack";
import rootReducer from "./redux/reducers";

export default function configureStore() {
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk, reduxPack))
);
return store;
}
16 changes: 16 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./client/components/App";
import "bootstrap/dist/css/bootstrap.min.css";
import { BrowserRouter, Route } from "react-router-dom";
import { Provider } from "react-redux";
import configureStore from "./client/store";

ReactDOM.render(
<Provider store={configureStore()}>
<BrowserRouter>
<Route path="/" component={App} />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
38 changes: 32 additions & 6 deletions src/server/connectors/index.js
Original file line number Diff line number Diff line change
@@ -2,13 +2,23 @@ let products = [
{
id: 1,
name: "Macbook",
brand: "Apple",
description: "Latest Macbook with 16GB ram and Quad core processor",
price: 65000,
url: "/img/macbook.jpeg"
},
{
id: 2,
name: "Keyboard",
brand: "Apple",
description: "Ergonomic keyboard",
price: 30000,
url: "/img/keyboard.jpeg"
},
{
id: 3,
name: "Keyboard",
brand: "Samsung",
description: "Ergonomic keyboard",
price: 3000,
url: "/img/keyboard.jpeg"
@@ -29,8 +39,20 @@ export function getProducts() {
return products;
}

export function getProduct(id) {
return products.find(product => product.id === id);
export function getProductById(id) {
return [products.find(product => product.id === id)];
}

export function getProductByBrand(brand) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(
products.filter(
product => product.brand.toLowerCase() === brand.toLowerCase()
)
);
}, 5000);
});
}

export function getCartItem(id) {
@@ -41,16 +63,20 @@ export function getCartItems() {
return cartItems;
}

export function addToCart(args) {
if (cartItems.find(c => c.productId === parseInt(args.productId, 10))) {
export function addToCart({ productId }) {
if (cartItems.find(c => c.productId === parseInt(productId, 10))) {
throw new Error("Product already in cart");
}
const newCartItem = {
id: cartItems.length + 1,
productId: parseInt(args.productId, 10)
product: products.find(p => p.id === productId)
};
cartItems.push(newCartItem);
return newCartItem;
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(newCartItem);
}, 5000);
});
}

export function deleteCartItem(args) {
21 changes: 13 additions & 8 deletions src/server/index.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@ import morgan from "morgan";
import {
getUser,
getProducts,
getProduct,
getProductById,
getProductByBrand,
getCartItems,
getCartItem,
addToCart,
@@ -13,7 +14,7 @@ import {
const PORT = 8000;

const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.use(
@@ -31,9 +32,9 @@ app.use(
})
);

app.use(function(req, res, next) {
setTimeout(next, 500);
});
// app.use(function(req, res, next) {
// setTimeout(next, 500);
// });

app.get("/api/user", function(req, res) {
res.json(getUser());
@@ -43,9 +44,13 @@ app.get("/api/products", function(req, res) {
res.json(getProducts());
});

app.get("/api/products/:id", function(req, res) {
app.get("/api/products/:id(\\d+)/", function(req, res) {
const id = parseInt(req.params.id, 10);
res.json(getProduct(id));
res.json(getProductById(id));
});

app.get("/api/products/:brand(\\w+)/", function(req, res) {
getProductByBrand(req.params.brand).then(response => res.json(response));
});

app.get("/api/cart-items", function(req, res) {
@@ -58,7 +63,7 @@ app.get("/api/cart-items/:id", function(req, res) {
});

app.post("/api/cart-items", function(req, res) {
res.json(addToCart(req.body));
addToCart(req.body).then(response => res.json(response));
});

app.post("/api/cart-items/:id", function(req, res) {
52 changes: 49 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -2238,6 +2238,10 @@ delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"

deline@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/deline/-/deline-1.0.4.tgz#6c05c87836926e1a1c63e47882f3d2eb2c6f14c9"

depd@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
@@ -3543,7 +3547,7 @@ hoek@4.x.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"

hoist-non-react-statics@^2.3.0:
hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"

@@ -3800,7 +3804,7 @@ interpret@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"

invariant@^2.2.1, invariant@^2.2.2:
invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
@@ -4908,6 +4912,10 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"

lodash-es@^4.17.5:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"

lodash._reinterpolate@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -4965,6 +4973,10 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"

lodash@4.17.10:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"

"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
@@ -6480,6 +6492,17 @@ react-portal@^4.1.2:
dependencies:
prop-types "^15.5.8"

react-redux@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
dependencies:
hoist-non-react-statics "^2.5.0"
invariant "^2.0.0"
lodash "^4.17.5"
lodash-es "^4.17.5"
loose-envify "^1.1.0"
prop-types "^15.6.0"

react-router-dom@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
@@ -6685,6 +6708,25 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"

redux-pack@0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/redux-pack/-/redux-pack-0.1.5.tgz#1973b26ef749dfc020e8a61d54cf3e55ec8ae4cd"
dependencies:
deline "^1.0.4"
invariant "^2.2.2"
uuid "^3.0.1"

redux-thunk@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"

redux@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
dependencies:
loose-envify "^1.1.0"
symbol-observable "^1.2.0"

regenerate@^1.2.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
@@ -7604,6 +7646,10 @@ symbol-observable@^0.2.2:
version "0.2.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"

symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"

symbol-tree@^3.2.1, symbol-tree@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
@@ -7989,7 +8035,7 @@ uuid@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"

uuid@^3.0.0, uuid@^3.1.0:
uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"