├── 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 |
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', () => );
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 |
--------------------------------------------------------------------------------
/src/__tests__/Loader.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom/extend-expect';
3 | import { render } from '@testing-library/react';
4 | import Loader from '../components/atoms/Loader/Loader';
5 |
6 | describe('Loader component', () => {
7 | it('renders Loader element', () => {
8 | const { getByTestId } = render();
9 |
10 | expect(getByTestId('loader')).toBeInTheDocument();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/__tests__/Plant.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom/extend-expect';
3 | import { render, fireEvent, wait } from '@testing-library/react';
4 | import Heroplant from '../components/atoms/Plant/Plant';
5 |
6 | describe('Plant component', () => {
7 | it('renders Plant element', () => {
8 | const { getByTestId } = render();
9 |
10 | expect(getByTestId('plant')).toBeInTheDocument();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { configure, addDecorator } from '@storybook/react';
3 | import { ThemeProvider } from 'styled-components';
4 | import { theme } from '../src/theme/MainTheme';
5 |
6 | function loadStories() {
7 | const req = require.context('../src/components', true, /\.stories\.js$/);
8 | req.keys().forEach(filename => req(filename));
9 | }
10 |
11 | addDecorator(story => {story()});
12 |
13 | configure(loadStories, module);
14 |
--------------------------------------------------------------------------------
/src/templates/MainTemplate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { ThemeProvider } from 'styled-components';
4 | import GlobalStyle from '../theme/GlobalStyles';
5 | import { theme } from '../theme/MainTheme';
6 |
7 | const MainTemplate = ({ children }) => (
8 |
9 |
10 | {children}
11 |
12 | );
13 | MainTemplate.propTypes = {
14 | children: PropTypes.element.isRequired,
15 | };
16 |
17 | export default MainTemplate;
18 |
--------------------------------------------------------------------------------
/src/firebase/Firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 | import 'firebase/auth';
3 |
4 | const config = {
5 | apiKey: 'AIzaSyDQUgkFwZSvZRXiOsdDi-4hTHtZJ192ZrI',
6 | authDomain: 'plantshop-5afda.firebaseapp.com',
7 | databaseURL: 'https://plantshop-5afda.firebaseio.com',
8 | projectId: 'plantshop-5afda',
9 | storageBucket: 'plantshop-5afda.appspot.com',
10 | messagingSenderId: '430055733967',
11 | appId: '1:430055733967:web:33684546e53be3c18315ba',
12 | };
13 | const fire = firebase.initializeApp(config);
14 | export { fire };
15 |
--------------------------------------------------------------------------------
/src/theme/MainTheme.js:
--------------------------------------------------------------------------------
1 | export const theme = {
2 | primaryColor: 'hsla(152, 94%, 33%, 0.5)',
3 | secondaryColor: 'hsla(204, 26%, 96%, 1)',
4 | fontColorPrimary: 'hsla(152, 94%, 33%, 1)',
5 | fontColorText: 'hsla(0, 0%, 33%, 0.5)',
6 | fontColorHeader: 'hsla(0, 0%, 0%, 0.5)',
7 | fontColorHeading: 'hsla(0, 0%, 0%, 1)',
8 | buttonColor: 'hsl(153, 91%, 48%, 60%)',
9 | halfPlantColor: 'hsl(153, 91%, 48%, 40%)',
10 | headingBeforeColor: ' hsla(152, 94%, 33%, 0.2)',
11 | light: 400,
12 | regular: 500,
13 | bold: 700,
14 | height: window.innerHeight * 0.01,
15 | };
16 |
--------------------------------------------------------------------------------
/src/assets/svg/Loader.svg:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/src/assets/svg/backArrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/serviceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | self.addEventListener('install', function(event) {
3 | console.log('SW Installed');
4 | event.waitUntil(
5 | caches.open('static').then(function(cache) {
6 | cache.addAll(['/']);
7 | }),
8 | );
9 | });
10 |
11 | self.addEventListener('activate', function() {
12 | console.log('SW Activated');
13 | });
14 |
15 | self.addEventListener('fetch', function(event) {
16 | event.respondWith(
17 | caches.match(event.request).then(function(res) {
18 | if (res) {
19 | return res;
20 | }
21 | return fetch(event.request);
22 | }),
23 | );
24 | });
25 |
--------------------------------------------------------------------------------
/src/components/atoms/Input/Input.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const StyledInput = styled.input`
4 | width: 24rem;
5 | padding: 15px 30px;
6 | font-size: 1.2rem;
7 | font-weight: ${({ theme }) => theme.regular};
8 | background-color: ${({ theme }) => theme.secondaryColor};
9 | padding: 10px 20px 10px 20px;
10 | border: none;
11 | @media only screen and (min-width: 500px) {
12 | width: 28rem;
13 | }
14 | @media only screen and (min-width: 700px) {
15 | width: 24rem;
16 | }
17 | ::placeholder {
18 | letter-spacing: 1px;
19 | color: ${({ theme }) => theme.fontColorText};
20 | }
21 | `;
22 |
23 | export default StyledInput;
24 |
--------------------------------------------------------------------------------
/src/views/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { CartContext } from '../context/CartContext';
3 | import Products from '../components/organisms/Products';
4 | import Hero from '../components/organisms/Hero';
5 | import Preferences from '../components/molecules/Preferences';
6 | import Header from '../components/organisms/Header';
7 |
8 | const Home = () => {
9 | const { plants, filtredPlants } = useContext(CartContext);
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default Home;
23 |
--------------------------------------------------------------------------------
/src/assets/svg/magify.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: node:11.10.1
6 |
7 | working_directory: ~/repo
8 |
9 | steps:
10 | - checkout
11 |
12 | # Download and cache dependencies
13 | - restore_cache:
14 | keys:
15 | - v1-dependencies-{{ checksum "package.json" }}
16 | # fallback to using the latest cache if no exact match is found
17 | - v1-dependencies-
18 |
19 | - run: npm install
20 |
21 | - save_cache:
22 | paths:
23 | - node_modules
24 | key: v1-dependencies-{{ checksum "package.json" }}
25 |
26 | # run tests!
27 | - run: npm run test
28 |
29 | - run: npm run build
30 |
31 | - run: npm run netlify:deploy
32 |
--------------------------------------------------------------------------------
/src/components/atoms/Text/Text.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | const Text = styled.p`
4 | ${({ main }) =>
5 | main &&
6 | css`
7 | color: ${({ theme }) => theme.fontColorHeading};
8 | font-size: 1.5rem;
9 | line-height: 2.25rem;
10 | font-weight: ${({ theme }) => theme.light};
11 | padding: 1rem 0;
12 | text-align: left;
13 | `}
14 | ${({ logo }) =>
15 | logo &&
16 | css`
17 | color: ${({ theme }) => theme.fontColorHeading};
18 | font-size: 1.5rem;
19 | text-transform: uppercase;
20 | cursor: pointer;
21 | `};
22 | ${({ errorMessage }) =>
23 | errorMessage &&
24 | css`
25 | display: block;
26 | color: red;
27 | font-size: 1.2rem;
28 | min-width: 200px;
29 | text-align: center;
30 | `};
31 | `;
32 | export default Text;
33 |
--------------------------------------------------------------------------------
/src/components/organisms/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Link } from 'react-router-dom';
4 | import Text from '../atoms/Text/Text';
5 | import HeaderIcons from '../molecules/HeaderIcons';
6 |
7 | const StyledHeader = styled.header`
8 | width: 100%;
9 | height: 10rem;
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 | padding: 3rem 2rem;
14 | margin: 0 auto;
15 |
16 | @media only screen and (min-width: 1300px) {
17 | padding: 4rem 4rem;
18 | }
19 | `;
20 |
21 | const StyledLink = styled(Link)`
22 | text-decoration: none;
23 | color: inherit;
24 | `;
25 | const Header = () => (
26 |
27 |
28 | Plants & Home
29 |
30 |
31 |
32 | );
33 |
34 | export default Header;
35 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb", "prettier", "prettier/react"],
3 | "parser": "babel-eslint",
4 | "env": {
5 | "jest": true
6 | },
7 | "globals": {
8 | "document": true,
9 | "localStorage": true,
10 | "fetch": true,
11 | "window": true
12 | },
13 |
14 | "rules": {
15 | "react/forbid-prop-types": 0,
16 | "import/prefer-default-export": 0,
17 | "import/no-extraneous-dependencies": 0,
18 | "import/no-unresolved": 1,
19 | "no-console": "off",
20 | "no-restricted-globals": 0,
21 | "react/jsx-filename-extension": [
22 | 1,
23 | {
24 | "extensions": [".js"]
25 | }
26 | ],
27 | "no-unused-vars": 0,
28 | "prefer-destructuring": 0
29 | },
30 | "settings": {
31 | "import/resolver": {
32 | "node": {
33 | "paths": ["./src"],
34 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/StripeButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import StripeCheckout from 'react-stripe-checkout';
4 |
5 | const StripeButton = ({ price }) => {
6 | const priceStripe = price * 100;
7 | const key = 'pk_test_5R7ANtmXdmeaY7ahK6WWSEvr00lBQBoamY';
8 | const onToken = token => console.log(token);
9 | return (
10 |
23 | );
24 | };
25 | StripeButton.propTypes = {
26 | price: PropTypes.number.isRequired,
27 | };
28 |
29 | export default StripeButton;
30 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Plants & Home",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "/android-icon-36x36.png",
7 | "sizes": "36x36",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "/android-icon-72x72.png",
17 | "sizes": "72x72",
18 | "type": "image/png"
19 | },
20 | {
21 | "src": "/android-icon-96x96.png",
22 | "sizes": "96x96",
23 | "type": "image/png"
24 | },
25 | {
26 | "src": "/android-icon-192x192.png",
27 | "sizes": "192x192",
28 | "type": "image/png"
29 | },
30 | {
31 | "src": "/plantsicon.png",
32 | "sizes": "512x512",
33 | "type": "image/png"
34 | }
35 | ],
36 | "start_url": ".",
37 | "display": "standalone",
38 | "theme_color": "#05a359",
39 | "background_color": "#f2f5f7",
40 | "orientation": "portrait"
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/atoms/Loader/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | const spin = keyframes`
5 | from {
6 | transform: rotate(0deg);
7 | }
8 | to {
9 | transform: rotate(360deg);
10 | }
11 | `;
12 |
13 | const StyledWrapper = styled.div`
14 | width: 100%;
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | `;
19 |
20 | const StyledLoaderCircle = styled.div`
21 | display: inline-block;
22 | box-sizing: border-box;
23 | padding: 30px;
24 | width: 8rem;
25 | height: 140px;
26 | `;
27 |
28 | const StyledCircle = styled.div`
29 | box-sizing: border-box;
30 | width: 5rem;
31 | height: 5rem;
32 | border-radius: 100%;
33 | border: 10px solid hsla(204, 26%, 96%, 1);
34 | border-top-color: hsla(152, 94%, 33%, 1);
35 | animation: ${spin} 1s infinite linear;
36 | `;
37 |
38 | const Loader = () => (
39 |
40 |
41 |
42 |
43 |
44 | );
45 | export default Loader;
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Olaf Sulich
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/components/atoms/Heading/Heading.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | const StyledHeading = styled.h2`
4 | ${({ main }) =>
5 | main &&
6 | css`
7 | color: ${({ theme }) => theme.fontColorHeading};
8 | font-size: 2.5rem;
9 | font-weight: ${({ theme }) => theme.bold};
10 | padding: 0.4rem 0;
11 | position: relative;
12 | display: inline-block;
13 | @media only screen and (min-width: 500px) {
14 | font-size: 3rem;
15 | }
16 | @media only screen and (min-width: 850px) {
17 | font-size: 3.6rem;
18 | }
19 | @media only screen and (min-width: 1200px) {
20 | font-size: 4rem;
21 | }
22 | @media only screen and (min-width: 1400px) {
23 | font-size: 4.4rem;
24 | }
25 |
26 | ::before {
27 | content: '';
28 | position: absolute;
29 | width: 100%;
30 | height: 40%;
31 | background-color: ${({ theme }) => theme.headingBeforeColor};
32 | z-index: -1;
33 | top: 55%;
34 | left: 10%;
35 | }
36 | `};
37 | `;
38 | export default StyledHeading;
39 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/CartButton.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, memo } from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { CartContext } from '../../../context/CartContext';
5 | import CartIcon from '../../../assets/svg/cart.svg';
6 | import Button from './Button';
7 |
8 | const StyledWrapper = styled.div`
9 | width: 2.6rem;
10 | height: 2.6rem;
11 | margin: 0 0.8rem;
12 | position: relative;
13 | `;
14 |
15 | const StyledButton = styled(Button)`
16 | background-image: url(${CartIcon});
17 | width: 100%;
18 | height: 100%;
19 | `;
20 |
21 | const StyledCounter = styled.span`
22 | top: 50%;
23 | left: 50%;
24 | transform: translate(-50%, -20%);
25 | position: absolute;
26 | font-size: 1rem;
27 | color: ${({ theme }) => theme.fontColorHeading};
28 | z-index: 2;
29 | `;
30 |
31 | const CartButton = memo(props => {
32 | const { cartItemsCount } = useContext(CartContext);
33 | const { onClick } = props;
34 | return (
35 |
36 |
37 | {cartItemsCount}
38 |
39 |
40 | );
41 | });
42 | CartButton.propTypes = {
43 | onClick: PropTypes.func,
44 | };
45 | CartButton.defaultProps = {
46 | onClick: null,
47 | };
48 | export default CartButton;
49 |
--------------------------------------------------------------------------------
/src/__tests__/Search.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom/extend-expect';
3 | import { render, fireEvent } from '@testing-library/react';
4 | import Search from '../components/atoms/Input/Search';
5 |
6 | describe('Search component', () => {
7 | it('renders search element', () => {
8 | const { getByTestId } = render();
9 |
10 | expect(getByTestId('search')).toBeInTheDocument();
11 | });
12 | it('displays default placeholder', () => {
13 | let placeholderText = 'search';
14 | const { getByPlaceholderText, rerender } = render();
15 |
16 | expect(getByPlaceholderText(placeholderText)).toBeInTheDocument();
17 |
18 | placeholderText = 'search';
19 |
20 | rerender();
21 |
22 | expect(getByPlaceholderText(placeholderText)).toBeInTheDocument();
23 | });
24 | it('displays proper value', () => {
25 | const { getByTestId } = render();
26 |
27 | const searchInput = getByTestId('searchInput');
28 |
29 | fireEvent.change(searchInput, { target: { value: 'search' } });
30 |
31 | expect(searchInput).toHaveValue('search');
32 | });
33 | it('displays proper name', () => {
34 | const { getByTestId } = render();
35 |
36 | const searchInput = getByTestId('searchInput');
37 |
38 | fireEvent.change(searchInput, { target: { name: 'search' } });
39 |
40 | expect(searchInput).toHaveValue('search');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/assets/svg/morphing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/CartUtils.js:
--------------------------------------------------------------------------------
1 | const addItemToCart = (cartItems, cartItemToAdd) => {
2 | const existingCartItem = cartItems.find(cartItem => cartItem.id === cartItemToAdd.id);
3 |
4 | if (existingCartItem) {
5 | return cartItems.map(cartItem =>
6 | cartItem.id === cartItemToAdd.id
7 | ? { ...cartItem, quantity: cartItem.quantity + 1 }
8 | : cartItem,
9 | );
10 | }
11 |
12 | return [...cartItems, { ...cartItemToAdd, quantity: 1 }];
13 | };
14 |
15 | const removeItemFromCart = (cartItems, cartItemToRemove) => {
16 | const existingCartItem = cartItems.find(cartItem => cartItem.id === cartItemToRemove.id);
17 |
18 | if (existingCartItem.quantity === 1) {
19 | return cartItems.filter(cartItem => cartItem.id !== cartItemToRemove.id);
20 | }
21 |
22 | return cartItems.map(cartItem =>
23 | cartItem.id === cartItemToRemove.id
24 | ? { ...cartItem, quantity: cartItem.quantity - 1 }
25 | : cartItem,
26 | );
27 | };
28 |
29 | const filterItemFromCart = (cartItems, item) =>
30 | cartItems.filter(cartItem => cartItem.id !== item.id);
31 |
32 | const getCartItemsCount = cartItems =>
33 | cartItems.reduce((accumalatedQuantity, cartItem) => accumalatedQuantity + cartItem.quantity, 0);
34 |
35 | const getCartTotal = cartItems =>
36 | cartItems.reduce(
37 | (accumalatedQuantity, cartItem) =>
38 | accumalatedQuantity + cartItem.quantity * cartItem.plantPrice,
39 | 0,
40 | );
41 |
42 | export { addItemToCart, removeItemFromCart, filterItemFromCart, getCartItemsCount, getCartTotal };
43 |
--------------------------------------------------------------------------------
/src/components/organisms/ErrorBoundary.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import PropTypes from 'prop-types';
4 | import Notfound from '../../assets/svg/NotFound.png';
5 | import Heading from '../atoms/Heading/Heading';
6 |
7 | const StyledWrapper = styled.div`
8 | margin: 5rem 0 0 0;
9 | width: 100%;
10 | height: 80vh;
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | flex-direction: column;
15 | `;
16 |
17 | const StyledSVGWrapper = styled.figure`
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | width: 35rem;
22 | height: 35rem;
23 |
24 | img {
25 | width: 100%;
26 | height: 100%;
27 | }
28 | `;
29 |
30 | const StyledHeading = styled(Heading)`
31 | margin-bottom: 3rem;
32 | `;
33 |
34 | class ErrorBoundary extends React.Component {
35 | state = {
36 | hasError: false,
37 | };
38 |
39 | static getDerivedStateFromError = error => {
40 | return { hasError: true };
41 | };
42 |
43 | render() {
44 | const { hasError } = this.state;
45 | const { children } = this.props;
46 |
47 | return hasError ? (
48 | <>
49 |
50 |
51 | Caution! This Page is Cordoned Off
52 |
53 |
54 |
55 |
56 |
57 | >
58 | ) : (
59 | children
60 | );
61 | }
62 | }
63 | ErrorBoundary.propTypes = {
64 | children: PropTypes.any.isRequired,
65 | };
66 |
67 | export default ErrorBoundary;
68 |
--------------------------------------------------------------------------------
/src/components/organisms/Hero.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect } from 'react';
2 | import styled from 'styled-components';
3 | import { CartContext } from '../../context/CartContext';
4 | import Heroplant from '../atoms/Plant/Plant';
5 | import StyledHeading from '../atoms/Heading/Heading';
6 |
7 | const StyledWrapper = styled.section`
8 | display: flex;
9 | align-items: center;
10 | justify-content: space-around;
11 | flex-direction: column;
12 | padding: 0 3rem;
13 | @media only screen and (min-width: 700px) {
14 | flex-direction: row;
15 | }
16 | @media only screen and (min-width: 1000px) {
17 | padding: 0 5rem;
18 | }
19 | @media only screen and (min-width: 1200px) {
20 | padding: 0 7rem;
21 | }
22 | @media only screen and (min-width: 1400px) {
23 | padding: 0 12rem;
24 | }
25 | `;
26 | const StyledWrapperHeading = styled.article`
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: space-between;
30 | align-items: center;
31 | padding: 0 1rem;
32 | margin-top: 4rem;
33 | @media only screen and (min-width: 700px) {
34 | margin-bottom: 13%;
35 | }
36 | @media only screen and (min-width: 700px) {
37 | align-items: flex-start;
38 | }
39 | `;
40 |
41 | const Hero = () => {
42 | const { clearColor } = useContext(CartContext);
43 |
44 | useEffect(() => {
45 | clearColor();
46 | });
47 |
48 | return (
49 |
50 |
51 | Say hello to
52 | home plants!
53 |
54 |
55 |
56 | );
57 | };
58 | export default Hero;
59 |
--------------------------------------------------------------------------------
/src/components/atoms/Plant/Plant.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Plant from '../PlantIcon/PlantIcon';
4 | import Morphing from '../../../assets/svg/morphing.svg';
5 |
6 | const StyledPlantWrapper = styled.figure`
7 | margin-top: 6rem;
8 | position: relative;
9 |
10 | width: 25rem;
11 | height: 27rem;
12 | @media only screen and (min-width: 700px) {
13 | width: 30rem;
14 | height: 34rem;
15 | margin-top: 2rem;
16 | }
17 | `;
18 |
19 | const StyledMorphing = styled.img`
20 | width: 90%;
21 | height: 90%;
22 | position: absolute;
23 | top: 20%;
24 | left: 50%;
25 | transform: translate(-50%, 0%);
26 | `;
27 |
28 | const StyledPlant = styled.div`
29 | width: 90%;
30 | height: 90%;
31 | position: absolute;
32 | top: 30%;
33 | left: 70%;
34 | transform: translate(-45%, -40%);
35 | `;
36 | const StyledPlantDefault = styled.div`
37 | width: 13rem;
38 | height: 10rem;
39 | margin-top: 2rem;
40 | position: absolute;
41 | top: -3rem;
42 | left: 0rem;
43 | @media only screen and (min-width: 700px) {
44 | width: 18rem;
45 | height: 12rem;
46 | top: -4rem;
47 | left: -1rem;
48 | margin-top: 0rem;
49 | }
50 | `;
51 |
52 | const StyledPlantIcon = styled(Plant)`
53 | width: 100%;
54 | height: 100%;
55 | `;
56 |
57 | const Heroplant = () => {
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 | export default Heroplant;
70 |
--------------------------------------------------------------------------------
/src/components/molecules/Product.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router-dom';
4 | import styled from 'styled-components';
5 |
6 | const StyledTitleWrapper = styled.section`
7 | position: relative;
8 | display: block;
9 | margin-bottom: 1.4em;
10 | `;
11 |
12 | const StyledTitle = styled.h3`
13 | position: absolute;
14 | z-index: 2;
15 | top: 70%;
16 | left: 0;
17 | color: #000;
18 | font-weight: font-weight: ${({ theme }) => theme.bold};;
19 | font-size: 1.15rem;
20 | background: #fff;
21 | padding: 0.65rem 1.6rem 0.65rem 1.1rem;
22 | `;
23 |
24 | const StyledImageWrapper = styled.figure`
25 | width: 100%;
26 | height: 100%;
27 | overflow: hidden;
28 | margin: 0 auto;
29 | position: relative;
30 | `;
31 |
32 | const StyledImage = styled.img`
33 | width: 100%;
34 | `;
35 |
36 | const StyledLink = styled(Link)`
37 | width: 100%;
38 | height: 100%;
39 | text-decoration: none;
40 | `;
41 |
42 | const StyledPirce = styled.span`
43 | font-weight: ${({ theme }) => theme.regular};
44 | font-size: 0.8rem;
45 | `;
46 |
47 | const Product = ({ title, src, slug, price }) => {
48 | return (
49 |
50 |
51 |
52 | {title}
53 | /${price}
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | Product.propTypes = {
64 | title: PropTypes.string.isRequired,
65 | src: PropTypes.string.isRequired,
66 | slug: PropTypes.string.isRequired,
67 | price: PropTypes.number.isRequired,
68 | };
69 | export default Product;
70 |
--------------------------------------------------------------------------------
/src/theme/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | const GlobalStyle = createGlobalStyle`
4 | @import url('https://fonts.googleapis.com/css?family=Montserrat:400,500,700&display=swap');
5 | *, *::before, *::after {
6 | box-sizing: border-box;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | margin:0;
10 | padding:0;
11 | outline:none;
12 |
13 | }
14 |
15 | button{
16 | background:none;
17 | }
18 |
19 | html {
20 | font-size: 62.5%;
21 |
22 |
23 | @media only screen and (min-width:425px){
24 | font-size: 67.5%;
25 | }
26 | @media only screen and (min-width:500px){
27 | font-size: 70.5%;
28 | }
29 | @media only screen and (min-width:750px){
30 | font-size: 75.5%;
31 | }
32 | @media only screen and (min-width:1000px){
33 | font-size: 77.5%;
34 | }
35 | @media only screen and (min-width: 1200px) {
36 | font-size: 80.5%;
37 | }
38 | @media only screen and (min-width: 1400px) {
39 | font-size: 82.5%;
40 | }
41 | @media only screen and (min-width: 1650px) {
42 | font-size: 85.5%;
43 | }
44 | @media only screen and (min-width: 1800px) {
45 | font-size: 100%;
46 | }
47 | }
48 |
49 | body {
50 | width:100%;
51 | height:auto;
52 | font-size: 1.6rem;
53 | font-family: "Montserrat", sans-serif;
54 | overflow-x:hidden;
55 |
56 | ::-webkit-scrollbar {
57 | width: 15px;
58 | }
59 | ::-webkit-scrollbar-track {
60 | background: hsla(204, 26%, 96%, 1);
61 | border-radius: 10px;
62 | }
63 |
64 | ::-webkit-scrollbar-thumb {
65 | background: hsla(152, 94%, 33%, 0.5);
66 | }
67 | }
68 | input[type="search"]::-webkit-search-cancel-button {
69 | display: none;
70 | }
71 | *:focus {
72 | outline-color:transparent;
73 | }
74 | `;
75 |
76 | export default GlobalStyle;
77 |
--------------------------------------------------------------------------------
/src/components/organisms/Products.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 | import Product from '../molecules/Product';
6 | import Text from '../atoms/Text/Text';
7 | import Loader from '../atoms/Loader/Loader';
8 |
9 | const StyledWrapper = styled.div`
10 | margin: 0 3rem;
11 | display: grid;
12 | grid-template-columns: 1fr;
13 | justify-items: center;
14 | align-items: center;
15 | grid-column-gap: 2rem;
16 | max-width: 100%;
17 | @media only screen and (min-width: 550px) {
18 | grid-template-columns: 1fr 1fr;
19 | }
20 | @media only screen and (min-width: 1000px) {
21 | grid-column-gap: 3rem;
22 | max-width: 70vw;
23 | margin: 0 auto;
24 | }
25 | @media only screen and (min-width: 1600px) {
26 | grid-column-gap: 3rem;
27 | max-width: 60vw;
28 | margin: 0 auto;
29 | }
30 | `;
31 | const StyledNoMatchWrapper = styled.div`
32 | width: 100%;
33 | min-height: 5rem;
34 | display: flex;
35 | align-items: center;
36 | justify-content: center;
37 | margin-bottom: 2rem;
38 | `;
39 |
40 | const Products = ({ plants }) => {
41 | const { loading } = useContext(CartContext);
42 |
43 | if (loading) {
44 | return ;
45 | }
46 | if (plants.length === 0) {
47 | return (
48 |
49 | No plants matched your search
50 |
51 | );
52 | }
53 |
54 | return (
55 |
56 | {plants.map(plant => (
57 |
64 | ))}
65 |
66 | );
67 | };
68 | Products.propTypes = {
69 | plants: PropTypes.array.isRequired,
70 | };
71 |
72 | export default Products;
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plantshop",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.0.0",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^8.0.3",
9 | "axios": "^0.21.1",
10 | "firebase": "^7.6.1",
11 | "jest-dom": "^4.0.0",
12 | "modern-normalize": "^0.5.0",
13 | "react": "^16.8.4",
14 | "react-dom": "^16.8.4",
15 | "react-hook-form": "^4.4.7",
16 | "react-router-dom": "^5.1.2",
17 | "react-scripts": "2.1.8",
18 | "react-stripe-checkout": "^2.6.3",
19 | "react-testing-library": "^8.0.1",
20 | "styled-components": "^4.1.3"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject",
27 | "storybook": "start-storybook",
28 | "build-storybook": "build-storybook -s public",
29 | "netlify:deploy": "netlify deploy --dir=./build -p -m \"$(git log -1 --pretty=%B)\""
30 | },
31 | "browserslist": [
32 | ">0.2%",
33 | "not dead",
34 | "not ie <= 11",
35 | "not op_mini all"
36 | ],
37 | "devDependencies": {
38 | "@storybook/react": "^5.0.3",
39 | "babel-plugin-styled-components": "^1.10.6",
40 | "datocms-client": "^3.0.14",
41 | "eslint": "^5.12.0",
42 | "eslint-config-airbnb": "^17.1.0",
43 | "eslint-config-prettier": "^4.1.0",
44 | "eslint-plugin-import": "^2.20.0",
45 | "eslint-plugin-jsx-a11y": "^6.2.3",
46 | "eslint-plugin-react": "^7.12.4",
47 | "husky": "^1.3.1",
48 | "lint-staged": "^8.1.5",
49 | "netlify-cli": "^2.30.0",
50 | "prettier": "^1.16.4",
51 | "storybook-react-router": "^1.0.8"
52 | },
53 | "husky": {
54 | "hooks": {
55 | "pre-commit": "lint-staged"
56 | }
57 | },
58 | "lint-staged": {
59 | "*.js": [
60 | "prettier --config .prettierrc --write",
61 | "eslint --fix",
62 | "git add"
63 | ]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/molecules/Preferences.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import styled from 'styled-components';
3 | import { CartContext } from '../../context/CartContext';
4 |
5 | import RangeInput from '../atoms/RangeInput/RangeInput';
6 | import SelectInput from '../atoms/SelectInput/SelectInput';
7 | import Search from '../atoms/Input/Search';
8 |
9 | const StyledFormWrapper = styled.div`
10 | margin-top: 3rem;
11 | padding: 0 3rem;
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | flex-direction: column;
16 | `;
17 |
18 | const StyledWrapper = styled.form`
19 | margin-top: 3rem;
20 | width: 100%;
21 | padding: 3rem 1rem;
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | flex-direction: column;
26 | @media only screen and (min-width: 700px) {
27 | flex-direction: row;
28 | width: 110%;
29 | margin: 3rem 0 0rem 0;
30 | }
31 | @media only screen and (min-width: 1300px) {
32 | margin: 3rem 0 1rem 0;
33 | }
34 | `;
35 |
36 | const Preferences = () => {
37 | const context = useContext(CartContext);
38 | const {
39 | type,
40 | price,
41 | minPrice,
42 | maxPrice,
43 | searchName,
44 | handleChangeSearch,
45 | handleChangeType,
46 | handleChangePrice,
47 | } = context;
48 | return (
49 |
50 |
51 |
58 |
59 |
67 |
68 |
69 | );
70 | };
71 | export default Preferences;
72 |
--------------------------------------------------------------------------------
/src/components/molecules/HeaderIcons.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, lazy, Suspense } from 'react';
2 | import styled from 'styled-components';
3 | import { Link } from 'react-router-dom';
4 | import PropTypes from 'prop-types';
5 | import Button from '../atoms/Button/Button';
6 | import CartButton from '../atoms/Button/CartButton';
7 | import { fire } from '../../firebase/Firebase';
8 | import Loader from '../atoms/Loader/Loader';
9 |
10 | const Cart = lazy(() => import('./Cart'));
11 |
12 | const StyledWrapper = styled.nav`
13 | display: flex;
14 | align-items: center;
15 | justify-content: flex-end;
16 | position: relative;
17 | `;
18 |
19 | const StyledLink = styled(Link)`
20 | text-decoration: none;
21 | `;
22 |
23 | const HeaderIcons = ({ isSinglePlant }) => {
24 | const [CartOpen, setCartOpen] = useState(false);
25 | const [pageWidth, setPageWidth] = useState(window.innerWidth);
26 |
27 | useEffect(() => {
28 | setPageWidth(window.innerWidth);
29 | }, []);
30 |
31 | const handleCartOpen = () => setCartOpen(prevState => !prevState);
32 | const handlelogout = () => fire.auth().signOut();
33 |
34 | return (
35 |
36 | {pageWidth <= 700 ? (
37 |
38 |
39 |
40 | ) : (
41 | <>
42 |
43 | }>
44 |
45 |
46 | >
47 | )}
48 | {isSinglePlant ? (
49 |
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 |
6 |
--------------------------------------------------------------------------------
/src/assets/svg/redPot.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/svg/greyPot.svg:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/svg/yellowPot.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | ## Performance 🚀
43 |
44 |
45 |
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 |
87 |
88 | {plantTitle}
89 |
90 |
93 | {quantity}
94 |
97 |
98 | ${plantPrice}
99 |
101 | );
102 | };
103 |
104 | CheckoutItem.propTypes = {
105 | plant: PropTypes.object.isRequired,
106 | };
107 | export default React.memo(CheckoutItem);
108 |
--------------------------------------------------------------------------------
/src/views/Checkout.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext, lazy, Suspense } from 'react';
2 | import styled from 'styled-components';
3 | import { CartContext } from '../context/CartContext';
4 | import PlantHalfPage from '../components/molecules/PlantHalfPage';
5 | import Header from '../components/organisms/Header';
6 | import StripeButton from '../components/atoms/Button/StripeButton';
7 | import Loader from '../components/atoms/Loader/Loader';
8 |
9 | const CheckoutItem = lazy(() => import('../components/molecules/CheckoutItem'));
10 | const StyledWrapper = styled.div`
11 | height: 100vh;
12 | display: flex;
13 | align-items: center;
14 | justify-content: space-between;
15 | flex-direction: column;
16 |
17 | @media only screen and (min-width: 1000px) {
18 | flex-direction: row;
19 | overflow: hidden;
20 | }
21 | `;
22 |
23 | const StyledCheckoutSection = styled.div`
24 | height: 100vh;
25 | width: 100%;
26 |
27 | display: flex;
28 | flex-direction: column;
29 | align-items: center;
30 | justify-content: space-around;
31 | margin-bottom: 4.3rem;
32 | `;
33 |
34 | const StyledCheckoutWrapper = styled.div`
35 | display: flex;
36 | align-items: center;
37 | justify-content: flex-start;
38 | flex-direction: column;
39 | `;
40 |
41 | const StyledProductsWrapper = styled.div`
42 | width: 100%;
43 | height: 45vh;
44 | display: flex;
45 | align-items: center;
46 | justify-content: flex-start;
47 | flex-direction: column;
48 | overflow-y: scroll;
49 | padding: 0 2rem;
50 |
51 | ::-webkit-scrollbar {
52 | width: 10px;
53 | }
54 | ::-webkit-scrollbar-track {
55 | background: ${({ theme }) => theme.secondaryColor};
56 | border-radius: 10px;
57 | }
58 |
59 | ::-webkit-scrollbar-thumb {
60 | background: ${({ theme }) => theme.primaryColor};
61 | }
62 | `;
63 | const StyledInfoWrapper = styled.div`
64 | width: 100%;
65 | display: flex;
66 | align-items: center;
67 | justify-content: flex-end;
68 | margin: 5rem 15rem 0 0;
69 | `;
70 |
71 | const StyledPrice = styled.span`
72 | align-self: center;
73 | justify-self: center;
74 | font-weight: ${({ theme }) => theme.bold};
75 | font-size: 2rem;
76 | margin-right: 2rem;
77 | `;
78 |
79 | const Checkout = () => {
80 | const [pageWidth, setPageWidth] = useState(window.innerWidth);
81 | const { cartItems, addItem, removeItem, cartTotal, clearColor } = useContext(CartContext);
82 |
83 | const updateDimensions = () => {
84 | setPageWidth(window.innerWidth);
85 | };
86 |
87 | useEffect(() => {
88 | window.addEventListener('resize', updateDimensions);
89 | clearColor();
90 | return () => {
91 | window.removeEventListener('resize', updateDimensions);
92 | };
93 | }, []);
94 | return (
95 |
96 | {pageWidth >= 1000 ? (
97 |
98 | ) : (
99 | <>
100 |
101 | >
102 | )}
103 |
104 |
105 | }>
106 |
107 | {cartItems.length ? (
108 | cartItems.map(cartItem => (
109 |
115 | ))
116 | ) : (
117 | cart is empty
118 | )}
119 |
120 |
121 |
122 |
123 | ${cartTotal}
124 |
125 |
126 |
127 |
128 |
129 | );
130 | };
131 |
132 | export default Checkout;
133 |
--------------------------------------------------------------------------------
/src/components/atoms/RangeInput/RangeInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 |
5 | const StyledInputWrapper = styled.div`
6 | width: 24rem;
7 | margin: 2rem 1.5rem;
8 |
9 | @media only screen and (min-width: 500px) {
10 | width: 28rem;
11 | }
12 | @media only screen and (min-width: 700px) {
13 | width: 24rem;
14 | margin: 0rem 1.5rem;
15 | }
16 | input[type='range'] {
17 | -webkit-appearance: none;
18 | background: none;
19 | cursor: pointer;
20 | }
21 | input[type='range']::-webkit-slider-runnable-track {
22 | height: 5px;
23 | background: ${({ theme }) => theme.secondaryColor};
24 | border: none;
25 | border-radius: 3px;
26 | cursor: pointer;
27 | }
28 | input[type='range']::-ms-track {
29 | height: 5px;
30 | background: ${({ theme }) => theme.secondaryColor};
31 | border: none;
32 | border-radius: 3px;
33 | cursor: pointer;
34 | }
35 | input[type='range']::-moz-range-track {
36 | height: 5px;
37 | background: ${({ theme }) => theme.secondaryColor};
38 | border: none;
39 | border-radius: 3px;
40 | cursor: pointer;
41 | }
42 | input[type='range']::-webkit-slider-thumb {
43 | -webkit-appearance: none;
44 | border: none;
45 | height: 16px;
46 | width: 16px;
47 | border-radius: 50%;
48 | background: ${({ theme }) => theme.fontColorPrimary};
49 | margin-top: -5px;
50 | position: relative;
51 | cursor: pointer;
52 | }
53 | input[type='range']::-ms-thumb {
54 | -webkit-appearance: none;
55 | border: none;
56 | height: 16px;
57 | width: 16px;
58 | border-radius: 50%;
59 | background: ${({ theme }) => theme.fontColorPrimary};
60 | margin-top: -5px;
61 | position: relative;
62 | cursor: pointer;
63 | }
64 | input[type='range']::-moz-range-thumb {
65 | -webkit-appearance: none;
66 | border: none;
67 | height: 16px;
68 | width: 16px;
69 | border-radius: 50%;
70 | background: ${({ theme }) => theme.fontColorPrimary};
71 | margin-top: -5px;
72 | position: relative;
73 | cursor: pointer;
74 | }
75 | input[type='range']:focus {
76 | &::-webkit-slider-thumb:after {
77 | position: absolute;
78 | cursor: pointer;
79 | top: -35px;
80 | left: 50%;
81 | transform: translateX(-50%);
82 | background: #eee;
83 | border-radius: 5px;
84 | color: ${({ theme }) => theme.fontColorPrimary};
85 | padding: 5px 10px;
86 | border: 2px solid ${({ theme }) => theme.fontColorPrimary};
87 | }
88 | &::-ms-thumb:after {
89 | cursor: pointer;
90 | position: absolute;
91 | top: -35px;
92 | left: 50%;
93 | transform: translateX(-50%);
94 | background: #eee;
95 | border-radius: 5px;
96 | color: ${({ theme }) => theme.fontColorPrimary};
97 | padding: 5px 10px;
98 | border: 2px solid ${({ theme }) => theme.fontColorPrimary};
99 | }
100 | &::-moz-range-thumb:after {
101 | cursor: pointer;
102 | position: absolute;
103 | top: -35px;
104 | left: 50%;
105 | transform: translateX(-50%);
106 | background: #eee;
107 | border-radius: 5px;
108 | color: #000;
109 | padding: 5px 10px;
110 | border: 2px solid #000;
111 | }
112 | }
113 | `;
114 |
115 | const StyledInput = styled.input`
116 | cursor: pointer;
117 | width: 100%;
118 | `;
119 |
120 | const StyledLabel = styled.label`
121 | display: block;
122 | font-size: 0.95rem;
123 | font-weight: ${({ theme }) => theme.regular};
124 | text-align: start;
125 | `;
126 |
127 | const RangeInput = ({ onChange, minPrice, maxPrice, value, name, price }) => {
128 | return (
129 |
130 |
131 | Price range ${minPrice} - ${price}
132 |
133 |
144 |
145 | );
146 | };
147 | RangeInput.propTypes = {
148 | name: PropTypes.string,
149 | onChange: PropTypes.func,
150 | value: PropTypes.any,
151 | minPrice: PropTypes.number,
152 | maxPrice: PropTypes.number,
153 | price: PropTypes.number,
154 | };
155 |
156 | RangeInput.defaultProps = {
157 | name: 'range',
158 | onChange: () => {},
159 | value: 50,
160 | minPrice: 0,
161 | maxPrice: 100,
162 | price: 15,
163 | };
164 | export default RangeInput;
165 |
--------------------------------------------------------------------------------
/src/views/SinglePlant.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { css } from 'styled-components';
3 | import { Link } from 'react-router-dom';
4 | import PropTypes from 'prop-types';
5 | import Heading from '../components/atoms/Heading/Heading';
6 | import Text from '../components/atoms/Text/Text';
7 | import Button from '../components/atoms/Button/Button';
8 | import PlantHalfPage from '../components/molecules/PlantHalfPage';
9 | import Header from '../components/organisms/Header';
10 | import FlowerPots from '../components/molecules/FlowerPots';
11 | import Modal from '../components/molecules/Modal';
12 | import { CartContext } from '../context/CartContext';
13 |
14 | const StyledWrapper = styled.div`
15 | min-height: 100vh;
16 | display: flex;
17 | align-items: center;
18 | flex-direction: column;
19 | justify-content: space-between;
20 | position: relative;
21 |
22 | @media only screen and (min-width: 1000px) {
23 | flex-direction: row;
24 | overflow: hidden;
25 | height: 100vh;
26 | }
27 |
28 | ${({ notfound }) =>
29 | notfound &&
30 | css`
31 | height: 50vh;
32 | flex-direction: column;
33 | justify-content: space-around;
34 | @media only screen and (min-width: 1000px) {
35 | flex-direction: column;
36 | }
37 | `}
38 | `;
39 | const StyledDeteailsWrapper = styled.div`
40 | width: 80%;
41 | height: 100%;
42 | display: flex;
43 | align-items: center;
44 | justify-content: space-around;
45 | flex-direction: column;
46 | padding: 6rem 4rem 0rem 2rem;
47 | @media only screen and (min-width: 1000px) {
48 | width: 50%;
49 | padding: 4rem 4rem 10rem 4rem;
50 | }
51 | `;
52 |
53 | const StyledTextWrapper = styled.section`
54 | width: 100%;
55 | display: flex;
56 | align-items: center;
57 | justify-content: center;
58 | flex-direction: column;
59 | margin: 3rem 0 0 0;
60 | @media only screen and (min-width: 1000px) {
61 | margin: 2rem 0 0 2rem;
62 | }
63 | `;
64 |
65 | const StyledTypeText = styled.span`
66 | color: ${({ theme }) => theme.fontColorHeading};
67 | font-weight: ${({ theme }) => theme.bold};
68 | margin: 0.5rem;
69 |
70 | ${({ price }) =>
71 | price &&
72 | css`
73 | color: ${({ theme }) => theme.fontColorHeading};
74 | font-size: 2rem;
75 | `};
76 | `;
77 |
78 | const StyledInfoWrapper = styled.article`
79 | margin-top: 2rem;
80 | `;
81 |
82 | const StyledPaymentWrapper = styled.div`
83 | margin: 1rem 0rem 7rem 0;
84 | width: 100%;
85 | display: flex;
86 | align-items: flex;
87 | justify-content: flex-end;
88 |
89 | @media only screen and (min-width: 1000px) {
90 | margin: 1rem 0 0 0;
91 | }
92 | `;
93 |
94 | const StyledButton = styled(Button)`
95 | margin-left: 1rem;
96 | width: 12rem;
97 | height: 4rem;
98 | `;
99 |
100 | const StyledHeading = styled(Heading)`
101 | font-size: 3.5rem;
102 |
103 | @media only screen and (min-width: 500px) {
104 | font-size: 4rem;
105 | }
106 |
107 | @media only screen and (min-width: 1000px) {
108 | font-size: 5rem;
109 | }
110 | `;
111 | const StyledLink = styled(Link)`
112 | text-decoration: none;
113 | `;
114 |
115 | class SinglePlant extends React.Component {
116 | static contextType = CartContext;
117 |
118 | constructor(props) {
119 | super(props);
120 |
121 | const {
122 | match: {
123 | params: { slug },
124 | },
125 | } = this.props;
126 |
127 | this.state = {
128 | slug,
129 | isModal: false,
130 | };
131 | }
132 |
133 | openModal = () => {
134 | this.setState({
135 | isModal: true,
136 | });
137 | };
138 |
139 | closeModal = () => {
140 | this.setState({
141 | isModal: false,
142 | });
143 | };
144 |
145 | render() {
146 | const { slug } = this.state;
147 | const { getPlant, addItem } = this.context;
148 | const plant = getPlant(slug);
149 | if (!plant) {
150 | return (
151 | <>
152 |
153 |
154 | No such plant could be found
155 |
156 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
253 | Olaf Sulich
254 | {' '}
255 |
256 |
257 |
258 |
259 | );
260 | };
261 |
262 | export default Login;
263 |
--------------------------------------------------------------------------------
/src/assets/svg/404.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------