diff --git a/README.md b/README.md index 028e4ec7..01b05a96 100644 --- a/README.md +++ b/README.md @@ -155,3 +155,55 @@ Thanks goes to these wonderful people This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! + +## Notes + +### Components + +Components + +``` +Checkout + +Header (navlinks) + Navlinks + Checkout button (Remove) + +Main (using BrowseRouter) + Process + Form + Input(s) + Submit button + + Footer (navigation buttons) + Navigation Button (Changing inside text depending on page) + +Sidebar + + +States + +Checkout Checkout information (object using context) + +Sidebar Disccount code (Easter egg) +``` + +### States + +State of each form. + +### Images/SVG + +\*Tick/Cross + +\*Lock + +\*PayPal/ApplePay + +\*VISA/MasterCard/AmericanExpress + +\*Back of a card (CVV Code) + +### Resources + +[Material UI](https://material-ui.com/getting-started/installation/) diff --git a/package-lock.json b/package-lock.json index f0fe0912..b13cbf9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2085,6 +2085,25 @@ "chalk": "^4.0.0" } }, + "@popperjs/core": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", + "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==" + }, + "@restart/context": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==" + }, + "@restart/hooks": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.26.tgz", + "integrity": "sha512-7Hwk2ZMYm+JLWcb7R9qIXk1OoUg1Z+saKWqZXlrvFwT3w6UArVNWgxYOzf+PJoK9zZejp8okPAKTctthhXLt5g==", + "requires": { + "lodash": "^4.17.20", + "lodash-es": "^4.17.20" + } + }, "@testing-library/dom": { "version": "7.30.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.30.4.tgz", @@ -2730,6 +2749,11 @@ "integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==", "dev": true }, + "@types/invariant": { + "version": "2.2.34", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.34.tgz", + "integrity": "sha512-lYUtmJ9BqUN688fGY1U1HZoWT1/Jrmgigx2loq4ZcJpICECm/Om3V314BxdzypO0u5PORKGMM6x0OXaljV1YFg==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -2765,6 +2789,39 @@ "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==", "dev": true }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "@types/react": { + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz", + "integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==", + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" + }, + "@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" + }, "@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", @@ -3612,6 +3669,11 @@ "readdirp": "~3.5.0" } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "clsx": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", @@ -3658,6 +3720,11 @@ "which": "^2.0.1" } }, + "csstype": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", + "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" + }, "deepmerge": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", @@ -3669,6 +3736,15 @@ "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==", "dev": true }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -8171,6 +8247,14 @@ } } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -13154,6 +13238,22 @@ } } }, + "prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "requires": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "dependencies": { + "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==" + } + } + }, "property-expr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", @@ -13188,6 +13288,40 @@ } } }, + "react-bootstrap": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.1.tgz", + "integrity": "sha512-ojEPQ6OtyIMdLg0Smofk+85PKN6MLKQX3bU0Vwmok/4yNa8DQ2vCGhO2IgHJvT+ERQZ4X+gAQcdn6msAHSwLBg==", + "requires": { + "@babel/runtime": "^7.14.0", + "@restart/context": "^2.1.4", + "@restart/hooks": "^0.3.26", + "@types/invariant": "^2.2.33", + "@types/prop-types": "^15.7.3", + "@types/react": ">=16.14.8", + "@types/react-transition-group": "^4.4.1", + "@types/warning": "^3.0.0", + "classnames": "^2.3.1", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "prop-types-extra": "^1.1.0", + "react-overlays": "^5.0.1", + "react-transition-group": "^4.4.1", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.5.tgz", + "integrity": "sha512-121rumjddw9c3NCQ55KGkyE1h/nzWhU/owjhw0l4mQrkzz4x9SGS1X8gFLraHwX7td3Yo4QTL+qj0NcIzN87BA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -13238,6 +13372,26 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-overlays": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.0.1.tgz", + "integrity": "sha512-plwUJieTBbLSrgvQ4OkkbTD/deXgxiJdNuKzo6n1RWE3OVnQIU5hffCGS/nvIuu6LpXFs2majbzaXY8rcUVdWA==", + "requires": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.8.6", + "@restart/hooks": "^0.3.26", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + } + }, "react-redux": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.3.tgz", @@ -29765,6 +29919,17 @@ } } }, + "react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", @@ -29883,6 +30048,17 @@ "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", "dev": true }, + "uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "requires": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + } + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -29893,6 +30069,14 @@ "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "web-vitals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-1.1.1.tgz", diff --git a/package.json b/package.json index 7a82bda3..78d9ba93 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "@testing-library/react": "^11.2.6", "@testing-library/user-event": "^13.1.5", "bootstrap": "^4.6.0", + "classnames": "^2.3.1", "clsx": "^1.1.1", "formik": "^2.2.6", "prop-types": "^15.7.2", "react": "^17.0.2", + "react-bootstrap": "^1.6.1", "react-dom": "^17.0.2", "react-redux": "^7.2.3", "react-router-dom": "^5.2.0", diff --git a/public/index.html b/public/index.html index aa069f27..7c0a1cc5 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,12 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + + + React Shopping Wizard @@ -39,5 +44,20 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> + + + diff --git a/src/App.js b/src/App.js index be1d876f..ae089435 100644 --- a/src/App.js +++ b/src/App.js @@ -3,42 +3,38 @@ import { BrowserRouter, Switch, Route } from "react-router-dom"; import Home from "./pages/Home"; import NewProduct from "./pages/NewProduct"; +import Checkout from "./pages/Checkout"; import * as api from "./api"; import useLocalStorage from "./hooks/useLocalStorage"; import loadLocalStorageItems from "./utils/loadLocalStorageItems"; -function buildNewCartItem(cartItem) { - if (cartItem.quantity >= cartItem.unitsInStock) { - return cartItem; - } - - return { - id: cartItem.id, - title: cartItem.title, - img: cartItem.img, - price: cartItem.price, - unitsInStock: cartItem.unitsInStock, - createdAt: cartItem.createdAt, - updatedAt: cartItem.updatedAt, - quantity: cartItem.quantity + 1, - }; -} +import CartContextProvider from "./components/ContextComponents/CartContextProvider"; +import LoginContextProvider from "./components/ContextComponents/LoginContextProvider"; + +// Checkout constants +import { + PROFILE, + BILLING, + PAYMENT, + SUMMARY, + PROFILE_URL, + BILLING_URL, + PAYMENT_URL, + SUMMARY_URL, + HOME_URL, + NEWPROD_URL, +} from "./utils/constants"; const PRODUCTS_LOCAL_STORAGE_KEY = "react-sc-state-products"; -const CART_ITEMS_LOCAL_STORAGE_KEY = "react-sc-state-cart-items"; function App() { const [products, setProducts] = useState(() => loadLocalStorageItems(PRODUCTS_LOCAL_STORAGE_KEY, []), ); - const [cartItems, setCartItems] = useState(() => - loadLocalStorageItems(CART_ITEMS_LOCAL_STORAGE_KEY, []), - ); useLocalStorage(products, PRODUCTS_LOCAL_STORAGE_KEY); - useLocalStorage(cartItems, CART_ITEMS_LOCAL_STORAGE_KEY); const [isLoading, setIsLoading] = useState(false); const [hasError, setHasError] = useState(false); @@ -62,55 +58,6 @@ function App() { } }, []); - function handleAddToCart(productId) { - const prevCartItem = cartItems.find((item) => item.id === productId); - const foundProduct = products.find((product) => product.id === productId); - - if (prevCartItem) { - const updatedCartItems = cartItems.map((item) => { - if (item.id !== productId) { - return item; - } - - if (item.quantity >= item.unitsInStock) { - return item; - } - - return { - ...item, - quantity: item.quantity + 1, - }; - }); - - setCartItems(updatedCartItems); - return; - } - - const updatedProduct = buildNewCartItem(foundProduct); - setCartItems((prevState) => [...prevState, updatedProduct]); - } - - function handleChange(event, productId) { - const updatedCartItems = cartItems.map((item) => { - if (item.id === productId && item.quantity <= item.unitsInStock) { - return { - ...item, - quantity: Number(event.target.value), - }; - } - - return item; - }); - - setCartItems(updatedCartItems); - } - - function handleRemove(productId) { - const updatedCartItems = cartItems.filter((item) => item.id !== productId); - - setCartItems(updatedCartItems); - } - function handleDownVote(productId) { const updatedProducts = products.map((product) => { if ( @@ -180,29 +127,42 @@ function App() { } return ( - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/src/components/AppHeader/AppHeader.js b/src/components/AppHeader/AppHeader.js index d40c427e..0f7eeb87 100644 --- a/src/components/AppHeader/AppHeader.js +++ b/src/components/AppHeader/AppHeader.js @@ -1,35 +1,82 @@ -import React from "react"; +import React, { useState, useContext } from "react"; import { NavLink } from "react-router-dom"; import "./AppHeader.scss"; +import Button from "../Button"; +import LoginModal from "../LoginModal"; + +import LoginContext from "../../context/login-context"; + +function AppHeader({ showNewProductForm, ...props }) { + const { data: loginData, setData: updateLoginData } = useContext( + LoginContext, + ); + const [showModal, setShowModal] = useState(false); + + function handleLogOut() { + updateLoginData({ + loginName: "", + loginPassword: "", + isLogged: false, + }); + } + + function handleShowModal() { + setShowModal(true); + } -function AppHeader({ ...props }) { return ( -
+
-
diff --git a/src/components/AppHeader/AppHeader.scss b/src/components/AppHeader/AppHeader.scss index b1869068..b7cea70e 100644 --- a/src/components/AppHeader/AppHeader.scss +++ b/src/components/AppHeader/AppHeader.scss @@ -1,2 +1,24 @@ -.AppHeader { +.navbar { + width: 100%; + .navbar-group { + display: flex; + justify-content: space-between; + align-items: center; + } + + .right-nav { + display: flex; + align-items: center; + .login-name { + color: blue; + margin-bottom: 0; + margin-right: 20px; + } + button { + min-width: 80px; + text-align: center; + border: 1px solid white; + border-radius: 3px; + } + } } diff --git a/src/components/CardSvg/CardSvg.js b/src/components/CardSvg/CardSvg.js new file mode 100644 index 00000000..f033f4c7 --- /dev/null +++ b/src/components/CardSvg/CardSvg.js @@ -0,0 +1,28 @@ +import React from "react"; + +import front from "../../img/payment/card-front.svg"; +import back from "../../img/payment/card-back.svg"; + +import "./CardSvg.scss"; + +function CardSvg({ isFront, cardName, cardNumber, cardDate, cardCVV }) { + return ( +
+ {isFront ? ( + <> + front card +

{cardName}

+

{cardNumber}

+

{cardDate}

+ + ) : ( + <> + back card +

{cardCVV}

+ + )} +
+ ); +} + +export default CardSvg; diff --git a/src/components/CardSvg/CardSvg.scss b/src/components/CardSvg/CardSvg.scss new file mode 100644 index 00000000..2d2e520b --- /dev/null +++ b/src/components/CardSvg/CardSvg.scss @@ -0,0 +1,29 @@ +.card-illustration { + position: relative; + min-width: 400px; + * { + position: absolute; + } + img { + width: 100%; + height: auto; + } + .card-name { + top: 200px; + left: 25px; + } + .card-number { + font-size: 24px; + top: 150px; + left: 25px; + } + .card-date { + top: 200px; + left: 330px; + } + .card-cvv { + font-style: italic; + top: 137px; + left: 45px; + } +} diff --git a/src/components/CardSvg/index.js b/src/components/CardSvg/index.js new file mode 100644 index 00000000..7e749808 --- /dev/null +++ b/src/components/CardSvg/index.js @@ -0,0 +1 @@ +export { default } from "./CardSvg"; diff --git a/src/components/Cart/Cart.js b/src/components/Cart/Cart.js index f9f76aca..e5517f0d 100644 --- a/src/components/Cart/Cart.js +++ b/src/components/Cart/Cart.js @@ -1,15 +1,31 @@ -import React from "react"; +import React, { useEffect, useState, useContext } from "react"; + +import { Link } from "react-router-dom"; import ShoppingCartItem from "../ShoppingCartItem"; import Button from "../Button"; -function getCartTotal(cart) { - return cart.reduce((accum, item) => { - return accum + item.price * item.quantity; - }, 0); -} +import { PROFILE_URL } from "../../utils/constants"; + +import CartContext from "../../context/cart-context"; +import LoginContext from "../../context/login-context"; + +import getCartTotal from "../../utils/getCartTotal"; + +function Cart({ ...props }) { + const { cartItems, remove, change } = useContext(CartContext); + const [hasCartItems, setHasCartItems] = useState(false); + + const { data: loginData } = useContext(LoginContext); + + useEffect(() => { + if (cartItems.length > 0) { + setHasCartItems(true); + } else { + setHasCartItems(false); + } + }, [cartItems, loginData.isLogged]); -function Cart({ cartItems, handleRemove, handleChange, ...props }) { return (