├── public ├── _redirects ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── apple-icon.png ├── plantsicon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-70x70.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── android-icon-144x144.png ├── android-icon-192x192.png ├── apple-icon-precomposed.png ├── browserconfig.xml ├── serviceWorker.js ├── manifest.json └── index.html ├── .prettierrc ├── src ├── assets │ └── svg │ │ ├── NotFound.png │ │ ├── plantsicon.png │ │ ├── plants-vector-free-icon-set-30.png │ │ ├── removeIcon.svg │ │ ├── arrow.svg │ │ ├── Loader.svg │ │ ├── backArrow.svg │ │ ├── magify.svg │ │ ├── morphing.svg │ │ ├── bluePot.svg │ │ ├── redPot.svg │ │ ├── greyPot.svg │ │ ├── yellowPot.svg │ │ ├── cart.svg │ │ ├── plant.svg │ │ └── 404.svg ├── setupTests.js ├── index.js ├── DatoCMS │ └── DatoCMS.js ├── components │ ├── atoms │ │ ├── Plant │ │ │ ├── Plant.stories.js │ │ │ └── Plant.js │ │ ├── Loader │ │ │ ├── Loader.stories.js │ │ │ └── Loader.js │ │ ├── Input │ │ │ ├── Input.stories.js │ │ │ ├── Search.stories.js │ │ │ ├── Input.js │ │ │ └── Search.js │ │ ├── Button │ │ │ ├── CartButton.stories.js │ │ │ ├── StripeButton.stories.js │ │ │ ├── Button.stories.js │ │ │ ├── StripeButton.js │ │ │ ├── CartButton.js │ │ │ └── Button.js │ │ ├── PlantIcon │ │ │ ├── PlantIcon.stories.js │ │ │ └── PlantIcon.js │ │ ├── RangeInput │ │ │ ├── RangeInput.stories.js │ │ │ └── RangeInput.js │ │ ├── Heading │ │ │ ├── Heading.stories.js │ │ │ └── Heading.js │ │ ├── SelectInput │ │ │ ├── SelectInput.stories.js │ │ │ └── SelectInput.js │ │ └── Text │ │ │ ├── Text.stories.js │ │ │ └── Text.js │ ├── organisms │ │ ├── Header.js │ │ ├── ErrorBoundary.js │ │ ├── Hero.js │ │ └── Products.js │ └── molecules │ │ ├── Product.js │ │ ├── Preferences.js │ │ ├── HeaderIcons.js │ │ ├── Modal.js │ │ ├── Cart.js │ │ ├── CartProduct.js │ │ ├── PlantHalfPage.js │ │ ├── FlowerPots.js │ │ └── CheckoutItem.js ├── __tests__ │ ├── Buton.test.js │ ├── Loader.test.js │ ├── Plant.test.js │ └── Search.test.js ├── templates │ └── MainTemplate.js ├── firebase │ └── Firebase.js ├── theme │ ├── MainTheme.js │ └── GlobalStyles.js ├── views │ ├── Home.js │ ├── Root.js │ ├── Checkout.js │ ├── SinglePlant.js │ └── Login.js ├── utils │ └── CartUtils.js └── context │ └── CartContext.js ├── .storybook ├── addons.js ├── preview-head.html └── config.js ├── .gitignore ├── .circleci └── config.yml ├── .eslintrc ├── LICENSE ├── package.json └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100 5 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /public/plantsicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/plantsicon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /src/assets/svg/NotFound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/src/assets/svg/NotFound.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/assets/svg/plantsicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/src/assets/svg/plantsicon.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/react/cleanup-after-each'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-knobs/register'; 4 | -------------------------------------------------------------------------------- /src/assets/svg/plants-vector-free-icon-set-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olafsulich/E-commerce-Plants-Shop/HEAD/src/assets/svg/plants-vector-free-icon-set-30.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './views/Root'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/DatoCMS/DatoCMS.js: -------------------------------------------------------------------------------- 1 | const SiteClient = require('datocms-client').SiteClient; 2 | 3 | const client = new SiteClient('2878717a758346046aadf66625054f'); 4 | 5 | export default client; 6 | -------------------------------------------------------------------------------- /src/components/atoms/Plant/Plant.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Plant from './Plant'; 4 | 5 | storiesOf('Atoms/Plant', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/components/atoms/Loader/Loader.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Loader from './Loader'; 4 | 5 | storiesOf('Atoms/Loader', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/__tests__/Buton.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Button from '../components/atoms/Button/Button'; 4 | 5 | it('test button', () => { 6 | render(); 7 | }); 8 | -------------------------------------------------------------------------------- /src/components/atoms/Input/Input.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Input from './Input'; 4 | 5 | storiesOf('Atoms/Input', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/components/atoms/Button/CartButton.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import CartButton from './CartButton'; 4 | 5 | storiesOf('Atoms/CartButton', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/components/atoms/PlantIcon/PlantIcon.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import PlantIcon from './PlantIcon'; 4 | 5 | storiesOf('Atoms/PlantIcon', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/components/atoms/RangeInput/RangeInput.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import RangeInput from './RangeInput'; 4 | 5 | storiesOf('Atoms/RangeInput', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/components/atoms/Button/StripeButton.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import StripeButton from './StripeButton'; 4 | 5 | storiesOf('Atoms/StripeButton', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/components/atoms/Heading/Heading.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Heading from './Heading'; 4 | 5 | storiesOf('Atoms/Heading', module).add('Main', () => Plants & Home); 6 | -------------------------------------------------------------------------------- /src/components/atoms/SelectInput/SelectInput.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import SelectInput from './SelectInput'; 4 | 5 | storiesOf('Atoms/SelectInput', module).add('Normal', () => ); 6 | -------------------------------------------------------------------------------- /src/components/atoms/Input/Search.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Search from './Search'; 4 | 5 | storiesOf('Atoms/Search', module).add('Normal', () => ( 6 | 7 | )); 8 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /src/assets/svg/removeIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/atoms/Text/Text.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Text from './Text'; 4 | 5 | storiesOf('Atoms/Text', module) 6 | .add('Normal', () => Simple text...) 7 | .add('Logo', () => Simple text...) 8 | .add('Error', () => Simple text...); 9 | -------------------------------------------------------------------------------- /src/components/atoms/Button/Button.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Button from './Button'; 4 | 5 | storiesOf('Atoms/Button', module) 6 | .add('Normal', () => ) 7 | .add('Active', () => ) 8 | .add('Secondary', () => ) 9 | .add('Remove', () => 52 | ) : ( 53 | 56 | )} 57 | 58 | ); 59 | }; 60 | HeaderIcons.propTypes = { 61 | isSinglePlant: PropTypes.bool, 62 | }; 63 | HeaderIcons.defaultProps = { 64 | isSinglePlant: null, 65 | }; 66 | export default HeaderIcons; 67 | -------------------------------------------------------------------------------- /src/assets/svg/bluePot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/svg/redPot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/svg/greyPot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/svg/yellowPot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/molecules/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import styled from 'styled-components'; 5 | import Button from '../atoms/Button/Button'; 6 | import Text from '../atoms/Text/Text'; 7 | 8 | const StyledWrapper = styled.div` 9 | z-index: ${({ isVisible }) => (isVisible ? '10' : '-1')}; 10 | position: absolute; 11 | top: 75%; 12 | right: 50%; 13 | transform: translate(50%, ${({ isVisible }) => (isVisible ? '-50%' : '0')}); 14 | width: 24rem; 15 | height: 13rem; 16 | border: 2px solid ${({ theme }) => theme.fontColorHeading}; 17 | background-color: #fff; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: space-around; 22 | will-change: opacity, transform; 23 | opacity: ${({ isVisible }) => (isVisible ? '1' : '0')}; 24 | transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out; 25 | @media only screen and (min-width: 600px) { 26 | top: 60%; 27 | } 28 | @media only screen and (min-width: 1000px) { 29 | top: 50%; 30 | } 31 | `; 32 | 33 | const StyledButtonsWrapper = styled.div` 34 | display: flex; 35 | align-items: center; 36 | justify-content: space-between; 37 | `; 38 | const StyledLink = styled(Link)` 39 | margin: 0 0 0 2rem; 40 | text-decoration: none; 41 | `; 42 | 43 | const StyledButton = styled(Button)` 44 | width: 8rem; 45 | height: 3rem; 46 | text-align: center; 47 | padding: 0; 48 | `; 49 | 50 | const Modal = ({ isVisible, handleModalChange }) => ( 51 | 52 | Added to cart 53 | 54 | 55 | Continue 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | Modal.propTypes = { 64 | isVisible: PropTypes.bool.isRequired, 65 | handleModalChange: PropTypes.func.isRequired, 66 | }; 67 | export default Modal; 68 | -------------------------------------------------------------------------------- /src/components/molecules/Cart.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import styled from 'styled-components'; 5 | import Button from '../atoms/Button/Button'; 6 | import CartProduct from './CartProduct'; 7 | import { CartContext } from '../../context/CartContext'; 8 | 9 | const StyledWrapper = styled.div` 10 | z-index: 100; 11 | position: absolute; 12 | top: 5rem; 13 | right: 1rem; 14 | width: 20rem; 15 | height: 24rem; 16 | border: 2px solid ${({ theme }) => theme.fontColorHeading}; 17 | background-color: #fff; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: space-between; 22 | will-change: opacity, transform; 23 | opacity: ${({ isVisible }) => (isVisible ? '1' : '0')}; 24 | transition: opacity 0.5s ease-in-out; 25 | `; 26 | 27 | const StyledProductsWrapper = styled.div` 28 | display: flex; 29 | flex-direction: column; 30 | align-items: flex-start; 31 | margin: 0rem 0 1rem 0; 32 | padding: 1rem 0 0 0; 33 | overflow-y: scroll; 34 | 35 | ::-webkit-scrollbar { 36 | display: none; 37 | } 38 | `; 39 | 40 | const StyledButton = styled(Button)` 41 | margin: 0 0 1.3rem 0; 42 | width: 10rem; 43 | height: 3rem; 44 | `; 45 | const StyledLink = styled(Link)` 46 | text-decoration: none; 47 | `; 48 | const Cart = ({ isVisible }) => { 49 | const { cartItems } = useContext(CartContext); 50 | 51 | return ( 52 | 53 | 54 | {cartItems.length ? ( 55 | cartItems.map(cartItem => ) 56 | ) : ( 57 | cart is empty 58 | )} 59 | 60 | 61 | 62 | Checkout 63 | 64 | 65 | 66 | ); 67 | }; 68 | Cart.propTypes = { 69 | isVisible: PropTypes.bool.isRequired, 70 | }; 71 | 72 | export default Cart; 73 | -------------------------------------------------------------------------------- /src/views/Root.js: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import { BrowserRouter, Switch, Route } from 'react-router-dom'; 3 | import CartProvider from '../context/CartContext'; 4 | import MainTemplate from '../templates/MainTemplate'; 5 | import { fire } from '../firebase/Firebase'; 6 | import Loader from '../components/atoms/Loader/Loader'; 7 | import ErrorBoundary from '../components/organisms/ErrorBoundary'; 8 | 9 | const Home = lazy(() => import('./Home')); 10 | const SinglePlant = lazy(() => import('./SinglePlant')); 11 | const Login = lazy(() => import('./Login')); 12 | const Checkout = lazy(() => import('./Checkout')); 13 | 14 | class Root extends React.Component { 15 | state = { 16 | user: {}, 17 | }; 18 | 19 | componentDidMount = () => { 20 | this.authListener(); 21 | }; 22 | 23 | authListener = () => { 24 | fire.auth().onAuthStateChanged(user => { 25 | if (user) { 26 | this.setState({ user }); 27 | localStorage.setItem('user', user.uid); 28 | } else { 29 | this.setState({ user: null }); 30 | localStorage.removeItem('user'); 31 | } 32 | }); 33 | }; 34 | 35 | render() { 36 | const { user } = this.state; 37 | return ( 38 | 39 | 40 | 41 |
42 | {!user ? ( 43 | }> 44 | 45 | 46 | ) : ( 47 | 48 | }> 49 | 50 | 51 | 52 | 53 | 54 | )} 55 |
56 |
57 |
58 |
59 | ); 60 | } 61 | } 62 | 63 | export default function ErrorBoundaryFunc(props) { 64 | return ( 65 | 66 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/molecules/CartProduct.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import Button from '../atoms/Button/Button'; 5 | import { CartContext } from '../../context/CartContext'; 6 | 7 | const StyledWrapper = styled.div` 8 | display: grid; 9 | grid-template-columns: repeat(3, 1fr); 10 | align-items: center; 11 | justify-content: space-between; 12 | z-index: 10; 13 | padding: 2.5rem 1.25rem 0 1.25rem; 14 | min-width: 200px; 15 | `; 16 | 17 | const StyledProductImage = styled.figure` 18 | width: 7.5rem; 19 | height: 5rem; 20 | margin-right: 0.8rem; 21 | 22 | img { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | `; 27 | 28 | const StyledInfoWrapper = styled.section` 29 | display: flex; 30 | align-items: flex-start; 31 | justify-content: space-around; 32 | flex-direction: column; 33 | margin-right: 1rem; 34 | `; 35 | 36 | const StyledTitle = styled.h3` 37 | color: ${({ theme }) => theme.fontColorHeading}; 38 | font-size: 1.2rem; 39 | font-weight: ${({ theme }) => theme.regular}; 40 | margin: 0 0 1.5rem 0; 41 | `; 42 | const StyledQuantity = styled.p` 43 | color: ${({ theme }) => theme.fontColorHeading}; 44 | font-size: 1rem; 45 | font-weight: ${({ theme }) => theme.light}; 46 | `; 47 | const StyledButton = styled(Button)` 48 | justify-self: center; 49 | `; 50 | 51 | const CartProduct = ({ plant }) => { 52 | const { clearItemFromCart } = useContext(CartContext); 53 | const { plantTitle, plantImage, plantPrice, quantity } = plant; 54 | 55 | return ( 56 | 57 | 58 | product picure 59 | 60 | 61 | {plantTitle} 62 | 63 | {quantity} x ${plantPrice} 64 | 65 | 66 | clearItemFromCart(plant)} /> 67 | 68 | ); 69 | }; 70 | CartProduct.propTypes = { 71 | plant: PropTypes.object.isRequired, 72 | }; 73 | export default React.memo(CartProduct); 74 | -------------------------------------------------------------------------------- /src/components/atoms/Input/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import magnifierIcon from '../../../assets/svg/magify.svg'; 5 | 6 | const SelectWrapper = styled.div` 7 | width: 100%; 8 | width: 24rem; 9 | margin: 2rem 1.5rem; 10 | @media only screen and (min-width: 500px) { 11 | width: 28rem; 12 | } 13 | @media only screen and (min-width: 700px) { 14 | width: 24rem; 15 | margin: 0rem 1.5rem; 16 | } 17 | `; 18 | 19 | const StyledInput = styled.input` 20 | width: 100%; 21 | height: 100%; 22 | padding: 15px 30px; 23 | font-size: 1.2rem; 24 | font-weight: ${({ theme }) => theme.regular}; 25 | background-color: ${({ theme }) => theme.secondaryColor}; 26 | padding: 10px 20px 10px 20px; 27 | border: none; 28 | border-radius: 50px; 29 | background-image: url(${magnifierIcon}); 30 | background-size: 15px; 31 | background-position: 92% 50%; 32 | background-repeat: no-repeat; 33 | 34 | ::placeholder { 35 | letter-spacing: 1px; 36 | color: ${({ theme }) => theme.fontColorText}; 37 | } 38 | `; 39 | const StyledLabel = styled.label` 40 | display: block; 41 | font-size: 0.95rem; 42 | font-weight: ${({ theme }) => theme.regular}; 43 | text-align: start; 44 | margin-bottom: 0.5rem; 45 | margin-left: 0.5rem; 46 | `; 47 | 48 | const Search = ({ name, value, onChange, placeholder }) => { 49 | return ( 50 | 51 | Search 52 | 63 | 64 | ); 65 | }; 66 | Search.propTypes = { 67 | name: PropTypes.string, 68 | onChange: PropTypes.func, 69 | value: PropTypes.string, 70 | placeholder: PropTypes.string, 71 | }; 72 | 73 | Search.defaultProps = { 74 | placeholder: 'search', 75 | name: 'search', 76 | onChange: () => {}, 77 | value: 'search', 78 | }; 79 | 80 | export default Search; 81 | -------------------------------------------------------------------------------- /src/assets/svg/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 12 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Project Overview 🎉 4 | 5 | E-commerce plants shop. I used 6 | Atomic Design for components architecture and 7 | Storybook for components documentation. 8 | When you first enter the page, you going to see sing in / sign up form. 9 | I used Firebase OAuth for authentication. After sign in you can search and filter plants according to your needs. I used Dato CMS for handling plants data. You can add your favourites plats to cart and finally buy them by Stripe. App supports CI/CD and Progressive Web Apps(PWA). 10 | 11 | ## Tech/framework used 🔧 12 | 13 | - React 14 | - Context API 15 | - Hooks 16 | - React Router 17 | - Styled-Compontens 18 | - Firebase OAuth 19 | - Dato CMS 20 | - StoryBook 21 | - Netlify 22 | - CircleCI 23 | - PWA 24 | - Stripe 25 | - Husky & Lint-staged 26 | - Tools: Webpack, Eslint, Prettier 27 | 28 | ## Screenshots 📺 29 | 30 |

31 | Screen Shot 32 |

33 | 34 |

35 | Screen Shot 36 |

37 | 38 |

39 | Screen Shot 40 |

41 | 42 | ## Performance 🚀 43 | 44 |

45 | Screen Shot 46 |

47 | 48 |

It may be diffrent on your device.

49 | 50 | ## Code Example/Issues 🔍 51 | 52 | If you have any issues, please let me know in the issues section or directly to olafsulich@gmail.com 53 | 54 | ## Installation 💾 55 | 56 | ```bash 57 | git clone https://github.com/olafsulich/E-commerce-Plants-Shop.git 58 | npm install 59 | npm run start 60 | ``` 61 | 62 | ## Sign in ❗️ 63 | 64 | - Email: TestUser@gmail.com 65 | - Password: TestUser1 66 | 67 | ## Credits 👏 68 | 69 | Big thanks to Bartosz Szczeciński from React Polska. Bartosz helps me with problem during development. 70 | 71 | ## Live 📍 72 | 73 | https://plants-and-home.netlify.com 74 | 75 | ## License 🔱 76 | 77 | Under license (MIT, Apache etc) 78 | 79 | MIT © [Olaf Sulich]() 80 | -------------------------------------------------------------------------------- /src/components/molecules/PlantHalfPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | import PropTypes from 'prop-types'; 5 | import Text from '../atoms/Text/Text'; 6 | import Heroplant from '../atoms/Plant/Plant'; 7 | import HeaderIcons from './HeaderIcons'; 8 | import Button from '../atoms/Button/Button'; 9 | 10 | const StyledPlantWrapper = styled.div` 11 | background: hsl(153, 91%, 48%, 40%); 12 | background-color: ${({ theme }) => theme.halfPlantColor}; 13 | width: 100%; 14 | height: 100vh; 15 | display: flex; 16 | overflow: hidden; 17 | align-items: center; 18 | justify-content: space-around; 19 | flex-direction: column; 20 | padding: 4rem 4rem 12rem 4rem; 21 | @media only screen and (min-width: 1000px) { 22 | width: 50%; 23 | } 24 | `; 25 | 26 | const StyledLogoWrapper = styled.section` 27 | margin: 0 0 10rem 1rem; 28 | width: 100%; 29 | display: flex; 30 | align-items: center; 31 | justify-content: space-between; 32 | `; 33 | const StyledLink = styled(Link)` 34 | text-decoration: none; 35 | `; 36 | const StyledLinkArrow = styled(Link)` 37 | text-decoration: none; 38 | margin-left: 6.5rem; 39 | @media only screen and (max-width: 500px) { 40 | margin-left: 0; 41 | } 42 | `; 43 | 44 | const PlantHalfPage = ({ isLoginPage, isSinglePlant, isBackArrow }) => { 45 | return ( 46 | 47 | 48 | {isBackArrow ? : null} 49 | {isBackArrow ? ( 50 | 51 | 52 | Plants & Home 53 | 54 | 55 | ) : ( 56 | 57 | 58 | Plants & Home 59 | 60 | 61 | )} 62 | 63 | {isLoginPage ? null : } 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | PlantHalfPage.propTypes = { 71 | isLoginPage: PropTypes.bool, 72 | isSinglePlant: PropTypes.bool, 73 | isBackArrow: PropTypes.bool, 74 | }; 75 | PlantHalfPage.defaultProps = { 76 | isLoginPage: null, 77 | isSinglePlant: null, 78 | isBackArrow: null, 79 | }; 80 | 81 | export default PlantHalfPage; 82 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Plants & Home 11 | 15 | 16 | 20 | 21 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 46 | 47 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/components/atoms/SelectInput/SelectInput.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { CartContext } from '../../../context/CartContext'; 5 | 6 | import Arrow from '../../../assets/svg/arrow.svg'; 7 | 8 | const SelectWrapper = styled.div` 9 | width: 100%; 10 | margin: 2rem 1.5rem; 11 | width: 24rem; 12 | @media only screen and (min-width: 500px) { 13 | width: 28rem; 14 | } 15 | @media only screen and (min-width: 700px) { 16 | width: 24rem; 17 | margin: 0rem 1.5rem; 18 | } 19 | `; 20 | 21 | const StyledSelect = styled.select` 22 | background: ${({ theme }) => theme.secondaryColor}; 23 | border: none; 24 | width: 100%; 25 | padding: 10px 20px 10px 20px; 26 | -moz-box-sizing: border-box; 27 | -webkit-box-sizing: border-box; 28 | box-sizing: border-box; 29 | position: relative; 30 | z-index: 2; 31 | cursor: pointer; 32 | -webkit-appearance: none; 33 | -moz-appearance: none; 34 | appearance: none; 35 | text-indent: 0%; 36 | text-overflow: ''; 37 | background-image: url(${Arrow}); 38 | background-position: 95% 50%; 39 | background-repeat: no-repeat; 40 | background-size: 20px 20px; 41 | color: ${({ theme }) => theme.fontColorText}; 42 | font-weight: ${({ theme }) => theme.regular}; 43 | font-size: 1.3rem; 44 | `; 45 | 46 | const StyledOption = styled.option` 47 | width: 100%; 48 | font-size: 1.1rem; 49 | text-transform: capitalize; 50 | 51 | &:hover, 52 | &:active, 53 | &:focus, 54 | &:checked { 55 | background: ${({ theme }) => theme.fontColorPrimary}; 56 | color: #fff; 57 | } 58 | `; 59 | const StyledLabel = styled.label` 60 | display: block; 61 | font-size: 0.95rem; 62 | font-weight: ${({ theme }) => theme.regular}; 63 | text-align: start; 64 | margin-bottom: 0.5rem; 65 | `; 66 | 67 | const SelectInput = props => { 68 | const context = useContext(CartContext); 69 | const { plants } = context; 70 | const getUnique = (items, value) => { 71 | return [...new Set(items.map(item => item[value]))]; 72 | }; 73 | let types = getUnique(plants, 'plantType'); 74 | types = ['all', ...types]; 75 | const { name, onChange, value } = props; 76 | 77 | types = types.map(item => { 78 | return ( 79 | 80 | {item} 81 | 82 | ); 83 | }); 84 | return ( 85 | 86 | Select type 87 | 88 | {types} 89 | 90 | 91 | ); 92 | }; 93 | SelectInput.propTypes = { 94 | name: PropTypes.string.isRequired, 95 | onChange: PropTypes.func.isRequired, 96 | value: PropTypes.string.isRequired, 97 | }; 98 | export default SelectInput; 99 | -------------------------------------------------------------------------------- /src/components/atoms/Button/Button.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import removeIcon from '../../../assets/svg/removeIcon.svg'; 3 | import backIcon from '../../../assets/svg/backArrow.svg'; 4 | 5 | const StyledButton = styled.button` 6 | display: block; 7 | color: ${({ theme }) => theme.fontColorHeading}; 8 | border: none; 9 | text-decoration: none; 10 | cursor: pointer; 11 | font-weight: ${({ theme }) => theme.light}; 12 | font-size: 1.5rem; 13 | font-family: inherit; 14 | ${({ active }) => 15 | active && 16 | css` 17 | position: relative; 18 | color: ${({ theme }) => theme.fontColorHeading}; 19 | font-size: 1.2rem; 20 | font-weight: ${({ theme }) => theme.regular}; 21 | margin-left: 0.6rem; 22 | ::before { 23 | content: ''; 24 | position: absolute; 25 | width: 100%; 26 | height: 50%; 27 | background-color: ${({ theme }) => theme.primaryColor}; 28 | z-index: -1; 29 | top: 60%; 30 | left: 15%; 31 | } 32 | `}; 33 | ${({ secondary }) => 34 | secondary && 35 | css` 36 | color: ${({ theme }) => theme.fontColorHeading}; 37 | font-weight: ${({ theme }) => theme.regular}; 38 | font-size: 1.3rem; 39 | background-color: ${({ theme }) => theme.buttonColor}; 40 | width: 8rem; 41 | height: 3rem; 42 | padding: 0.5rem; 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | `}; 47 | ${({ quantity }) => 48 | quantity && 49 | css` 50 | font-size: 1.3rem; 51 | cursor: pointer; 52 | color: ${({ theme }) => theme.fontColorPrimary}; 53 | `}; 54 | 55 | ${({ remove }) => 56 | remove && 57 | css` 58 | align-self: center; 59 | justify-self: center; 60 | width: 20px; 61 | height: 20px; 62 | cursor: pointer; 63 | background-image: url(${removeIcon}); 64 | `}; 65 | 66 | ${({ back }) => 67 | back && 68 | css` 69 | align-self: center; 70 | justify-self: center; 71 | width: 40px; 72 | height: 19px; 73 | cursor: pointer; 74 | background-image: url(${backIcon}); 75 | @media only screen and (max-width: 500px) { 76 | display: none; 77 | } 78 | `}; 79 | ${({ logoutMain }) => 80 | logoutMain && 81 | css` 82 | color: ${({ theme }) => theme.fontColorPrimary}; 83 | background-color: ${({ theme }) => theme.secondaryColor}; 84 | padding: 0.6rem 1.8rem; 85 | font-size: 1.3rem; 86 | `}; 87 | 88 | ${({ logoutSinglePlant }) => 89 | logoutSinglePlant && 90 | css` 91 | color: ${({ theme }) => theme.fontColorHeading}; 92 | background-color: ${({ theme }) => theme.primaryColor}; 93 | padding: 0.6rem 1.8rem; 94 | font-size: 1.3rem; 95 | `}; 96 | `; 97 | export default StyledButton; 98 | -------------------------------------------------------------------------------- /src/components/molecules/FlowerPots.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import { CartContext } from '../../context/CartContext'; 4 | import Text from '../atoms/Text/Text'; 5 | import greyPot from '../../assets/svg/greyPot.svg'; 6 | import bluePot from '../../assets/svg/bluePot.svg'; 7 | import yellowPot from '../../assets/svg/yellowPot.svg'; 8 | import redPot from '../../assets/svg/redPot.svg'; 9 | 10 | const StyledWrapper = styled.div` 11 | padding: 2rem; 12 | width: 100%; 13 | display: grid; 14 | grid-template-columns: repeat(2, 1fr); 15 | grid-gap: 1rem; 16 | align-items: center; 17 | justify-items: center; 18 | @media only screen and (min-width: 400px) { 19 | grid-template-columns: repeat(4, 1fr); 20 | } 21 | `; 22 | 23 | const StyledPot = styled.figure` 24 | cursor: pointer; 25 | width: 7rem; 26 | height: 7rem; 27 | 28 | ${({ active }) => 29 | active && 30 | css` 31 | border: solid 4px #f3f6f8; 32 | `} 33 | `; 34 | 35 | const StyledText = styled(Text)` 36 | font-size: 2.4rem; 37 | `; 38 | 39 | const StyledImage = styled.img` 40 | width: 100%; 41 | height: 100%; 42 | padding: 0.8rem; 43 | outline: dashed 4px #f3f6f8; 44 | :focus { 45 | outline: solid 4px #f3f6f8; 46 | } 47 | `; 48 | 49 | const FlowerPots = () => { 50 | const { changeColor } = useContext(CartContext); 51 | const handleOnKeyPress = e => { 52 | if (e.keyCode === 0) { 53 | changeColor(e); 54 | } 55 | }; 56 | 57 | return ( 58 | <> 59 | Flowerpot preview 60 | 61 | 62 | 71 | 72 | 73 | 82 | 83 | 84 | 93 | 94 | 95 | 104 | 105 | 106 | 107 | ); 108 | }; 109 | 110 | export default FlowerPots; 111 | -------------------------------------------------------------------------------- /src/components/molecules/CheckoutItem.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import Button from '../atoms/Button/Button'; 5 | import { CartContext } from '../../context/CartContext'; 6 | 7 | const StyledWrapper = styled.section` 8 | display: grid; 9 | grid-template-columns: repeat(5, 1fr); 10 | align-items: start; 11 | justify-content: start; 12 | z-index: 10; 13 | padding: 1.5rem 0.5rem 0 0; 14 | grid-column-gap: 1.5rem; 15 | `; 16 | 17 | const StyledProductImage = styled.figure` 18 | width: 7.5rem; 19 | height: 5rem; 20 | 21 | img { 22 | width: 100%; 23 | height: 100%; 24 | } 25 | @media only screen and (min-width: 500px) { 26 | width: 8.5rem; 27 | } 28 | @media only screen and (min-width: 800px) { 29 | width: 9rem; 30 | } 31 | `; 32 | 33 | const StyledTitle = styled.h3` 34 | align-self: center; 35 | justify-self: center; 36 | color: ${({ theme }) => theme.fontColorHeading}; 37 | font-size: 1.3rem; 38 | font-weight: ${({ theme }) => theme.light}; 39 | @media only screen and (min-width: 500px) { 40 | font-size: 1.5rem; 41 | } 42 | @media only screen and (min-width: 800px) { 43 | font-size: 1.6rem; 44 | } 45 | `; 46 | const StyledQuantityWrapper = styled.div` 47 | align-self: center; 48 | justify-self: center; 49 | display: flex; 50 | `; 51 | 52 | const StyledQuantityValue = styled.span` 53 | align-self: center; 54 | padding: 0 1rem; 55 | color: ${({ theme }) => theme.fontColorHeading}; 56 | font-size: 1.2rem; 57 | font-weight: ${({ theme }) => theme.regular}; 58 | @media only screen and (min-width: 500px) { 59 | font-size: 1.4rem; 60 | } 61 | @media only screen and (min-width: 800px) { 62 | font-size: 1.5rem; 63 | } 64 | `; 65 | 66 | const StyledPrice = styled.span` 67 | align-self: center; 68 | justify-self: center; 69 | font-weight: ${({ theme }) => theme.regular}; 70 | font-size: 1.2rem; 71 | @media only screen and (min-width: 500px) { 72 | font-size: 1.4rem; 73 | } 74 | @media only screen and (min-width: 800px) { 75 | font-size: 1.5rem; 76 | } 77 | `; 78 | 79 | const CheckoutItem = ({ plant }) => { 80 | const { plantTitle, plantImage, plantPrice, quantity } = plant; 81 | const { addItem, removeItem, clearItemFromCart } = useContext(CartContext); 82 | 83 | return ( 84 | 85 | 86 | pic 87 | 88 | {plantTitle} 89 | 90 | 93 | {quantity} 94 | 97 | 98 | ${plantPrice} 99 | 159 | 160 | 161 | 162 | ); 163 | } 164 | const { plantTitle, plantPrice, plantDescription, plantType } = plant; 165 | const { isModal } = this.state; 166 | return ( 167 | 168 | 169 | 170 | 171 | {plantTitle} 172 | 173 | 174 | type: 175 | {plantType} 176 | 177 | {plantDescription} 178 | 179 | 180 | ${plantPrice} 181 | { 185 | addItem(plant); 186 | this.openModal(); 187 | }} 188 | > 189 | Add to cart 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | ); 198 | } 199 | } 200 | 201 | SinglePlant.propTypes = { 202 | match: PropTypes.any.isRequired, 203 | }; 204 | export default SinglePlant; 205 | -------------------------------------------------------------------------------- /src/context/CartContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import DatoCMSData from '../DatoCMS/DatoCMS'; 4 | 5 | import { 6 | addItemToCart, 7 | removeItemFromCart, 8 | filterItemFromCart, 9 | getCartItemsCount, 10 | getCartTotal, 11 | } from '../utils/CartUtils'; 12 | 13 | export const CartContext = createContext({ 14 | plants: [], 15 | filtredPlants: [], 16 | cartItems: [], 17 | addItem: () => {}, 18 | removeItem: () => {}, 19 | clearItemFromCart: () => {}, 20 | getPlant: () => {}, 21 | handleChange: () => {}, 22 | filterPlants: [], 23 | cartItemsCount: 0, 24 | cartTotal: 0, 25 | price: 0, 26 | minPrice: 0, 27 | maxPrice: 0, 28 | type: '', 29 | searchName: '', 30 | hex1: '#B5B5B5', 31 | hex2: '#485550', 32 | hex3: '#4B6358', 33 | changeColor: () => {}, 34 | clearColor: () => {}, 35 | loading: false, 36 | user: {}, 37 | }); 38 | 39 | const CartProvider = ({ children }) => { 40 | const [cartItems, setCartItems] = useState([]); 41 | const [cartItemsCount, setCartItemsCount] = useState(0); 42 | const [cartTotal, setCartTotal] = useState(0); 43 | const [plants, setPlants] = useState([]); 44 | const [minPrice, setMinPrice] = useState(0); 45 | const [maxPrice, setMaxPrice] = useState(0); 46 | const [filtredPlants, setFiltredPlants] = useState([]); 47 | const [price, setPrice] = useState(0); 48 | const [type, setType] = useState(''); 49 | const [searchName, setSearchName] = useState(''); 50 | const [hex1, setHex1] = useState('#B5B5B5'); 51 | const [hex2, setHex2] = useState('#485550'); 52 | const [hex3, setHex3] = useState('#4B6358'); 53 | const [loading, setLoading] = useState(true); 54 | 55 | const changeColor = e => { 56 | const color1 = e.target.getAttribute('data-hex1'); 57 | const color2 = e.target.getAttribute('data-hex2'); 58 | const color3 = e.target.getAttribute('data-hex3'); 59 | setHex1(color1); 60 | setHex2(color2); 61 | setHex3(color3); 62 | }; 63 | const clearColor = () => { 64 | setHex1('#B5B5B5'); 65 | setHex2('#485550'); 66 | setHex3('#4B6358'); 67 | }; 68 | 69 | const addItem = item => setCartItems(addItemToCart(cartItems, item)); 70 | const removeItem = item => setCartItems(removeItemFromCart(cartItems, item)); 71 | const clearItemFromCart = item => setCartItems(filterItemFromCart(cartItems, item)); 72 | 73 | const getPlant = slug => { 74 | const templatePlants = [...plants]; 75 | const plantSlug = templatePlants.find(plant => plant.plantSlug === slug); 76 | return plantSlug; 77 | }; 78 | 79 | const handleChangeSearch = e => { 80 | e.preventDefault(); 81 | const value = e.target.value; 82 | setSearchName(value); 83 | }; 84 | 85 | const handleFilteringPlantsByName = () => { 86 | let tempPlants = [...plants]; 87 | if (searchName !== '') { 88 | tempPlants = plants.filter(plant => { 89 | const regex = new RegExp(searchName, 'gi'); 90 | return plant.plantTitle.match(regex); 91 | }); 92 | setFiltredPlants(tempPlants); 93 | return tempPlants; 94 | } 95 | setFiltredPlants(tempPlants); 96 | return tempPlants; 97 | }; 98 | useEffect(() => { 99 | handleFilteringPlantsByName(); 100 | }, [searchName]); 101 | 102 | const handleChangeType = e => { 103 | e.preventDefault(); 104 | const value = e.target.value; 105 | setType(value); 106 | }; 107 | 108 | const handleFilteringPlantsByType = () => { 109 | let tempPlants = [...plants]; 110 | if (type !== 'all') { 111 | tempPlants = tempPlants.filter(plant => plant.plantType === type); 112 | } 113 | setFiltredPlants(tempPlants); 114 | return tempPlants; 115 | }; 116 | useEffect(() => { 117 | handleFilteringPlantsByType(); 118 | }, [type]); 119 | 120 | const handleChangePrice = e => { 121 | e.preventDefault(); 122 | const value = e.target.value; 123 | setPrice(value); 124 | }; 125 | 126 | const handleFilteringPlantsByPrice = () => { 127 | let tempPlants = [...plants]; 128 | tempPlants = tempPlants.filter(plant => plant.plantPrice <= price); 129 | setFiltredPlants(tempPlants); 130 | return tempPlants; 131 | }; 132 | useEffect(() => { 133 | handleFilteringPlantsByPrice(); 134 | }, [price]); 135 | 136 | const dataList = productsDataItems => { 137 | const template = productsDataItems.map(item => { 138 | const singlePlant = { ...item }; 139 | return singlePlant; 140 | }); 141 | return template; 142 | }; 143 | 144 | useEffect(() => { 145 | const getPlantsData = async () => { 146 | const response = await DatoCMSData.items.all().then(dataPlant => { 147 | setPlants(dataList(dataPlant)); 148 | setMaxPrice(Math.max(...dataPlant.map(plant => plant.plantPrice))); 149 | setMinPrice(Math.min(...dataPlant.map(plant => plant.plantPrice))); 150 | setFiltredPlants(dataPlant); 151 | setPrice(Math.max(...dataPlant.map(plant => plant.plantPrice))); 152 | setCartItemsCount(getCartItemsCount(cartItems)); 153 | setCartTotal(getCartTotal(cartItems)); 154 | setLoading(false); 155 | }); 156 | return response; 157 | }; 158 | getPlantsData(); 159 | }, [cartItems]); 160 | 161 | return ( 162 | 189 | {children} 190 | 191 | ); 192 | }; 193 | CartProvider.propTypes = { 194 | children: PropTypes.any.isRequired, 195 | }; 196 | 197 | export default CartProvider; 198 | -------------------------------------------------------------------------------- /src/assets/svg/plant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 15 | 19 | 23 | 27 | 31 | 35 | 39 | 43 | 47 | 51 | 55 | -------------------------------------------------------------------------------- /src/components/atoms/PlantIcon/PlantIcon.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { CartContext } from '../../../context/CartContext'; 3 | 4 | const PlantIcon = () => { 5 | const { hex1, hex2, hex3 } = useContext(CartContext); 6 | return ( 7 | 8 | 9 | 10 | 15 | 16 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 62 | ); 63 | }; 64 | 65 | export default PlantIcon; 66 | -------------------------------------------------------------------------------- /src/views/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useForm } from 'react-hook-form'; 4 | import { fire } from '../firebase/Firebase'; 5 | import Input from '../components/atoms/Input/Input'; 6 | import Heading from '../components/atoms/Heading/Heading'; 7 | import Button from '../components/atoms/Button/Button'; 8 | import PlantHalfPage from '../components/molecules/PlantHalfPage'; 9 | import Text from '../components/atoms/Text/Text'; 10 | 11 | const StyledWrapper = styled.div` 12 | min-height: 100vh; 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-around; 16 | flex-direction: column; 17 | 18 | @media only screen and (min-width: 1000px) { 19 | flex-direction: row; 20 | overflow: hidden !important; 21 | height: 100vh; 22 | } 23 | `; 24 | 25 | const StyledFormWrapper = styled.div` 26 | min-height: 80vh; 27 | margin-top: 8rem; 28 | width: 50%; 29 | height: 100%; 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-around; 33 | flex-direction: column; 34 | @media only screen and (min-width: 1000px) { 35 | margin-bottom: 0rem; 36 | min-height: auto; 37 | } 38 | `; 39 | 40 | const StyledForm = styled.form` 41 | width: 50%; 42 | padding: 3rem 1rem; 43 | display: flex; 44 | justify-content: flex-start; 45 | align-items: center; 46 | flex-direction: column; 47 | text-align: left; 48 | `; 49 | 50 | const StyledInput = styled(Input)` 51 | position: relative; 52 | padding: 1.2rem 0.5rem; 53 | :last-of-type { 54 | margin: 1.5rem 0 1rem 0; 55 | } 56 | 57 | ::placeholder { 58 | color: transparent; 59 | } 60 | 61 | :not(:placeholder-shown) + label, 62 | :focus + label { 63 | transform: translate(0, -50%); 64 | cursor: pointer; 65 | } 66 | 67 | :focus + ::placeholder { 68 | color: inherit; 69 | } 70 | 71 | :focus { 72 | outline: 0; 73 | } 74 | `; 75 | 76 | const StyledHeadingWrapper = styled.div` 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | width: 25rem; 81 | `; 82 | const StyledHeading = styled(Heading)` 83 | text-transform: uppercase; 84 | font-size: 4rem; 85 | margin-bottom: 2rem; 86 | `; 87 | const StyledButtonsWrapper = styled.div` 88 | display: flex; 89 | align-items: center; 90 | padding-left: 2rem; 91 | `; 92 | const StyledButton = styled(Button)` 93 | width: 10rem; 94 | margin: 2rem 2rem 0 0; 95 | `; 96 | 97 | const StyledFooter = styled.footer` 98 | width: 100%; 99 | text-align: center; 100 | font-size: 1.2rem; 101 | margin-top: 2rem; 102 | `; 103 | 104 | const StyledTextWrapper = styled.div` 105 | margin-top: 2rem; 106 | width: 25rem; 107 | font-size: 1.2rem; 108 | display: flex; 109 | align-items: center; 110 | justify-content: center; 111 | `; 112 | 113 | const StyledAuthor = styled.a` 114 | text-decoration: none; 115 | margin: 0 0 0 0.2rem; 116 | color: inherit; 117 | `; 118 | 119 | const StyledInputLabelWrapper = styled.div` 120 | display: flex; 121 | flex-flow: column-reverse; 122 | position: relative; 123 | 124 | input + label { 125 | line-height: 1; 126 | height: 4rem; 127 | transition: transform 0.25s, opacity 0.25s ease-in-out; 128 | transform-origin: 0 0; 129 | transform: translate(10px, 20%); 130 | position: absolute; 131 | } 132 | `; 133 | 134 | const StyledLabel = styled.label` 135 | letter-spacing: 1px; 136 | color: ${({ theme }) => theme.fontColorText}; 137 | font-size: 1rem; 138 | position: absolute; 139 | user-select: none; 140 | `; 141 | 142 | const Login = () => { 143 | const { register, handleSubmit, errors } = useForm(); 144 | const [email, setEmail] = useState(''); 145 | const [password, setPassword] = useState(''); 146 | const [newAccount, setNewAccount] = useState(false); 147 | 148 | const handleEmailChange = e => { 149 | setEmail(e.target.value); 150 | }; 151 | 152 | const handlePasswordChange = e => { 153 | setPassword(e.target.value); 154 | }; 155 | 156 | const handleNewAccount = e => { 157 | e.preventDefault(); 158 | setNewAccount(prevNewAccount => !prevNewAccount); 159 | }; 160 | 161 | const handleSignin = () => { 162 | fire 163 | .auth() 164 | .signInWithEmailAndPassword(email, password) 165 | /* eslint-disable */ 166 | .catch(error => alert(`Your email or password is incorrect, please check your data`)); 167 | }; 168 | 169 | const handleSignup = () => { 170 | fire 171 | .auth() 172 | .createUserWithEmailAndPassword(email, password) 173 | .catch(error => alert(`Email is already in use, sign in or use other email`)); 174 | }; 175 | 176 | return ( 177 | 178 | 179 | 180 | 181 | 182 | {newAccount ? 'Sign up' : 'Sign in'} 183 | 184 | 185 | 197 | Email 198 | 199 | 200 | {errors.email && errors.email.type === 'required' && ( 201 | Email is required 202 | )} 203 | {errors.email && errors.email.type === 'pattern' && ( 204 | Email is invalid please add @ 205 | )} 206 | 207 | 208 | 220 | Password 221 | 222 | 223 | {errors.password && errors.password.type === 'required' && ( 224 | Password is required 225 | )} 226 | {errors.password && errors.password.type === 'pattern' && ( 227 | 228 | Password should contain min. 8 characters, one uppercase letter, one lowercase letter 229 | and number 230 | 231 | )} 232 | 233 | 234 | 235 | {newAccount ? 'Sign up' : 'Sign in'} 236 | 237 | 238 | 239 | {newAccount ? 'Have account?' : "Haven't got account?"} 240 | 243 | 244 | 245 | 246 | 247 | Made with{' '} 248 | 249 | 💚 250 | {' '} 251 | by 252 | {' '} 255 | 256 | 257 | 258 | 259 | ); 260 | }; 261 | 262 | export default Login; 263 | -------------------------------------------------------------------------------- /src/assets/svg/404.svg: -------------------------------------------------------------------------------- 1 | Taken --------------------------------------------------------------------------------