diff --git a/src/App.jsx b/src/App.jsx index d28c0b3..067fbf6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,61 +1,56 @@ +import { useState, useEffect } from 'react' import './styles/reset.css' import './styles/index.css' - +import Header from "./Components/Header/Header.jsx" +import Cart from "./Components/Cart/Cart.jsx" +import Credit from "./Components/Credit/Credit.jsx" import initialStoreItems from './store-items' + /* Here's what a store item should look like { id: '001-beetroot', name: 'beetroot', - price: 0.35 + price: 0.35, + type: "vegetable" } What should a cart item look like? 🤔 */ -console.log(initialStoreItems) +//console.log(initialStoreItems) export default function App() { - // Setup state here... + const [cartItems, setCartItems] = useState([]) + + useEffect(() => { + const initialCart = [] + initialStoreItems?.forEach((entry) => { + initialCart.push({...entry, quantity: 0}) + }) + setCartItems(initialCart) + },[]) + + const addItemToCart = (itemId) => { + const items = cartItems + const itemIndex = items.findIndex((i) => i.id == itemId) + const updatedItem = items.at(itemIndex) + updatedItem.quantity += 1 + setCartItems([...items]) + } + + const removeItemFromCart = (itemId) => { + const updateItem = cartItems + updateItem[updateItem.findIndex((i) => i.id === itemId)].quantity -= 1 + setCartItems([...updateItem]) + } return ( <> -
-

Greengrocers

- -
-
-

Your Cart

-
- -
-
-
-

Total

-
-
- £0.00 -
-
-
-
- Icons made by - - Icongeek26 - - from - - www.flaticon.com - -
+
+ + ) } diff --git a/src/Components/Cart/Cart.css b/src/Components/Cart/Cart.css new file mode 100644 index 0000000..fb2d2cf --- /dev/null +++ b/src/Components/Cart/Cart.css @@ -0,0 +1,118 @@ +/* Cart */ + +#cart { + height: 55vh; + padding: 1rem; + + display: grid; + grid-template-rows: auto 1fr auto; + grid-gap: 1rem; + justify-content: center; + + border-top: 2px solid #00675b; + } + + #cart h2 { + margin-bottom: 0; + text-align: center; + } + + .cart--item-list-container { + min-width: 320px; + height: 100%; + padding: 0 1rem; + + overflow-y: scroll; + + border-radius: 0.5rem; + border: 2px solid #757575; + } + + @media only screen and (max-width: 450px) { + .cart--item-list-container { + border: none; + } + } + + .cart--item-list li { + padding: 1rem 0; + + display: grid; + grid-template-columns: 24px minmax(150px, 1fr) repeat(3, auto); + grid-gap: 0.5rem; + align-items: center; + + border-bottom: 1px dotted #000000; + + font-size: 1.25rem; + } + + .cart--item-list li:last-child { + border-bottom: none; + } + + .cart--item-icon { + width: 24px; + } + + .center { + display: grid; + place-items: center; + } + + .quantity-btn { + width: 20px; + height: 20px; + padding: 0; + + border-radius: 0.25rem; + + font-weight: bold; + } + + .remove-btn { + border: 2px solid #d32f2f; + background-color: #ffcdd2; + color: #d32f2f; + } + + .add-btn { + border: 2px solid #388e3c; + background-color: #c8e6c9; + color: #388e3c; + } + + .quantity-text { + width: 24px; + height: 24px; + + border-radius: 0.25rem; + border: 2px solid #757575; + color: #757575; + + text-align: center; + font-size: 0.75rem; + font-weight: bold; + } + + /* Total */ + + .total-section { + padding: 0.5rem 1rem; + + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + } + + .total-number { + font-weight: bold; + } + + @media only screen and (max-width: 450px) { + .total-section { + border-top: 2px solid #00675b; + border-bottom: 2px solid #00675b; + } + } + \ No newline at end of file diff --git a/src/Components/Cart/Cart.jsx b/src/Components/Cart/Cart.jsx new file mode 100644 index 0000000..d1547b5 --- /dev/null +++ b/src/Components/Cart/Cart.jsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react' +import "./Cart.css" +import TotalCost from "./TotalCost/TotalCost.jsx" +import PropTypes from 'prop-types' + +const Cart = ({cart, increment, decrement}) => { + const [cartPrice, setCartPrice] = useState(0) + + useEffect(() => { + let price = 0 + cart.forEach((item) => { + price += (parseFloat(item.price) * parseInt(item.quantity)) + }) + setCartPrice(price.toFixed(2)) + }, [cart]) + + return ( +
+

Your Cart

+
+
    + {cart?.filter((e) => e.quantity !== 0) + .map((entry, index) => +
  • + +

    {entry.name}

    + + {entry.quantity} + +
  • + )} +
+
+ +
+ ) +} + +Cart.propTypes = { + cart: PropTypes.array, + increment: PropTypes.func, + decrement: PropTypes.func, +} + +export default Cart \ No newline at end of file diff --git a/src/Components/Cart/TotalCost/TotalCost.jsx b/src/Components/Cart/TotalCost/TotalCost.jsx new file mode 100644 index 0000000..d4cfb7e --- /dev/null +++ b/src/Components/Cart/TotalCost/TotalCost.jsx @@ -0,0 +1,14 @@ +const TotalCost = ({cartPrice}) => { + return ( +
+
+

Total

+
+
+ £{cartPrice} +
+
+ ) +} + +export default TotalCost \ No newline at end of file diff --git a/src/Components/Credit/Credit.jsx b/src/Components/Credit/Credit.jsx new file mode 100644 index 0000000..55030b8 --- /dev/null +++ b/src/Components/Credit/Credit.jsx @@ -0,0 +1,19 @@ +const Credit = () => { + return ( +
+ Icons made by + + Icongeek26 + + from + + www.flaticon.com + +
+ ) +} + +export default Credit \ No newline at end of file diff --git a/src/Components/Header/Header.css b/src/Components/Header/Header.css new file mode 100644 index 0000000..c1e0eee --- /dev/null +++ b/src/Components/Header/Header.css @@ -0,0 +1,46 @@ +/* Store */ + +#store { + height: 45vh; + padding: 1rem; + overflow-y: scroll; + padding-left: 150px; + + background-color: #e7f4ea; + } + + #store h1 { + text-align: center; + } + + .store--item-list { + display: grid; + grid-template-columns: repeat(4, 125px); + grid-gap: 1rem; + justify-content: center; + } + + .store--item-list li { + display: grid; + place-items: center; + grid-row-gap: 0.5rem; + } + + .store--item-icon { + width: 125px; + height: 125px; + } + + @media only screen and (max-width: 800px) { + .store--item-list { + grid-template-columns: repeat(3, 125px); + } + } + + @media only screen and (max-width: 500px) { + .store--item-list { + grid-template-columns: repeat(2, 125px); + } + } + + \ No newline at end of file diff --git a/src/Components/Header/Header.jsx b/src/Components/Header/Header.jsx new file mode 100644 index 0000000..f8be351 --- /dev/null +++ b/src/Components/Header/Header.jsx @@ -0,0 +1,42 @@ +import { useState } from 'react' +import "./Header.css" +import SortAndFilter from "./SortAndFilter/SortAndFilter.jsx" +import ProductView from "./ProductView/ProductView.jsx" +import PropTypes from 'prop-types' + +const Header = ({storeItems, addFunction}) => { + const [filteredItems, setFilteredItems] = useState(storeItems) + const [detailView, setDetailView] = useState(undefined) + + const capitalizeString = (string) => { + return (string.substring(0,1).toUpperCase() + string.substring(1,string.length)) + } + + return ( +
+

Greengrocers

+ + {detailView && } +
    + {filteredItems.map((entry, index) => +
  • + setDetailView(entry)} + /> +

    {capitalizeString(entry.name)}

    + +
  • + )} +
+
+ ) +} + +Header.propTypes = { + storeItems: PropTypes.arrayOf(PropTypes.object), + addFunction: PropTypes.func, +} + +export default Header \ No newline at end of file diff --git a/src/Components/Header/ProductView/ProductView.css b/src/Components/Header/ProductView/ProductView.css new file mode 100644 index 0000000..ff6e187 --- /dev/null +++ b/src/Components/Header/ProductView/ProductView.css @@ -0,0 +1,33 @@ +.product-view-container { + position: fixed; + top: 5%; + left: 80%; + width: 200px; + border: solid black 1px; + border-radius: 10%; + padding: 10px; + box-sizing: border-box; +} + +#close-component { + position: fixed; + width: 30px; + left: 88.5%; + top: 5.5%; + z-index: 5; +} + +.product-view-container > img { + width: 100px; + position: relative; + top: 5%; + left: 5%; +} + +.product-view-container > h3 { + font-size: 26px; +} + +.product-view-container > p { + font-size: 18px; +} \ No newline at end of file diff --git a/src/Components/Header/ProductView/ProductView.jsx b/src/Components/Header/ProductView/ProductView.jsx new file mode 100644 index 0000000..a4adc91 --- /dev/null +++ b/src/Components/Header/ProductView/ProductView.jsx @@ -0,0 +1,29 @@ +import "./ProductView.css" +import PropTypes from 'prop-types' + +const capitalizeString = (string) => { + return (string.substring(0,1).toUpperCase() + string.substring(1,string.length)) +} + +const ProductView = ({product, setDetailView}) => { + return ( +
+ setDetailView(undefined)} + src="https://uxwing.com/wp-content/themes/uxwing/download/checkmark-cross/close-round-icon.png" + /> +

{capitalizeString(product.name)}

+

Category: {product.type}

+

Price: £{product.price}

+ +
+ ) +} + +ProductView.propTypes = { + product: PropTypes.object, + setDetailView: PropTypes.func, +} + +export default ProductView \ No newline at end of file diff --git a/src/Components/Header/SortAndFilter/FilterProduce/FilterProduce.css b/src/Components/Header/SortAndFilter/FilterProduce/FilterProduce.css new file mode 100644 index 0000000..ba102c0 --- /dev/null +++ b/src/Components/Header/SortAndFilter/FilterProduce/FilterProduce.css @@ -0,0 +1,3 @@ +.filter-and-sort-container > .filter-type > label { + font-style: oblique; +} \ No newline at end of file diff --git a/src/Components/Header/SortAndFilter/FilterProduce/FilterProduce.jsx b/src/Components/Header/SortAndFilter/FilterProduce/FilterProduce.jsx new file mode 100644 index 0000000..6bb237f --- /dev/null +++ b/src/Components/Header/SortAndFilter/FilterProduce/FilterProduce.jsx @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types' +import "./FilterProduce.css" + +const FilterProduce = ({typeFilter, toggleFilter}) => { + return ( +
+ +
+ +
+ ) +} + +FilterProduce.propTypes = { + typeFilter: PropTypes.object, + toggleFilter: PropTypes.func, +} + +export default FilterProduce \ No newline at end of file diff --git a/src/Components/Header/SortAndFilter/SortAndFilter.css b/src/Components/Header/SortAndFilter/SortAndFilter.css new file mode 100644 index 0000000..bfc2e90 --- /dev/null +++ b/src/Components/Header/SortAndFilter/SortAndFilter.css @@ -0,0 +1,41 @@ +.filter-and-sort-container { + position: absolute; + left: 30%; + width: 130px; +} + +@media only screen and (max-width: 1450px) { + .filter-and-sort-container { + left: 20%; + } +} + + +@media only screen and (max-width: 1200px) { + .filter-and-sort-container { + left: 10%; + } +} + +@media only screen and (max-width: 600px) { + .filter-and-sort-container { + left: 5%; + } +} + +.filter-and-sort-container > p { + font-weight: bold; +} + +.filter-and-sort-container > div > button { + background: #aee6bb; + display: flex; + justify-self: center; + align-self: center; + border-radius: 30%; + border: none; +} + +.filter-and-sort-container > div > button:hover { + background-color:gainsboro; +} \ No newline at end of file diff --git a/src/Components/Header/SortAndFilter/SortAndFilter.jsx b/src/Components/Header/SortAndFilter/SortAndFilter.jsx new file mode 100644 index 0000000..9b0926e --- /dev/null +++ b/src/Components/Header/SortAndFilter/SortAndFilter.jsx @@ -0,0 +1,48 @@ +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import "./SortAndFilter.css" +import FilterProduce from "./FilterProduce/FilterProduce.jsx" + +const SortAndFilter = ({storeItems, setFilteredItems}) => { + const [sortAsc, setSortAsc] = useState(true) + const [typeFilter, setTypeFilter] = useState({ + fruit: false, + vegetable: false, + }) + + const toggleFilter = (type) => { + setTypeFilter({...typeFilter, [type]: !typeFilter[type]}) + } + + useEffect(() => { + setFilteredItems( + [...storeItems.filter((item) => !typeFilter[item.type]) + .sort((a,b) => sortAsc ? + a.name.localeCompare(b.name) : + b.name.localeCompare(a.name) + ) + ] + ) + },[typeFilter, storeItems, sortAsc, setFilteredItems]) + + + return ( +
+

Show categories:

+ +

Sort produce:

+
+ +
+
+ ) +} + +SortAndFilter.propTypes = { + storeItems: PropTypes.arrayOf(PropTypes.object), + setFilteredItems: PropTypes.func, +} + +export default SortAndFilter \ No newline at end of file diff --git a/src/store-items.js b/src/store-items.js index 8e8013c..cd263c8 100644 --- a/src/store-items.js +++ b/src/store-items.js @@ -3,52 +3,62 @@ const storeItems = [ { id: "001-beetroot", name: "beetroot", - price: 0.35 + price: 0.15, + type: "vegetable" }, { id: "002-carrot", name: "carrot", - price: 0.35 + price: 0.25, + type: "vegetable" }, { id: "003-apple", name: "apple", - price: 0.35 + price: 0.35, + type: "fruit" }, { id: "004-apricot", name: "apricot", - price: 0.35 + price: 0.35, + type: "fruit" }, { id: "005-avocado", name: "avocado", - price: 0.35 + price: 0.55, + type: "fruit" }, { id: "006-bananas", name: "bananas", - price: 0.35 + price: 0.35, + type: "fruit" }, { id: "007-bell-pepper", name: "bell pepper", - price: 0.35 + price: 0.30, + type: "vegetable" }, { id: "008-berry", name: "berry", - price: 0.35 + price: 0.285, + type: "fruit" }, { id: "009-blueberry", name: "blueberry", - price: 0.35 + price: 0.275, + type: "fruit" }, { id: "010-eggplant", name: "eggplant", - price: 0.35 + price: 0.33, + type: "vegetable" } ] diff --git a/src/styles/index.css b/src/styles/index.css index 0a04c2b..3cf78ba 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -20,165 +20,3 @@ button { font-size: 0.725rem; } -/* Store */ - -#store { - height: 40vh; - padding: 1rem; - - overflow-y: scroll; - - background-color: #e7f4ea; -} - -#store h1 { - text-align: center; -} - -.store--item-list { - display: grid; - grid-template-columns: repeat(4, 125px); - grid-gap: 1rem; - justify-content: center; -} - -.store--item-list li { - display: grid; - place-items: center; - grid-row-gap: 0.5rem; -} - -.store--item-icon { - width: 125px; - height: 125px; -} - -@media only screen and (max-width: 600px) { - .store--item-list { - grid-template-columns: repeat(3, 125px); - } -} - -@media only screen and (max-width: 450px) { - .store--item-list { - grid-template-columns: repeat(2, 125px); - } -} - -/* Cart */ - -#cart { - height: 60vh; - padding: 1rem; - - display: grid; - grid-template-rows: auto 1fr auto; - grid-gap: 1rem; - justify-content: center; - - border-top: 2px solid #00675b; -} - -#cart h2 { - margin-bottom: 0; - text-align: center; -} - -.cart--item-list-container { - min-width: 320px; - height: 100%; - padding: 0 1rem; - - overflow-y: scroll; - - border-radius: 0.5rem; - border: 2px solid #757575; -} - -@media only screen and (max-width: 450px) { - .cart--item-list-container { - border: none; - } -} - -.cart--item-list li { - padding: 1rem 0; - - display: grid; - grid-template-columns: 24px minmax(150px, 1fr) repeat(3, auto); - grid-gap: 0.5rem; - align-items: center; - - border-bottom: 1px dotted #000000; - - font-size: 1.25rem; -} - -.cart--item-list li:last-child { - border-bottom: none; -} - -.cart--item-icon { - width: 24px; -} - -.center { - display: grid; - place-items: center; -} - -.quantity-btn { - width: 20px; - height: 20px; - padding: 0; - - border-radius: 0.25rem; - - font-weight: bold; -} - -.remove-btn { - border: 2px solid #d32f2f; - background-color: #ffcdd2; - color: #d32f2f; -} - -.add-btn { - border: 2px solid #388e3c; - background-color: #c8e6c9; - color: #388e3c; -} - -.quantity-text { - width: 24px; - height: 24px; - - border-radius: 0.25rem; - border: 2px solid #757575; - color: #757575; - - text-align: center; - font-size: 0.75rem; - font-weight: bold; -} - -/* Total */ - -.total-section { - padding: 0.5rem 1rem; - - display: grid; - grid-template-columns: 1fr auto; - align-items: center; -} - -.total-number { - font-weight: bold; -} - -@media only screen and (max-width: 450px) { - .total-section { - border-top: 2px solid #00675b; - border-bottom: 2px solid #00675b; - } -}