diff --git a/README.md b/README.md index 4853ea3..94d99c8 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,33 @@ npm start ## Tasks -Please publish your work to a fork of this repo. You're welcome (but not required) to add any libraries you think would be helpful. +1. [Implement Responsive Design](/tasks/01-responsive-design.md) -Note: You're encouraged to show your work by including multiple commits - we'll be looking through your fork's git history. +Originally I opted to follow the prototype v.2 design and I made good progress. However, as I started working on the responsive design the CSS was not smooth and my biggest problem was moving the 'Add to cart' button into the product item. Using position relative was not ideal but it worked. Another issue was moving the cart to upper right corner. I did manage this by moving the CartContainer component above the Title component. Looking back I simply could have placed both in a container and used display flex, and float to their respective sides. In any case, after finishing this first draft I moved on to the cart logic. + +After completing the cart functionality I returned to complete the responsive design. I am familiar with using media queries to alter CSS rules at a given screen or device size. However, I restarted all the CSS in order to start with flexible to have better control on moving elements. I also chose to follow the prototype v.1 design to start completely fresh and not to try to check and rewrite the CSS rules. The biggest issue I had and couldn't resolve properly was manipulating the items in the 'Cart" and in the 'Product List independent of one another. For example, in the 'Cart' the element texts have limited mobility due to inheriting CSS rules from the Product component. As I mentioned before the 'Add to cart' button was moved with position relative. I wasn't sure how, if at all possible, to move a button into another component unless I created a button component but that would be inefficient. + +Three breakpoints were assigned in order to use for the responsive design. I completed those three but I added a couple more in between in order to create a smoother change when scrolling screen sizes. With more time I would have wanted to have the cart render on a different page and when a user added an item only the 'Your cart is empty' text would change to the total amount. By clicking the cart element a user would then see the items and add, decrease, and remove as well. In this way, I could have styled the cart more closely to the prototype. + +

1280px

+ +![1280](https://user-images.githubusercontent.com/32649354/53599781-db130e80-3b5c-11e9-899d-e99eab064b55.png) + +

768px

+ +![768](https://user-images.githubusercontent.com/32649354/53599837-00a01800-3b5d-11e9-8c91-0ba74a950155.png) + +

360px

+ +![320](https://user-images.githubusercontent.com/32649354/53599881-157cab80-3b5d-11e9-86f2-93591a47f958.png) -1. [Implement Responsive Design](/tasks/01-responsive-design.md) 2. [Enhance Cart Functionality](/tasks/02-cart-enhancements.md) + +I studied the action and reducer code from "Add to cart' in order to create the 'Delete from cart' feature. To see if an action was called and the state changed I implemented the Redux Dev Tool in order to monitor the state. Just as 'Add to cart' retrieved the state in order to add an item, so did the 'Delete from cart' retrieved the state and deleted an item based on its id. Originally I placed the 'Delete' in the products container however, I soon realized it was meant to be in the cart and moved it soon after. + +Creating the 'Increase quantity' and 'Decrease quantity' features would essentially be 'Add' and 'Delete' but only effecting the quantity. I had issues with the 'Increase and 'Decrease' actions not running as it was evident in the redux dev tool. Correcting typos fixed this and by trying the code and checking the redux tool I was able to confirm that each increased and decreased item referred to the same id and pin. I also added logic to the buttons so that when increasing is allowed when there is the inventory available else the button is disabled. The same logic for the decrease is applied except when there is available quantity in the cart. With more time I would have explored working on the 'Checkout' feature, having the cart disabled, and showing an empty cart in a separate webpage. + 3. [Hook Up Product API](/tasks/03-product-api.md) -Please also update this README file: we'd love to see notes on your decision-making process, links to the most exciting pieces of code, or anything else that will give us additional context when reviewing your assessment. +Initially I overcomplicated the solution by creating two separate files for fetching data and connecting to the endpoint. I did not connect to the API and therefore opted to use the Shop.js file instead. With some research I was able to send a request to the API and console log the response which returned an array of objects. My biggest issue was getting this data to render on to the page. Also, while researching online I found that the 'getProducts' code could be shortened if the 'timeout' was never used. I found an online reference that mapped through and array of objects and then assigned the key and value pairs to a variable. In doing so, each object was built with the correct values. By following this code I was able to retrieve each object/item with their id title, and value. Where as before I returned the whole array I now had access to each object individually. However, my final issue was being able to render this data which after much research I couldn't solve. Most of the solutions I encountered revolved around setting the state to the values of the object. This was not goal as I was not dealing with JSX, components, or React for that matter. With more time I would pair program with someone else and search more on fetching and rendering data with JavaScript and not with React. + diff --git a/assets/Design.sketch b/assets/Design.sketch deleted file mode 100644 index dcfdae0..0000000 Binary files a/assets/Design.sketch and /dev/null differ diff --git a/package-lock.json b/package-lock.json index f74f964..242eca1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3155,7 +3155,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3176,12 +3177,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3196,17 +3199,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3323,7 +3329,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3335,6 +3342,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3349,6 +3357,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3356,12 +3365,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3380,6 +3391,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3460,7 +3472,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3472,6 +3485,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3557,7 +3571,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3593,6 +3608,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3612,6 +3628,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3655,12 +3672,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -7179,12 +7198,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7204,7 +7225,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", diff --git a/public/index.html b/public/index.html index 5fb87aa..3990f12 100644 --- a/public/index.html +++ b/public/index.html @@ -3,6 +3,7 @@ + Redux Shopping Cart Example diff --git a/src/actions/index.js b/src/actions/index.js index 5c332dc..6d35f10 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -17,12 +17,41 @@ const addToCartUnsafe = productId => ({ productId }) +const deleteFromCartUnsafe = productId => ({ + type: types.DELETE_FROM_CART, + productId +}) + +const increaseCartUnsafe = productId => ({ + type: types.INCREASE_QUANTITY, + productId +}) + +const decreaseCartUnsafe = productId => ({ + type: types.DECREASE_QUANTITY, + productId +}) + export const addToCart = productId => (dispatch, getState) => { if (getState().products.byId[productId].inventory > 0) { dispatch(addToCartUnsafe(productId)) } } +export const deleteFromCart = productId => (dispatch, getState) => { + if (getState().products.byId[productId].inventory > 0) { + dispatch(deleteFromCartUnsafe(productId)) + } +} + +export const increaseQuantity = productId => (dispatch, getState) => { + getState(dispatch(increaseCartUnsafe(productId))) +} + +export const decreaseQuantity = productId => (dispatch, getState) => { + getState(dispatch(decreaseCartUnsafe(productId))) +} + export const checkout = products => (dispatch, getState) => { const { cart } = getState() @@ -37,4 +66,4 @@ export const checkout = products => (dispatch, getState) => { // Replace the line above with line below to rollback on failure: // dispatch({ type: types.CHECKOUT_FAILURE, cart }) }) -} +} \ No newline at end of file diff --git a/src/api/products.json b/src/api/products.json index 184feee..cfa9441 100644 --- a/src/api/products.json +++ b/src/api/products.json @@ -1,5 +1,5 @@ [ - {"id": 1, "title": "Chronograph", "price": 500.01, "inventory": 2}, - {"id": 2, "title": "Quartz", "price": 10.99, "inventory": 10}, - {"id": 3, "title": "Weekender", "price": 19.99, "inventory": 5} + {"id": 1, "image": "http://static2.worldtempus.com/cache/product/c/h/chopard-superfast-chrono-porsche-919-edition-watch-face-view_688x688.jpg", "title": "Watch", "price": 500.01, "inventory": 2}, + {"id": 2, "image": "https://mockuphone.com/static/images/devices/apple-ipad-black-landscape.png", "title": "Ipad", "price": 200.99, "inventory": 5}, + {"id": 3, "image": "http://www.allwhitebackground.com/images/1/Printed-T-Shirts-8.jpg", "title": "T-Shirt", "price": 19.99, "inventory": 20} ] diff --git a/src/api/productsApi.js b/src/api/productsApi.js new file mode 100644 index 0000000..8dabb96 --- /dev/null +++ b/src/api/productsApi.js @@ -0,0 +1,6 @@ +import { get } from './request' +const BASE_URL = 'http://tech.work.co/shopping-cart/products.json'; + +export function getItems() { + return get(BASE_URL) +} \ No newline at end of file diff --git a/src/api/request.js b/src/api/request.js new file mode 100644 index 0000000..c891d7d --- /dev/null +++ b/src/api/request.js @@ -0,0 +1,12 @@ +function request(url, options = {}, data) { + if(data) options.body = JSON.stringify(data); + + return fetch(url, options) + .then(response => [response.ok, response.json()]) + .then(([ok, json]) => { + if(ok) return json; + throw json.message || json.error || json.errors || json; + }); +} + +export const get = url => request(url); \ No newline at end of file diff --git a/src/api/shop.js b/src/api/shop.js index aa979ad..d07b4f9 100644 --- a/src/api/shop.js +++ b/src/api/shop.js @@ -1,11 +1,30 @@ -/** - * Mocking client-server processing - */ -import _products from './products.json' -const TIMEOUT = 100 +// import _products from './products.json'; + +// export default { +// getProducts: cb => cb(_products), +// buyProducts: (payload,cb) => cb() +// } + +const URL = 'http://tech.work.co/shopping-cart/products.json' + +const fetchData = () => { + return fetch(URL) + .then(response => response.json()) + .then(response => { + response.map(product => { + const item = { id: product.id, productTitle: product.productTitle, inventory: product.inventory, value: product.price.value }; + console.log(item) + }) + }) +} export default { - getProducts: (cb, timeout) => setTimeout(() => cb(_products), timeout || TIMEOUT), - buyProducts: (payload, cb, timeout) => setTimeout(() => cb(), timeout || TIMEOUT) + getProducts: cb => fetchData(URL), + buyProducts: (payload, cb) => cb() } + +// export default { +// getProducts: (cb, timeout) => setTimeout(() => cb(fetchData().then(response => response)), timeout || TIMEOUT), +// buyProducts: (payload, cb, timeout) => setTimeout(() => cb(), timeout || TIMEOUT), +// } diff --git a/src/assets/chrono.png b/src/assets/chrono.png new file mode 100644 index 0000000..d200025 Binary files /dev/null and b/src/assets/chrono.png differ diff --git a/src/assets/ipad.png b/src/assets/ipad.png new file mode 100644 index 0000000..8a75a5e Binary files /dev/null and b/src/assets/ipad.png differ diff --git a/src/assets/tshirt.png b/src/assets/tshirt.png new file mode 100644 index 0000000..ea5d72a Binary files /dev/null and b/src/assets/tshirt.png differ diff --git a/src/components/Cart.css b/src/components/Cart.css new file mode 100644 index 0000000..fff40c6 --- /dev/null +++ b/src/components/Cart.css @@ -0,0 +1,133 @@ +:local(.cart){ + display: flex; + float: left; + margin: 1.5rem 0rem; + width: 25%; + justify-content: center; +} + +@media only screen and (max-width: 544px) { + :local(.cart){ + display: block; + margin: 0; + width: 100%; + } +} + +@media only screen and (max-width: 320px) { + :local(.cart){ + display: block; + margin: 0; + width: 100%; + } +} + +.logo { + font-size: 3.5rem; + justify-items: start; +} + +@media only screen and (max-width: 768px) { + .logo { + font-size: 2.5rem; + } +} + +@media only screen and (max-width: 544px) { + .logo { + font-size: 2rem; + } +} + +@media only screen and (max-width: 320px) { + .logo { + font-size: 2rem; + } +} + +#i { + font-size: 1.25rem; + color: rgb(146, 146, 146); +} + +.del { + color: rgb(150, 150, 150); + border: none; + background-color: white; + margin: 1rem; +} + +.dec { + border: none; + color: white; + background-color: silver; + border-radius: .20rem; + padding: .5rem 1rem; + margin: 1rem; +} + +.dec:hover { + background-color: rgb(150, 150, 150); +} + +.inc { + border: none; + background-color: silver; + color: white; + border-radius: .20rem; + padding: .5rem 1rem; + margin: 1rem; +} + +.inc:hover { + background-color: rgb(150, 150, 150); +} + +.check { + border: none; + color: white; + background-color: #3eab93; + border-radius: .20rem; + padding: .5rem 1rem; + margin: 1rem; +} + +.check:hover { + background-color: #27987f ; +} + +@media only screen and (min-width: 1400px) { + .power { + display: block; + position: relative; + left: 4rem; + bottom: 3rem; + } +} + +@media only screen and (max-width: 768px) { + .power { + display: block; + position: relative; + right: 1rem; + top: 0rem; + } +} + +@media only screen and (max-width: 544px) { + .power { + display: block; + position: relative; + left: 5.5rem; + top: -3rem; + } +} + +@media only screen and (max-width: 320px) { + .power { + display: block; + position: relative; + left: 0rem; + top: 0rem; + } +} \ No newline at end of file diff --git a/src/components/Cart.js b/src/components/Cart.js index c9adc2f..a427eda 100644 --- a/src/components/Cart.js +++ b/src/components/Cart.js @@ -1,31 +1,59 @@ import React from 'react' import PropTypes from 'prop-types' import Product from './Product' +import styles from './Cart.css' -const Cart = ({ products, total, onCheckoutClicked }) => { +const Cart = ({ products, total, image, onCheckoutClicked, onIncreaseQuantityClicked, onDecreaseQuantityClicked, onDeleteFromCartClicked }) => { const hasProducts = products.length > 0 const nodes = hasProducts ? ( products.map(product => - +
+ +
+ + {product.quantity} + + +
+ {/*
*/} +
) ) : ( - Please add some products to cart. + +

Your cart is empty 

+ ) return ( -
-

Your Cart

-
{nodes}
-

Total: ${total}

- +
+
+
+

Shopping
Cart

+
{nodes}
+ {image} + {/*

Total: ${total} 

+ */} +
+
) } diff --git a/src/components/Product.css b/src/components/Product.css new file mode 100644 index 0000000..4bbd79c --- /dev/null +++ b/src/components/Product.css @@ -0,0 +1,114 @@ +:local(.product){ + display: flex; + flex-direction: column; + padding-top: 4rem; +} + +@media only screen and (max-width: 1280px) { + :local(.product) { + padding-top: 3.5rem; + } +} + +@media only screen and (max-width: 768px) { + :local(.product) { + padding-top: 3rem; + } +} + +@media only screen and (max-width: 544px) { + :local(.product) { + padding-top: 3rem; + } +} + +@media only screen and (max-width: 320px) { + :local(.product) { + padding-top: 1.5rem; + } +} + +img { + width: 30%; + float: left; +} + +@media only screen and (max-width: 768px) { + img { + width: 40%; + } +} + +@media only screen and (max-width: 320px) { + img { + width: 100%; + float: none; + margin: 0; + } +} + +.Image { + border: silver 1px solid; +} + +.info { + float: left; + margin: 2rem; +} + +@media only screen and (max-width: 1400px) { + .info { + margin: 1rem; + } +} + +@media only screen and (max-width: 1280px) { + .info { + margin: 1rem; + } +} + +@media only screen and (max-width: 768px) { + .info { + margin: 1rem; + } +} + +@media only screen and (max-width: 320px) { + .info { + float: none; + margin: 0; + } +} + +.price{ + float: right; + margin: 2rem; +} + +@media only screen and (max-width: 1400px) { + .price { + margin: 1rem; + } +} + + @media only screen and (max-width: 1280px) { + .price { + margin: 1rem; + } +} + +@media only screen and (max-width: 768px) { + .price { + margin: 1rem; + } +} + +@media only screen and (max-width: 320px) { + .price { + float: none; + margin: 0; + } +} + + diff --git a/src/components/Product.js b/src/components/Product.js index f40181d..d11a64a 100644 --- a/src/components/Product.js +++ b/src/components/Product.js @@ -1,16 +1,22 @@ import React from 'react' import PropTypes from 'prop-types' +import styles from './Product.css' -const Product = ({ price, inventory, title }) => ( -
- {title} - ${price}{inventory ? ` x ${inventory}` : null} +const Product = ({ price, quantity, title, image }) => ( +
+
+ +

{title}
{quantity ? ` Remaining ${quantity}` : null}

+

${price}

+
) Product.propTypes = { price: PropTypes.number, - inventory: PropTypes.number, - title: PropTypes.string + quantity: PropTypes.number, + title: PropTypes.string, + image: PropTypes.string } export default Product diff --git a/src/components/ProductItem.css b/src/components/ProductItem.css new file mode 100644 index 0000000..bd9de7f --- /dev/null +++ b/src/components/ProductItem.css @@ -0,0 +1,91 @@ +:local(.productitem){ + display: flex; + align-items: center; +} + +@media only screen and (min-width: 1401px) { + .btn { + position: relative; + bottom: 3rem; + right: 6rem; + } +} + +@media only screen and (max-width: 1400px) { + .btn { + position: relative; + bottom: 3rem; + right: 5.5rem; + } +} + +@media only screen and (max-width: 1280px) { + .btn { + position: relative; + bottom: 3rem; + right: 4.5rem; + } +} + +@media only screen and (max-width: 1180px) { + .btn { + position: relative; + bottom: 3rem; + right: 4rem; + } +} + +@media only screen and (max-width: 1024px) { + .btn { + position: relative; + bottom: 3rem; + right: 2.5rem; + } +} + +@media only screen and (max-width: 768px) { + .btn { + position: relative; + bottom: 3rem; + left: 2rem; + } +} + +@media only screen and (max-width: 544px) { + .btn { + position: relative; + bottom: 3rem; + left: 2rem; + } +} + +@media only screen and (max-width: 320px) { + .btn { + position: relative; + bottom: 0; + left: 0rem; + } +} + +.add { + border: none; + color: white; + background-color: #3eab93; + border-radius: .20rem; + padding: .5rem 1rem; + margin: 1rem; +} + +.add:hover { + background-color: #27987f; +} + +.add:disabled { + background-color: #dfdfdf; + cursor: pointer; +} + +hr { + border-top: silver solid 1px; +} + diff --git a/src/components/ProductItem.js b/src/components/ProductItem.js index f8f8131..2824b01 100644 --- a/src/components/ProductItem.js +++ b/src/components/ProductItem.js @@ -1,18 +1,26 @@ import React from 'react' import PropTypes from 'prop-types' import Product from './Product' +import styles from './ProductItem.css' const ProductItem = ({ product, onAddToCartClicked }) => ( -
- - +
+
+
+ +
+ +
+
) @@ -20,9 +28,10 @@ ProductItem.propTypes = { product: PropTypes.shape({ title: PropTypes.string.isRequired, price: PropTypes.number.isRequired, + image: PropTypes.string.isRequired, inventory: PropTypes.number.isRequired }).isRequired, - onAddToCartClicked: PropTypes.func.isRequired + onAddToCartClicked: PropTypes.func.isRequired, } export default ProductItem diff --git a/src/components/ProductList.css b/src/components/ProductList.css new file mode 100644 index 0000000..6209718 --- /dev/null +++ b/src/components/ProductList.css @@ -0,0 +1,23 @@ +:local(.productlist){ + display: flex; + float: right; + width: 65%; + margin: 1rem; + justify-content: center; +} + +@media only screen and (max-width: 544px) { + :local(.productlist){ + display: block; + margin: 0; + width: 100%; + } +} + +@media only screen and (max-width: 320px) { + :local(.productlist){ + display: block; + margin: 0; + width: 100%; + } +} diff --git a/src/components/ProductsList.js b/src/components/ProductsList.js index f0265fc..3a3a7d3 100644 --- a/src/components/ProductsList.js +++ b/src/components/ProductsList.js @@ -1,16 +1,15 @@ import React from 'react' import PropTypes from 'prop-types' +import styles from './ProductList.css' -const ProductsList = ({ title, children }) => ( -
-

{title}

+const ProductsList = ({ children }) => ( +
{children}
) ProductsList.propTypes = { children: PropTypes.node, - title: PropTypes.string.isRequired } export default ProductsList diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js index 6cbabea..3e357f8 100644 --- a/src/constants/ActionTypes.js +++ b/src/constants/ActionTypes.js @@ -1,4 +1,7 @@ export const ADD_TO_CART = 'ADD_TO_CART' +export const DELETE_FROM_CART = 'DELETE_FROM_CART' +export const INCREASE_QUANTITY = 'INCREASE_QUANTITY' +export const DECREASE_QUANTITY = 'DECREASE_QUANTITY' export const CHECKOUT_REQUEST = 'CHECKOUT_REQUEST' export const CHECKOUT_SUCCESS = 'CHECKOUT_SUCCESS' export const CHECKOUT_FAILURE = 'CHECKOUT_FAILURE' diff --git a/src/containers/App.js b/src/containers/App.js index 7625918..5903ed3 100644 --- a/src/containers/App.js +++ b/src/containers/App.js @@ -4,11 +4,8 @@ import CartContainer from './CartContainer' const App = () => (
-

Shopping Cart Example

-
- -
+
) diff --git a/src/containers/CartContainer.js b/src/containers/CartContainer.js index ac36ff7..3d45285 100644 --- a/src/containers/CartContainer.js +++ b/src/containers/CartContainer.js @@ -1,15 +1,18 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { checkout } from '../actions' +import { checkout, increaseQuantity, decreaseQuantity, deleteFromCart } from '../actions' import { getTotal, getCartProducts } from '../reducers' import Cart from '../components/Cart' -const CartContainer = ({ products, total, checkout }) => ( +const CartContainer = ({ products, total, checkout, increaseQuantity, decreaseQuantity, deleteFromCart }) => ( checkout(products)} /> + onCheckoutClicked={() => checkout(products)} + onIncreaseQuantityClicked={increaseQuantity} + onDecreaseQuantityClicked={decreaseQuantity} + onDeleteFromCartClicked={deleteFromCart}/> ) CartContainer.propTypes = { @@ -20,7 +23,7 @@ CartContainer.propTypes = { quantity: PropTypes.number.isRequired })).isRequired, total: PropTypes.string, - checkout: PropTypes.func.isRequired + checkout: PropTypes.func.isRequired, } const mapStateToProps = (state) => ({ @@ -30,5 +33,5 @@ const mapStateToProps = (state) => ({ export default connect( mapStateToProps, - { checkout } -)(CartContainer) + { checkout, increaseQuantity, decreaseQuantity, deleteFromCart } +)(CartContainer) \ No newline at end of file diff --git a/src/containers/ProductsContainer.js b/src/containers/ProductsContainer.js index 9108a97..e6518f2 100644 --- a/src/containers/ProductsContainer.js +++ b/src/containers/ProductsContainer.js @@ -12,7 +12,8 @@ const ProductsContainer = ({ products, addToCart }) => ( addToCart(product.id)} /> + onAddToCartClicked={() => addToCart(product.id)} + /> )} ) @@ -24,7 +25,7 @@ ProductsContainer.propTypes = { price: PropTypes.number.isRequired, inventory: PropTypes.number.isRequired })).isRequired, - addToCart: PropTypes.func.isRequired + addToCart: PropTypes.func.isRequired, } const mapStateToProps = state => ({ @@ -34,4 +35,4 @@ const mapStateToProps = state => ({ export default connect( mapStateToProps, { addToCart } -)(ProductsContainer) +)(ProductsContainer) \ No newline at end of file diff --git a/src/index.js b/src/index.js index d166264..91564c7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,26 @@ import React from 'react' import { render } from 'react-dom' -import { createStore, applyMiddleware } from 'redux' +import { createStore, applyMiddleware, compose } from 'redux' import { Provider } from 'react-redux' import { createLogger } from 'redux-logger' import thunk from 'redux-thunk' import reducer from './reducers' import { getAllProducts } from './actions' import App from './containers/App' +import './styles/main.css'; const middleware = [ thunk ]; if (process.env.NODE_ENV !== 'production') { middleware.push(createLogger()); } +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + const store = createStore( reducer, + composeEnhancers( applyMiddleware(...middleware) + ) ) store.dispatch(getAllProducts()) @@ -25,4 +30,4 @@ render( , document.getElementById('root') -) +) \ No newline at end of file diff --git a/src/reducers/cart.js b/src/reducers/cart.js index 7d87e42..65fcec2 100644 --- a/src/reducers/cart.js +++ b/src/reducers/cart.js @@ -1,5 +1,8 @@ import { ADD_TO_CART, + DELETE_FROM_CART, + INCREASE_QUANTITY, + DECREASE_QUANTITY, CHECKOUT_REQUEST, CHECKOUT_FAILURE } from '../constants/ActionTypes' @@ -16,6 +19,8 @@ const addedIds = (state = initialState.addedIds, action) => { return state } return [ ...state, action.productId ] + case DELETE_FROM_CART: + return state.filter(id => id !== action.productId); default: return state } @@ -28,6 +33,18 @@ const quantityById = (state = initialState.quantityById, action) => { return { ...state, [productId]: (state[productId] || 0) + 1 } + case DELETE_FROM_CART: + return { ...state, + [action.productId]: state[action.productId] - 1 + } + case INCREASE_QUANTITY: + return { ...state, + [action.productId]: state[action.productId] + 1 + } + case DECREASE_QUANTITY: + return { ...state, + [action.productId]: state[action.productId] - 1 + } default: return state } @@ -52,4 +69,4 @@ const cart = (state = initialState, action) => { } } -export default cart +export default cart \ No newline at end of file diff --git a/src/reducers/products.js b/src/reducers/products.js index 01dba2c..09b55b0 100644 --- a/src/reducers/products.js +++ b/src/reducers/products.js @@ -1,5 +1,5 @@ import { combineReducers } from 'redux' -import { RECEIVE_PRODUCTS, ADD_TO_CART } from '../constants/ActionTypes' +import { RECEIVE_PRODUCTS, ADD_TO_CART, DELETE_FROM_CART, INCREASE_QUANTITY, DECREASE_QUANTITY } from '../constants/ActionTypes' const products = (state, action) => { switch (action.type) { @@ -8,6 +8,21 @@ const products = (state, action) => { ...state, inventory: state.inventory - 1 } + case DELETE_FROM_CART: + return { + ...state, + inventory: state.inventory + 1 + }; + case INCREASE_QUANTITY: + return { + ...state, + inventory: state.inventory - 1 + } + case DECREASE_QUANTITY: + return { + ...state, + inventory: state.inventory + 1 + }; default: return state } @@ -53,4 +68,4 @@ export const getProduct = (state, id) => state.byId[id] export const getVisibleProducts = state => - state.visibleIds.map(id => getProduct(state, id)) + state.visibleIds.map(id => getProduct(state, id)) \ No newline at end of file diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..9d61320 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,28 @@ +@import url('https://fonts.googleapis.com/css?family=Lora'); + +button:hover { + cursor: pointer; +} + +button:disabled { + background-color: #dfdfdf; + cursor: pointer; +} + +* { + box-sizing: border-box; +} + +body { + display: flex; + text-align: center; + margin: 3rem; + font-family: 'Lora', serif; +} + +.acme { + display: flex; + width: 25%; + margin: 1rem; + justify-content: center; +} \ No newline at end of file