diff --git a/package.json b/package.json index 3c33187..fc61684 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/client/components/AddToCart.js b/src/client/components/AddToCart.js new file mode 100644 index 0000000..f26366c --- /dev/null +++ b/src/client/components/AddToCart.js @@ -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 ( + + ); + } +} + +const mapStateToProps = state => ({ + cartItems: state.cartItems +}); + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators(actionCreators, dispatch) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(AddToCart); diff --git a/src/client/components/App.js b/src/client/components/App.js new file mode 100644 index 0000000..6fb283b --- /dev/null +++ b/src/client/components/App.js @@ -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 ( +
+
+ + + + + + + + + +
+ ); + } +} diff --git a/src/client/components/Cart.js b/src/client/components/Cart.js new file mode 100644 index 0000000..82fe003 --- /dev/null +++ b/src/client/components/Cart.js @@ -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 ( + +
+
{cartItem.product.name}
+
+ +
+
+
+ ); +} + +export class Cart extends React.Component { + render() { + const { cartItems } = this.props; + if (!cartItems.ids.length) { + return Cart is empty; + } + return ( + + {Object.values(cartItems.byId).map(cartItem => ( + + ))} + + ); + } +} + +const mapStateToProps = state => ({ + cartItems: state.cartItems +}); + +export default connect(mapStateToProps)(Cart); diff --git a/src/client/components/CartBadge.js b/src/client/components/CartBadge.js new file mode 100644 index 0000000..3784d1e --- /dev/null +++ b/src/client/components/CartBadge.js @@ -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 ? ( + {cartItems.ids.length} + ) : null; + } +} + +const mapStateToProps = state => ({ + cartItems: state.cartItems +}); + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators(actions, dispatch) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(CartBadge); diff --git a/src/client/components/DeleteCartItem.js b/src/client/components/DeleteCartItem.js new file mode 100644 index 0000000..eaf110a --- /dev/null +++ b/src/client/components/DeleteCartItem.js @@ -0,0 +1,7 @@ +import React from "react"; + +export default class DeleteCartItem extends React.Component { + render() { + return delete; + } +} diff --git a/src/client/components/Header.js b/src/client/components/Header.js new file mode 100644 index 0000000..bb1fc5b --- /dev/null +++ b/src/client/components/Header.js @@ -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 ( +
+ + Apollo Store + + +
+ ); + } +} diff --git a/src/client/components/Home.js b/src/client/components/Home.js new file mode 100644 index 0000000..b4af15f --- /dev/null +++ b/src/client/components/Home.js @@ -0,0 +1,7 @@ +import React from "react"; + +export default class Home extends React.Component { + render() { + return
Welcome to apollo store
; + } +} diff --git a/src/client/components/Price.js b/src/client/components/Price.js new file mode 100644 index 0000000..51afe37 --- /dev/null +++ b/src/client/components/Price.js @@ -0,0 +1,5 @@ +import React from "react"; + +export default function Price({ value }) { + return ₹ {value}; +} diff --git a/src/client/components/Product.js b/src/client/components/Product.js new file mode 100644 index 0000000..4166004 --- /dev/null +++ b/src/client/components/Product.js @@ -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 ( + + + product + + + {product.name} +
{product.description}
+
+ Price: +
+ +
+
+ ); + } +} + +const mapStateToProps = (state, props) => ({ + products: state.products +}); + +const mapDisptachToProps = dispatch => ({ + actions: bindActionCreators(actionCreators, dispatch) +}); + +export default connect(mapStateToProps, mapDisptachToProps)(Product); diff --git a/src/client/components/ProductSelect.js b/src/client/components/ProductSelect.js new file mode 100644 index 0000000..dbf9b72 --- /dev/null +++ b/src/client/components/ProductSelect.js @@ -0,0 +1,7 @@ +import React from "react"; + +export default class ProductSelect extends React.Component { + render() { + return ; + } +} diff --git a/src/client/components/Products.js b/src/client/components/Products.js new file mode 100644 index 0000000..de57f24 --- /dev/null +++ b/src/client/components/Products.js @@ -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 ( + +
+
+ + {product.name} +
+
+ +
+
+
+ ); +} + +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 ( +
+ Products + + {Object.values(products.byId).map(product => ( + + ))} + +
+ ); + } +} + +const mapStateToProps = state => ({ + products: state.products +}); + +const mapDispatchToProps = dispatch => ({ + productActions: bindActionCreators(productActionCreators, dispatch) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Products); diff --git a/src/client/redux/actionTypes/index.js b/src/client/redux/actionTypes/index.js new file mode 100644 index 0000000..ce5bcf9 --- /dev/null +++ b/src/client/redux/actionTypes/index.js @@ -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"; diff --git a/src/client/redux/actions/index.js b/src/client/redux/actions/index.js new file mode 100644 index 0000000..e6e82b8 --- /dev/null +++ b/src/client/redux/actions/index.js @@ -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"); + } + }) + }); + }; +}; diff --git a/src/client/redux/reducers/cartItems.js b/src/client/redux/reducers/cartItems.js new file mode 100644 index 0000000..0d111ab --- /dev/null +++ b/src/client/redux/reducers/cartItems.js @@ -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; + } +} diff --git a/src/client/redux/reducers/index.js b/src/client/redux/reducers/index.js new file mode 100644 index 0000000..2b6075b --- /dev/null +++ b/src/client/redux/reducers/index.js @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import products from "./products"; +import cartItems from "./cartItems"; + +export default combineReducers({ + products, + cartItems +}); diff --git a/src/client/redux/reducers/products.js b/src/client/redux/reducers/products.js new file mode 100644 index 0000000..599b945 --- /dev/null +++ b/src/client/redux/reducers/products.js @@ -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; + } +} diff --git a/src/client/redux/transformers/transformGetCartItemsApi.js b/src/client/redux/transformers/transformGetCartItemsApi.js new file mode 100644 index 0000000..03647f6 --- /dev/null +++ b/src/client/redux/transformers/transformGetCartItemsApi.js @@ -0,0 +1,10 @@ +export const transformGetCartItemsApi = data => ({ + byId: data.reduce( + (obj, cartItem) => ({ + ...obj, + [cartItem.id]: cartItem + }), + {} + ), + ids: data.map(cartItem => cartItem.id) +}); diff --git a/src/client/redux/transformers/transformProductsApi.js b/src/client/redux/transformers/transformProductsApi.js new file mode 100644 index 0000000..0e3980e --- /dev/null +++ b/src/client/redux/transformers/transformProductsApi.js @@ -0,0 +1,10 @@ +export const transformProductsApi = data => ({ + byId: data.reduce( + (obj, product) => ({ + ...obj, + [product.id]: product + }), + {} + ), + ids: data.map(product => product.id) +}); diff --git a/src/client/store.js b/src/client/store.js new file mode 100644 index 0000000..08e9bfd --- /dev/null +++ b/src/client/store.js @@ -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; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ec1f0e8 --- /dev/null +++ b/src/index.js @@ -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( + + + + + , + document.getElementById("root") +); diff --git a/src/server/connectors/index.js b/src/server/connectors/index.js index c9a4fce..63195c3 100644 --- a/src/server/connectors/index.js +++ b/src/server/connectors/index.js @@ -2,6 +2,7 @@ let products = [ { id: 1, name: "Macbook", + brand: "Apple", description: "Latest Macbook with 16GB ram and Quad core processor", price: 65000, url: "/img/macbook.jpeg" @@ -9,6 +10,15 @@ let products = [ { 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) { diff --git a/src/server/index.js b/src/server/index.js index 65ac35b..3df183d 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -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) { diff --git a/yarn.lock b/yarn.lock index 824a933..03de3f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"