├── .prettierrc ├── src ├── services │ ├── sort │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── reducer.js │ │ └── __tests__ │ │ │ └── actions.test.js │ ├── total │ │ ├── actionTypes.js │ │ ├── reducer.js │ │ └── actions.js │ ├── filters │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── reducer.js │ │ └── __tests__ │ │ │ └── actions.test.js │ ├── shelf │ │ ├── actionTypes.js │ │ ├── reducer.js │ │ └── actions.js │ ├── cart │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── reducer.js │ │ └── __tests__ │ │ │ └── actions.test.js │ ├── util.js │ ├── __tests__ │ │ └── util.test.js │ ├── reducers.js │ └── store.js ├── static │ ├── bag-icon.png │ ├── products │ │ ├── 100_1.jpg │ │ ├── 100_2.jpg │ │ ├── 101_1.jpg │ │ ├── 101_2.jpg │ │ ├── 5619496040738316_1.jpg │ │ ├── 5619496040738316_2.jpg │ │ ├── 6090484789343891_1.jpg │ │ ├── 6090484789343891_2.jpg │ │ ├── 8552515751438644_1.jpg │ │ ├── 8552515751438644_2.jpg │ │ ├── 876661122392077_1.jpg │ │ ├── 876661122392077_2.jpg │ │ ├── 9197907543445676_1.jpg │ │ ├── 9197907543445676_2.jpg │ │ ├── 10412368723880252_1.jpg │ │ ├── 10412368723880252_2.jpg │ │ ├── 10547961582846888_1.jpg │ │ ├── 10547961582846888_2.jpg │ │ ├── 10686354557628304_1.jpg │ │ ├── 10686354557628304_2.jpg │ │ ├── 11033926921508488_1.jpg │ │ ├── 11033926921508488_2.jpg │ │ ├── 11600983276356164_1.jpg │ │ ├── 11600983276356164_2.jpg │ │ ├── 11854078013954528_1.jpg │ │ ├── 11854078013954528_2.jpg │ │ ├── 12064273040195392_1.jpg │ │ ├── 12064273040195392_2.jpg │ │ ├── 18532669286405344_1.jpg │ │ ├── 18532669286405344_2.jpg │ │ ├── 18644119330491310_1.jpg │ │ ├── 18644119330491310_2.jpg │ │ ├── 27250082398145996_1.jpg │ │ ├── 27250082398145996_2.jpg │ │ ├── 39876704341265610_1.jpg │ │ ├── 39876704341265610_2.jpg │ │ ├── 51498472915966370_1.jpg │ │ └── 51498472915966370_2.jpg │ └── sprite_delete-icon.png ├── components │ ├── Spinner │ │ ├── __tests__ │ │ │ └── Spinner.test.js │ │ ├── index.js │ │ └── style.scss │ ├── Thumb │ │ ├── __tests__ │ │ │ └── Thumb.tests.js │ │ └── index.js │ ├── Shelf │ │ ├── ProductList │ │ │ ├── index.js │ │ │ └── Product │ │ │ │ ├── __tests__ │ │ │ │ └── Product.test.js │ │ │ │ └── index.js │ │ ├── Sort │ │ │ ├── __tests__ │ │ │ │ └── Sort.test.js │ │ │ └── index.js │ │ ├── Filter │ │ │ ├── __tests__ │ │ │ │ └── Filter.test.js │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── ShelfHeader │ │ │ ├── __tests__ │ │ │ │ └── ShelfHeader.test.js │ │ │ └── index.js │ │ ├── __tests__ │ │ │ └── Shelf.test.js │ │ ├── index.js │ │ └── style.scss │ ├── github │ │ ├── StarButton.js │ │ ├── style.css │ │ └── Corner.js │ ├── App │ │ ├── index.js │ │ └── __tests__ │ │ │ └── App.test.js │ ├── Selectbox │ │ ├── __tests__ │ │ │ └── Selectbox.test.js │ │ └── index.js │ ├── Checkbox │ │ ├── __tests__ │ │ │ └── Checkbox.test.js │ │ └── index.js │ └── FloatCart │ │ ├── CartProduct │ │ ├── __tests__ │ │ │ └── CartProduct.test.js │ │ └── index.js │ │ ├── __tests__ │ │ └── FloatCart.test.js │ │ ├── index.js │ │ └── style.scss ├── Root.js ├── index.js ├── setupTests.js ├── index.scss └── __tests__ │ └── integrations.test.js ├── .firebaserc ├── public ├── favicon.ico ├── manifest.json ├── index.html └── normalize.css ├── doc ├── react-shopping-cart.gif └── react-shopping-cart-min.gif ├── firebase.json ├── .editorconfig ├── .gitignore ├── server ├── app.js └── data │ └── products.json ├── .circleci └── config.yml ├── LICENSE.md ├── e2e └── test.js ├── package.json ├── README.md ├── .firebase └── hosting.YnVpbGQ.cache └── wdio.conf.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /src/services/sort/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_SORT = 'UPDATE_SORT'; 2 | -------------------------------------------------------------------------------- /src/services/total/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_CART = 'UPDATE_CART'; 2 | -------------------------------------------------------------------------------- /src/services/filters/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_FILTER = 'UPDATE_FILTER'; 2 | -------------------------------------------------------------------------------- /src/services/shelf/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const FETCH_PRODUCTS = 'FETCH_PRODUCTS'; 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "react-shopping-cart-67954" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/public/favicon.ico -------------------------------------------------------------------------------- /src/static/bag-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/bag-icon.png -------------------------------------------------------------------------------- /doc/react-shopping-cart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/doc/react-shopping-cart.gif -------------------------------------------------------------------------------- /src/static/products/100_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/100_1.jpg -------------------------------------------------------------------------------- /src/static/products/100_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/100_2.jpg -------------------------------------------------------------------------------- /src/static/products/101_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/101_1.jpg -------------------------------------------------------------------------------- /src/static/products/101_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/101_2.jpg -------------------------------------------------------------------------------- /doc/react-shopping-cart-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/doc/react-shopping-cart-min.gif -------------------------------------------------------------------------------- /src/static/sprite_delete-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/sprite_delete-icon.png -------------------------------------------------------------------------------- /src/static/products/5619496040738316_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/5619496040738316_1.jpg -------------------------------------------------------------------------------- /src/static/products/5619496040738316_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/5619496040738316_2.jpg -------------------------------------------------------------------------------- /src/static/products/6090484789343891_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/6090484789343891_1.jpg -------------------------------------------------------------------------------- /src/static/products/6090484789343891_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/6090484789343891_2.jpg -------------------------------------------------------------------------------- /src/static/products/8552515751438644_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/8552515751438644_1.jpg -------------------------------------------------------------------------------- /src/static/products/8552515751438644_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/8552515751438644_2.jpg -------------------------------------------------------------------------------- /src/static/products/876661122392077_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/876661122392077_1.jpg -------------------------------------------------------------------------------- /src/static/products/876661122392077_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/876661122392077_2.jpg -------------------------------------------------------------------------------- /src/static/products/9197907543445676_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/9197907543445676_1.jpg -------------------------------------------------------------------------------- /src/static/products/9197907543445676_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/9197907543445676_2.jpg -------------------------------------------------------------------------------- /src/static/products/10412368723880252_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/10412368723880252_1.jpg -------------------------------------------------------------------------------- /src/static/products/10412368723880252_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/10412368723880252_2.jpg -------------------------------------------------------------------------------- /src/static/products/10547961582846888_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/10547961582846888_1.jpg -------------------------------------------------------------------------------- /src/static/products/10547961582846888_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/10547961582846888_2.jpg -------------------------------------------------------------------------------- /src/static/products/10686354557628304_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/10686354557628304_1.jpg -------------------------------------------------------------------------------- /src/static/products/10686354557628304_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/10686354557628304_2.jpg -------------------------------------------------------------------------------- /src/static/products/11033926921508488_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/11033926921508488_1.jpg -------------------------------------------------------------------------------- /src/static/products/11033926921508488_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/11033926921508488_2.jpg -------------------------------------------------------------------------------- /src/static/products/11600983276356164_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/11600983276356164_1.jpg -------------------------------------------------------------------------------- /src/static/products/11600983276356164_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/11600983276356164_2.jpg -------------------------------------------------------------------------------- /src/static/products/11854078013954528_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/11854078013954528_1.jpg -------------------------------------------------------------------------------- /src/static/products/11854078013954528_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/11854078013954528_2.jpg -------------------------------------------------------------------------------- /src/static/products/12064273040195392_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/12064273040195392_1.jpg -------------------------------------------------------------------------------- /src/static/products/12064273040195392_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/12064273040195392_2.jpg -------------------------------------------------------------------------------- /src/static/products/18532669286405344_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/18532669286405344_1.jpg -------------------------------------------------------------------------------- /src/static/products/18532669286405344_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/18532669286405344_2.jpg -------------------------------------------------------------------------------- /src/static/products/18644119330491310_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/18644119330491310_1.jpg -------------------------------------------------------------------------------- /src/static/products/18644119330491310_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/18644119330491310_2.jpg -------------------------------------------------------------------------------- /src/static/products/27250082398145996_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/27250082398145996_1.jpg -------------------------------------------------------------------------------- /src/static/products/27250082398145996_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/27250082398145996_2.jpg -------------------------------------------------------------------------------- /src/static/products/39876704341265610_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/39876704341265610_1.jpg -------------------------------------------------------------------------------- /src/static/products/39876704341265610_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/39876704341265610_2.jpg -------------------------------------------------------------------------------- /src/static/products/51498472915966370_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/51498472915966370_1.jpg -------------------------------------------------------------------------------- /src/static/products/51498472915966370_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/react-shopping-cart/master/src/static/products/51498472915966370_2.jpg -------------------------------------------------------------------------------- /src/services/sort/actions.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_SORT } from './actionTypes'; 2 | 3 | export const updateSort = sort => ({ 4 | type: UPDATE_SORT, 5 | payload: sort 6 | }); 7 | -------------------------------------------------------------------------------- /src/services/filters/actions.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_FILTER } from './actionTypes'; 2 | 3 | export const updateFilters = filters => ({ 4 | type: UPDATE_FILTER, 5 | payload: filters 6 | }); 7 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Spinner/__tests__/Spinner.test.js: -------------------------------------------------------------------------------- 1 | import Spinner from '../'; 2 | 3 | it('mounts without crashing', () => { 4 | const wrapped = mount(); 5 | wrapped.unmount(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/services/cart/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOAD_CART = 'LOAD_CART'; 2 | export const ADD_PRODUCT = 'ADD_PRODUCT'; 3 | export const REMOVE_PRODUCT = 'REMOVE_PRODUCT'; 4 | export const UPDATE_CART = 'UPDATE_CART'; 5 | -------------------------------------------------------------------------------- /src/components/Thumb/__tests__/Thumb.tests.js: -------------------------------------------------------------------------------- 1 | import Thumb from '../'; 2 | 3 | const src = 'https://bit.ly/2QIKw60'; 4 | 5 | it('mount without crashing', () => { 6 | const wrapped = mount(); 7 | wrapped.unmount(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/Spinner/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './style.scss'; 4 | 5 | export default () => ( 6 |
7 |
8 |
9 |
10 |
11 |
12 | ); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = null -------------------------------------------------------------------------------- /src/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import store from './services/store'; 5 | 6 | const Root = ({ children, initialState = {} }) => ( 7 | {children} 8 | ); 9 | 10 | export default Root; 11 | -------------------------------------------------------------------------------- /src/components/Shelf/ProductList/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Product from './Product'; 4 | 5 | const ProductList = ({ products }) => { 6 | return products.map(p => { 7 | return ; 8 | }); 9 | }; 10 | 11 | export default ProductList; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import App from './components/App'; 6 | import Root from './Root'; 7 | 8 | import './index.scss'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { shallow, render, mount } from 'enzyme'; 3 | import adapter from 'enzyme-adapter-react-16'; 4 | 5 | Enzyme.configure({ adapter: new adapter() }); 6 | 7 | /* Globals only for tests */ 8 | global.React = React; 9 | global.shallow = shallow; 10 | global.render = render; 11 | global.mount = mount; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/services/sort/reducer.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_SORT } from './actionTypes'; 2 | 3 | const initialState = { 4 | type: '' 5 | }; 6 | 7 | export default function(state = initialState, action) { 8 | switch (action.type) { 9 | case UPDATE_SORT: 10 | return { 11 | ...state, 12 | type: action.payload 13 | }; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Shelf/Sort/__tests__/Sort.test.js: -------------------------------------------------------------------------------- 1 | import Sort from '../'; 2 | import Root from '../../../../Root'; 3 | 4 | const initialState = { 5 | sort: { 6 | type: 'highestprice' 7 | } 8 | }; 9 | 10 | it('mounts without crashing', () => { 11 | const wrapped = mount( 12 | 13 | 14 | 15 | ); 16 | wrapped.unmount(); 17 | }); 18 | -------------------------------------------------------------------------------- /src/services/filters/reducer.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_FILTER } from './actionTypes'; 2 | 3 | const initialState = { 4 | items: [] 5 | }; 6 | 7 | export default function(state = initialState, action) { 8 | switch (action.type) { 9 | case UPDATE_FILTER: 10 | return { 11 | ...state, 12 | items: action.payload 13 | }; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/util.js: -------------------------------------------------------------------------------- 1 | export const formatPrice = (x, currency) => { 2 | switch (currency) { 3 | case 'BRL': 4 | return x.toFixed(2).replace('.', ','); 5 | default: 6 | return x.toFixed(2); 7 | } 8 | }; 9 | 10 | export const productsAPI = 11 | 'https://react-shopping-cart-67954.firebaseio.com/products.json'; 12 | // export const productsAPI = "http://localhost:8001/api/products"; 13 | -------------------------------------------------------------------------------- /src/services/shelf/reducer.js: -------------------------------------------------------------------------------- 1 | import { FETCH_PRODUCTS } from './actionTypes'; 2 | 3 | const initialState = { 4 | products: [] 5 | }; 6 | 7 | export default function(state = initialState, action) { 8 | switch (action.type) { 9 | case FETCH_PRODUCTS: 10 | return { 11 | ...state, 12 | products: action.payload 13 | }; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Shelf/Filter/__tests__/Filter.test.js: -------------------------------------------------------------------------------- 1 | import Filter from '../'; 2 | import Root from '../../../../Root'; 3 | 4 | const initialState = { 5 | filters: { 6 | items: ['XS', 'S'] 7 | } 8 | }; 9 | 10 | it('mounts without crashing', () => { 11 | const wrapped = mount( 12 | 13 | 14 | 15 | ); 16 | wrapped.unmount(); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/Shelf/ShelfHeader/__tests__/ShelfHeader.test.js: -------------------------------------------------------------------------------- 1 | import Root from '../../../../Root'; 2 | import ShelfHeader from '..'; 3 | import Sort from '../../Sort'; 4 | 5 | it('shows a sort component', () => { 6 | const wrapped = mount( 7 | 8 | 9 | 10 | ); 11 | expect(wrapped.find(Sort).length).toEqual(1); 12 | wrapped.unmount(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/services/cart/actions.js: -------------------------------------------------------------------------------- 1 | import { LOAD_CART, ADD_PRODUCT, REMOVE_PRODUCT } from './actionTypes'; 2 | 3 | export const loadCart = products => ({ 4 | type: LOAD_CART, 5 | payload: products 6 | }); 7 | 8 | export const addProduct = product => ({ 9 | type: ADD_PRODUCT, 10 | payload: product 11 | }); 12 | 13 | export const removeProduct = product => ({ 14 | type: REMOVE_PRODUCT, 15 | payload: product 16 | }); 17 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const express = require('express'); 4 | const cors = require('cors'); 5 | 6 | const app = express(); 7 | app.use(cors()); 8 | 9 | const port = 8001; 10 | 11 | app.get('/api/products', (req, res) => { 12 | res.sendFile(path.join(__dirname, 'data', 'products.json')); 13 | }); 14 | 15 | app.listen(port, () => { 16 | console.log(`[products] API listening on port ${port}.`); 17 | }); 18 | -------------------------------------------------------------------------------- /src/services/__tests__/util.test.js: -------------------------------------------------------------------------------- 1 | import { formatPrice } from '../util'; 2 | 3 | describe('util', () => { 4 | describe('formatPrice()', () => { 5 | it('should replace dot by comma when currency is BRL', () => { 6 | expect(formatPrice(10, 'BRL')).toEqual('10,00'); 7 | }); 8 | 9 | it('should by default return decimal separated by dot', () => { 10 | expect(formatPrice(10)).toEqual('10.00'); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/services/filters/__tests__/actions.test.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | import * as types from '../actionTypes'; 3 | 4 | describe('filter actions', () => { 5 | it('should return expected payload', () => { 6 | const text = 'L'; 7 | 8 | const expectedAction = { 9 | type: types.UPDATE_FILTER, 10 | payload: text 11 | }; 12 | 13 | expect(actions.updateFilters(text)).toEqual(expectedAction); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/services/sort/__tests__/actions.test.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | import * as types from '../actionTypes'; 3 | 4 | describe('sort actions', () => { 5 | it('should return expected payload', () => { 6 | const text = 'lowestprice'; 7 | 8 | const expectedAction = { 9 | type: types.UPDATE_SORT, 10 | payload: text 11 | }; 12 | 13 | expect(actions.updateSort(text)).toEqual(expectedAction); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/services/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import shelfReducer from './shelf/reducer'; 3 | import cartReducer from './cart/reducer'; 4 | import totalReducer from './total/reducer'; 5 | import filtersReducer from './filters/reducer'; 6 | import sortReducer from './sort/reducer'; 7 | 8 | export default combineReducers({ 9 | shelf: shelfReducer, 10 | cart: cartReducer, 11 | total: totalReducer, 12 | filters: filtersReducer, 13 | sort: sortReducer 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/Thumb/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Thumb = props => { 5 | return ( 6 |
7 | {props.alt} 8 |
9 | ); 10 | }; 11 | 12 | Thumb.propTypes = { 13 | alt: PropTypes.string, 14 | title: PropTypes.string, 15 | classes: PropTypes.string, 16 | src: PropTypes.string.isRequired 17 | }; 18 | 19 | export default Thumb; 20 | -------------------------------------------------------------------------------- /src/components/github/StarButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const StarButton = () => ( 4 |
5 | Leave a star on Github if this repository was useful :) 6 | Star 7 |
8 | ); 9 | 10 | export default StarButton; 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/services/total/reducer.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_CART } from './actionTypes'; 2 | 3 | const initialState = { 4 | data: { 5 | productQuantity: 0, 6 | installments: 0, 7 | totalPrice: 0, 8 | currencyId: 'USD', 9 | currencyFormat: '$' 10 | } 11 | }; 12 | 13 | export default function(state = initialState, action) { 14 | switch (action.type) { 15 | case UPDATE_CART: 16 | return { 17 | ...state, 18 | data: action.payload 19 | }; 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/github/style.css: -------------------------------------------------------------------------------- 1 | .github-corner:hover .octo-arm { 2 | animation: octocat-wave 560ms ease-in-out; 3 | } 4 | @keyframes octocat-wave { 5 | 0%, 6 | 100% { 7 | transform: rotate(0); 8 | } 9 | 20%, 10 | 60% { 11 | transform: rotate(-25deg); 12 | } 13 | 40%, 14 | 80% { 15 | transform: rotate(10deg); 16 | } 17 | } 18 | @media (max-width: 500px) { 19 | .github-corner:hover .octo-arm { 20 | animation: none; 21 | } 22 | .github-corner .octo-arm { 23 | animation: octocat-wave 560ms ease-in-out; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Shelf/ShelfHeader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Sort from '../Sort'; 5 | 6 | const ShelfHeader = props => { 7 | return ( 8 |
9 | 10 | {props.productsLength} Product(s) found. 11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | ShelfHeader.propTypes = { 18 | productsLength: PropTypes.number.isRequired 19 | }; 20 | 21 | export default ShelfHeader; 22 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Shelf from '../Shelf'; 4 | import Filter from '../Shelf/Filter'; 5 | import GithubCorner from '../github/Corner'; 6 | import FloatCart from '../FloatCart'; 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/components/App/__tests__/App.test.js: -------------------------------------------------------------------------------- 1 | import Root from '../../../Root'; 2 | import App from '../'; 3 | 4 | import Shelf from '../../Shelf'; 5 | import FloatCart from '../../FloatCart'; 6 | 7 | let wrapped; 8 | 9 | beforeEach(() => { 10 | wrapped = mount( 11 | 12 | 13 | 14 | ); 15 | }); 16 | 17 | afterEach(() => { 18 | wrapped.unmount(); 19 | }); 20 | 21 | it('shows a shelf', () => { 22 | expect(wrapped.find(Shelf).length).toEqual(1); 23 | }); 24 | 25 | it('shows a floating cart', () => { 26 | expect(wrapped.find(FloatCart).length).toEqual(1); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/Selectbox/__tests__/Selectbox.test.js: -------------------------------------------------------------------------------- 1 | import Selectbox from '..'; 2 | 3 | const optionsMock = [ 4 | { value: '', label: 'Select' }, 5 | { value: 'lowestprice', label: 'Lowest to highest' }, 6 | { value: 'highestprice', label: 'Highest to lowest' } 7 | ]; 8 | 9 | let wrapped; 10 | 11 | beforeEach(() => { 12 | wrapped = mount( 13 | {}} /> 14 | ); 15 | }); 16 | 17 | afterEach(() => { 18 | wrapped.unmount(); 19 | }); 20 | 21 | it('mount with 3 option element', () => { 22 | expect(wrapped.find('option').length).toEqual(3); 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/Shelf/ProductList/Product/__tests__/Product.test.js: -------------------------------------------------------------------------------- 1 | import Product from '..'; 2 | import Root from '../../../../../Root'; 3 | 4 | const productMock = { 5 | id: 12, 6 | sku: 12064273040195392, 7 | title: 'Cat Tee Black T-Shirt', 8 | description: '4 MSL', 9 | availableSizes: ['S', 'XS'], 10 | style: 'Black with custom print', 11 | price: 10.9, 12 | installments: 9, 13 | currencyId: 'USD', 14 | currencyFormat: '$', 15 | isFreeShipping: true 16 | }; 17 | 18 | it('mount without crashing', () => { 19 | const wrapped = mount( 20 | 21 | {}} /> 22 | 23 | ); 24 | wrapped.unmount(); 25 | }); 26 | -------------------------------------------------------------------------------- /src/services/cart/reducer.js: -------------------------------------------------------------------------------- 1 | import { LOAD_CART, ADD_PRODUCT, REMOVE_PRODUCT } from './actionTypes'; 2 | 3 | const initialState = { 4 | products: [] 5 | }; 6 | 7 | export default function(state = initialState, action) { 8 | switch (action.type) { 9 | case LOAD_CART: 10 | return { 11 | ...state, 12 | products: action.payload 13 | }; 14 | case ADD_PRODUCT: 15 | return { 16 | ...state, 17 | productToAdd: Object.assign({}, action.payload) 18 | }; 19 | case REMOVE_PRODUCT: 20 | return { 21 | ...state, 22 | productToRemove: Object.assign({}, action.payload) 23 | }; 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/services/total/actions.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_CART } from './actionTypes'; 2 | 3 | export const updateCart = cartProducts => dispatch => { 4 | let productQuantity = cartProducts.reduce((sum, p) => { 5 | sum += p.quantity; 6 | return sum; 7 | }, 0); 8 | 9 | let totalPrice = cartProducts.reduce((sum, p) => { 10 | sum += p.price * p.quantity; 11 | return sum; 12 | }, 0); 13 | 14 | let installments = cartProducts.reduce((greater, p) => { 15 | greater = p.installments > greater ? p.installments : greater; 16 | return greater; 17 | }, 0); 18 | 19 | let cartTotal = { 20 | productQuantity, 21 | installments, 22 | totalPrice, 23 | currencyId: 'USD', 24 | currencyFormat: '$' 25 | }; 26 | 27 | dispatch({ 28 | type: UPDATE_CART, 29 | payload: cartTotal 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | witmy: &witmy 2 | docker: 3 | - image: circleci/node:8.11.3 4 | 5 | version: 2 6 | jobs: 7 | build: 8 | <<: *witmy 9 | steps: 10 | - checkout 11 | - run: npm install 12 | - run: npm run test 13 | - run: npm run build 14 | - persist_to_workspace: 15 | root: . 16 | paths: 17 | - . 18 | deploy: 19 | <<: *witmy 20 | steps: 21 | - attach_workspace: 22 | at: . 23 | - run: 24 | name: Deploy Master to Firebase 25 | command: ./node_modules/.bin/firebase deploy --token=$FIREBASE_DEPLOY_TOKEN 26 | workflows: 27 | version: 2 28 | build-deploy: 29 | jobs: 30 | - build 31 | - deploy: 32 | requires: 33 | - build 34 | filters: 35 | branches: 36 | only: master 37 | -------------------------------------------------------------------------------- /src/services/store.js: -------------------------------------------------------------------------------- 1 | import { compose, createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducers'; 4 | 5 | export default initialState => { 6 | initialState = 7 | JSON.parse(window.localStorage.getItem('state')) || initialState; 8 | const middleware = [thunk]; 9 | 10 | const store = createStore( 11 | rootReducer, 12 | initialState, 13 | compose( 14 | applyMiddleware(...middleware) 15 | /* window.__REDUX_DEVTOOLS_EXTENSION__ && 16 | window.__REDUX_DEVTOOLS_EXTENSION__() */ 17 | ) 18 | ); 19 | 20 | store.subscribe(() => { 21 | const state = store.getState(); 22 | const persist = { 23 | cart: state.cart, 24 | total: state.total 25 | }; 26 | 27 | window.localStorage.setItem('state', JSON.stringify(persist)); 28 | }); 29 | 30 | return store; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/Checkbox/__tests__/Checkbox.test.js: -------------------------------------------------------------------------------- 1 | import Checkbox from '..'; 2 | 3 | const label = 'M'; 4 | let wrapped; 5 | 6 | beforeEach(() => { 7 | wrapped = mount( {}} />); 8 | }); 9 | 10 | afterEach(() => { 11 | wrapped.unmount(); 12 | }); 13 | 14 | it('should toggle isChecked state when input change', () => { 15 | const input = wrapped.find('input'); 16 | 17 | /* isChecked should start with false */ 18 | expect(wrapped.state().isChecked).toEqual(false); 19 | input.simulate('change'); 20 | /* Then toggle to true */ 21 | expect(wrapped.state().isChecked).toEqual(true); 22 | input.simulate('change'); 23 | /* And then to false */ 24 | expect(wrapped.state().isChecked).toEqual(false); 25 | }); 26 | 27 | it('should have innerText equals label propertie', () => { 28 | const text = wrapped.find('span').text(); 29 | expect(text).toEqual(label); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Selectbox/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Selectbox extends Component { 5 | static propTypes = { 6 | options: PropTypes.array.isRequired, 7 | classes: PropTypes.string, 8 | handleOnChange: PropTypes.func.isRequired 9 | }; 10 | 11 | state = { 12 | selected: '' 13 | }; 14 | 15 | createOptions = options => 16 | options.map(o => ( 17 | 20 | )); 21 | 22 | onChange = e => { 23 | this.props.handleOnChange(e.target.value); 24 | }; 25 | 26 | render() { 27 | const { classes, options } = this.props; 28 | 29 | return ( 30 | 33 | ); 34 | } 35 | } 36 | 37 | export default Selectbox; 38 | -------------------------------------------------------------------------------- /src/components/FloatCart/CartProduct/__tests__/CartProduct.test.js: -------------------------------------------------------------------------------- 1 | import CartProduct from '../'; 2 | 3 | const productMock = { 4 | id: 13, 5 | sku: 51498472915966366, 6 | title: 'Dark Thug Blue-Navy T-Shirt', 7 | description: '', 8 | availableSizes: ['M'], 9 | style: 'Front print and paisley print', 10 | price: 29.45, 11 | installments: 5, 12 | currencyId: 'USD', 13 | currencyFormat: '$', 14 | isFreeShipping: true 15 | }; 16 | 17 | let wrapped; 18 | 19 | beforeEach(() => { 20 | wrapped = mount( 21 | {}} /> 22 | ); 23 | }); 24 | 25 | afterEach(() => { 26 | wrapped.unmount(); 27 | }); 28 | 29 | it('append class shelf-item--mouseover when mouseover x button', () => { 30 | expect(wrapped.find('.shelf-item').hasClass('shelf-item--mouseover')).toEqual( 31 | false 32 | ); 33 | wrapped.find('.shelf-item__del').simulate('mouseover'); 34 | expect(wrapped.find('.shelf-item').hasClass('shelf-item--mouseover')).toEqual( 35 | true 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/Spinner/style.scss: -------------------------------------------------------------------------------- 1 | .spinner.lds-ring { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | margin-left: -32px; 6 | margin-top: -32px; 7 | width: 64px; 8 | height: 64px; 9 | z-index: 10; 10 | border-radius: 5px; 11 | background-color: #000; 12 | 13 | div { 14 | box-sizing: border-box; 15 | display: block; 16 | position: absolute; 17 | width: 51px; 18 | height: 51px; 19 | margin: 6px; 20 | border: 6px solid #fff; 21 | border-radius: 50%; 22 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 23 | border-color: #fff transparent transparent transparent; 24 | &:nth-child(1) { 25 | animation-delay: -0.45s; 26 | } 27 | &:nth-child(2) { 28 | animation-delay: -0.3s; 29 | } 30 | &:nth-child(3) { 31 | animation-delay: -0.15s; 32 | } 33 | } 34 | 35 | @keyframes lds-ring { 36 | 0% { 37 | transform: rotate(0deg); 38 | } 39 | 40 | 100% { 41 | transform: rotate(360deg); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) Copyright (c) 2018 Jefferson Ribeiro 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/components/Shelf/Sort/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { connect } from 'react-redux'; 5 | import { updateSort } from '../../../services/sort/actions'; 6 | import Selectbox from '../../Selectbox'; 7 | 8 | const sortBy = [ 9 | { value: '', label: 'Select' }, 10 | { value: 'lowestprice', label: 'Lowest to highest' }, 11 | { value: 'highestprice', label: 'Highest to lowest' } 12 | ]; 13 | 14 | class Sort extends Component { 15 | static propTypes = { 16 | updateSort: PropTypes.func.isRequired, 17 | sort: PropTypes.string.isRequired 18 | }; 19 | 20 | handleSort = value => { 21 | this.props.updateSort(value); 22 | }; 23 | 24 | render() { 25 | return ( 26 |
27 | Order by 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | const mapStateToProps = state => ({ 35 | sort: state.sort.type 36 | }); 37 | 38 | export default connect( 39 | mapStateToProps, 40 | { updateSort } 41 | )(Sort); 42 | -------------------------------------------------------------------------------- /e2e/test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | describe('Shopping cart', () => { 4 | it('should add a product to cart and remove it', () => { 5 | browser.url('https://react-shopping-cart-67954.firebaseapp.com/'); 6 | browser.waitForText('.shelf-item'); 7 | 8 | /* Open float cart */ 9 | browser.click('.bag--float-cart-closed'); 10 | 11 | /* Bag should start with 0 products */ 12 | browser.waitForText('.bag__quantity'); 13 | let bagProductsQtd = browser.getText('.bag__quantity'); 14 | expect(bagProductsQtd).to.equal('0'); 15 | 16 | /* Add a product to cart */ 17 | browser.click('.shelf-item'); 18 | browser.pause(100); 19 | 20 | /* And it should have 1 product in it now */ 21 | bagProductsQtd = browser.getText('.bag__quantity'); 22 | expect(bagProductsQtd).to.equal('1'); 23 | 24 | /* Remove the product from cart and now it should show 0 products in bag */ 25 | browser.click('.shelf-item__del'); 26 | browser.pause(100); 27 | bagProductsQtd = browser.getText('.bag__quantity'); 28 | expect(bagProductsQtd).to.equal('0'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Checkbox extends Component { 5 | static propTypes = { 6 | label: PropTypes.string.isRequired, 7 | handleCheckboxChange: PropTypes.func.isRequired 8 | }; 9 | 10 | state = { 11 | isChecked: false 12 | }; 13 | 14 | toggleCheckboxChange = () => { 15 | const { handleCheckboxChange, label } = this.props; 16 | 17 | this.setState(({ isChecked }) => ({ 18 | isChecked: !isChecked 19 | })); 20 | 21 | handleCheckboxChange(label); 22 | }; 23 | 24 | render() { 25 | const { label, classes } = this.props; 26 | const { isChecked } = this.state; 27 | 28 | return ( 29 |
30 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | export default Checkbox; 46 | -------------------------------------------------------------------------------- /src/services/shelf/actions.js: -------------------------------------------------------------------------------- 1 | import { FETCH_PRODUCTS } from './actionTypes'; 2 | import axios from 'axios'; 3 | 4 | import { productsAPI } from '../util'; 5 | 6 | const compare = { 7 | lowestprice: (a, b) => { 8 | if (a.price < b.price) return -1; 9 | if (a.price > b.price) return 1; 10 | return 0; 11 | }, 12 | highestprice: (a, b) => { 13 | if (a.price > b.price) return -1; 14 | if (a.price < b.price) return 1; 15 | return 0; 16 | } 17 | }; 18 | 19 | export const fetchProducts = (filters, sortBy, callback) => dispatch => { 20 | return axios 21 | .get(productsAPI) 22 | .then(res => { 23 | let { products } = res.data; 24 | 25 | if (!!filters && filters.length > 0) { 26 | products = products.filter(p => 27 | filters.find(f => p.availableSizes.find(size => size === f)) 28 | ); 29 | } 30 | 31 | if (!!sortBy) { 32 | products = products.sort(compare[sortBy]); 33 | } 34 | 35 | if (!!callback) { 36 | callback(); 37 | } 38 | 39 | return dispatch({ 40 | type: FETCH_PRODUCTS, 41 | payload: products 42 | }); 43 | }) 44 | .catch(err => { 45 | console.log('Could not fetch products. Try again later.'); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/FloatCart/__tests__/FloatCart.test.js: -------------------------------------------------------------------------------- 1 | import Root from '../../../Root'; 2 | import FloatCart from '..'; 3 | import CartProduct from '../CartProduct'; 4 | 5 | const initialState = { 6 | cart: { 7 | products: [ 8 | { 9 | id: 12, 10 | sku: 12064273040195392, 11 | title: 'Cat Tee Black T-Shirt', 12 | description: '4 MSL', 13 | availableSizes: ['S', 'XS'], 14 | style: 'Black with custom print', 15 | price: 10.9, 16 | installments: 9, 17 | currencyId: 'USD', 18 | currencyFormat: '$', 19 | isFreeShipping: true 20 | }, 21 | { 22 | id: 13, 23 | sku: 51498472915966366, 24 | title: 'Dark Thug Blue-Navy T-Shirt', 25 | description: '', 26 | availableSizes: ['M'], 27 | style: 'Front print and paisley print', 28 | price: 29.45, 29 | installments: 5, 30 | currencyId: 'USD', 31 | currencyFormat: '$', 32 | isFreeShipping: true 33 | } 34 | ] 35 | } 36 | }; 37 | 38 | let wrapped; 39 | 40 | beforeEach(() => { 41 | wrapped = mount( 42 | 43 | 44 | 45 | ); 46 | }); 47 | 48 | afterEach(() => { 49 | wrapped.unmount(); 50 | }); 51 | 52 | it('should mount with 2 products in it', () => { 53 | expect(wrapped.find(CartProduct).length).toEqual(2); 54 | }); 55 | -------------------------------------------------------------------------------- /src/services/cart/__tests__/actions.test.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | import * as types from '../actionTypes'; 3 | 4 | const mockProduct = { 5 | id: 12, 6 | sku: 12064273040195392, 7 | title: 'Cat Tee Black T-Shirt', 8 | description: '4 MSL', 9 | availableSizes: ['S', 'XS'], 10 | style: 'Black with custom print', 11 | price: 10.9, 12 | installments: 9, 13 | currencyId: 'USD', 14 | currencyFormat: '$', 15 | isFreeShipping: true 16 | }; 17 | 18 | describe('floatCart actions', () => { 19 | describe('loadCart', () => { 20 | it('should return expected payload', () => { 21 | const expectedAction = { 22 | type: types.LOAD_CART, 23 | payload: mockProduct 24 | }; 25 | 26 | expect(actions.loadCart(mockProduct)).toEqual(expectedAction); 27 | }); 28 | }); 29 | 30 | describe('addProduct', () => { 31 | it('should return expected payload', () => { 32 | const expectedAction = { 33 | type: types.ADD_PRODUCT, 34 | payload: mockProduct 35 | }; 36 | 37 | expect(actions.addProduct(mockProduct)).toEqual(expectedAction); 38 | }); 39 | }); 40 | 41 | describe('removeProduct', () => { 42 | it('should return expected payload', () => { 43 | const expectedAction = { 44 | type: types.REMOVE_PRODUCT, 45 | payload: mockProduct 46 | }; 47 | 48 | expect(actions.removeProduct(mockProduct)).toEqual(expectedAction); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/Shelf/Filter/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { connect } from 'react-redux'; 5 | import { updateFilters } from '../../../services/filters/actions'; 6 | import Checkbox from '../../Checkbox'; 7 | import GithubStarButton from '../../github/StarButton'; 8 | 9 | import './style.scss'; 10 | 11 | const availableSizes = ['XS', 'S', 'M', 'ML', 'L', 'XL', 'XXL']; 12 | 13 | class Filter extends Component { 14 | static propTypes = { 15 | updateFilters: PropTypes.func.isRequired, 16 | filters: PropTypes.array 17 | }; 18 | 19 | componentDidMount() { 20 | this.selectedCheckboxes = new Set(); 21 | } 22 | 23 | toggleCheckbox = label => { 24 | if (this.selectedCheckboxes.has(label)) { 25 | this.selectedCheckboxes.delete(label); 26 | } else { 27 | this.selectedCheckboxes.add(label); 28 | } 29 | 30 | this.props.updateFilters(Array.from(this.selectedCheckboxes)); 31 | }; 32 | 33 | createCheckbox = label => ( 34 | 40 | ); 41 | 42 | createCheckboxes = () => availableSizes.map(this.createCheckbox); 43 | 44 | render() { 45 | return ( 46 |
47 |

Sizes:

48 | {this.createCheckboxes()} 49 | 50 |
51 | ); 52 | } 53 | } 54 | 55 | const mapStateToProps = state => ({ 56 | filters: state.filters.items 57 | }); 58 | 59 | export default connect( 60 | mapStateToProps, 61 | { updateFilters } 62 | )(Filter); 63 | -------------------------------------------------------------------------------- /src/components/github/Corner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './style.css'; 4 | 5 | export default () => ( 6 | 11 | 39 | 40 | ); 41 | -------------------------------------------------------------------------------- /src/components/Shelf/__tests__/Shelf.test.js: -------------------------------------------------------------------------------- 1 | import Shelf from '..'; 2 | import Root from '../../../Root'; 3 | import ShelfHeader from '../ShelfHeader'; 4 | import ProductList from '../ProductList'; 5 | import Product from '../ProductList/Product'; 6 | 7 | const initialState = { 8 | shelf: { 9 | products: [ 10 | { 11 | id: 12, 12 | sku: 12064273040195392, 13 | title: 'Cat Tee Black T-Shirt', 14 | description: '4 MSL', 15 | availableSizes: ['S', 'XS'], 16 | style: 'Black with custom print', 17 | price: 10.9, 18 | installments: 9, 19 | currencyId: 'USD', 20 | currencyFormat: '$', 21 | isFreeShipping: true 22 | }, 23 | { 24 | id: 13, 25 | sku: 51498472915966366, 26 | title: 'Dark Thug Blue-Navy T-Shirt', 27 | description: '', 28 | availableSizes: ['M'], 29 | style: 'Front print and paisley print', 30 | price: 29.45, 31 | installments: 5, 32 | currencyId: 'USD', 33 | currencyFormat: '$', 34 | isFreeShipping: true 35 | } 36 | ] 37 | } 38 | }; 39 | 40 | let wrapped; 41 | 42 | beforeEach(() => { 43 | wrapped = mount( 44 | 45 | 46 | 47 | ); 48 | }); 49 | 50 | afterEach(() => { 51 | wrapped.unmount(); 52 | }); 53 | 54 | it('shows 2 products component', () => { 55 | expect(wrapped.find(Product).length).toEqual(2); 56 | }); 57 | 58 | it('shows a shelf header with 2 products', () => { 59 | expect(wrapped.find(ShelfHeader).props().productsLength).toEqual(2); 60 | }); 61 | 62 | it('shows a product list component', () => { 63 | expect(wrapped.find(ProductList).length).toEqual(1); 64 | }); 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "babel-polyfill": "^6.26.0", 8 | "concurrently": "^4.0.1", 9 | "cors": "^2.8.5", 10 | "express": "^4.16.4", 11 | "moxios": "^0.4.0", 12 | "react": "^16.6.1", 13 | "react-dom": "^16.6.1", 14 | "react-redux": "^5.1.1", 15 | "react-scripts": "^2.1.3", 16 | "redux": "^4.0.1", 17 | "redux-thunk": "^2.3.0" 18 | }, 19 | "scripts": { 20 | "start": "concurrently \"npm run server\" \"react-scripts start\"", 21 | "server": "nodemon server/app", 22 | "wdio": "wdio", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "test:coverage": "npm run test -- --coverage", 26 | "format": "prettier --write \"**/*.+(js|json|css)\"", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "jest": { 33 | "collectCoverageFrom": [ 34 | "src/**/*.{js,jsx}", 35 | "!/node_modules/", 36 | "!src/index.js", 37 | "!src/Root.js" 38 | ] 39 | }, 40 | "browserslist": [ 41 | ">0.2%", 42 | "not dead", 43 | "not ie <= 11", 44 | "not op_mini all" 45 | ], 46 | "devDependencies": { 47 | "chai": "^4.2.0", 48 | "enzyme": "^3.7.0", 49 | "enzyme-adapter-react-16": "^1.7.0", 50 | "enzyme-to-json": "^3.3.4", 51 | "fetch-mock": "^7.2.5", 52 | "firebase-tools": "^6.2.2", 53 | "node-sass": "^4.10.0", 54 | "nodemon": "^1.18.6", 55 | "prop-types": "^15.6.2", 56 | "react-test-renderer": "^16.6.3", 57 | "redux-mock-store": "^1.5.3", 58 | "sinon": "^7.1.1", 59 | "wdio-mocha-framework": "^0.6.4", 60 | "wdio-selenium-standalone-service": "0.0.12", 61 | "wdio-spec-reporter": "^0.1.5", 62 | "webdriverio": "^4.14.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/FloatCart/CartProduct/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Thumb from './../../Thumb'; 5 | import { formatPrice } from '../../../services/util'; 6 | 7 | class CartProduct extends Component { 8 | static propTypes = { 9 | product: PropTypes.object.isRequired, 10 | removeProduct: PropTypes.func.isRequired 11 | }; 12 | 13 | state = { 14 | isMouseOver: false 15 | }; 16 | 17 | handleMouseOver = () => { 18 | this.setState({ isMouseOver: true }); 19 | }; 20 | 21 | handleMouseOut = () => { 22 | this.setState({ isMouseOver: false }); 23 | }; 24 | 25 | render() { 26 | const { product, removeProduct } = this.props; 27 | 28 | const classes = ['shelf-item']; 29 | 30 | if (!!this.state.isMouseOver) { 31 | classes.push('shelf-item--mouseover'); 32 | } 33 | 34 | return ( 35 |
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 |
59 | 60 | 61 |
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 [![CircleCI](https://circleci.com/gh/jeffersonRibeiro/react-shopping-cart.svg?style=svg)](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 | [![Edit app](https://codesandbox.io/static/img/play-codesandbox.svg)](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.} capabilities list of capabilities details 146 | */ 147 | // onPrepare: function (config, capabilities) { 148 | // }, 149 | /** 150 | * Gets executed just before initialising the webdriver session and test framework. It allows you 151 | * to manipulate configurations depending on the capability or spec. 152 | * @param {Object} config wdio configuration object 153 | * @param {Array.} capabilities list of capabilities details 154 | * @param {Array.} specs List of spec file paths that are to be run 155 | */ 156 | // beforeSession: function (config, capabilities, specs) { 157 | // }, 158 | /** 159 | * Gets executed before test execution begins. At this point you can access to all global 160 | * variables like `browser`. It is the perfect place to define custom commands. 161 | * @param {Array.} capabilities list of capabilities details 162 | * @param {Array.} specs List of spec file paths that are to be run 163 | */ 164 | // before: function (capabilities, specs) { 165 | // }, 166 | /** 167 | * Runs before a WebdriverIO command gets executed. 168 | * @param {String} commandName hook command name 169 | * @param {Array} args arguments that command would receive 170 | */ 171 | // beforeCommand: function (commandName, args) { 172 | // }, 173 | 174 | /** 175 | * Hook that gets executed before the suite starts 176 | * @param {Object} suite suite details 177 | */ 178 | // beforeSuite: function (suite) { 179 | // }, 180 | /** 181 | * Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts. 182 | * @param {Object} test test details 183 | */ 184 | // beforeTest: function (test) { 185 | // }, 186 | /** 187 | * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 188 | * beforeEach in Mocha) 189 | */ 190 | // beforeHook: function () { 191 | // }, 192 | /** 193 | * Hook that gets executed _after_ a hook within the suite ends (e.g. runs after calling 194 | * afterEach in Mocha) 195 | */ 196 | // afterHook: function () { 197 | // }, 198 | /** 199 | * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) ends. 200 | * @param {Object} test test details 201 | */ 202 | // afterTest: function (test) { 203 | // }, 204 | /** 205 | * Hook that gets executed after the suite has ended 206 | * @param {Object} suite suite details 207 | */ 208 | // afterSuite: function (suite) { 209 | // }, 210 | 211 | /** 212 | * Runs after a WebdriverIO command gets executed 213 | * @param {String} commandName hook command name 214 | * @param {Array} args arguments that command would receive 215 | * @param {Number} result 0 - command success, 1 - command error 216 | * @param {Object} error error object if any 217 | */ 218 | // afterCommand: function (commandName, args, result, error) { 219 | // }, 220 | /** 221 | * Gets executed after all tests are done. You still have access to all global variables from 222 | * the test. 223 | * @param {Number} result 0 - test pass, 1 - test fail 224 | * @param {Array.} capabilities list of capabilities details 225 | * @param {Array.} specs List of spec file paths that ran 226 | */ 227 | // after: function (result, capabilities, specs) { 228 | // }, 229 | /** 230 | * Gets executed right after terminating the webdriver session. 231 | * @param {Object} config wdio configuration object 232 | * @param {Array.} capabilities list of capabilities details 233 | * @param {Array.} specs List of spec file paths that ran 234 | */ 235 | // afterSession: function (config, capabilities, specs) { 236 | // }, 237 | /** 238 | * Gets executed after all workers got shut down and the process is about to exit. 239 | * @param {Object} exitCode 0 - success, 1 - fail 240 | * @param {Object} config wdio configuration object 241 | * @param {Array.} capabilities list of capabilities details 242 | */ 243 | // onComplete: function(exitCode, config, capabilities) { 244 | // } 245 | }; 246 | --------------------------------------------------------------------------------