36 |
this.handleMouseOver()}
39 | onMouseOut={() => this.handleMouseOut()}
40 | onClick={() => removeProduct(product)}
41 | />
42 |
47 |
48 |
{product.title}
49 |
50 | {`${product.availableSizes[0]} | ${product.style}`}
51 | Quantity: {product.quantity}
52 |
53 |
54 |
55 |
{`${product.currencyFormat} ${formatPrice(product.price)}`}
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | export default CartProduct;
63 |
--------------------------------------------------------------------------------
/src/components/Shelf/Filter/style.scss:
--------------------------------------------------------------------------------
1 | .filters {
2 | width: 15%;
3 | margin-right: 15px;
4 |
5 | .star-button-container {
6 | text-align: center;
7 | small {
8 | color: #aaa;
9 | margin-bottom: 8px;
10 | display: inline-block;
11 | }
12 | }
13 |
14 | .title {
15 | margin-top: 2px;
16 | margin-bottom: 20px;
17 | }
18 |
19 | &-available-size {
20 | display: inline-block;
21 | margin-bottom: 10px;
22 | /* Customize the label (the container) */
23 | label {
24 | display: inline-block;
25 | position: relative;
26 | cursor: pointer;
27 | font-size: 22px;
28 | -webkit-user-select: none;
29 | -moz-user-select: none;
30 | -ms-user-select: none;
31 | user-select: none;
32 | width: 35px;
33 | height: 35px;
34 | font-size: 0.8em;
35 | margin-bottom: 8px;
36 | margin-right: 8px;
37 | border-radius: 50%;
38 | line-height: 35px;
39 | text-align: center;
40 |
41 | /* On mouse-over, add a grey background color */
42 | &:hover input ~ .checkmark {
43 | border: 1px solid #1b1a20;
44 | }
45 |
46 | /* When the checkbox is checked, add a blue background */
47 | & input:checked ~ .checkmark {
48 | background-color: #1b1a20;
49 | color: #ececec;
50 | }
51 |
52 | /* Show the checkmark when checked */
53 | & input:checked ~ .checkmark:after {
54 | display: block;
55 | }
56 |
57 | input {
58 | position: absolute;
59 | opacity: 0;
60 | cursor: pointer;
61 | }
62 |
63 | /* Create a custom checkbox */
64 | .checkmark {
65 | position: absolute;
66 | top: 0;
67 | left: 0;
68 | width: 35px;
69 | height: 35px;
70 | font-size: 0.8em;
71 | border-radius: 50%;
72 | line-height: 35px;
73 | text-align: center;
74 | color: #1b1a20;
75 | background-color: #ececec;
76 |
77 | border: 1px solid transparent;
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/Shelf/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | import { fetchProducts } from '../../services/shelf/actions';
6 |
7 | import Spinner from '../Spinner';
8 | import ShelfHeader from './ShelfHeader';
9 | import ProductList from './ProductList';
10 |
11 | import './style.scss';
12 |
13 | class Shelf extends Component {
14 | static propTypes = {
15 | fetchProducts: PropTypes.func.isRequired,
16 | products: PropTypes.array.isRequired,
17 | filters: PropTypes.array,
18 | sort: PropTypes.string
19 | };
20 |
21 | state = {
22 | isLoading: false
23 | };
24 |
25 | componentDidMount() {
26 | this.handleFetchProducts();
27 | }
28 |
29 | componentWillReceiveProps(nextProps) {
30 | const { filters: nextFilters, sort: nextSort } = nextProps;
31 |
32 | if (nextFilters !== this.props.filters) {
33 | this.handleFetchProducts(nextFilters, undefined);
34 | }
35 |
36 | if (nextSort !== this.props.sort) {
37 | this.handleFetchProducts(undefined, nextSort);
38 | }
39 | }
40 |
41 | handleFetchProducts = (
42 | filters = this.props.filters,
43 | sort = this.props.sort
44 | ) => {
45 | this.setState({ isLoading: true });
46 | this.props.fetchProducts(filters, sort, () => {
47 | this.setState({ isLoading: false });
48 | });
49 | };
50 |
51 | render() {
52 | const { products } = this.props;
53 | const { isLoading } = this.state;
54 |
55 | return (
56 |
57 | {isLoading && }
58 |
62 |
63 | );
64 | }
65 | }
66 |
67 | const mapStateToProps = state => ({
68 | products: state.shelf.products,
69 | filters: state.filters.items,
70 | sort: state.sort.type
71 | });
72 |
73 | export default connect(
74 | mapStateToProps,
75 | { fetchProducts }
76 | )(Shelf);
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 🛍️ Simple ecommerce cart application [](https://circleci.com/gh/jeffersonRibeiro/react-shopping-cart)
2 |
3 |
4 |
5 |
6 |
7 |
8 | ## Basic Overview - [Live Demo](https://react-shopping-cart-67954.firebaseapp.com/)
9 |
10 | This simple shopping cart prototype shows how React components and Redux can be used to build a
11 | friendly user experience with instant visual updates and scaleable code in ecommerce applications.
12 |
13 | #### Features
14 |
15 | - Add and remove products from the floating cart
16 | - Sort products by highest to lowest and lowest to highest price
17 | - Filter products by available sizes
18 | - Products persist in floating cart after page reloads
19 | - Unit tests, integration tests and e2e testing
20 | - Responsive design
21 |
22 | ## Getting started
23 |
24 | Try playing with the code on CodeSandbox :)
25 |
26 | [](https://codesandbox.io/s/74rykw70qq)
27 |
28 | ## Build/Run
29 |
30 | #### Requirements
31 |
32 | - Node.js
33 | - NPM
34 |
35 | ```javascript
36 |
37 | /* First, Install the needed packages */
38 | npm install
39 |
40 | /* Then start both Node and React */
41 | npm start
42 |
43 | /* To run the tests */
44 | npm run test
45 |
46 | /* Running e2e tests */
47 | npm run wdio
48 |
49 |
50 | ```
51 |
52 | ## About tests
53 |
54 | - Unit tests
55 | - All components have at least a basic smoke test
56 | - Integration tests
57 | - Fetch product and add to cart properly
58 | - e2e
59 | - Webdriverio - Add and remove product from cart
60 |
61 | ### Copyright and license
62 |
63 | The MIT License (MIT). Please see License File for more information.
64 |
65 |
66 |
67 |
68 |

69 |
70 | A little project by Jefferson Ribeiro
71 |
72 |
--------------------------------------------------------------------------------
/src/components/Shelf/ProductList/Product/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | import Thumb from '../../../Thumb';
6 | import { formatPrice } from '../../../../services/util';
7 | import { addProduct } from '../../../../services/cart/actions';
8 |
9 | const Product = ({ product, addProduct }) => {
10 | product.quantity = 1;
11 |
12 | let formattedPrice = formatPrice(product.price, product.currencyId);
13 |
14 | let productInstallment;
15 |
16 | if (!!product.installments) {
17 | const installmentPrice = product.price / product.installments;
18 |
19 | productInstallment = (
20 |
21 | or {product.installments} x
22 |
23 | {product.currencyFormat}
24 | {formatPrice(installmentPrice, product.currencyId)}
25 |
26 |
27 | );
28 | }
29 |
30 | return (
31 |
addProduct(product)}
34 | data-sku={product.sku}
35 | >
36 | {product.isFreeShipping && (
37 |
Free shipping
38 | )}
39 |
44 |
{product.title}
45 |
46 |
47 | {product.currencyFormat}
48 | {formattedPrice.substr(0, formattedPrice.length - 3)}
49 | {formattedPrice.substr(formattedPrice.length - 3, 3)}
50 |
51 | {productInstallment}
52 |
53 |
Add to cart
54 |
55 | );
56 | };
57 |
58 | Product.propTypes = {
59 | product: PropTypes.object.isRequired,
60 | addProduct: PropTypes.func.isRequired
61 | };
62 |
63 | export default connect(
64 | null,
65 | { addProduct }
66 | )(Product);
67 |
--------------------------------------------------------------------------------
/src/components/Shelf/style.scss:
--------------------------------------------------------------------------------
1 | .shelf-container {
2 | display: flex;
3 | flex-wrap: wrap;
4 | width: 85%;
5 | min-height: 600px;
6 |
7 | &-header {
8 | width: 100%;
9 | margin-bottom: 10px;
10 |
11 | .products-found {
12 | float: left;
13 | margin: 0;
14 | margin-top: 8px;
15 | }
16 |
17 | .sort {
18 | float: right;
19 |
20 | select {
21 | background-color: #fff;
22 | outline: none;
23 | border: 1px solid #ececec;
24 | border-radius: 2px;
25 | margin-left: 10px;
26 | width: auto;
27 | height: 35px;
28 | cursor: pointer;
29 |
30 | &:hover {
31 | border: 1px solid #5b5a5e;
32 | }
33 | }
34 | }
35 | }
36 |
37 | .shelf-item {
38 | width: 25%;
39 | position: relative;
40 | text-align: center;
41 | box-sizing: border-box;
42 | padding: 10px;
43 | margin-bottom: 30px;
44 | border: 1px solid transparent;
45 | cursor: pointer;
46 |
47 | &:hover {
48 | border: 1px solid #eee;
49 |
50 | .shelf-item__buy-btn {
51 | background-color: #eabf00;
52 | }
53 | }
54 |
55 | .shelf-stopper {
56 | position: absolute;
57 | color: #ececec;
58 | top: 10px;
59 | right: 10px;
60 | padding: 5px;
61 | font-size: 0.6em;
62 | background-color: #1b1a20;
63 | cursor: default;
64 | }
65 |
66 | &__thumb {
67 | img {
68 | width: 100%;
69 | }
70 | }
71 |
72 | &__title {
73 | position: relative;
74 | padding: 0 20px;
75 | height: 45px;
76 |
77 | &::before {
78 | content: '';
79 | width: 20px;
80 | height: 2px;
81 | background-color: #eabf00;
82 | position: absolute;
83 | bottom: 0;
84 | left: 50%;
85 | margin-left: -10px;
86 | }
87 | }
88 |
89 | &__price {
90 | height: 60px;
91 |
92 | .val {
93 | b {
94 | font-size: 1.5em;
95 | margin-left: 5px;
96 | }
97 | }
98 |
99 | .installment {
100 | color: #9c9b9b;
101 | }
102 | }
103 |
104 | &__buy-btn {
105 | background-color: #1b1a20;
106 | color: #fff;
107 | padding: 15px 0;
108 | margin-top: 10px;
109 | cursor: pointer;
110 | // border-bottom: 2px solid #151419;
111 |
112 | transition: background-color 0.2s;
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Roboto');
2 |
3 | body {
4 | margin: 0;
5 | padding: 0;
6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
7 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
8 | sans-serif;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | color: #1b1a20;
16 | font-family: 'Roboto', sans-serif;
17 | }
18 |
19 | main {
20 | display: flex;
21 | padding: 20px 2%;
22 | max-width: 1200px;
23 | margin: 50px auto 0 auto;
24 | }
25 |
26 | @media only screen and (max-width: 1024px) {
27 | body {
28 | .filters {
29 | width: 20%;
30 | }
31 |
32 | .shelf-container {
33 | width: 80%;
34 |
35 | .shelf-item {
36 | width: 33.33%;
37 | }
38 | }
39 | }
40 | }
41 |
42 | @media only screen and (max-width: 640px) {
43 | body {
44 | .filters {
45 | width: 25%;
46 | }
47 |
48 | .shelf-container {
49 | width: 75%;
50 |
51 | .shelf-item {
52 | width: 50%;
53 | padding: 10px;
54 |
55 | &__title {
56 | margin-top: 5px;
57 | padding: 0;
58 | }
59 | }
60 | }
61 |
62 | .float-cart {
63 | width: 100%;
64 | right: -100%;
65 |
66 | &--open {
67 | right: 0;
68 | }
69 |
70 | &__close-btn {
71 | left: 0px;
72 | z-index: 2;
73 | background-color: #1b1a20;
74 | }
75 |
76 | &__header {
77 | padding: 25px 0;
78 | }
79 | }
80 | }
81 | }
82 |
83 | @media only screen and (max-width: 460px) {
84 | body {
85 | main {
86 | display: flex;
87 | flex-wrap: wrap;
88 | padding: 2%;
89 | margin-top: 42px;
90 | }
91 |
92 | .filters {
93 | width: 100%;
94 | margin-right: 0;
95 | text-align: center;
96 |
97 | .title {
98 | margin-bottom: 15px;
99 | }
100 | }
101 |
102 | .shelf-container-header {
103 | .products-found {
104 | width: 100%;
105 | text-align: center;
106 | margin: 10px 0;
107 | }
108 |
109 | .sort {
110 | width: 100%;
111 | text-align: center;
112 | }
113 | }
114 |
115 | .shelf-container {
116 | width: 100%;
117 |
118 | .shelf-item {
119 | width: 50%;
120 |
121 | &__buy-btn {
122 | display: none;
123 | }
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/__tests__/integrations.test.js:
--------------------------------------------------------------------------------
1 | import moxios from 'moxios';
2 |
3 | import Root from '../Root';
4 | import App from '../components/App';
5 | import ShelfHeader from '../components/Shelf/ShelfHeader';
6 | import Product from '../components/Shelf/ProductList/Product';
7 | import CartProduct from '../components/FloatCart/CartProduct';
8 |
9 | import { productsAPI } from '../services/util';
10 |
11 | /*
12 | - Request the products;
13 | - check if the quantity returned is correct;
14 | - add 1 product to the cart and make sure it has been added correctly.
15 | */
16 |
17 | const productsMock = {
18 | products: [
19 | {
20 | id: 12,
21 | sku: 12064273040195392,
22 | title: 'Cat Tee Black T-Shirt',
23 | description: '4 MSL',
24 | availableSizes: ['S', 'XS'],
25 | style: 'Black with custom print',
26 | price: 10.9,
27 | installments: 9,
28 | currencyId: 'USD',
29 | currencyFormat: '$',
30 | isFreeShipping: true
31 | },
32 | {
33 | id: 13,
34 | sku: 51498472915966366,
35 | title: 'Dark Thug Blue-Navy T-Shirt',
36 | description: '',
37 | availableSizes: ['M'],
38 | style: 'Front print and paisley print',
39 | price: 29.45,
40 | installments: 5,
41 | currencyId: 'USD',
42 | currencyFormat: '$',
43 | isFreeShipping: true
44 | }
45 | ]
46 | };
47 |
48 | beforeEach(() => {
49 | moxios.install();
50 | moxios.stubRequest(productsAPI, {
51 | status: 200,
52 | response: productsMock
53 | });
54 | });
55 |
56 | afterEach(() => {
57 | moxios.uninstall();
58 | });
59 |
60 | describe('Integrations', () => {
61 | it('should fetch 2 products and add 1 to cart', done => {
62 | const wrapped = mount(
63 |
64 |
65 |
66 | );
67 |
68 | /* Before fetch the shelf should contain 0 products in it */
69 | expect(wrapped.find(ShelfHeader).props().productsLength).toEqual(0);
70 |
71 | moxios.wait(() => {
72 | wrapped.update();
73 |
74 | /* and then after fetch, should contain 2 */
75 | expect(wrapped.find(ShelfHeader).props().productsLength).toEqual(2);
76 |
77 | /* Cart should start with 0 products */
78 | expect(wrapped.find(CartProduct).length).toEqual(0);
79 |
80 | /* Click to add product to cart */
81 | wrapped
82 | .find(Product)
83 | .at(0)
84 | .simulate('click');
85 |
86 | /* Then after one product is added to cart, it should have 1 in it */
87 | expect(wrapped.find(CartProduct).length).toEqual(1);
88 |
89 | wrapped.unmount();
90 | done();
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
43 |
React Shopping Cart
44 |
45 |
46 |
49 |
50 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/.firebase/hosting.YnVpbGQ.cache:
--------------------------------------------------------------------------------
1 | manifest.json,1544571236320,6aaf3d87dd9bb1e96f9c691c50d0ed378ed98a120a76dea78291281834cd36e9
2 | 404.html,1546657424215,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
3 | precache-manifest.a2806c2cc70f935f4e37d114ebc0d351.js,1546657319601,82d0cd29c9b3965153448432e1f522d56bc9699155e278d6aca55bd68c51cb01
4 | normalize.css,1544571236321,825cbaf69007eddb6ea5f7bad1d2c410bc2bf5fde49236df7ec4f4ebb762e22a
5 | asset-manifest.json,1546657319601,8ffc1726a71baa3983073dba3f9cfe6ccef50327179f2ec17f6a14d20705654d
6 | index.html,1546657319600,5a2a49d3f380ef8c91b4e433fbbe75be583f9c48961cb2f0bce40021487be3eb
7 | favicon.ico,1545590992702,9627d588085a4d2460f9524c0024cb7adedf6eca6233f2f1c63d95dcaed89b6f
8 | service-worker.js,1546657319601,bc768226fbdc5e7d633af515e5d5a56aee44f8e1976c335ccd6700fc1f93acfc
9 | static/css/main.e5ddfe7f.chunk.css,1546657319606,272b9c02efc4d527e3d9ce388ef71af6def8c522828c941192bfec5eb1b6741f
10 | static/js/runtime~main.229c360f.js.map,1546657319612,b2f1f5578e572791ed8967e3d0090b7eb2ec5f9d87d1bd433d4d7ffdb5d15f5e
11 | static/css/main.e5ddfe7f.chunk.css.map,1546657319612,5fc10c9ed7883a6dc3ba1fcfaa0b4324de0c8b3ce6d34dcf853d40211e10c9a4
12 | static/js/runtime~main.229c360f.js,1546657319611,dbe189fe130313d04dc42edcaa021db9317ce6d58c07ab66fe538fdfe41fe6d7
13 | static/media/18644119330491312_2.eb35a657.jpg,1546657319611,a984e4d492013937aa466b736a37a10fb95a4e910b1ab62ee0139298fdf69078
14 | static/media/876661122392077_1.76d63530.jpg,1546657319611,0e84e35b29fea32a1f228560bb7a549b19a8725f0089ebc74966e99755343377
15 | static/media/10547961582846888_1.6ffa45d5.jpg,1546657319610,285935552b1acff4617351794249eadf3cb34960d2f007e1acef0e1b988c1214
16 | static/media/11033926921508488_1.cb8727d9.jpg,1546657319610,78fece86b7b276c1a676090b152f7db2349338372f6bc7c027fe63bcb0a86f15
17 | static/media/10412368723880252_1.854f9ebd.jpg,1546657319602,c065466ecd1fa4d2a4669ef9494294800ebf5e8658d4eaeda73f9c49882b7d3d
18 | static/media/11600983276356164_1.1fd27374.jpg,1546657319610,47f5ae2180468063baf18842eb54f30a8f9a08919960582aea516d4b5f8598c0
19 | static/media/11854078013954528_1.16d87c7b.jpg,1546657319610,f5aa5e955fd0b513fd280d95ffe1ba0dc483e8e94c22fe80eed2c612e69aee4f
20 | static/media/10686354557628304_1.b047a598.jpg,1546657319610,8b3236a4004aa6fde7bb266d5b1f1e2c2267bb0a94d97b7d7a3e1fa64cdf819f
21 | static/media/12064273040195392_1.4edb5154.jpg,1546657319610,5bcc6e7cab8879b1a9999e9fa11ef0ca7ce3f33d64f70d82e9c7ba950dbaf479
22 | static/media/18532669286405344_1.9d1a7699.jpg,1546657319610,8311234c16caf04d0457cd5d2922177fd67f293b58da00a7743a39013e44f968
23 | static/media/18644119330491312_1.d10d8287.jpg,1546657319610,5559025ac6c553f1be45b745d2bfe069fc5f498e51ec57a8fa4cd21225a22770
24 | static/media/18644119330491310_1.7bbbf40e.jpg,1546657319610,cf182361b800629b66013294953977ca12130df1ac2c5b7f56e951bf618202db
25 | static/media/39876704341265610_1.c9fb4794.jpg,1546657319611,3f585b68ce5179b58748fc7cf2495fea17015a695d948a4d03560f2978c8005a
26 | static/media/27250082398145996_1.5a5265ad.jpg,1546657319611,a9d70633fa856029698e104bcd885e188770aa7cabc6a1ae201e2d45dceb4787
27 | static/media/51498472915966370_1.8da09d0b.jpg,1546657319611,64ce817348cb575b2151dd291c7ac124fd483574407f7baffdfe039d84f4b3e5
28 | static/media/5619496040738316_1.d6803810.jpg,1546657319611,37fb86480efc7f6ba3aaa1ba0b54fafef156b23bcc210e740077c34409a87b53
29 | static/media/6090484789343891_1.a998813f.jpg,1546657319611,e312dd719229ca6994e6fc24cd51c1c59324e22caac08447aae9b4f610db48cd
30 | static/media/8552515751438644_1.08690d27.jpg,1546657319611,5559051e90755a86f0c1e6cf5ab36fc97cf9e161dbf5f7cd751fb5358bec62df
31 | static/media/9197907543445676_1.a5707e84.jpg,1546657319611,2846359e65756a76c9be7294e68a3f2955b3540dbc4f0060e1eb9623dfcb3f57
32 | static/js/1.e36b2a5b.chunk.js,1546657319611,7a5c0b7b89fbf58d17a40d702e4f76f559cb7797fb5569266ec54038e5187397
33 | static/js/main.5a94a4d0.chunk.js,1546657319607,7bc65cd7a0ca76711cdc68ae88f8557d3ec40ad262f9f71ced49cd52cddc6853
34 | static/js/main.5a94a4d0.chunk.js.map,1546657319612,b6c05000f94f164bc7160634aecc2c326cacf49532006582dbc9e30f8af5f8a8
35 | static/js/1.e36b2a5b.chunk.js.map,1546657319612,db6700ff7c344574049f3ea1c1b16825a9ce46e4ae8f9f8dd1bfec8519c06062
36 |
--------------------------------------------------------------------------------
/src/components/FloatCart/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { connect } from 'react-redux';
5 | import { loadCart, removeProduct } from '../../services/cart/actions';
6 | import { updateCart } from '../../services/total/actions';
7 | import CartProduct from './CartProduct';
8 | import { formatPrice } from '../../services/util';
9 |
10 | import './style.scss';
11 |
12 | class FloatCart extends Component {
13 | static propTypes = {
14 | loadCart: PropTypes.func.isRequired,
15 | updateCart: PropTypes.func.isRequired,
16 | cartProducts: PropTypes.array.isRequired,
17 | newProduct: PropTypes.object,
18 | removeProduct: PropTypes.func,
19 | productToRemove: PropTypes.object
20 | };
21 |
22 | state = {
23 | isOpen: false
24 | };
25 |
26 | componentWillReceiveProps(nextProps) {
27 | if (nextProps.newProduct !== this.props.newProduct) {
28 | this.addProduct(nextProps.newProduct);
29 | }
30 |
31 | if (nextProps.productToRemove !== this.props.productToRemove) {
32 | this.removeProduct(nextProps.productToRemove);
33 | }
34 | }
35 |
36 | openFloatCart = () => {
37 | this.setState({ isOpen: true });
38 | };
39 |
40 | closeFloatCart = () => {
41 | this.setState({ isOpen: false });
42 | };
43 |
44 | addProduct = product => {
45 | const { cartProducts, updateCart } = this.props;
46 | let productAlreadyInCart = false;
47 |
48 | cartProducts.forEach(cp => {
49 | if (cp.id === product.id) {
50 | cp.quantity += product.quantity;
51 | productAlreadyInCart = true;
52 | }
53 | });
54 |
55 | if (!productAlreadyInCart) {
56 | cartProducts.push(product);
57 | }
58 |
59 | updateCart(cartProducts);
60 | this.openFloatCart();
61 | };
62 |
63 | removeProduct = product => {
64 | const { cartProducts, updateCart } = this.props;
65 |
66 | const index = cartProducts.findIndex(p => p.id === product.id);
67 | if (index >= 0) {
68 | cartProducts.splice(index, 1);
69 | updateCart(cartProducts);
70 | }
71 | };
72 |
73 | proceedToCheckout = () => {
74 | const {
75 | totalPrice,
76 | productQuantity,
77 | currencyFormat,
78 | currencyId
79 | } = this.props.cartTotal;
80 |
81 | if (!productQuantity) {
82 | alert('Add some product in the cart!');
83 | } else {
84 | alert(
85 | `Checkout - Subtotal: ${currencyFormat} ${formatPrice(
86 | totalPrice,
87 | currencyId
88 | )}`
89 | );
90 | }
91 | };
92 |
93 | render() {
94 | const { cartTotal, cartProducts, removeProduct } = this.props;
95 |
96 | const products = cartProducts.map(p => {
97 | return (
98 |
99 | );
100 | });
101 |
102 | let classes = ['float-cart'];
103 |
104 | if (!!this.state.isOpen) {
105 | classes.push('float-cart--open');
106 | }
107 |
108 | return (
109 |
110 | {/* If cart open, show close (x) button */}
111 | {this.state.isOpen && (
112 |
this.closeFloatCart()}
114 | className="float-cart__close-btn"
115 | >
116 | X
117 |
118 | )}
119 |
120 | {/* If cart is closed, show bag with quantity of product and open cart action */}
121 | {!this.state.isOpen && (
122 |
this.openFloatCart()}
124 | className="bag bag--float-cart-closed"
125 | >
126 | {cartTotal.productQuantity}
127 |
128 | )}
129 |
130 |
131 |
132 |
133 | {cartTotal.productQuantity}
134 |
135 | Cart
136 |
137 |
138 |
139 | {products}
140 | {!products.length && (
141 |
142 | Add some products in the cart
143 | :)
144 |
145 | )}
146 |
147 |
148 |
149 |
SUBTOTAL
150 |
151 |
152 | {`${cartTotal.currencyFormat} ${formatPrice(
153 | cartTotal.totalPrice,
154 | cartTotal.currencyId
155 | )}`}
156 |
157 |
158 | {!!cartTotal.installments && (
159 |
160 | {`OR UP TO ${cartTotal.installments} x ${
161 | cartTotal.currencyFormat
162 | } ${formatPrice(
163 | cartTotal.totalPrice / cartTotal.installments,
164 | cartTotal.currencyId
165 | )}`}
166 |
167 | )}
168 |
169 |
170 |
this.proceedToCheckout()} className="buy-btn">
171 | Checkout
172 |
173 |
174 |
175 |
176 | );
177 | }
178 | }
179 |
180 | const mapStateToProps = state => ({
181 | cartProducts: state.cart.products,
182 | newProduct: state.cart.productToAdd,
183 | productToRemove: state.cart.productToRemove,
184 | cartTotal: state.total.data
185 | });
186 |
187 | export default connect(
188 | mapStateToProps,
189 | { loadCart, updateCart, removeProduct }
190 | )(FloatCart);
191 |
--------------------------------------------------------------------------------
/src/components/FloatCart/style.scss:
--------------------------------------------------------------------------------
1 | .float-cart {
2 | position: fixed;
3 | top: 0;
4 | right: -450px;
5 | width: 450px;
6 | height: 100%;
7 | background-color: #1b1a20;
8 | box-sizing: border-box;
9 |
10 | transition: right 0.2s;
11 |
12 | &--open {
13 | right: 0;
14 | }
15 |
16 | &__close-btn {
17 | width: 50px;
18 | height: 50px;
19 | color: #ececec;
20 | background-color: #1b1a20;
21 | text-align: center;
22 | line-height: 50px;
23 | position: absolute;
24 | top: 0;
25 | left: -50px;
26 | cursor: pointer;
27 |
28 | &:hover {
29 | background-color: #212027;
30 | }
31 | }
32 |
33 | .bag {
34 | width: 40px;
35 | height: 40px;
36 | position: relative;
37 | display: inline-block;
38 | vertical-align: middle;
39 | margin-right: 15px;
40 | background-image: url('../../static/bag-icon.png');
41 | background-repeat: no-repeat;
42 | background-size: contain;
43 | background-position: center;
44 |
45 | &--float-cart-closed {
46 | position: absolute;
47 | background-color: #000;
48 | background-size: 50%;
49 | left: -60px;
50 | width: 60px;
51 | height: 60px;
52 | cursor: pointer;
53 |
54 | .bag__quantity {
55 | bottom: 5px;
56 | right: 10px;
57 | }
58 | }
59 |
60 | &__quantity {
61 | display: inline-block;
62 | width: 18px;
63 | height: 18px;
64 | color: #0c0b10;
65 | font-weight: bold;
66 | font-size: 0.7em;
67 | text-align: center;
68 | line-height: 18px;
69 | border-radius: 50%;
70 | background-color: #eabf00;
71 | position: absolute;
72 | bottom: -5px;
73 | right: 0px;
74 | }
75 | }
76 |
77 | &__header {
78 | color: #ececec;
79 | box-sizing: border-box;
80 | text-align: center;
81 | padding: 45px 0;
82 |
83 | .header-title {
84 | font-weight: bold;
85 | font-size: 1.2em;
86 | vertical-align: middle;
87 | }
88 | }
89 |
90 | &__shelf-container {
91 | position: relative;
92 | min-height: 280px;
93 | padding-bottom: 200px;
94 |
95 | .shelf-empty {
96 | color: #ececec;
97 | text-align: center;
98 | line-height: 40px;
99 | }
100 |
101 | .shelf-item {
102 | position: relative;
103 | box-sizing: border-box;
104 | padding: 5%;
105 |
106 | transition: background-color 0.2s, opacity 0.2s;
107 |
108 | &::before {
109 | content: '';
110 | width: 90%;
111 | height: 2px;
112 | background-color: rgba(0, 0, 0, 0.2);
113 | position: absolute;
114 | top: 0;
115 | left: 5%;
116 | }
117 |
118 | &--mouseover {
119 | background: #0c0b10;
120 |
121 | .shelf-item__details {
122 | .title,
123 | .desc {
124 | text-decoration: line-through;
125 | opacity: 0.6;
126 | }
127 | }
128 |
129 | .shelf-item__price {
130 | text-decoration: line-through;
131 | opacity: 0.6;
132 | }
133 | }
134 |
135 | &__del {
136 | width: 16px;
137 | height: 16px;
138 | top: 15px;
139 | right: 5%;
140 | border-radius: 50%;
141 | position: absolute;
142 | background-size: auto 100%;
143 | background-image: url('../../static/sprite_delete-icon.png');
144 | background-repeat: no-repeat;
145 | z-index: 2;
146 | cursor: pointer;
147 |
148 | &:hover {
149 | background-position-x: -17px;
150 | }
151 | }
152 |
153 | &__thumb,
154 | &__details,
155 | &__price {
156 | display: inline-block;
157 | vertical-align: middle;
158 | }
159 |
160 | &__thumb {
161 | vertical-align: middle;
162 | width: 15%;
163 | margin-right: 3%;
164 |
165 | img {
166 | width: 100%;
167 | height: auto;
168 | }
169 | }
170 | &__details {
171 | width: 57%;
172 |
173 | .title {
174 | color: #ececec;
175 | margin: 0;
176 | }
177 |
178 | .desc {
179 | color: #5b5a5e;
180 | margin: 0;
181 | }
182 | }
183 | &__price {
184 | color: #eabf00;
185 | text-align: right;
186 | width: 25%;
187 | }
188 | }
189 | }
190 |
191 | &__footer {
192 | box-sizing: border-box;
193 | padding: 5%;
194 | position: absolute;
195 | bottom: 0;
196 | width: 100%;
197 | height: 200px;
198 | z-index: 2;
199 | background-color: #1b1a20;
200 |
201 | &::before {
202 | content: '';
203 | width: 100%;
204 | height: 20px;
205 | display: block;
206 | position: absolute;
207 | top: -20px;
208 | left: 0;
209 | background: linear-gradient(to top, rgba(0, 0, 0, 0.2), transparent);
210 | }
211 |
212 | .sub,
213 | .sub-price {
214 | color: #5b5a5e;
215 | vertical-align: middle;
216 | display: inline-block;
217 | }
218 |
219 | .sub {
220 | width: 20%;
221 | }
222 |
223 | .sub-price {
224 | width: 80%;
225 | text-align: right;
226 |
227 | &__val,
228 | &__installment {
229 | margin: 0;
230 | }
231 |
232 | &__val {
233 | color: #eabf00;
234 | font-size: 22px;
235 | }
236 | }
237 |
238 | .buy-btn {
239 | color: #ececec;
240 | text-transform: uppercase;
241 | background-color: #0c0b10;
242 | text-align: center;
243 | padding: 15px 0;
244 | margin-top: 40px;
245 | cursor: pointer;
246 |
247 | transition: background-color 0.2s;
248 |
249 | &:hover {
250 | background-color: #000;
251 | }
252 | }
253 | }
254 | }
255 |
256 | /* MAC scrollbar para desktop*/
257 | @media screen and (min-width: 640px) {
258 | .float-cart__content::-webkit-scrollbar {
259 | -webkit-appearance: none;
260 | width: 10px;
261 | background-color: rgba(0, 0, 0, 0.2);
262 | padding: 10px;
263 | }
264 | .float-cart__content::-webkit-scrollbar-thumb {
265 | border-radius: 4px;
266 | background-color: #0c0b10;
267 | }
268 | }
269 |
270 | .float-cart__content {
271 | height: 100%;
272 | overflow-y: scroll;
273 | }
274 |
--------------------------------------------------------------------------------
/server/data/products.json:
--------------------------------------------------------------------------------
1 | {
2 | "products": [
3 | {
4 | "id": 12,
5 | "sku": 12064273040195392,
6 | "title": "Cat Tee Black T-Shirt",
7 | "description": "4 MSL",
8 | "availableSizes": ["S", "XS"],
9 | "style": "Black with custom print",
10 | "price": 10.9,
11 | "installments": 9,
12 | "currencyId": "USD",
13 | "currencyFormat": "$",
14 | "isFreeShipping": true
15 | },
16 |
17 | {
18 | "id": 13,
19 | "sku": 51498472915966366,
20 | "title": "Dark Thug Blue-Navy T-Shirt",
21 | "description": "",
22 | "availableSizes": ["M"],
23 | "style": "Front print and paisley print",
24 | "price": 29.45,
25 | "installments": 5,
26 | "currencyId": "USD",
27 | "currencyFormat": "$",
28 | "isFreeShipping": true
29 | },
30 |
31 | {
32 | "id": 14,
33 | "sku": 10686354557628303,
34 | "title": "Sphynx Tie Dye Wine T-Shirt",
35 | "description": "GPX Poly 1",
36 | "availableSizes": ["X", "L", "XL"],
37 | "style": "Front tie dye print",
38 | "price": 9.0,
39 | "installments": 3,
40 | "currencyId": "USD",
41 | "currencyFormat": "$",
42 | "isFreeShipping": true
43 | },
44 |
45 | {
46 | "id": 15,
47 | "sku": 11033926921508487,
48 | "title": "Skuul",
49 | "description": "Treino 2014",
50 | "availableSizes": ["X", "L", "XL", "XXL"],
51 | "style": "Black T-Shirt with front print",
52 | "price": 14.0,
53 | "installments": 5,
54 | "currencyId": "USD",
55 | "currencyFormat": "$",
56 | "isFreeShipping": true
57 | },
58 |
59 | {
60 | "id": 11,
61 | "sku": 39876704341265606,
62 | "title": "Wine Skul T-Shirt",
63 | "description": "",
64 | "availableSizes": ["X", "L"],
65 | "style": "Wine",
66 | "price": 13.25,
67 | "installments": 3,
68 | "currencyId": "USD",
69 | "currencyFormat": "$",
70 | "isFreeShipping": true
71 | },
72 |
73 | {
74 | "id": 16,
75 | "sku": 10412368723880253,
76 | "title": "Short Sleeve T-Shirt",
77 | "description": "",
78 | "availableSizes": ["XS", "X", "L", "ML", "XL"],
79 | "style": "Grey",
80 | "price": 75.0,
81 | "installments": 5,
82 | "currencyId": "USD",
83 | "currencyFormat": "$",
84 | "isFreeShipping": true
85 | },
86 |
87 | {
88 | "id": 0,
89 | "sku": 8552515751438644,
90 | "title": "Cat Tee Black T-Shirt",
91 | "description": "14/15 s/nº",
92 | "availableSizes": ["X", "L", "XL", "XXL"],
93 | "style": "Branco com listras pretas",
94 | "price": 10.9,
95 | "installments": 9,
96 | "currencyId": "USD",
97 | "currencyFormat": "$",
98 | "isFreeShipping": true
99 | },
100 |
101 | {
102 | "id": 1,
103 | "sku": 18644119330491312,
104 | "title": "Sphynx Tie Dye Grey T-Shirt",
105 | "description": "14/15 s/nº",
106 | "availableSizes": ["X", "L", "XL", "XXL"],
107 | "style": "Preta com listras brancas",
108 | "price": 10.9,
109 | "installments": 9,
110 | "currencyId": "USD",
111 | "currencyFormat": "$",
112 | "isFreeShipping": true
113 | },
114 |
115 | {
116 | "id": 2,
117 | "sku": 11854078013954528,
118 | "title": "Danger Knife Grey",
119 | "description": "14/15 s/nº",
120 | "availableSizes": ["X", "L"],
121 | "style": "Branco com listras pretas",
122 | "price": 14.9,
123 | "installments": 7,
124 | "currencyId": "USD",
125 | "currencyFormat": "$",
126 | "isFreeShipping": true
127 | },
128 |
129 | {
130 | "id": 3,
131 | "sku": 876661122392077,
132 | "title": "White DGK Script Tee",
133 | "description": "2014 s/nº",
134 | "availableSizes": ["X", "L"],
135 | "style": "Preto com listras brancas",
136 | "price": 14.9,
137 | "installments": 7,
138 | "currencyId": "USD",
139 | "currencyFormat": "$",
140 | "isFreeShipping": true
141 | },
142 |
143 | {
144 | "id": 4,
145 | "sku": 9197907543445677,
146 | "title": "Born On The Streets",
147 | "description": "14/15 s/nº - Jogador",
148 | "availableSizes": ["XL"],
149 | "style": "Branco com listras pretas",
150 | "price": 25.9,
151 | "installments": 12,
152 | "currencyId": "USD",
153 | "currencyFormat": "$",
154 | "isFreeShipping": false
155 | },
156 |
157 | {
158 | "id": 5,
159 | "sku": 10547961582846888,
160 | "title": "Tso 3D Short Sleeve T-Shirt A",
161 | "description": "14/15 + Camiseta 1º Mundial",
162 | "availableSizes": ["X", "L", "XL"],
163 | "style": "Preto",
164 | "price": 10.9,
165 | "installments": 9,
166 | "currencyId": "USD",
167 | "currencyFormat": "$",
168 | "isFreeShipping": false
169 | },
170 |
171 | {
172 | "id": 6,
173 | "sku": 6090484789343891,
174 | "title": "Man Tie Dye Cinza Grey T-Shirt",
175 | "description": "Goleiro 13/14",
176 | "availableSizes": ["XL", "XXL"],
177 | "style": "Branco",
178 | "price": 49.9,
179 | "installments": 0,
180 | "currencyId": "USD",
181 | "currencyFormat": "$",
182 | "isFreeShipping": true
183 | },
184 |
185 | {
186 | "id": 7,
187 | "sku": 18532669286405342,
188 | "title": "Crazy Monkey Black T-Shirt",
189 | "description": "1977 Infantil",
190 | "availableSizes": ["S"],
191 | "style": "Preto com listras brancas",
192 | "price": 22.5,
193 | "installments": 4,
194 | "currencyId": "USD",
195 | "currencyFormat": "$",
196 | "isFreeShipping": true
197 | },
198 |
199 | {
200 | "id": 8,
201 | "sku": 5619496040738316,
202 | "title": "Tso 3D Black T-Shirt",
203 | "description": "",
204 | "availableSizes": ["XL"],
205 | "style": "Azul escuro",
206 | "price": 18.7,
207 | "installments": 4,
208 | "currencyId": "USD",
209 | "currencyFormat": "$",
210 | "isFreeShipping": false
211 | },
212 |
213 | {
214 | "id": 9,
215 | "sku": 11600983276356165,
216 | "title": "Crazy Monkey Grey",
217 | "description": "",
218 | "availableSizes": ["L", "XL"],
219 | "style": "",
220 | "price": 134.9,
221 | "installments": 5,
222 | "currencyId": "USD",
223 | "currencyFormat": "$",
224 | "isFreeShipping": true
225 | },
226 |
227 | {
228 | "id": 10,
229 | "sku": 27250082398145995,
230 | "title": "On The Streets Black T-Shirt",
231 | "description": "",
232 | "availableSizes": ["L", "XL"],
233 | "style": "",
234 | "price": 49.0,
235 | "installments": 9,
236 | "currencyId": "USD",
237 | "currencyFormat": "$",
238 | "isFreeShipping": true
239 | }
240 | ]
241 | }
242 |
--------------------------------------------------------------------------------
/public/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | -ms-text-size-adjust: 100%; /* 2 */
11 | -webkit-text-size-adjust: 100%; /* 2 */
12 | }
13 |
14 | /**
15 | * Remove default margin.
16 | */
17 |
18 | body {
19 | margin: 0;
20 | }
21 |
22 | /* HTML5 display definitions
23 | ========================================================================== */
24 |
25 | /**
26 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
27 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
28 | * and Firefox.
29 | * Correct `block` display not defined for `main` in IE 11.
30 | */
31 |
32 | article,
33 | aside,
34 | details,
35 | figcaption,
36 | figure,
37 | footer,
38 | header,
39 | hgroup,
40 | main,
41 | menu,
42 | nav,
43 | section,
44 | summary {
45 | display: block;
46 | }
47 |
48 | /**
49 | * 1. Correct `inline-block` display not defined in IE 8/9.
50 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
51 | */
52 |
53 | audio,
54 | canvas,
55 | progress,
56 | video {
57 | display: inline-block; /* 1 */
58 | vertical-align: baseline; /* 2 */
59 | }
60 |
61 | /**
62 | * Prevent modern browsers from displaying `audio` without controls.
63 | * Remove excess height in iOS 5 devices.
64 | */
65 |
66 | audio:not([controls]) {
67 | display: none;
68 | height: 0;
69 | }
70 |
71 | /**
72 | * Address `[hidden]` styling not present in IE 8/9/10.
73 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
74 | */
75 |
76 | [hidden],
77 | template {
78 | display: none;
79 | }
80 |
81 | /* Links
82 | ========================================================================== */
83 |
84 | /**
85 | * Remove the gray background color from active links in IE 10.
86 | */
87 |
88 | a {
89 | background-color: transparent;
90 | }
91 |
92 | /**
93 | * Improve readability when focused and also mouse hovered in all browsers.
94 | */
95 |
96 | a:active,
97 | a:hover {
98 | outline: 0;
99 | }
100 |
101 | /* Text-level semantics
102 | ========================================================================== */
103 |
104 | /**
105 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
106 | */
107 |
108 | abbr[title] {
109 | border-bottom: 1px dotted;
110 | }
111 |
112 | /**
113 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
114 | */
115 |
116 | b,
117 | strong {
118 | font-weight: bold;
119 | }
120 |
121 | /**
122 | * Address styling not present in Safari and Chrome.
123 | */
124 |
125 | dfn {
126 | font-style: italic;
127 | }
128 |
129 | /**
130 | * Address variable `h1` font-size and margin within `section` and `article`
131 | * contexts in Firefox 4+, Safari, and Chrome.
132 | */
133 |
134 | h1 {
135 | font-size: 2em;
136 | margin: 0.67em 0;
137 | }
138 |
139 | /**
140 | * Address styling not present in IE 8/9.
141 | */
142 |
143 | mark {
144 | background: #ff0;
145 | color: #000;
146 | }
147 |
148 | /**
149 | * Address inconsistent and variable font size in all browsers.
150 | */
151 |
152 | small {
153 | font-size: 80%;
154 | }
155 |
156 | /**
157 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
158 | */
159 |
160 | sub,
161 | sup {
162 | font-size: 75%;
163 | line-height: 0;
164 | position: relative;
165 | vertical-align: baseline;
166 | }
167 |
168 | sup {
169 | top: -0.5em;
170 | }
171 |
172 | sub {
173 | bottom: -0.25em;
174 | }
175 |
176 | /* Embedded content
177 | ========================================================================== */
178 |
179 | /**
180 | * Remove border when inside `a` element in IE 8/9/10.
181 | */
182 |
183 | img {
184 | border: 0;
185 | }
186 |
187 | /**
188 | * Correct overflow not hidden in IE 9/10/11.
189 | */
190 |
191 | svg:not(:root) {
192 | overflow: hidden;
193 | }
194 |
195 | /* Grouping content
196 | ========================================================================== */
197 |
198 | /**
199 | * Address margin not present in IE 8/9 and Safari.
200 | */
201 |
202 | figure {
203 | margin: 1em 40px;
204 | }
205 |
206 | /**
207 | * Address differences between Firefox and other browsers.
208 | */
209 |
210 | hr {
211 | box-sizing: content-box;
212 | height: 0;
213 | }
214 |
215 | /**
216 | * Contain overflow in all browsers.
217 | */
218 |
219 | pre {
220 | overflow: auto;
221 | }
222 |
223 | /**
224 | * Address odd `em`-unit font size rendering in all browsers.
225 | */
226 |
227 | code,
228 | kbd,
229 | pre,
230 | samp {
231 | font-family: monospace, monospace;
232 | font-size: 1em;
233 | }
234 |
235 | /* Forms
236 | ========================================================================== */
237 |
238 | /**
239 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
240 | * styling of `select`, unless a `border` property is set.
241 | */
242 |
243 | /**
244 | * 1. Correct color not being inherited.
245 | * Known issue: affects color of disabled elements.
246 | * 2. Correct font properties not being inherited.
247 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
248 | */
249 |
250 | button,
251 | input,
252 | optgroup,
253 | select,
254 | textarea {
255 | color: inherit; /* 1 */
256 | font: inherit; /* 2 */
257 | margin: 0; /* 3 */
258 | }
259 |
260 | /**
261 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
262 | */
263 |
264 | button {
265 | overflow: visible;
266 | }
267 |
268 | /**
269 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
270 | * All other form control elements do not inherit `text-transform` values.
271 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
272 | * Correct `select` style inheritance in Firefox.
273 | */
274 |
275 | button,
276 | select {
277 | text-transform: none;
278 | }
279 |
280 | /**
281 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
282 | * and `video` controls.
283 | * 2. Correct inability to style clickable `input` types in iOS.
284 | * 3. Improve usability and consistency of cursor style between image-type
285 | * `input` and others.
286 | */
287 |
288 | button,
289 | html input[type="button"], /* 1 */
290 | input[type="reset"],
291 | input[type="submit"] {
292 | -webkit-appearance: button; /* 2 */
293 | cursor: pointer; /* 3 */
294 | }
295 |
296 | /**
297 | * Re-set default cursor for disabled elements.
298 | */
299 |
300 | button[disabled],
301 | html input[disabled] {
302 | cursor: default;
303 | }
304 |
305 | /**
306 | * Remove inner padding and border in Firefox 4+.
307 | */
308 |
309 | button::-moz-focus-inner,
310 | input::-moz-focus-inner {
311 | border: 0;
312 | padding: 0;
313 | }
314 |
315 | /**
316 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
317 | * the UA stylesheet.
318 | */
319 |
320 | input {
321 | line-height: normal;
322 | }
323 |
324 | /**
325 | * It's recommended that you don't attempt to style these elements.
326 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
327 | *
328 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
329 | * 2. Remove excess padding in IE 8/9/10.
330 | */
331 |
332 | input[type="checkbox"],
333 | input[type="radio"] {
334 | box-sizing: border-box; /* 1 */
335 | padding: 0; /* 2 */
336 | }
337 |
338 | /**
339 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
340 | * `font-size` values of the `input`, it causes the cursor style of the
341 | * decrement button to change from `default` to `text`.
342 | */
343 |
344 | input[type="number"]::-webkit-inner-spin-button,
345 | input[type="number"]::-webkit-outer-spin-button {
346 | height: auto;
347 | }
348 |
349 | /**
350 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
351 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
352 | * (include `-moz` to future-proof).
353 | */
354 |
355 | input[type="search"] {
356 | -webkit-appearance: textfield; /* 1 */ /* 2 */
357 | box-sizing: content-box;
358 | }
359 |
360 | /**
361 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
362 | * Safari (but not Chrome) clips the cancel button when the search input has
363 | * padding (and `textfield` appearance).
364 | */
365 |
366 | input[type="search"]::-webkit-search-cancel-button,
367 | input[type="search"]::-webkit-search-decoration {
368 | -webkit-appearance: none;
369 | }
370 |
371 | /**
372 | * Define consistent border, margin, and padding.
373 | */
374 |
375 | fieldset {
376 | border: 1px solid #c0c0c0;
377 | margin: 0 2px;
378 | padding: 0.35em 0.625em 0.75em;
379 | }
380 |
381 | /**
382 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
383 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
384 | */
385 |
386 | legend {
387 | border: 0; /* 1 */
388 | padding: 0; /* 2 */
389 | }
390 |
391 | /**
392 | * Remove default vertical scrollbar in IE 8/9/10/11.
393 | */
394 |
395 | textarea {
396 | overflow: auto;
397 | }
398 |
399 | /**
400 | * Don't inherit the `font-weight` (applied by a rule above).
401 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
402 | */
403 |
404 | optgroup {
405 | font-weight: bold;
406 | }
407 |
408 | /* Tables
409 | ========================================================================== */
410 |
411 | /**
412 | * Remove most spacing between table cells.
413 | */
414 |
415 | table {
416 | border-collapse: collapse;
417 | border-spacing: 0;
418 | }
419 |
420 | td,
421 | th {
422 | padding: 0;
423 | }
424 |
--------------------------------------------------------------------------------
/wdio.conf.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | //
3 | // ==================
4 | // Specify Test Files
5 | // ==================
6 | // Define which test specs should run. The pattern is relative to the directory
7 | // from which `wdio` was called. Notice that, if you are calling `wdio` from an
8 | // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
9 | // directory is where your package.json resides, so `wdio` will be called from there.
10 | //
11 | specs: ['./e2e/**/test.js'],
12 | // Patterns to exclude.
13 | exclude: [
14 | // 'path/to/excluded/files'
15 | ],
16 | //
17 | // ============
18 | // Capabilities
19 | // ============
20 | // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
21 | // time. Depending on the number of capabilities, WebdriverIO launches several test
22 | // sessions. Within your capabilities you can overwrite the spec and exclude options in
23 | // order to group specific specs to a specific capability.
24 | //
25 | // First, you can define how many instances should be started at the same time. Let's
26 | // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
27 | // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
28 | // files and you set maxInstances to 10, all spec files will get tested at the same time
29 | // and 30 processes will get spawned. The property handles how many capabilities
30 | // from the same test should run tests.
31 | //
32 | maxInstances: 10,
33 | //
34 | // If you have trouble getting all important capabilities together, check out the
35 | // Sauce Labs platform configurator - a great tool to configure your capabilities:
36 | // https://docs.saucelabs.com/reference/platforms-configurator
37 | //
38 | capabilities: [
39 | {
40 | // maxInstances can get overwritten per capability. So if you have an in-house Selenium
41 | // grid with only 5 firefox instances available you can make sure that not more than
42 | // 5 instances get started at a time.
43 | maxInstances: 5,
44 | //
45 | browserName: 'chrome'
46 | }
47 | ],
48 | //
49 | // ===================
50 | // Test Configurations
51 | // ===================
52 | // Define all options that are relevant for the WebdriverIO instance here
53 | //
54 | // By default WebdriverIO commands are executed in a synchronous way using
55 | // the wdio-sync package. If you still want to run your tests in an async way
56 | // e.g. using promises you can set the sync option to false.
57 | sync: true,
58 | //
59 | // Level of logging verbosity: silent | verbose | command | data | result | error
60 | logLevel: 'silent',
61 | //
62 | // Enables colors for log output.
63 | coloredLogs: true,
64 | //
65 | // Warns when a deprecated command is used
66 | deprecationWarnings: true,
67 | //
68 | // If you only want to run your tests until a specific amount of tests have failed use
69 | // bail (default is 0 - don't bail, run all tests).
70 | bail: 0,
71 | //
72 | // Saves a screenshot to a given path if a command fails.
73 | screenshotPath: './errorShots/',
74 | //
75 | // Set a base URL in order to shorten url command calls. If your `url` parameter starts
76 | // with `/`, the base url gets prepended, not including the path portion of your baseUrl.
77 | // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
78 | // gets prepended directly.
79 | baseUrl: 'http://localhost:3000',
80 | //
81 | // Default timeout for all waitFor* commands.
82 | waitforTimeout: 10000,
83 | //
84 | // Default timeout in milliseconds for request
85 | // if Selenium Grid doesn't send response
86 | connectionRetryTimeout: 90000,
87 | //
88 | // Default request retries count
89 | connectionRetryCount: 3,
90 | //
91 | // Initialize the browser instance with a WebdriverIO plugin. The object should have the
92 | // plugin name as key and the desired plugin options as properties. Make sure you have
93 | // the plugin installed before running any tests. The following plugins are currently
94 | // available:
95 | // WebdriverCSS: https://github.com/webdriverio/webdrivercss
96 | // WebdriverRTC: https://github.com/webdriverio/webdriverrtc
97 | // Browserevent: https://github.com/webdriverio/browserevent
98 | // plugins: {
99 | // webdrivercss: {
100 | // screenshotRoot: 'my-shots',
101 | // failedComparisonsRoot: 'diffs',
102 | // misMatchTolerance: 0.05,
103 | // screenWidth: [320,480,640,1024]
104 | // },
105 | // webdriverrtc: {},
106 | // browserevent: {}
107 | // },
108 | //
109 | // Test runner services
110 | // Services take over a specific job you don't want to take care of. They enhance
111 | // your test setup with almost no effort. Unlike plugins, they don't add new
112 | // commands. Instead, they hook themselves up into the test process.
113 | services: ['selenium-standalone'],
114 | //
115 | // Framework you want to run your specs with.
116 | // The following are supported: Mocha, Jasmine, and Cucumber
117 | // see also: http://webdriver.io/guide/testrunner/frameworks.html
118 | //
119 | // Make sure you have the wdio adapter package for the specific framework installed
120 | // before running any tests.
121 | framework: 'mocha',
122 | //
123 | // Test reporter for stdout.
124 | // The only one supported by default is 'dot'
125 | // see also: http://webdriver.io/guide/reporters/dot.html
126 | reporters: ['spec'],
127 |
128 | //
129 | // Options to be passed to Mocha.
130 | // See the full list at http://mochajs.org/
131 | mochaOpts: {
132 | ui: 'bdd'
133 | }
134 | //
135 | // =====
136 | // Hooks
137 | // =====
138 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
139 | // it and to build services around it. You can either apply a single function or an array of
140 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
141 | // resolved to continue.
142 | /**
143 | * Gets executed once before all workers get launched.
144 | * @param {Object} config wdio configuration object
145 | * @param {Array.