diff --git a/componen-hierarchy.png b/componen-hierarchy.png new file mode 100644 index 0000000..97fb568 Binary files /dev/null and b/componen-hierarchy.png differ diff --git a/index.html b/index.html index c1c82ab..35ed4ff 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/package-lock.json b/package-lock.json index ea547b5..c946b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "react-greengrocers", "version": "0.0.0", "dependencies": { + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -2809,7 +2810,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3059,7 +3059,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3121,8 +3120,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-refresh": { "version": "0.14.0", diff --git a/package.json b/package.json index 25ab709..2cc6983 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 03e658b..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import './styles/reset.css' -import './styles/index.css' - -import initialStoreItems from './store-items' - -export default function App() { - return ( - <> -
-

Greengrocers

- -
-
-

Your Cart

-
- -
-
-
-

Total

-
-
- £0.00 -
-
-
-
- Icons made by - - Icongeek26 - - from - - www.flaticon.com - -
- - ) -} diff --git a/src/components/App.jsx b/src/components/App.jsx new file mode 100644 index 0000000..275f8ff --- /dev/null +++ b/src/components/App.jsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import Header from "./Header"; +import Store from "./Store"; +import Cart from "./Cart"; +import TotalSection from "./TotalSection"; +import Filter from "./Filter"; +import Sort from "./Sort"; +import storeItems from "../store-items"; +import "../styles/index.css"; + +const App = () => { + const [items, setItems] = useState(storeItems); + const [cart, setCart] = useState([]); + const [filter, setFilter] = useState("all"); + const [sort, setSort] = useState("alphabetical"); + const [selectedItem, setSelectedItem] = useState(null); + + const filteredItems = items.filter( + (item) => filter === "all" || item.type === filter + ); + + const sortedItems = filteredItems.sort((a, b) => { + if (sort === "alphabetical") { + return a.name.localeCompare(b.name); + } else if (sort === "price") { + return a.price - b.price; + } + return 0; + }); + + return ( +
+
+
+ + +
+
+ + + +
+ {selectedItem && ( + setSelectedItem(null)} /> + )} +
+ ); +}; + +export default App; diff --git a/src/components/Cart.jsx b/src/components/Cart.jsx new file mode 100644 index 0000000..a78bd03 --- /dev/null +++ b/src/components/Cart.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import PropTypes from "prop-types"; +import CartItem from "./CartItem"; + +const Cart = ({ cart, setCart }) => ( +
+

Your Cart

+
+ +
+
+); + +Cart.propTypes = { + cart: PropTypes.arrayOf(PropTypes.object).isRequired, + setCart: PropTypes.func.isRequired, +}; + +export default Cart; diff --git a/src/components/CartItem.jsx b/src/components/CartItem.jsx new file mode 100644 index 0000000..0ce5c80 --- /dev/null +++ b/src/components/CartItem.jsx @@ -0,0 +1,69 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const CartItem = ({ item, cart, setCart }) => { + const removeFromCart = () => { + const updatedCart = [...cart]; + const itemIndex = updatedCart.findIndex( + (cartItem) => cartItem.id === item.id + ); + + if (itemIndex > -1) { + updatedCart[itemIndex].quantity -= 1; + + if (updatedCart[itemIndex].quantity === 0) { + updatedCart.splice(itemIndex, 1); + } + } + + setCart(updatedCart); + }; + + const addToCart = () => { + const updatedCart = [...cart]; + const itemIndex = updatedCart.findIndex( + (cartItem) => cartItem.id === item.id + ); + + if (itemIndex > -1) { + updatedCart[itemIndex].quantity += 1; + } + + setCart(updatedCart); + }; + + return ( +
  • + {item.name} +

    {item.name}

    + + {item.quantity} + +
  • + ); +}; + +CartItem.propTypes = { + item: PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + price: PropTypes.number.isRequired, + quantity: PropTypes.number.isRequired, + }).isRequired, + cart: PropTypes.arrayOf(PropTypes.object).isRequired, + setCart: PropTypes.func.isRequired, +}; + +export default CartItem; diff --git a/src/components/Filter.jsx b/src/components/Filter.jsx new file mode 100644 index 0000000..92ced3d --- /dev/null +++ b/src/components/Filter.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const Filter = ({ filter, setFilter }) => { + const handleFilterChange = (event) => { + setFilter(event.target.value); + }; + + return ( +
    + + +
    + ); +}; + +Filter.propTypes = { + filter: PropTypes.string.isRequired, + setFilter: PropTypes.func.isRequired, +}; + +export default Filter; diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..6220dc5 --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,9 @@ +import React from "react"; + +const Header = () => ( +
    +

    Greengrocers

    +
    +); + +export default Header; diff --git a/src/components/ItemDetail.jsx b/src/components/ItemDetail.jsx new file mode 100644 index 0000000..738b53f --- /dev/null +++ b/src/components/ItemDetail.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const ItemDetail = ({ item, onClose }) => { + return ( +
    +

    {item.name}

    +

    Type: {item.type}

    +

    Price: £{item.price.toFixed(2)}

    + +
    + ); +}; + +ItemDetail.propTypes = { + item: PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + price: PropTypes.number.isRequired, + type: PropTypes.string.isRequired, + }).isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default ItemDetail; diff --git a/src/components/Sort.jsx b/src/components/Sort.jsx new file mode 100644 index 0000000..a027bc7 --- /dev/null +++ b/src/components/Sort.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const Sort = ({ sort, setSort }) => { + const handleSortChange = (event) => { + setSort(event.target.value); + }; + + return ( +
    + + +
    + ); +}; + +Sort.propTypes = { + sort: PropTypes.string.isRequired, + setSort: PropTypes.func.isRequired, +}; + +export default Sort; diff --git a/src/components/Store.jsx b/src/components/Store.jsx new file mode 100644 index 0000000..be3cfdc --- /dev/null +++ b/src/components/Store.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import PropTypes from "prop-types"; +import StoreItem from "./StoreItem"; + +const Store = ({ items, cart, setCart, setSelectedItem }) => ( + +); + +Store.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + cart: PropTypes.arrayOf(PropTypes.object).isRequired, + setCart: PropTypes.func.isRequired, + setSelectedItem: PropTypes.func.isRequired, +}; + +export default Store; diff --git a/src/components/StoreItem.jsx b/src/components/StoreItem.jsx new file mode 100644 index 0000000..bfe5b90 --- /dev/null +++ b/src/components/StoreItem.jsx @@ -0,0 +1,45 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const StoreItem = ({ item, cart, setCart, setSelectedItem }) => { + const addToCart = () => { + const updatedCart = [...cart]; + const itemIndex = updatedCart.findIndex( + (cartItem) => cartItem.id === item.id + ); + + if (itemIndex > -1) { + updatedCart[itemIndex].quantity += 1; + } else { + updatedCart.push({ ...item, quantity: 1 }); + } + + setCart(updatedCart); + }; + + return ( +
  • +
    setSelectedItem(item)}> + {item.name} +
    + +
  • + ); +}; + +StoreItem.propTypes = { + item: PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + price: PropTypes.number.isRequired, + type: PropTypes.string.isRequired, + }).isRequired, + cart: PropTypes.arrayOf(PropTypes.object).isRequired, + setCart: PropTypes.func.isRequired, + setSelectedItem: PropTypes.func.isRequired, +}; + +export default StoreItem; diff --git a/src/components/TotalSection.jsx b/src/components/TotalSection.jsx new file mode 100644 index 0000000..c9236eb --- /dev/null +++ b/src/components/TotalSection.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const TotalSection = ({ cart }) => { + const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0); + + return ( +
    +
    +

    Total

    +
    +
    + £{total.toFixed(2)} +
    +
    + ); +}; + +TotalSection.propTypes = { + cart: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default TotalSection; diff --git a/src/main.jsx b/src/main.jsx index 51a8c58..7333669 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,9 +1,7 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.jsx' +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./components/App"; +import "./styles/reset.css"; +import "./styles/index.css"; -ReactDOM.createRoot(document.getElementById('root')).render( - - - , -) +ReactDOM.render(, document.getElementById("root")); diff --git a/src/store-items.js b/src/store-items.js index 96dd2ea..21cc0e6 100644 --- a/src/store-items.js +++ b/src/store-items.js @@ -1,75 +1,38 @@ +const storeItems = [ + { + id: 1, + name: "Beetroot", + icon: "001-beetroot", + price: 0.5, + type: "vegetable", + }, + { id: 2, name: "Carrot", icon: "002-carrot", price: 0.3, type: "vegetable" }, + { id: 3, name: "Apple", icon: "003-apple", price: 0.4, type: "fruit" }, + { id: 4, name: "Banana", icon: "006-bananas", price: 0.2, type: "fruit" }, + { + id: 5, + name: "Bell Pepper", + icon: "007-bell-pepper", + price: 0.6, + type: "vegetable", + }, + { id: 6, name: "Berry", icon: "008-berry", price: 0.5, type: "fruit" }, + { + id: 7, + name: "Blueberry", + icon: "009-blueberry", + price: 0.7, + type: "fruit", + }, + { + id: 8, + name: "Eggplant", + icon: "010-eggplant", + price: 0.9, + type: "vegetable", + }, + { id: 9, name: "Avocado", icon: "005-avocado", price: 1.2, type: "fruit" }, + { id: 10, name: "Apricot", icon: "004-apricot", price: 1.0, type: "fruit" }, +]; -const storeItems = [ - { - id: "001-beetroot", - name: "beetroot", - price: 0.35, - description: "The beetroot is the taproot portion of a beet plant, usually known in North America as beets while the vegetable is referred to as beetroot in British English, and also known as the table beet, garden beet, red beet, dinner beet or golden beet.", - type: "vegetable" - }, - { - id: "002-carrot", - name: "carrot", - price: 0.35, - description: "The carrot is a root vegetable, typically orange in color, though heirloom variants including purple, black, red, white, and yellow cultivars exist, all of which are domesticated forms of the wild carrot, Daucus carota, native to Europe and Southwestern Asia.", - type: "vegetable" - }, - { - id: "003-apple", - name: "apple", - price: 0.35, - description: "An apple is a round, edible fruit produced by an apple tree (Malus spp., among them the domestic or orchard apple; Malus domestica).", - type: "fruit" - }, - { - id: "004-apricot", - name: "apricot", - price: 0.35, - description: "An apricot is a fruit, or the tree that bears the fruit, of several species in the genus Prunus.", - type: "fruit" - }, - { - id: "005-avocado", - name: "avocado", - price: 0.35, - description: "The avocado, alligator pear or avocado pear (Persea americana) is a medium-sized, evergreen tree in the laurel family (Lauraceae).", - type: "fruit" - }, - { - id: "006-bananas", - name: "bananas", - price: 0.35, - description: "A banana is an elongated, edible fruit – botanically a berry – produced by several kinds of large herbaceous flowering plants in the genus Musa.", - type: "fruit" - }, - { - id: "007-bell-pepper", - name: "bell pepper", - price: 0.35, - description: "The bell pepper (also known as sweet pepper, pepper, capsicum /ˈkæpsɪkəm/ or in some places, mangoes) is the fruit of plants in the Grossum Group of the species Capsicum annuum.", - type: "fruit" - }, - { - id: "008-berry", - name: "berry", - price: 0.35, - description: "A berry is a small, pulpy, and often edible fruit. Typically, berries are juicy, rounded, brightly colored, sweet, sour or tart, and do not have a stone or pit, although many pips or seeds may be present.", - type: "fruit" - }, - { - id: "009-blueberry", - name: "blueberry", - price: 0.35, - description: "Blueberry is a widely distributed and widespread group of perennial flowering plant with blue or purple berries.", - type: "fruit" - }, - { - id: "010-eggplant", - name: "eggplant", - price: 0.35, - description: "Eggplant, aubergine, brinjal, or baigan is a plant species in the nightshade family Solanaceae. Solanum melongena is grown worldwide for its edible fruit.", - type: "vegetable" - } -] - -export default storeItems +export default storeItems; diff --git a/src/styles/index.css b/src/styles/index.css index 0a04c2b..914a72f 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,7 +1,6 @@ @import "./reset.css"; /* Typography */ - body { font-family: "Trebuchet MS", "Lucida Sans", Arial, sans-serif; } @@ -12,22 +11,21 @@ h2 { } /* Buttons */ - button { padding: 0.25rem 0.5rem; - text-transform: uppercase; font-size: 0.725rem; } -/* Store */ +.add-to-cart { + white-space: nowrap; +} +/* Store */ #store { height: 40vh; padding: 1rem; - overflow-y: scroll; - background-color: #e7f4ea; } @@ -66,16 +64,13 @@ button { } /* 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; } @@ -88,9 +83,7 @@ button { min-width: 320px; height: 100%; padding: 0 1rem; - overflow-y: scroll; - border-radius: 0.5rem; border: 2px solid #757575; } @@ -103,14 +96,11 @@ button { .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; } @@ -131,9 +121,7 @@ button { width: 20px; height: 20px; padding: 0; - border-radius: 0.25rem; - font-weight: bold; } @@ -152,21 +140,17 @@ button { .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;